Files
sionrui/openspec/proposals/video-cover-optimization.md
sion123 27d1c53b49 feat(material): remove video cover extraction and simplify upload API
- 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
2026-03-04 22:37:31 +08:00

12 KiB
Raw Blame History

视频封面加载优化方案

问题背景

当前问题

/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 行):

// 视频文件不生成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 清理历史数据(可选)

风险与应对

风险 1OSS 截帧计费

风险:阿里云 OSS 视频截帧会产生少量费用 应对:费用极低(每千次约 0.025 元),可接受

风险 2截帧失败

风险:某些视频格式可能截帧失败 应对:前端增加默认封面兜底显示

风险 3历史数据

风险:历史数据有 coverBase64 但无 fileUrl 应对:历史数据保留,查询时优先使用 fileUrl 截帧


回滚方案

如果 OSS 截帧方案出现问题,可快速回滚:

  1. 恢复 coverBase64 参数
  2. 恢复 handleCoverUpload 方法
  3. 分页查询返回 coverBase64

验收标准

功能验收

  • 视频上传成功,不传 coverBase64
  • 分页接口返回 previewUrlOSS 截帧 URL
  • 分页接口不返回 coverBase64
  • 前端正常显示视频封面

性能验收

  • 分页接口响应体 < 100KB
  • 分页接口响应时间 < 1 秒
  • 封面图片正常加载

文档版本v1.1 更新日期2026-03-04