feat: 功能优化
This commit is contained in:
@@ -65,6 +65,13 @@ public class AppTikFileGroupController {
|
||||
return success(fileGroupService.getGroupList());
|
||||
}
|
||||
|
||||
@GetMapping("/list-by-category/{category}")
|
||||
@Operation(summary = "按分类查询分组列表")
|
||||
@Parameter(name = "category", description = "分类", required = true, example = "MIX")
|
||||
public CommonResult<List<AppTikFileGroupRespVO>> getGroupListByCategory(@PathVariable String category) {
|
||||
return success(fileGroupService.getGroupListByCategory(category));
|
||||
}
|
||||
|
||||
@PostMapping("/add-files")
|
||||
@Operation(summary = "将文件添加到分组")
|
||||
public CommonResult<Boolean> addFilesToGroups(@Valid @RequestBody AppTikFileGroupAddFilesReqVO reqVO) {
|
||||
|
||||
@@ -45,8 +45,10 @@ public class AppTikUserFileController {
|
||||
@Parameter(description = "视频封面 base64(可选,data URI 格式)")
|
||||
@RequestParam(value = "coverBase64", required = false) String coverBase64,
|
||||
@Parameter(description = "视频时长(秒)")
|
||||
@RequestParam(value = "duration", required = false) Integer duration) {
|
||||
return success(userFileService.uploadFile(file, fileCategory, coverBase64, duration));
|
||||
@RequestParam(value = "duration", required = false) Integer duration,
|
||||
@Parameter(description = "分组编号(可选)")
|
||||
@RequestParam(value = "groupId", required = false) Long groupId) {
|
||||
return success(userFileService.uploadFile(file, fileCategory, coverBase64, duration, groupId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@@ -91,5 +93,25 @@ public class AppTikUserFileController {
|
||||
return success(userFileService.getPreviewUrl(fileId, type));
|
||||
}
|
||||
|
||||
@GetMapping("/presigned-url")
|
||||
@Operation(summary = "获取预签名URL")
|
||||
@Parameter(name = "fileName", description = "文件名", required = true, example = "video.mp4")
|
||||
@Parameter(name = "fileCategory", description = "文件分类(video/audio/image)", required = true, example = "video")
|
||||
@Parameter(name = "groupId", description = "分组编号(可选)")
|
||||
@Parameter(name = "fileSize", description = "文件大小(字节)")
|
||||
public CommonResult<Object> getPresignedUrl(
|
||||
@RequestParam("fileName") String fileName,
|
||||
@RequestParam("fileCategory") String fileCategory,
|
||||
@RequestParam(value = "groupId", required = false) Long groupId,
|
||||
@RequestParam(value = "fileSize", required = false) Long fileSize) {
|
||||
return success(userFileService.getPresignedUrl(fileName, fileCategory, groupId, fileSize));
|
||||
}
|
||||
|
||||
@PostMapping("/complete-upload")
|
||||
@Operation(summary = "确认上传")
|
||||
public CommonResult<Object> completeUpload(@RequestBody Object request) {
|
||||
return success(userFileService.completeUpload(request));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ public class TikFileGroupDO extends TenantBaseDO {
|
||||
* 分组描述
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 分组分类
|
||||
*/
|
||||
private String category;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
|
||||
@@ -42,5 +42,13 @@ public interface TikFileGroupService {
|
||||
*/
|
||||
List<AppTikFileGroupRespVO> getGroupList();
|
||||
|
||||
/**
|
||||
* 按分类查询分组列表
|
||||
*
|
||||
* @param category 分类:MIX 或 DIGITAL_HUMAN
|
||||
* @return 分组列表
|
||||
*/
|
||||
List<AppTikFileGroupRespVO> getGroupListByCategory(String category);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +56,16 @@ public class TikFileGroupServiceImpl implements TikFileGroupService {
|
||||
// 创建分组
|
||||
TikFileGroupDO group = BeanUtils.toBean(createReqVO, TikFileGroupDO.class)
|
||||
.setUserId(userId);
|
||||
// 设置默认值
|
||||
if (group.getSort() == null) {
|
||||
group.setSort(0);
|
||||
}
|
||||
if (group.getParentId() == null) {
|
||||
group.setParentId(0L);
|
||||
}
|
||||
if (group.getCategory() == null || group.getCategory().isEmpty()) {
|
||||
group.setCategory("MIX"); // 默认分类
|
||||
}
|
||||
fileGroupMapper.insert(group);
|
||||
|
||||
log.info("[createGroup][用户({})创建分组成功,分组编号({})]", userId, group.getId());
|
||||
@@ -130,6 +134,35 @@ public class TikFileGroupServiceImpl implements TikFileGroupService {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppTikFileGroupRespVO> getGroupListByCategory(String category) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
|
||||
List<TikFileGroupDO> groups = fileGroupMapper.selectList(
|
||||
new LambdaQueryWrapperX<TikFileGroupDO>()
|
||||
.eq(TikFileGroupDO::getUserId, userId)
|
||||
.eq(TikFileGroupDO::getTenantId, tenantId)
|
||||
.eq(TikFileGroupDO::getCategory, category)
|
||||
.eq(TikFileGroupDO::getDeleted, false)
|
||||
.orderByAsc(TikFileGroupDO::getSort)
|
||||
.orderByDesc(TikFileGroupDO::getCreateTime)
|
||||
);
|
||||
|
||||
// 转换为VO并查询文件数量
|
||||
return CollectionUtils.convertList(groups, group -> {
|
||||
AppTikFileGroupRespVO vo = BeanUtils.toBean(group, AppTikFileGroupRespVO.class);
|
||||
// 查询分组下的文件数量(使用tik_user_file表的groupId字段)
|
||||
Long fileCount = userFileMapper.selectCount(
|
||||
new LambdaQueryWrapperX<TikUserFileDO>()
|
||||
.eq(TikUserFileDO::getUserId, userId)
|
||||
.eq(TikUserFileDO::getGroupId, group.getId())
|
||||
);
|
||||
vo.setFileCount(fileCount != null ? fileCount : 0L);
|
||||
return vo;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验分组存在且属于当前用户
|
||||
*/
|
||||
|
||||
@@ -42,5 +42,17 @@ public interface TikOssInitService {
|
||||
*/
|
||||
String getOssDirectoryByCategory(Long userId, String fileCategory);
|
||||
|
||||
/**
|
||||
* 基于分类和分组获取OSS目录路径
|
||||
* 新路径格式:/user-files/{category}/{date}/{groupName}/
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param category 分类:MIX 或 DIGITAL_HUMAN
|
||||
* @param groupName 分组名称
|
||||
* @param dateStr 日期字符串(YYYY-MM-DD格式)
|
||||
* @return OSS目录路径
|
||||
*/
|
||||
String getOssDirectoryByCategoryAndGroup(Long userId, String category, String groupName, String dateStr);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -180,5 +180,48 @@ public class TikOssInitServiceImpl implements TikOssInitService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于分类和分组获取OSS目录路径
|
||||
* 新路径格式:/user-files/{category}/{date}/{groupName}/
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param category 分类:MIX 或 DIGITAL_HUMAN
|
||||
* @param groupName 分组名称
|
||||
* @param dateStr 日期字符串(YYYY-MM-DD格式)
|
||||
* @return OSS目录路径
|
||||
*/
|
||||
public String getOssDirectoryByCategoryAndGroup(Long userId, String category, String groupName, String dateStr) {
|
||||
// 确保OSS已初始化
|
||||
ensureOssInitialized(userId);
|
||||
|
||||
// 构建新格式的路径
|
||||
// 路径格式:{mobileMd5}/{tenantId}/user-files/{category}/{date}/{groupName}
|
||||
TikUserOssInitDO ossInit = ossInitMapper.selectByUserId(userId);
|
||||
if (ossInit == null || ossInit.getInitStatus() == 0) {
|
||||
throw exception(OSS_INIT_FAILED);
|
||||
}
|
||||
|
||||
// 获取基础路径(去掉tenantId部分)
|
||||
String basePath = ossInit.getMobileMd5();
|
||||
|
||||
// 构建完整路径
|
||||
StringBuilder pathBuilder = new StringBuilder();
|
||||
pathBuilder.append(basePath)
|
||||
.append("/user-files/")
|
||||
.append(category.toLowerCase())
|
||||
.append("/")
|
||||
.append(dateStr);
|
||||
|
||||
// 如果有分组名,添加到路径
|
||||
if (groupName != null && !groupName.trim().isEmpty()) {
|
||||
// 对分组名进行URL安全处理
|
||||
String safeGroupName = groupName.trim()
|
||||
.replaceAll("[^a-zA-Z0-9一-鿿_-]", "_"); // 保留中文、字母、数字、下划线和连字符
|
||||
pathBuilder.append("/").append(safeGroupName);
|
||||
}
|
||||
|
||||
return pathBuilder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,10 @@ public interface TikUserFileService {
|
||||
* @param fileCategory 文件分类(video/generate/audio/mix/voice)
|
||||
* @param coverBase64 视频封面 base64(可选,data URI 格式)
|
||||
* @param duration 视频时长(秒,可选)
|
||||
* @param groupId 分组编号(可选)
|
||||
* @return 文件编号
|
||||
*/
|
||||
Long uploadFile(MultipartFile file, String fileCategory, String coverBase64, Integer duration);
|
||||
Long uploadFile(MultipartFile file, String fileCategory, String coverBase64, Integer duration, Long groupId);
|
||||
|
||||
/**
|
||||
* 分页查询文件列表
|
||||
@@ -74,5 +75,24 @@ public interface TikUserFileService {
|
||||
*/
|
||||
String getCachedPresignUrl(String url, Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 获取预签名URL(直传模式)
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @param fileCategory 文件分类
|
||||
* @param groupId 分组编号(可选)
|
||||
* @param fileSize 文件大小(可选)
|
||||
* @return 预签名URL信息
|
||||
*/
|
||||
Object getPresignedUrl(String fileName, String fileCategory, Long groupId, Long fileSize);
|
||||
|
||||
/**
|
||||
* 确认上传(直传模式)
|
||||
*
|
||||
* @param request 上传确认请求
|
||||
* @return 上传结果
|
||||
*/
|
||||
Object completeUpload(Object request);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
|
||||
@@ -54,6 +55,21 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
/** 预签名URL过期时间(24小时,单位:秒) */
|
||||
private static final int PRESIGN_URL_EXPIRATION_SECONDS = 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* 上传文件的前缀,是否包含日期
|
||||
*
|
||||
* 目的:按照日期,进行分目录
|
||||
*/
|
||||
private static boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||
|
||||
/**
|
||||
* 上传文件的后缀,是否包含时间戳
|
||||
*
|
||||
* 目的:保证文件的唯一性,避免覆盖
|
||||
* 定制:可按需调整成 UUID、或者其他方式
|
||||
*/
|
||||
private static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
@Resource
|
||||
private TikUserFileMapper userFileMapper;
|
||||
|
||||
@@ -73,39 +89,75 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
private FileConfigService fileConfigService;
|
||||
|
||||
@Override
|
||||
public Long uploadFile(MultipartFile file, String fileCategory, String coverBase64, Integer duration) {
|
||||
public Long uploadFile(MultipartFile file, String fileCategory, String coverBase64, Integer duration, Long groupId) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
|
||||
// ========== 第一阶段:校验和准备(不在事务中) ==========
|
||||
// ========== 第一阶段:校验和准备 ==========
|
||||
// 1. 校验文件分类和配额
|
||||
validateUploadRequest(fileCategory, file.getSize());
|
||||
|
||||
// 2. 获取OSS基础目录路径
|
||||
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, fileCategory);
|
||||
|
||||
// ========== 第二阶段:处理文件和上传 ==========
|
||||
UploadFileContext context = processFileAndUpload(file, baseDirectory);
|
||||
|
||||
// ========== 第三阶段:保存数据库 ==========
|
||||
try {
|
||||
return saveFileRecord(userId, file, fileCategory, context.fileUrl, context.filePath,
|
||||
coverBase64, baseDirectory, context.infraFileId, duration, groupId);
|
||||
} catch (Exception e) {
|
||||
log.error("[uploadFile][保存数据库失败]", e);
|
||||
deleteOssFile(context.infraFileId, context.filePath, context.fileUrl);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传上下文
|
||||
*/
|
||||
private static class UploadFileContext {
|
||||
String fileUrl;
|
||||
String filePath;
|
||||
Long infraFileId;
|
||||
String fileName;
|
||||
String fileType;
|
||||
Long fileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一校验上传请求
|
||||
*/
|
||||
private void validateUploadRequest(String fileCategory, Long fileSize) {
|
||||
// 1. 校验文件分类
|
||||
if (TikFileCategoryEnum.valueOfValue(fileCategory) == null) {
|
||||
throw exception(FILE_CATEGORY_INVALID);
|
||||
}
|
||||
|
||||
// 2. 校验配额(只校验,不更新)
|
||||
quotaService.validateStorage(userId, file.getSize());
|
||||
// 2. 校验配额
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (fileSize != null && fileSize > 0) {
|
||||
quotaService.validateStorage(userId, fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 获取OSS基础目录路径(不包含日期和文件名)
|
||||
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, fileCategory);
|
||||
/**
|
||||
* 统一处理文件和上传逻辑
|
||||
*/
|
||||
private UploadFileContext processFileAndUpload(MultipartFile file, String baseDirectory) {
|
||||
UploadFileContext context = new UploadFileContext();
|
||||
|
||||
// 4. 读取文件内容(只读取一次,避免重复读取)
|
||||
// 1. 读取文件内容
|
||||
byte[] fileContent;
|
||||
try {
|
||||
fileContent = IoUtil.readBytes(file.getInputStream());
|
||||
} catch (Exception e) {
|
||||
log.error("[uploadFile][文件读取失败]", e);
|
||||
log.error("[processFileAndUpload][文件读取失败]", e);
|
||||
throw exception(FILE_NOT_EXISTS, "文件读取失败");
|
||||
}
|
||||
|
||||
// ========== 第二阶段:上传到OSS并保存文件记录(不在事务中,优先执行) ==========
|
||||
// 采用业界成熟方案:直接使用 fileMapper.insert() 获取文件ID,避免通过 URL 查询
|
||||
String fileUrl;
|
||||
String filePath;
|
||||
Long infraFileId;
|
||||
|
||||
try {
|
||||
// 1. 处理文件名和类型
|
||||
// 2. 处理文件名和类型
|
||||
String fileName = file.getOriginalFilename();
|
||||
String fileType = file.getContentType();
|
||||
if (StrUtil.isEmpty(fileType)) {
|
||||
@@ -121,18 +173,16 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 生成上传路径(与 FileService 保持一致)
|
||||
filePath = generateUploadPath(fileName, baseDirectory);
|
||||
// 3. 生成上传路径
|
||||
String filePath = generateUploadPath(fileName, baseDirectory);
|
||||
|
||||
// 3. 上传到OSS
|
||||
// 4. 上传到OSS
|
||||
FileClient client = fileConfigService.getMasterFileClient();
|
||||
Assert.notNull(client, "客户端(master) 不能为空");
|
||||
String presignedUrl = client.upload(fileContent, filePath, fileType);
|
||||
|
||||
// 3.1 移除预签名URL中的签名参数,获取基础URL(用于存储)
|
||||
fileUrl = HttpUtils.removeUrlQuery(presignedUrl);
|
||||
|
||||
// 4. 保存到 infra_file 表,直接获取文件ID(MyBatis Plus 会自动填充自增ID)
|
||||
// 5. 保存到 infra_file 表
|
||||
String fileUrl = HttpUtils.removeUrlQuery(presignedUrl);
|
||||
FileDO infraFile = new FileDO()
|
||||
.setConfigId(client.getId())
|
||||
.setName(fileName)
|
||||
@@ -141,23 +191,45 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
.setType(fileType)
|
||||
.setSize((int) file.getSize());
|
||||
fileMapper.insert(infraFile);
|
||||
infraFileId = infraFile.getId(); // MyBatis Plus 会自动填充自增ID
|
||||
|
||||
log.info("[uploadFile][文件上传成功,文件编号({})]", infraFileId);
|
||||
// 6. 设置上下文
|
||||
context.fileUrl = fileUrl;
|
||||
context.filePath = filePath;
|
||||
context.infraFileId = infraFile.getId();
|
||||
context.fileName = fileName;
|
||||
context.fileType = fileType;
|
||||
context.fileSize = file.getSize();
|
||||
|
||||
log.info("[processFileAndUpload][文件上传成功,文件编号({})]", context.infraFileId);
|
||||
return context;
|
||||
} catch (Exception e) {
|
||||
log.error("[uploadFile][上传OSS失败]", e);
|
||||
log.error("[processFileAndUpload][上传OSS失败]", e);
|
||||
throw exception(FILE_NOT_EXISTS, "上传OSS失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 第三阶段:保存数据库(在事务中,如果失败则删除OSS文件) ==========
|
||||
try {
|
||||
return saveFileRecord(userId, file, fileCategory, fileUrl, filePath, coverBase64, baseDirectory, infraFileId, duration);
|
||||
} catch (Exception e) {
|
||||
// 数据库保存失败,删除已上传的OSS文件
|
||||
log.error("[uploadFile][保存数据库失败]", e);
|
||||
deleteOssFile(infraFileId, filePath, fileUrl);
|
||||
throw e; // 重新抛出异常
|
||||
/**
|
||||
* 统一处理视频封面上传
|
||||
*/
|
||||
private String handleCoverUpload(String coverBase64, String fileName, String fileType, String baseDirectory) {
|
||||
if (StrUtil.isBlank(coverBase64) || !StrUtil.containsIgnoreCase(fileType, "video")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String base64Data = coverBase64.contains(",")
|
||||
? coverBase64.substring(coverBase64.indexOf(",") + 1)
|
||||
: coverBase64;
|
||||
byte[] coverBytes = java.util.Base64.getDecoder().decode(base64Data);
|
||||
String coverFileName = fileName.replaceFirst("\\.[^.]+$", "_cover.jpg");
|
||||
String uploadedUrl = fileApi.createFile(coverBytes, coverFileName, baseDirectory, "image/jpeg");
|
||||
if (StrUtil.isNotBlank(uploadedUrl) && !uploadedUrl.contains("data:image")) {
|
||||
return HttpUtils.removeUrlQuery(uploadedUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[handleCoverUpload][视频封面处理失败: {}]", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,60 +237,17 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long saveFileRecord(Long userId, MultipartFile file, String fileCategory,
|
||||
String fileUrl, String filePath, String coverBase64, String baseDirectory, Long infraFileId, Integer duration) {
|
||||
// 7. 验证 infraFileId 不为空(必须在保存记录之前检查)
|
||||
String fileUrl, String filePath, String coverBase64, String baseDirectory, Long infraFileId, Integer duration, Long groupId) {
|
||||
// 验证 infraFileId 不为空
|
||||
if (infraFileId == null) {
|
||||
log.error("[saveFileRecord][infra_file.id 为空,无法保存文件记录,用户({}),URL({})]", userId, fileUrl);
|
||||
throw exception(FILE_NOT_EXISTS, "文件记录保存失败:无法获取文件ID");
|
||||
}
|
||||
|
||||
// 8. 处理视频封面(如果有前端传递的 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")) {
|
||||
// 移除预签名URL中的签名参数,获取基础URL(用于存储)
|
||||
coverUrl = HttpUtils.removeUrlQuery(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);
|
||||
// 封面处理失败不影响主流程,继续保存文件记录
|
||||
}
|
||||
}
|
||||
// 处理视频封面
|
||||
String fileName = file.getOriginalFilename();
|
||||
String fileType = file.getContentType();
|
||||
String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory);
|
||||
|
||||
// 9. 创建文件记录(保存完整路径,包含封面URL和Base64)
|
||||
TikUserFileDO userFile = new TikUserFileDO()
|
||||
@@ -232,7 +261,8 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
.setFilePath(filePath) // 保存完整的OSS路径(由FileService生成)
|
||||
.setCoverUrl(coverUrl) // 设置封面URL(如果有)
|
||||
.setCoverBase64(StrUtil.isNotBlank(coverBase64) ? coverBase64 : null) // 保存原始base64数据(如果有)
|
||||
.setDuration(duration); // 设置视频时长(如果有)
|
||||
.setDuration(duration) // 设置视频时长(如果有)
|
||||
.setGroupId(groupId); // 设置分组编号(如果有)
|
||||
|
||||
userFileMapper.insert(userFile);
|
||||
|
||||
@@ -274,9 +304,6 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
private String generateUploadPath(String name, String directory) {
|
||||
// 1. 生成前缀、后缀
|
||||
String prefix = null;
|
||||
boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||
boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
if (PATH_PREFIX_DATE_ENABLE) {
|
||||
prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
|
||||
}
|
||||
@@ -530,7 +557,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
|
||||
/**
|
||||
* 从URL中提取文件路径
|
||||
*
|
||||
*
|
||||
* @param url 文件URL
|
||||
* @return 文件路径
|
||||
*/
|
||||
@@ -544,7 +571,7 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
if (url.contains("?")) {
|
||||
cleanUrl = url.substring(0, url.indexOf("?"));
|
||||
}
|
||||
|
||||
|
||||
// 如果URL包含域名,提取路径部分
|
||||
if (cleanUrl.contains("://")) {
|
||||
int pathStart = cleanUrl.indexOf("/", cleanUrl.indexOf("://") + 3);
|
||||
@@ -568,5 +595,109 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPresignedUrl(String fileName, String fileCategory, Long groupId, Long fileSize) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
|
||||
// 1. 校验请求
|
||||
validateUploadRequest(fileCategory, fileSize);
|
||||
|
||||
// 2. 获取OSS基础目录路径
|
||||
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, fileCategory);
|
||||
|
||||
// 3. 生成文件路径
|
||||
String filePath = generateUploadPath(fileName, baseDirectory);
|
||||
|
||||
// 4. 生成预签名URL
|
||||
FileClient client = fileConfigService.getMasterFileClient();
|
||||
Assert.notNull(client, "客户端(master) 不能为空");
|
||||
|
||||
String presignedUrl = client.presignPutUrl(filePath);
|
||||
String visitUrl = client.presignGetUrl(filePath, null);
|
||||
|
||||
// 5. 构建返回结果
|
||||
return Map.of(
|
||||
"presignedUrl", presignedUrl,
|
||||
"uploadUrl", HttpUtils.removeUrlQuery(visitUrl),
|
||||
"fileKey", filePath,
|
||||
"expiresIn", PRESIGN_URL_EXPIRATION_SECONDS,
|
||||
"method", "PUT",
|
||||
"headers", Map.of(
|
||||
"Content-Type", FileTypeUtils.getMineType(fileName)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object completeUpload(Object request) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
|
||||
// 解析请求参数
|
||||
Map<String, Object> params = (Map<String, Object>) request;
|
||||
String fileKey = (String) params.get("fileKey");
|
||||
String fileName = (String) params.get("fileName");
|
||||
String fileCategory = (String) params.get("fileCategory");
|
||||
Long fileSize = params.get("fileSize") != null ? ((Number) params.get("fileSize")).longValue() : 0L;
|
||||
String fileType = (String) params.get("fileType");
|
||||
Long groupId = params.get("groupId") != null ? ((Number) params.get("groupId")).longValue() : null;
|
||||
String coverBase64 = (String) params.get("coverBase64");
|
||||
Integer duration = params.get("duration") != null ? ((Number) params.get("duration")).intValue() : null;
|
||||
|
||||
// 1. 校验请求
|
||||
validateUploadRequest(fileCategory, fileSize);
|
||||
|
||||
// 2. 获取文件访问URL
|
||||
FileClient client = fileConfigService.getMasterFileClient();
|
||||
Assert.notNull(client, "客户端(master) 不能为空");
|
||||
String fileUrl = client.presignGetUrl(fileKey, null);
|
||||
fileUrl = HttpUtils.removeUrlQuery(fileUrl);
|
||||
|
||||
// 3. 保存到 infra_file 表
|
||||
FileDO infraFile = new FileDO()
|
||||
.setConfigId(client.getId())
|
||||
.setName(fileName)
|
||||
.setPath(fileKey)
|
||||
.setUrl(fileUrl)
|
||||
.setType(fileType)
|
||||
.setSize(fileSize.intValue());
|
||||
fileMapper.insert(infraFile);
|
||||
Long infraFileId = infraFile.getId();
|
||||
|
||||
// 4. 处理视频封面
|
||||
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, fileCategory);
|
||||
String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory);
|
||||
|
||||
// 5. 保存到 tik_user_file 表
|
||||
TikUserFileDO userFile = new TikUserFileDO()
|
||||
.setUserId(userId)
|
||||
.setFileId(infraFileId)
|
||||
.setFileName(fileName)
|
||||
.setFileType(fileType)
|
||||
.setFileCategory(fileCategory)
|
||||
.setFileSize(fileSize)
|
||||
.setFileUrl(fileUrl)
|
||||
.setFilePath(fileKey)
|
||||
.setCoverUrl(coverUrl)
|
||||
.setCoverBase64(coverBase64)
|
||||
.setDuration(duration)
|
||||
.setGroupId(groupId);
|
||||
userFileMapper.insert(userFile);
|
||||
|
||||
// 6. 更新配额
|
||||
quotaService.increaseUsedStorage(userId, fileSize);
|
||||
|
||||
log.info("[completeUpload][用户({})直传上传完成,infraFileId({}),userFileId({})]",
|
||||
userId, infraFileId, userFile.getId());
|
||||
|
||||
// 7. 返回结果
|
||||
return Map.of(
|
||||
"infraFileId", infraFileId,
|
||||
"userFileId", userFile.getId(),
|
||||
"fileUrl", fileUrl
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ public class AppTikFileGroupCreateReqVO {
|
||||
@Schema(description = "分组描述", example = "存放常用素材")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "分组分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "MIX")
|
||||
@NotBlank(message = "分组分类不能为空")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ public class AppTikFileGroupRespVO {
|
||||
@Schema(description = "分组描述", example = "存放常用素材")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "分组分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "MIX")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer sort;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user