# 视频封面加载优化方案 ## 问题背景 ### 当前问题 `/api/tik/file/page` 接口加载时间长,主要原因是返回了 `coverBase64` 字段。 ### 性能影响分析 | 指标 | 当前状态 | 影响 | |------|---------|------| | 单个视频封面 base64 | 300KB - 2MB | 响应体膨胀 | | 分页 20 条记录 | 6MB - 40MB | 严重拖慢加载 | | 数据库存储 | 大文本字段 | 查询性能下降 | ### 根本原因 1. 前端上传时传了 `coverBase64` 2. `coverBase64` 直接存数据库,未上传到 OSS 3. 分页查询返回了完整的 base64 数据 --- ## 解决方案:阿里云 OSS 视频截帧 ### 方案概述 利用阿里云 OSS 的视频截帧能力,通过 URL 参数直接获取视频封面,无需额外存储。 ### 方案优势 | 维度 | 优势 | |------|------| | **存储成本** | 零额外存储,不存封面图 | | **代码改动** | 小,精简冗余逻辑 | | **维护成本** | 低,OSS 自动处理 | | **性能** | CDN 加速,加载快 | | **兼容性** | 历史数据自动支持 | ### 技术原理 阿里云 OSS 视频截帧 API: ``` https://bucket.oss-cn-hangzhou.aliyuncs.com/video.mp4?x-oss-process=video/snapshot,t_0,f_jpg,w_300,m_fast ``` 参数说明: - `t_0` - 截取第 0 毫秒(第一帧) - `f_jpg` - 输出 JPG 格式 - `w_300` - 宽度 300px(高度自动) - `m_fast` - 快速模式 --- ## 实施计划 ### 改动文件清单 | 文件 | 改动内容 | |------|---------| | `TikUserFileServiceImpl.java` | 修改 3 个方法,删除 1 个方法 | | `AppTikUserFileController.java` | 移除 coverBase64 参数 | | `TikUserFileDO.java` | 可选:移除 coverBase64 字段 | | `AppTikUserFileRespVO.java` | 可选:移除 coverBase64 字段 | | `useUpload.js`(前端) | 移除 coverBase64 参数 | --- ### 阶段 1:后端代码修改 #### 1.1 修改 `getFilePage` 方法(分页查询) **文件**:`TikUserFileServiceImpl.java` **修改前**(第 268-303 行): ```java // 视频文件不生成OSS预签名URL(前端使用coverBase64缓存) if (isVideo) { vo.setCoverUrl(null); vo.setThumbnailUrl(null); vo.setPreviewUrl(null); return vo; } ``` **修改后**: ```java // 视频文件:使用 OSS 截帧,不返回 base64 if (isVideo) { vo.setCoverBase64(null); vo.setCoverUrl(null); vo.setThumbnailUrl(null); // 使用 OSS 视频截帧作为封面 if (StrUtil.isNotBlank(file.getFileUrl())) { String snapshotUrl = generateVideoSnapshotUrl(file.getFileUrl()); vo.setPreviewUrl(snapshotUrl); } return vo; } ``` #### 1.2 新增 `generateVideoSnapshotUrl` 方法 **文件**:`TikUserFileServiceImpl.java` ```java /** * 生成阿里云 OSS 视频截帧 URL * @param videoUrl 视频文件 URL * @return 截帧 URL */ private String generateVideoSnapshotUrl(String videoUrl) { if (StrUtil.isBlank(videoUrl)) { return null; } // 阿里云 OSS 视频截帧参数 return videoUrl + "?x-oss-process=video/snapshot,t_0,f_jpg,w_300,m_fast"; } ``` #### 1.3 修改 `completeUpload` 方法(直传确认) **文件**:`TikUserFileServiceImpl.java`(第 480-542 行) **修改前**: ```java String coverBase64 = (String) params.get("coverBase64"); // ... String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory); // ... .setCoverUrl(coverUrl) .setCoverBase64(coverBase64) ``` **修改后**: ```java // 移除 coverBase64 参数获取 // String coverBase64 = (String) params.get("coverBase64"); // 删除 // 移除 handleCoverUpload 调用 // String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory); // 删除 // 保存记录时不再存储封面相关字段 TikUserFileDO userFile = new TikUserFileDO() .setUserId(userId) .setFileId(infraFileId) .setFileName(fileName) .setDisplayName(displayName) .setFileType(fileType) .setFileCategory(fileCategory) .setFileSize(fileSize) .setFileUrl(fileUrl) .setFilePath(fileKey) // .setCoverUrl(coverUrl) // 删除 // .setCoverBase64(coverBase64) // 删除 .setDuration(duration) .setGroupId(groupId); ``` #### 1.4 修改 `saveFileRecord` 方法(传统上传) **文件**:`TikUserFileServiceImpl.java`(第 205-243 行) **修改前**: ```java public Long saveFileRecord(Long userId, MultipartFile file, String fileCategory, String fileUrl, String filePath, String coverBase64, String baseDirectory, Long infraFileId, Integer duration, Long groupId) { // ... String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory); // ... .setCoverUrl(coverUrl) .setCoverBase64(StrUtil.isNotBlank(coverBase64) ? coverBase64 : null) ``` **修改后**: ```java public Long saveFileRecord(Long userId, MultipartFile file, String fileCategory, String fileUrl, String filePath, String baseDirectory, Long infraFileId, Integer duration, Long groupId) { // 移除 coverBase64 参数 // ... // 移除 handleCoverUpload 调用 // ... TikUserFileDO userFile = new TikUserFileDO() // ... 其他字段保持不变 // .setCoverUrl(coverUrl) // 删除 // .setCoverBase64(coverBase64) // 删除 .setDuration(duration) .setGroupId(groupId); ``` #### 1.5 修改 `uploadFile` 方法入口 **文件**:`TikUserFileServiceImpl.java`(第 76-92 行) **修改前**: ```java public Long uploadFile(MultipartFile file, String fileCategory, String coverBase64, Integer duration, Long groupId) { // ... return saveFileRecord(userId, file, fileCategory, context.fileUrl, context.filePath, coverBase64, baseDirectory, context.infraFileId, duration, groupId); } ``` **修改后**: ```java public Long uploadFile(MultipartFile file, String fileCategory, Integer duration, Long groupId) { // 移除 coverBase64 参数 // ... return saveFileRecord(userId, file, fileCategory, context.fileUrl, context.filePath, baseDirectory, context.infraFileId, duration, groupId); } ``` #### 1.6 删除 `handleCoverUpload` 方法 **文件**:`TikUserFileServiceImpl.java`(第 183-203 行) **直接删除整个方法**,不再需要。 #### 1.7 修改 Controller 接口 **文件**:`AppTikUserFileController.java`(第 40-53 行) **修改前**: ```java @PostMapping("/upload") public CommonResult uploadFile( @RequestParam("file") MultipartFile file, @RequestParam("fileCategory") String fileCategory, @RequestParam(value = "coverBase64", required = false) String coverBase64, @RequestParam(value = "duration", required = false) Integer duration, @RequestParam(value = "groupId", required = false) Long groupId) { return success(userFileService.uploadFile(file, fileCategory, coverBase64, duration, groupId)); } ``` **修改后**: ```java @PostMapping("/upload") public CommonResult uploadFile( @RequestParam("file") MultipartFile file, @RequestParam("fileCategory") String fileCategory, @RequestParam(value = "duration", required = false) Integer duration, @RequestParam(value = "groupId", required = false) Long groupId) { return success(userFileService.uploadFile(file, fileCategory, duration, groupId)); } ``` #### 1.8 修改 Service 接口 **文件**:`TikUserFileService.java` **修改前**: ```java Long uploadFile(MultipartFile file, String fileCategory, String coverBase64, Integer duration, Long groupId); ``` **修改后**: ```java Long uploadFile(MultipartFile file, String fileCategory, Integer duration, Long groupId); ``` --- ### 阶段 2:前端代码修改 #### 2.1 修改 `useUpload.js` **文件**:`frontend/app/web-gold/src/composables/useUpload.js` **修改 upload 方法**(第 131-141 行): ```javascript // 修改前 const upload = async (file, options = {}) => { const { fileCategory, groupId = null, coverBase64 = null, // 删除此行 duration: inputDuration, // ... } = options ``` **修改 completeUpload 调用**(第 189-198 行): ```javascript // 修改前 const completeData = await MaterialService.completeUpload({ fileKey: presignedData.data.fileKey, fileName: file.name, fileCategory, fileSize: file.size, fileType: file.type, groupId, coverBase64, // 删除此行 duration }) ``` #### 2.2 修改 `MaterialUploadModal.vue` **文件**:`frontend/app/web-gold/src/components/material/MaterialUploadModal.vue` **移除导入**: ```javascript // 删除此行 import { isVideoFile, extractVideoCover } from '@/utils/video-cover' ``` **移除截屏逻辑**: ```javascript // 删除相关变量和调用 // const fileCoverMap = await extractVideoCovers(videoFiles) // coverBase64: fileWithCover.coverBase64 ``` #### 2.3 删除 `video-cover.ts` **文件**:`frontend/app/web-gold/src/utils/video-cover.ts` **整个文件删除**,不再需要前端截屏。 --- ### 阶段 3:数据库清理(可选) ```sql -- 清理历史 coverBase64 数据,释放存储空间 UPDATE tik_user_file SET cover_base64 = NULL, cover_url = NULL WHERE file_type LIKE '%video%'; ``` --- ### 阶段 4:可选 - 移除废弃字段 如果确定不再需要,可以移除以下字段: **TikUserFileDO.java**: ```java // 删除这两个字段 private String coverUrl; private String coverBase64; ``` **AppTikUserFileRespVO.java**: ```java // 删除这两个字段 private String coverUrl; private String coverBase64; ``` > ⚠️ 注意:移除字段需要数据库迁移,建议先保留字段但不再使用。 --- ## 性能对比 ### 优化前 ``` 请求: /api/tik/file/page?pageNo=1&pageSize=20 响应体: 10MB - 40MB (含 base64) 加载时间: 5-15 秒 ``` ### 优化后 ``` 请求: /api/tik/file/page?pageNo=1&pageSize=20 响应体: 20KB - 50KB (仅 URL) 加载时间: < 500ms 封面加载: CDN 并行加载,每张 ~30KB ``` ### 提升效果 | 指标 | 优化前 | 优化后 | 提升 | |------|--------|--------|------| | 响应体大小 | 10-40MB | 20-50KB | **99%↓** | | 接口响应时间 | 5-15s | <500ms | **95%↓** | | 数据库存储 | 大文本 | 无 | **100%↓** | --- ## 改动汇总 | 类型 | 文件 | 改动 | |------|------|------| | 后端 | `TikUserFileServiceImpl.java` | 修改 4 个方法,删除 1 个方法 | | 后端 | `TikUserFileService.java` | 修改接口签名 | | 后端 | `AppTikUserFileController.java` | 移除 coverBase64 参数 | | 前端 | `useUpload.js` | 移除 coverBase64 参数 | | 前端 | `MaterialUploadModal.vue` | 移除截屏调用 | | 前端 | `video-cover.ts` | **删除整个文件** | | 数据库 | `tik_user_file` 表 | 清理历史数据(可选) | --- ## 风险与应对 ### 风险 1:OSS 截帧计费 **风险**:阿里云 OSS 视频截帧会产生少量费用 **应对**:费用极低(每千次约 0.025 元),可接受 ### 风险 2:截帧失败 **风险**:某些视频格式可能截帧失败 **应对**:前端增加默认封面兜底显示 ### 风险 3:历史数据 **风险**:历史数据有 coverBase64 但无 fileUrl **应对**:历史数据保留,查询时优先使用 fileUrl 截帧 --- ## 回滚方案 如果 OSS 截帧方案出现问题,可快速回滚: 1. 恢复 `coverBase64` 参数 2. 恢复 `handleCoverUpload` 方法 3. 分页查询返回 `coverBase64` --- ## 验收标准 ### 功能验收 - [ ] 视频上传成功,不传 coverBase64 - [ ] 分页接口返回 `previewUrl`(OSS 截帧 URL) - [ ] 分页接口不返回 `coverBase64` - [ ] 前端正常显示视频封面 ### 性能验收 - [ ] 分页接口响应体 < 100KB - [ ] 分页接口响应时间 < 1 秒 - [ ] 封面图片正常加载 --- *文档版本:v1.1* *更新日期:2026-03-04*