From 159eb835d63b7490692e2c38f025943f8e1bf73a Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Sun, 23 Nov 2025 15:03:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/web-gold/src/views/dh/Video.vue | 6 +- .../service/DigitalHumanTaskServiceImpl.java | 6 + .../service/LatentsyncPollingService.java | 130 ++++++++++++------ 3 files changed, 95 insertions(+), 47 deletions(-) diff --git a/frontend/app/web-gold/src/views/dh/Video.vue b/frontend/app/web-gold/src/views/dh/Video.vue index 5ec3126844..36ecaadc6c 100644 --- a/frontend/app/web-gold/src/views/dh/Video.vue +++ b/frontend/app/web-gold/src/views/dh/Video.vue @@ -513,8 +513,8 @@ const loadLastTask = async () => { currentTaskId.value = lastTaskId // 如果任务是成功状态,显示结果 - if (task.status === 'SUCCESS' && task.videoUrl) { - previewVideoUrl.value = task.videoUrl + if (task.status === 'SUCCESS' && task.resultVideoUrl) { + previewVideoUrl.value = task.resultVideoUrl currentTaskStatus.value = 'SUCCESS' message.success('已自动加载最近一次任务结果') } else if (task.status === 'PROCESSING') { @@ -556,7 +556,7 @@ const startPollingTask = () => { if (task.status === 'SUCCESS') { clearInterval(pollingInterval.value) pollingInterval.value = null - previewVideoUrl.value = task.videoUrl + previewVideoUrl.value = task.resultVideoUrl isGenerating.value = false currentTaskStatus.value = 'SUCCESS' // 保存成功的任务ID diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java index d9f47f6894..dd18b58a65 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/DigitalHumanTaskServiceImpl.java @@ -386,6 +386,12 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService { // 设置当前步骤描述 respVO.setCurrentStepDesc(DigitalHumanTaskStepEnum.getDesc(task.getCurrentStep())); + // 对 resultVideoUrl 进行预签名处理 + if (StrUtil.isNotBlank(task.getResultVideoUrl())) { + String presignedUrl = fileApi.presignGetUrl(task.getResultVideoUrl(), PRESIGN_URL_EXPIRATION_SECONDS); + respVO.setResultVideoUrl(presignedUrl); + } + return respVO; } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java index e8eed31334..1b0d480600 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java @@ -6,6 +6,10 @@ import cn.hutool.http.HttpResponse; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.service.file.FileConfigService; import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserFileDO; import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper; import cn.iocoder.yudao.module.tik.file.service.TikOssInitService; @@ -45,6 +49,8 @@ public class LatentsyncPollingService { private final TikOssInitService ossInitService; private final cn.iocoder.yudao.module.infra.api.file.FileApi fileApi; private final TikUserFileMapper userFileMapper; + private final FileMapper fileMapper; + private final FileConfigService fileConfigService; /** * Redis键前缀 @@ -223,7 +229,7 @@ public class LatentsyncPollingService { log.info("[handleTaskCompleted][任务({})视频已保存到OSS][url={}]", taskId, saveResult.getUrl()); } catch (Exception e) { log.warn("[handleTaskCompleted][任务({})保存视频失败,使用原URL][error={}]", taskId, e.getMessage()); - saveResult = new OssSaveResult(videoUrl, 0, null); // 降级处理 + saveResult = new OssSaveResult(videoUrl, 0, null, null); // 降级处理 } // 更新任务状态为成功 @@ -360,14 +366,14 @@ public class LatentsyncPollingService { } /** - * 保存视频到OSS - 流式处理优化内存 - * 返回保存结果,包含URL和文件大小 + * 保存视频到OSS - 直接保存到 infra_file 避免重复 + * 返回保存结果,包含URL、文件大小和文件ID */ private OssSaveResult saveVideoToOss(TikDigitalHumanTaskDO task, String remoteVideoUrl) throws Exception { log.info("[saveVideoToOss][任务({})开始下载并保存视频到OSS][remoteUrl={}]", task.getId(), remoteVideoUrl); try { - // 1. 下载远程视频文件(流式处理避免OOM) + // 1. 下载远程视频文件 byte[] videoBytes = downloadRemoteFile(remoteVideoUrl); // 2. 内存检查:超过50MB记录警告 @@ -376,33 +382,72 @@ public class LatentsyncPollingService { log.warn("[saveVideoToOss][任务({})视频文件较大][size={}MB]", task.getId(), sizeMB); } - // 3. 获取OSS目录路径 + // 3. 获取OSS目录和文件名 Long userId = task.getUserId(); String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, "generate"); + String fileName = String.format("数字人视频_%d_%d.mp4", task.getId(), System.currentTimeMillis()); - // 4. 生成文件名 - String fileName = String.format("task_%d_%d.mp4", task.getId(), System.currentTimeMillis()); + // 4. 获取FileClient并上传到OSS + FileClient client = fileConfigService.getMasterFileClient(); + if (client == null) { + throw new Exception("获取FileClient失败"); + } - // 5. 保存到OSS - String ossUrl = fileApi.createFile(videoBytes, fileName, baseDirectory, "video/mp4"); + // 5. 生成上传路径(包含日期前缀和时间戳后缀) + String filePath = generateUploadPath(fileName, baseDirectory); - // 6. 移除预签名URL中的签名参数,获取基础URL - String cleanOssUrl = HttpUtils.removeUrlQuery(ossUrl); + // 6. 上传到OSS + String presignedUrl = client.upload(videoBytes, filePath, "video/mp4"); - // 7. 生成文件路径(用于后续删除) - String filePath = baseDirectory + "/" + fileName; + // 7. 移除预签名参数,获取基础URL + String cleanUrl = HttpUtils.removeUrlQuery(presignedUrl); - log.info("[saveVideoToOss][任务({})视频保存到OSS完成][directory={}, fileName={}, ossUrl={}]", - task.getId(), baseDirectory, fileName, cleanOssUrl); - return new OssSaveResult(cleanOssUrl, videoBytes.length, filePath); + // 8. 保存到 infra_file 表 + FileDO infraFile = new FileDO() + .setConfigId(client.getId()) + .setName(fileName) + .setPath(filePath) + .setUrl(cleanUrl) + .setType("video/mp4") + .setSize(videoBytes.length); + fileMapper.insert(infraFile); + Long infraFileId = infraFile.getId(); + + log.info("[saveVideoToOss][任务({})视频保存完成][infraFileId={}, size={}MB]", + task.getId(), infraFileId, sizeMB); + return new OssSaveResult(cleanUrl, videoBytes.length, filePath, infraFileId); } catch (Exception e) { - log.error("[saveVideoToOss][任务({})保存视频到OSS失败][remoteUrl={}]", task.getId(), remoteVideoUrl, e); - // 如果保存失败,返回原始URL(降级处理) - return new OssSaveResult(remoteVideoUrl, 0, null); + log.error("[saveVideoToOss][任务({})保存视频失败][remoteUrl={}]", task.getId(), remoteVideoUrl, e); + return new OssSaveResult(remoteVideoUrl, 0, null, null); } } + /** + * 生成上传路径(与 FileService 保持一致) + */ + private String generateUploadPath(String name, String directory) { + String prefix = cn.hutool.core.date.LocalDateTimeUtil.format( + cn.hutool.core.date.LocalDateTimeUtil.now(), + cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN); + String suffix = String.valueOf(System.currentTimeMillis()); + + String ext = cn.hutool.core.io.FileUtil.extName(name); + if (StrUtil.isNotEmpty(ext)) { + name = cn.hutool.core.io.FileUtil.mainName(name) + "_" + suffix + "." + ext; + } else { + name = name + "_" + suffix; + } + + if (StrUtil.isNotEmpty(prefix)) { + name = prefix + "/" + name; + } + if (StrUtil.isNotEmpty(directory)) { + name = directory + "/" + name; + } + return name; + } + /** * OSS保存结果 */ @@ -410,11 +455,13 @@ public class LatentsyncPollingService { private final String url; private final int fileSize; private final String filePath; + private final Long infraFileId; - public OssSaveResult(String url, int fileSize, String filePath) { + public OssSaveResult(String url, int fileSize, String filePath, Long infraFileId) { this.url = url; this.fileSize = fileSize; this.filePath = filePath; + this.infraFileId = infraFileId; } public String getUrl() { @@ -428,6 +475,10 @@ public class LatentsyncPollingService { public String getFilePath() { return filePath; } + + public Long getInfraFileId() { + return infraFileId; + } } /** @@ -453,45 +504,36 @@ public class LatentsyncPollingService { /** * 保存结果视频到用户文件表 - * 这样用户就可以在素材库中查看和管理生成的文件 */ private void saveResultVideoToUserFiles(TikDigitalHumanTaskDO task, OssSaveResult saveResult) { try { Long userId = task.getUserId(); - if (userId == null) { - log.warn("[saveResultVideoToUserFiles][任务({})用户ID为空,无法保存文件]", task.getId()); + Long infraFileId = saveResult.getInfraFileId(); + + // 验证必要参数 + if (userId == null || infraFileId == null) { + log.warn("[saveResultVideoToUserFiles][任务({})参数不完整,无法保存][userId={}, infraFileId={}]", + task.getId(), userId, infraFileId); return; } - // 验证结果视频URL - String resultVideoUrl = saveResult.getUrl(); - if (StrUtil.isBlank(resultVideoUrl)) { - log.warn("[saveResultVideoToUserFiles][任务({})结果视频URL为空,无法保存文件]", task.getId()); - return; - } - - // 生成文件名(使用任务ID和时间戳) - String fileName = String.format("数字人视频_%d_%d.mp4", task.getId(), System.currentTimeMillis()); - // 创建用户文件记录 TikUserFileDO userFile = new TikUserFileDO(); userFile.setUserId(userId); - userFile.setFileName(fileName); + userFile.setFileId(infraFileId); + userFile.setFileName(String.format("数字人视频_%d_%d.mp4", task.getId(), System.currentTimeMillis())); userFile.setFileType("video/mp4"); - userFile.setFileCategory("generate"); // 文件分类:生成文件 - userFile.setFileUrl(resultVideoUrl); - userFile.setFilePath(saveResult.getFilePath()); // 设置文件路径(用于后续删除) - userFile.setFileSize((long) saveResult.getFileSize()); // 设置文件大小 - userFile.setDescription("数字人生成结果视频,任务ID:" + task.getId()); + userFile.setFileCategory("generate"); + userFile.setFileUrl(saveResult.getUrl()); + userFile.setFilePath(saveResult.getFilePath()); + userFile.setFileSize((long) saveResult.getFileSize()); - // 插入到用户文件表 userFileMapper.insert(userFile); - log.info("[saveResultVideoToUserFiles][任务({})结果视频已保存到用户文件表,文件编号({})]", - task.getId(), userFile.getId()); + log.info("[saveResultVideoToUserFiles][任务({})文件记录已保存][userFileId={}, infraFileId={}]", + task.getId(), userFile.getId(), infraFileId); } catch (Exception e) { - // 保存失败不影响主流程,只记录日志 - log.error("[saveResultVideoToUserFiles][任务({})保存结果视频到用户文件表失败]", task.getId(), e); + log.error("[saveResultVideoToUserFiles][任务({})保存失败]", task.getId(), e); } }