- Remove extractVideoCoverOptional function and related video cover processing - Update MaterialService.uploadFile method signature to remove coverBase64 parameter - Simplify uploadAndIdentifyVideo function by removing cover generation logic - Remove loading indicator from VideoSelector component during video preview - Add presignGetUrlWithProcess method to FileClient interface for processed file URLs - Add logging support to S3FileClient implementation
12 KiB
视频封面加载优化方案
问题背景
当前问题
/api/tik/file/page 接口加载时间长,主要原因是返回了 coverBase64 字段。
性能影响分析
| 指标 | 当前状态 | 影响 |
|---|---|---|
| 单个视频封面 base64 | 300KB - 2MB | 响应体膨胀 |
| 分页 20 条记录 | 6MB - 40MB | 严重拖慢加载 |
| 数据库存储 | 大文本字段 | 查询性能下降 |
根本原因
- 前端上传时传了
coverBase64 coverBase64直接存数据库,未上传到 OSS- 分页查询返回了完整的 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 行):
// 视频文件不生成OSS预签名URL(前端使用coverBase64缓存)
if (isVideo) {
vo.setCoverUrl(null);
vo.setThumbnailUrl(null);
vo.setPreviewUrl(null);
return vo;
}
修改后:
// 视频文件:使用 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
/**
* 生成阿里云 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 行)
修改前:
String coverBase64 = (String) params.get("coverBase64");
// ...
String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory);
// ...
.setCoverUrl(coverUrl)
.setCoverBase64(coverBase64)
修改后:
// 移除 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 行)
修改前:
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)
修改后:
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 行)
修改前:
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);
}
修改后:
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 行)
修改前:
@PostMapping("/upload")
public CommonResult<Long> 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));
}
修改后:
@PostMapping("/upload")
public CommonResult<Long> 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
修改前:
Long uploadFile(MultipartFile file, String fileCategory, String coverBase64, Integer duration, Long groupId);
修改后:
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 行):
// 修改前
const upload = async (file, options = {}) => {
const {
fileCategory,
groupId = null,
coverBase64 = null, // 删除此行
duration: inputDuration,
// ...
} = options
修改 completeUpload 调用(第 189-198 行):
// 修改前
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
移除导入:
// 删除此行
import { isVideoFile, extractVideoCover } from '@/utils/video-cover'
移除截屏逻辑:
// 删除相关变量和调用
// const fileCoverMap = await extractVideoCovers(videoFiles)
// coverBase64: fileWithCover.coverBase64
2.3 删除 video-cover.ts
文件:frontend/app/web-gold/src/utils/video-cover.ts
整个文件删除,不再需要前端截屏。
阶段 3:数据库清理(可选)
-- 清理历史 coverBase64 数据,释放存储空间
UPDATE tik_user_file
SET cover_base64 = NULL, cover_url = NULL
WHERE file_type LIKE '%video%';
阶段 4:可选 - 移除废弃字段
如果确定不再需要,可以移除以下字段:
TikUserFileDO.java:
// 删除这两个字段
private String coverUrl;
private String coverBase64;
AppTikUserFileRespVO.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 截帧方案出现问题,可快速回滚:
- 恢复
coverBase64参数 - 恢复
handleCoverUpload方法 - 分页查询返回
coverBase64
验收标准
功能验收
- 视频上传成功,不传 coverBase64
- 分页接口返回
previewUrl(OSS 截帧 URL) - 分页接口不返回
coverBase64 - 前端正常显示视频封面
性能验收
- 分页接口响应体 < 100KB
- 分页接口响应时间 < 1 秒
- 封面图片正常加载
文档版本:v1.1 更新日期:2026-03-04