feat: 功能
This commit is contained in:
@@ -513,8 +513,8 @@ const loadLastTask = async () => {
|
|||||||
currentTaskId.value = lastTaskId
|
currentTaskId.value = lastTaskId
|
||||||
|
|
||||||
// 如果任务是成功状态,显示结果
|
// 如果任务是成功状态,显示结果
|
||||||
if (task.status === 'SUCCESS' && task.videoUrl) {
|
if (task.status === 'SUCCESS' && task.resultVideoUrl) {
|
||||||
previewVideoUrl.value = task.videoUrl
|
previewVideoUrl.value = task.resultVideoUrl
|
||||||
currentTaskStatus.value = 'SUCCESS'
|
currentTaskStatus.value = 'SUCCESS'
|
||||||
message.success('已自动加载最近一次任务结果')
|
message.success('已自动加载最近一次任务结果')
|
||||||
} else if (task.status === 'PROCESSING') {
|
} else if (task.status === 'PROCESSING') {
|
||||||
@@ -556,7 +556,7 @@ const startPollingTask = () => {
|
|||||||
if (task.status === 'SUCCESS') {
|
if (task.status === 'SUCCESS') {
|
||||||
clearInterval(pollingInterval.value)
|
clearInterval(pollingInterval.value)
|
||||||
pollingInterval.value = null
|
pollingInterval.value = null
|
||||||
previewVideoUrl.value = task.videoUrl
|
previewVideoUrl.value = task.resultVideoUrl
|
||||||
isGenerating.value = false
|
isGenerating.value = false
|
||||||
currentTaskStatus.value = 'SUCCESS'
|
currentTaskStatus.value = 'SUCCESS'
|
||||||
// 保存成功的任务ID
|
// 保存成功的任务ID
|
||||||
|
|||||||
@@ -386,6 +386,12 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
|||||||
// 设置当前步骤描述
|
// 设置当前步骤描述
|
||||||
respVO.setCurrentStepDesc(DigitalHumanTaskStepEnum.getDesc(task.getCurrentStep()));
|
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;
|
return respVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.http.HttpUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
|
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
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.dataobject.TikUserFileDO;
|
||||||
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper;
|
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper;
|
||||||
import cn.iocoder.yudao.module.tik.file.service.TikOssInitService;
|
import cn.iocoder.yudao.module.tik.file.service.TikOssInitService;
|
||||||
@@ -45,6 +49,8 @@ public class LatentsyncPollingService {
|
|||||||
private final TikOssInitService ossInitService;
|
private final TikOssInitService ossInitService;
|
||||||
private final cn.iocoder.yudao.module.infra.api.file.FileApi fileApi;
|
private final cn.iocoder.yudao.module.infra.api.file.FileApi fileApi;
|
||||||
private final TikUserFileMapper userFileMapper;
|
private final TikUserFileMapper userFileMapper;
|
||||||
|
private final FileMapper fileMapper;
|
||||||
|
private final FileConfigService fileConfigService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis键前缀
|
* Redis键前缀
|
||||||
@@ -223,7 +229,7 @@ public class LatentsyncPollingService {
|
|||||||
log.info("[handleTaskCompleted][任务({})视频已保存到OSS][url={}]", taskId, saveResult.getUrl());
|
log.info("[handleTaskCompleted][任务({})视频已保存到OSS][url={}]", taskId, saveResult.getUrl());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[handleTaskCompleted][任务({})保存视频失败,使用原URL][error={}]", taskId, e.getMessage());
|
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 - 流式处理优化内存
|
* 保存视频到OSS - 直接保存到 infra_file 避免重复
|
||||||
* 返回保存结果,包含URL和文件大小
|
* 返回保存结果,包含URL、文件大小和文件ID
|
||||||
*/
|
*/
|
||||||
private OssSaveResult saveVideoToOss(TikDigitalHumanTaskDO task, String remoteVideoUrl) throws Exception {
|
private OssSaveResult saveVideoToOss(TikDigitalHumanTaskDO task, String remoteVideoUrl) throws Exception {
|
||||||
log.info("[saveVideoToOss][任务({})开始下载并保存视频到OSS][remoteUrl={}]", task.getId(), remoteVideoUrl);
|
log.info("[saveVideoToOss][任务({})开始下载并保存视频到OSS][remoteUrl={}]", task.getId(), remoteVideoUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 下载远程视频文件(流式处理避免OOM)
|
// 1. 下载远程视频文件
|
||||||
byte[] videoBytes = downloadRemoteFile(remoteVideoUrl);
|
byte[] videoBytes = downloadRemoteFile(remoteVideoUrl);
|
||||||
|
|
||||||
// 2. 内存检查:超过50MB记录警告
|
// 2. 内存检查:超过50MB记录警告
|
||||||
@@ -376,33 +382,72 @@ public class LatentsyncPollingService {
|
|||||||
log.warn("[saveVideoToOss][任务({})视频文件较大][size={}MB]", task.getId(), sizeMB);
|
log.warn("[saveVideoToOss][任务({})视频文件较大][size={}MB]", task.getId(), sizeMB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 获取OSS目录路径
|
// 3. 获取OSS目录和文件名
|
||||||
Long userId = task.getUserId();
|
Long userId = task.getUserId();
|
||||||
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, "generate");
|
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, "generate");
|
||||||
|
String fileName = String.format("数字人视频_%d_%d.mp4", task.getId(), System.currentTimeMillis());
|
||||||
|
|
||||||
// 4. 生成文件名
|
// 4. 获取FileClient并上传到OSS
|
||||||
String fileName = String.format("task_%d_%d.mp4", task.getId(), System.currentTimeMillis());
|
FileClient client = fileConfigService.getMasterFileClient();
|
||||||
|
if (client == null) {
|
||||||
|
throw new Exception("获取FileClient失败");
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 保存到OSS
|
// 5. 生成上传路径(包含日期前缀和时间戳后缀)
|
||||||
String ossUrl = fileApi.createFile(videoBytes, fileName, baseDirectory, "video/mp4");
|
String filePath = generateUploadPath(fileName, baseDirectory);
|
||||||
|
|
||||||
// 6. 移除预签名URL中的签名参数,获取基础URL
|
// 6. 上传到OSS
|
||||||
String cleanOssUrl = HttpUtils.removeUrlQuery(ossUrl);
|
String presignedUrl = client.upload(videoBytes, filePath, "video/mp4");
|
||||||
|
|
||||||
// 7. 生成文件路径(用于后续删除)
|
// 7. 移除预签名参数,获取基础URL
|
||||||
String filePath = baseDirectory + "/" + fileName;
|
String cleanUrl = HttpUtils.removeUrlQuery(presignedUrl);
|
||||||
|
|
||||||
log.info("[saveVideoToOss][任务({})视频保存到OSS完成][directory={}, fileName={}, ossUrl={}]",
|
// 8. 保存到 infra_file 表
|
||||||
task.getId(), baseDirectory, fileName, cleanOssUrl);
|
FileDO infraFile = new FileDO()
|
||||||
return new OssSaveResult(cleanOssUrl, videoBytes.length, filePath);
|
.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) {
|
} catch (Exception e) {
|
||||||
log.error("[saveVideoToOss][任务({})保存视频到OSS失败][remoteUrl={}]", task.getId(), remoteVideoUrl, e);
|
log.error("[saveVideoToOss][任务({})保存视频失败][remoteUrl={}]", task.getId(), remoteVideoUrl, e);
|
||||||
// 如果保存失败,返回原始URL(降级处理)
|
return new OssSaveResult(remoteVideoUrl, 0, null, null);
|
||||||
return new OssSaveResult(remoteVideoUrl, 0, 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保存结果
|
* OSS保存结果
|
||||||
*/
|
*/
|
||||||
@@ -410,11 +455,13 @@ public class LatentsyncPollingService {
|
|||||||
private final String url;
|
private final String url;
|
||||||
private final int fileSize;
|
private final int fileSize;
|
||||||
private final String filePath;
|
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.url = url;
|
||||||
this.fileSize = fileSize;
|
this.fileSize = fileSize;
|
||||||
this.filePath = filePath;
|
this.filePath = filePath;
|
||||||
|
this.infraFileId = infraFileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
@@ -428,6 +475,10 @@ public class LatentsyncPollingService {
|
|||||||
public String getFilePath() {
|
public String getFilePath() {
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getInfraFileId() {
|
||||||
|
return infraFileId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -453,45 +504,36 @@ public class LatentsyncPollingService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存结果视频到用户文件表
|
* 保存结果视频到用户文件表
|
||||||
* 这样用户就可以在素材库中查看和管理生成的文件
|
|
||||||
*/
|
*/
|
||||||
private void saveResultVideoToUserFiles(TikDigitalHumanTaskDO task, OssSaveResult saveResult) {
|
private void saveResultVideoToUserFiles(TikDigitalHumanTaskDO task, OssSaveResult saveResult) {
|
||||||
try {
|
try {
|
||||||
Long userId = task.getUserId();
|
Long userId = task.getUserId();
|
||||||
if (userId == null) {
|
Long infraFileId = saveResult.getInfraFileId();
|
||||||
log.warn("[saveResultVideoToUserFiles][任务({})用户ID为空,无法保存文件]", task.getId());
|
|
||||||
|
// 验证必要参数
|
||||||
|
if (userId == null || infraFileId == null) {
|
||||||
|
log.warn("[saveResultVideoToUserFiles][任务({})参数不完整,无法保存][userId={}, infraFileId={}]",
|
||||||
|
task.getId(), userId, infraFileId);
|
||||||
return;
|
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();
|
TikUserFileDO userFile = new TikUserFileDO();
|
||||||
userFile.setUserId(userId);
|
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.setFileType("video/mp4");
|
||||||
userFile.setFileCategory("generate"); // 文件分类:生成文件
|
userFile.setFileCategory("generate");
|
||||||
userFile.setFileUrl(resultVideoUrl);
|
userFile.setFileUrl(saveResult.getUrl());
|
||||||
userFile.setFilePath(saveResult.getFilePath()); // 设置文件路径(用于后续删除)
|
userFile.setFilePath(saveResult.getFilePath());
|
||||||
userFile.setFileSize((long) saveResult.getFileSize()); // 设置文件大小
|
userFile.setFileSize((long) saveResult.getFileSize());
|
||||||
userFile.setDescription("数字人生成结果视频,任务ID:" + task.getId());
|
|
||||||
|
|
||||||
// 插入到用户文件表
|
|
||||||
userFileMapper.insert(userFile);
|
userFileMapper.insert(userFile);
|
||||||
|
|
||||||
log.info("[saveResultVideoToUserFiles][任务({})结果视频已保存到用户文件表,文件编号({})]",
|
log.info("[saveResultVideoToUserFiles][任务({})文件记录已保存][userFileId={}, infraFileId={}]",
|
||||||
task.getId(), userFile.getId());
|
task.getId(), userFile.getId(), infraFileId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 保存失败不影响主流程,只记录日志
|
log.error("[saveResultVideoToUserFiles][任务({})保存失败]", task.getId(), e);
|
||||||
log.error("[saveResultVideoToUserFiles][任务({})保存结果视频到用户文件表失败]", task.getId(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user