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

426 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 视频封面加载优化方案
## 问题背景
### 当前问题
`/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<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));
}
```
**修改后**
```java
@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`
**修改前**
```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` 表 | 清理历史数据(可选) |
---
## 风险与应对
### 风险 1OSS 截帧计费
**风险**:阿里云 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*