feat: 优化

This commit is contained in:
2025-11-16 22:59:26 +08:00
parent 1540e44288
commit bd2e8b3291
10 changed files with 550 additions and 178 deletions

View File

@@ -41,8 +41,10 @@ public class AppTikUserFileController {
public CommonResult<Long> uploadFile(
@RequestParam("file") MultipartFile file,
@Parameter(description = "文件分类video/generate/audio/mix/voice", required = true)
@RequestParam("fileCategory") String fileCategory) {
return success(userFileService.uploadFile(file, fileCategory));
@RequestParam("fileCategory") String fileCategory,
@Parameter(description = "视频封面 base64可选data URI 格式)")
@RequestParam(value = "coverBase64", required = false) String coverBase64) {
return success(userFileService.uploadFile(file, fileCategory, coverBase64));
}
@GetMapping("/page")

View File

@@ -59,14 +59,14 @@ public class TikUserFileDO extends TenantBaseDO {
* 文件存储路径
*/
private String filePath;
/**
* OSS根路径用户手机MD5
*/
private String ossRootPath;
/**
* 封面图URL视频文件的封面图
*/
private String coverUrl;
/**
* 封面图Base64视频文件的封面图原始base64数据可选
*/
private String coverBase64;
/**
* 缩略图URL图片文件的缩略图
*/

View File

@@ -19,9 +19,10 @@ public interface TikUserFileService {
*
* @param file 文件
* @param fileCategory 文件分类video/generate/audio/mix/voice
* @param coverBase64 视频封面 base64可选data URI 格式)
* @return 文件编号
*/
Long uploadFile(MultipartFile file, String fileCategory);
Long uploadFile(MultipartFile file, String fileCategory, String coverBase64);
/**
* 分页查询文件列表

View File

@@ -42,6 +42,9 @@ import static cn.iocoder.yudao.module.tik.enmus.ErrorCodeConstants.*;
@Slf4j
public class TikUserFileServiceImpl implements TikUserFileService {
/** 预签名URL过期时间1小时单位 */
private static final int PRESIGN_URL_EXPIRATION_SECONDS = 3600;
@Resource
private TikUserFileMapper userFileMapper;
@@ -58,7 +61,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
private FileMapper fileMapper;
@Override
public Long uploadFile(MultipartFile file, String fileCategory) {
public Long uploadFile(MultipartFile file, String fileCategory, String coverBase64) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
Long tenantId = TenantContextHolder.getTenantId();
@@ -112,7 +115,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
// ========== 第三阶段保存数据库在事务中如果失败则删除OSS文件 ==========
try {
return saveFileRecord(userId, file, fileCategory, fileUrl, filePath);
return saveFileRecord(userId, file, fileCategory, fileUrl, filePath, coverBase64, baseDirectory);
} catch (Exception e) {
// 数据库保存失败删除已上传的OSS文件
log.error("[uploadFile][保存数据库失败准备删除OSS文件URL({})]", fileUrl, e);
@@ -126,11 +129,55 @@ public class TikUserFileServiceImpl implements TikUserFileService {
*/
@Transactional(rollbackFor = Exception.class)
public Long saveFileRecord(Long userId, MultipartFile file, String fileCategory,
String fileUrl, String filePath) {
// 7. 获取OSS根路径
String ossRootPath = ossInitService.getOssRootPath(userId);
String fileUrl, String filePath, String coverBase64, String baseDirectory) {
// 7. 处理视频封面(如果有前端传递的 base64 封面,先处理封面再插入主记录)
String coverUrl = null;
if (StrUtil.isNotBlank(coverBase64) && StrUtil.containsIgnoreCase(file.getContentType(), "video")) {
try {
// 解析 base64格式data:image/jpeg;base64,/9j/4AAQ...
String base64Data = coverBase64;
if (base64Data.contains(",")) {
base64Data = base64Data.substring(base64Data.indexOf(",") + 1);
}
// 验证 base64 数据不为空
if (StrUtil.isBlank(base64Data)) {
log.warn("[saveFileRecord][视频封面 base64 数据为空,跳过封面处理]");
} else {
// 解码 base64
byte[] coverBytes = java.util.Base64.getDecoder().decode(base64Data);
// 验证解码后的数据不为空
if (coverBytes == null || coverBytes.length == 0) {
log.warn("[saveFileRecord][视频封面 base64 解码后数据为空,跳过封面处理]");
} else {
// 生成封面文件名原文件名_cover.jpg
String originalName = file.getOriginalFilename();
String coverFileName = originalName != null && originalName.contains(".")
? originalName.replaceFirst("\\.[^.]+$", "_cover.jpg")
: "cover.jpg";
// 上传封面到 OSS使用相同的目录结构
String uploadedUrl = fileApi.createFile(coverBytes, coverFileName, baseDirectory, "image/jpeg");
// 严格验证:确保返回的是有效的 URL而不是 base64 字符串
if (StrUtil.isNotBlank(uploadedUrl) && !uploadedUrl.equals(coverBase64) && !uploadedUrl.contains("data:image")) {
coverUrl = uploadedUrl;
log.info("[saveFileRecord][视频封面上传成功封面URL({})]", coverUrl);
} else {
log.error("[saveFileRecord][视频封面上传返回无效URL跳过保存封面。返回URL: {}", uploadedUrl);
}
}
}
} catch (IllegalArgumentException e) {
log.warn("[saveFileRecord][视频封面 base64 解析失败,格式错误: {}]", e.getMessage());
} catch (Exception e) {
log.error("[saveFileRecord][视频封面上传失败,错误信息: {}]", e.getMessage(), e);
// 封面处理失败不影响主流程,继续保存文件记录
}
}
// 8. 创建文件记录(保存完整路径)
// 8. 创建文件记录(保存完整路径包含封面URL和Base64
TikUserFileDO userFile = new TikUserFileDO()
.setUserId(userId)
.setFileId(null) // 显式设置为nullfile_id是可选的用于关联infra_file表
@@ -140,20 +187,11 @@ public class TikUserFileServiceImpl implements TikUserFileService {
.setFileSize(file.getSize())
.setFileUrl(fileUrl)
.setFilePath(filePath) // 保存完整的OSS路径由FileService生成
.setOssRootPath(ossRootPath);
.setCoverUrl(coverUrl) // 设置封面URL如果有
.setCoverBase64(StrUtil.isNotBlank(coverBase64) ? coverBase64 : null); // 保存原始base64数据如果有
userFileMapper.insert(userFile);
// 9. 异步生成预览图(视频封面或图片缩略图)
// TODO: 后续实现视频封面和图片缩略图生成
// if (StrUtil.containsIgnoreCase(file.getContentType(), "video")) {
// generateVideoCoverAsync(userFile.getId(), fileContent, file.getOriginalFilename(),
// file.getContentType(), baseDirectory);
// } else if (FileTypeUtils.isImage(file.getContentType())) {
// generateImageThumbnailAsync(userFile.getId(), fileContent, file.getOriginalFilename(),
// file.getContentType(), baseDirectory);
// }
// 10. 更新配额
// 9. 更新配额
quotaService.increaseUsedStorage(userId, file.getSize());
log.info("[saveFileRecord][用户({})保存文件记录成功,文件编号({})]", userId, userFile.getId());
@@ -229,15 +267,30 @@ public class TikUserFileServiceImpl implements TikUserFileService {
vo.setIsVideo(isVideo);
vo.setIsImage(isImage);
// 生成封面和缩略图的预签名URL避免重复调用
String coverUrlPresigned = StrUtil.isNotBlank(file.getCoverUrl())
? getCachedPresignUrl(file.getCoverUrl(), PRESIGN_URL_EXPIRATION_SECONDS)
: null;
String thumbnailUrlPresigned = StrUtil.isNotBlank(file.getThumbnailUrl())
? getCachedPresignUrl(file.getThumbnailUrl(), PRESIGN_URL_EXPIRATION_SECONDS)
: null;
// 设置封面和缩略图URL
vo.setCoverUrl(coverUrlPresigned);
vo.setThumbnailUrl(thumbnailUrlPresigned);
// 生成预览URL优先使用封面/缩略图否则使用原文件URL
String previewUrl = null;
if (isVideo && StrUtil.isNotBlank(file.getCoverUrl())) {
previewUrl = getCachedPresignUrl(file.getCoverUrl(), 3600);
} else if (isImage && StrUtil.isNotBlank(file.getThumbnailUrl())) {
previewUrl = getCachedPresignUrl(file.getThumbnailUrl(), 3600);
if (isVideo) {
// 视频优先使用封面没有封面时使用原视频URL
previewUrl = coverUrlPresigned != null
? coverUrlPresigned
: getCachedPresignUrl(file.getFileUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
} else if (isImage) {
// 图片没有缩略图时使用原图
previewUrl = getCachedPresignUrl(file.getFileUrl(), 3600);
// 图片:优先使用缩略图,没有缩略图时使用原图
previewUrl = thumbnailUrlPresigned != null
? thumbnailUrlPresigned
: getCachedPresignUrl(file.getFileUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
}
vo.setPreviewUrl(previewUrl);
@@ -318,7 +371,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
}
// 生成预签名URL1小时有效期
return getCachedPresignUrl(file.getFileUrl(), 3600);
return getCachedPresignUrl(file.getFileUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
}
@Override
@@ -337,7 +390,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
}
// 生成预签名URL1小时有效期
return getCachedPresignUrl(file.getFileUrl(), 3600);
return getCachedPresignUrl(file.getFileUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
}
@Override
@@ -351,26 +404,44 @@ public class TikUserFileServiceImpl implements TikUserFileService {
}
// 根据类型返回预览URL
String previewUrl = null;
if (StrUtil.equals(type, "cover") && StrUtil.isNotBlank(file.getCoverUrl())) {
// 视频封面
previewUrl = file.getCoverUrl();
} else if (StrUtil.equals(type, "thumbnail") && StrUtil.isNotBlank(file.getThumbnailUrl())) {
// 图片缩略图
previewUrl = file.getThumbnailUrl();
} else if (FileTypeUtils.isImage(file.getFileType())) {
// 图片没有缩略图时,使用原图
previewUrl = file.getFileUrl();
} else if (StrUtil.containsIgnoreCase(file.getFileType(), "video") && StrUtil.isNotBlank(file.getCoverUrl())) {
// 视频使用封面
previewUrl = file.getCoverUrl();
} else {
// 其他情况返回原文件URL
previewUrl = file.getFileUrl();
}
String previewUrl = determinePreviewUrl(file, type);
// 生成预签名URL1小时有效期
return getCachedPresignUrl(previewUrl, 3600);
return getCachedPresignUrl(previewUrl, PRESIGN_URL_EXPIRATION_SECONDS);
}
/**
* 确定预览URL
*
* @param file 文件对象
* @param type 预览类型cover/thumbnail可选
* @return 预览URL
*/
private String determinePreviewUrl(TikUserFileDO file, String type) {
// 明确指定封面类型
if (StrUtil.equals(type, "cover") && StrUtil.isNotBlank(file.getCoverUrl())) {
return file.getCoverUrl();
}
// 明确指定缩略图类型
if (StrUtil.equals(type, "thumbnail") && StrUtil.isNotBlank(file.getThumbnailUrl())) {
return file.getThumbnailUrl();
}
// 根据文件类型自动选择
boolean isVideo = StrUtil.containsIgnoreCase(file.getFileType(), "video");
boolean isImage = FileTypeUtils.isImage(file.getFileType());
if (isVideo && StrUtil.isNotBlank(file.getCoverUrl())) {
return file.getCoverUrl();
}
if (isImage) {
return StrUtil.isNotBlank(file.getThumbnailUrl())
? file.getThumbnailUrl()
: file.getFileUrl();
}
// 默认返回原文件URL
return file.getFileUrl();
}
@Override

View File

@@ -44,6 +44,9 @@ public class AppTikUserFileRespVO {
@Schema(description = "封面图URL视频文件的封面图")
private String coverUrl;
@Schema(description = "封面图Base64视频文件的封面图原始base64数据可选")
private String coverBase64;
@Schema(description = "缩略图URL图片文件的缩略图")
private String thumbnailUrl;