一、 项目背景
保险、基金、银行等众多行业在做技术平台时都会需要一种能够准确了解用户操作行为的方式方法。诸如通过埋点、平台监控、视频可回溯等,通过技术手段,保存用户操作轨迹
,以此规范安全销售、平台健康检查、出现纠纷时可追溯、问题出现有证据。 为规范保险行业销售行为,zf出台相关政策:
在本人开发系统之前,全网似乎也有几家公司已经在推广销售双录系统,但是介于价格较高,且本人有兴趣挑战这种比较大型的平台系统。对未知领域充满无限好奇心的驱动力
使我一步一步将一套完整的视频可回溯项目比较完整的实现了。在此先给自己鼓个掌!👍👍😁
二、前期准备
行业研究:
- 怎么将用户在web前端应用(pc端、h5移动端等)的操作轨迹通过视频的形式保存下来可以说是一个比较难实现的技术要求。
- 最初想法:寻找录屏插件、通过轮询截图,最后生成视频、等等不太可行的方案都想过。后来经过各种问度娘,搜github, 技术论坛;
- 最终锁定到了github一个非常优秀的开源框架rrweb上。一句话,这个框架核心思想就是,将用户操作的行为通过
mutationObsever
js原生方法将dom按照时间顺序保存成对象数组,数组里是包含唯一的dom、value、事件等。实现原理可以看这篇文章戳这里
功能要素:
- 该开源框架提供了核心部分,但是怎么利用这个底层框架,开发一套完整的系统也是一件不简单的事;
- 该系统需要包含以下几个大模块:
-
- 放在web项目的录屏sdk(应具备无代码浸入方式接入)
-
- 需要一套后台管理系统,分为前端界面和后端服务
-
- 数据存储方法论,数据治理、数据清洗等
-
- 系统应具备安全稳定,方便部署应用的能力
技术方案:
- 本人前端出身,对宇宙后端第一大语言Java学习的还不够,自知学海无涯、无心无力边学边用,java就果断放弃。
- 选择了同样优秀但是目前仍然比较新的后端开发框架tegg, tegg是eggjs的升级版本,加入了ts元素和注解方式,约定优于配置是其灵魂,加入respository我认为是最好用的,扯远了。
- 技术核心如下:
- 探针sdk(录屏)(vue3+ts+vite+pako+loadsh-es等,也包括打包所用工具css、js文件注入,js混淆加密等)
- 后端管理系统(前端)(vue2+ant-design-vue+vue全家桶+rrweb+loadsh+store等)
- 后端管理系统(后端)(tegg框架相关+ali-oss+mysql+egg-redis+pako+puppeteer+rrweb+rrweb-player等)
- 项目管理:本项目采用pnpm monorepo微前端管理体系。eslint+pretter+commitlint提交规范配置+stylelint样式规范等前端基础工程化配置。
- 存储:使用mysql数据库+redis缓存数据库+阿里云oss永久存储
- 部署:采用Dockerfile及docker-compose.yml配合docker容器部署
- 发布:使用jenkins代码发布工具+gitee版本仓库
- 服务器: 云服务器
三、 项目开发
1. 录屏sdk
-
sdk应具备的能力:
- 视频文件events的收集上报
- sdk鉴权方式
- sdk与使用方业务关联(例如,绑定订单id,通过订单id查询此视频)
- 业务方需主动触发结束录屏动作
- 需要配置业务web哪个页面需要录屏
- 功能上考虑节流防抖,click、scroll、keyup页面事件操作时的上报策略等
-
sdk服务于sdk探针:
- 本项目使用sdk与sdk服务(node)相互配合达到一行代码注入使用的能力。
- js混淆加密使项目更加安全。
2. 后端管理系统
- 功能模块:
- 权限管理:菜单管理、角色管理、用户管理
- 可回溯管理:商户配置、录屏配置、视频列表、视频播放、sdk版本管理
- 后端核心
项目采用tegg微服务方式,权限模块,公共模块,录屏模块,html模版view模块
最最核心是如何存储视频文件数据,思想和方法论是最关键的,前端只要用户操作每隔1s调一次接口,然后后端保存缓存文件。
本项目采取三段式分片定时任务处理数据存储,合并和压缩上传功能。
在不频繁处理数据库操作的基础思想下,定时任务分批处理。
这个地方,想了比较久,甚至过年开车回家,在路上就一直在想方案,也没想出来,年后回来,在某一刻想通了哈哈哈。就是这么神奇
贴一段核心代码
js
/**
** @Author: wangke
** @Date: 2024/02/08 20:03:59
** @Description: 查询videoProgress=2时 将 screenRecordVideos 视频txt文件压缩归档,场景就是,用户在订单完成后,迟迟没有支付,或者,很长时间才到达结束页,这时,结束页要再次归档
*/
async scheduleVideoFileZip(searchParams, type: number) {
const videoPath = this.config.recordEventDir + Constant.SCREEN_RECORD_VIDEO_DIR
const fileArr = zipFileFunc(videoPath)
setTimeout(async () => {
const deviceIdArr = []
if (!fileArr.length) return false
for (let i = 0; i < fileArr.length; i++) {
deviceIdArr.push(fileArr[i].split('.')[0] as never)
}
// 查找数据,然后将zip文件归档云存储
searchParams = { deviceId: deviceIdArr, ...searchParams } // 当数据库里有设备id时才存到oss上,避免脏数据归档
const videoList = await this.VideoListRepository.queryAll(searchParams)
if (videoList.length <= 0) return
for (let i = 0; i < videoList.length; i++) {
const txtFileName = videoList[i].deviceId + this.config.video_file_suffix
const zipFileName = videoList[i].deviceId + this.config.video_zip_file_suffix
const txtFileInfo = this.config.recordEventDir + Constant.SCREEN_RECORD_VIDEO_DIR + txtFileName
const fileInfo = this.config.recordEventDir + Constant.SCREEN_RECORD_VIDEO_DIR + zipFileName
if (await isExistFileFunc(txtFileInfo)) {
await Oss.ossPut('zip/' + dateFileDirFunc() + zipFileName, fileInfo)
const videoSize = getFileSize(fileInfo)
let videoParams = {
videoSize,
videoStatus: Constant.VIDEO_HAVE_MAKE,
videoUrl: 'zip/' + dateFileDirFunc() + zipFileName,
// videoUrl: ossRes.url
videoProgress: ''
}
if (type === 2) {
videoParams = { ...videoParams, videoProgress: '3' }
}
const updateRes = await this.VideoListRepository.update(videoList[i].deviceId, videoParams)
if (updateRes) {
fsync.unlinkSync(fileInfo)
type === 2 && fsync.unlinkSync(txtFileInfo)
}
}
}
}, 1000)
}
四、 项目打包
- sdk通过vite打包成可插拔式的插件,并且进行加密处理
- 时光机后管系统前端用vue-cli脚手架打包
- 后管的服务端使用egg eggctl
- 每个单独项目使用docker 部署
五、项目部署
- 云服务器