生成记录
This commit is contained in:
@@ -123,14 +123,18 @@
|
||||
<a-tag v-else-if="file.isImage" color="blue">图片</a-tag>
|
||||
<a-tag v-else color="gray">文件</a-tag>
|
||||
</div>
|
||||
<!-- 删除图标 -->
|
||||
<div class="material-item__delete" @click.stop="handleDeleteFile(file)">
|
||||
<DeleteOutlined />
|
||||
</div>
|
||||
<!-- 分组图标 -->
|
||||
<div class="material-item__group" @click.stop="handleSingleGroup(file)">
|
||||
<TagsOutlined />
|
||||
</div>
|
||||
<!-- 下载图标 -->
|
||||
<div class="material-item__download" @click.stop="handleDownloadFile(file)">
|
||||
<DownloadOutlined />
|
||||
</div>
|
||||
<!-- 删除图标 -->
|
||||
<div class="material-item__delete" @click.stop="handleDeleteFile(file)">
|
||||
<DeleteOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件信息 -->
|
||||
<div class="material-item__info">
|
||||
@@ -249,7 +253,8 @@ import {
|
||||
SearchOutlined,
|
||||
FileOutlined,
|
||||
DeleteOutlined,
|
||||
TagsOutlined
|
||||
TagsOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { MaterialService, MaterialGroupService } from '@/api/material'
|
||||
import { MixService } from '@/api/mix'
|
||||
@@ -425,6 +430,24 @@ const handleDeleteFile = (file) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const handleDownloadFile = (file) => {
|
||||
if (!file?.previewUrl) {
|
||||
message.warning('文件地址无效')
|
||||
return
|
||||
}
|
||||
|
||||
const fileUrl = file.previewUrl
|
||||
const link = document.createElement('a')
|
||||
link.href = fileUrl
|
||||
link.download = file.fileName || 'download'
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
message.success('开始下载')
|
||||
}
|
||||
|
||||
// 文件点击
|
||||
const handleFileClick = (file) => {
|
||||
const isSelected = selectedFileIds.value.includes(file.id)
|
||||
@@ -853,7 +876,7 @@ const handleGroupCancel = () => {
|
||||
.material-item__group {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 42px; /* 在删除按钮左边 */
|
||||
right: 76px; /* 在下载按钮左边 */
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(24, 144, 255, 0.9);
|
||||
@@ -868,6 +891,24 @@ const handleGroupCancel = () => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.material-item__download {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 42px; /* 在删除按钮左边 */
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(82, 196, 26, 0.9);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.material-item:hover .material-item__delete {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -876,6 +917,10 @@ const handleGroupCancel = () => {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.material-item:hover .material-item__download {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.material-item__delete:hover {
|
||||
background: rgb(255, 77, 79);
|
||||
transform: scale(1.1);
|
||||
@@ -886,6 +931,11 @@ const handleGroupCancel = () => {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.material-item__download:hover {
|
||||
background: rgb(82, 196, 26);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.material-item__info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ 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.tik.file.dal.dataobject.TikUserFileDO;
|
||||
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.voice.dal.dataobject.TikDigitalHumanTaskDO;
|
||||
import cn.iocoder.yudao.module.tik.voice.dal.mysql.TikDigitalHumanTaskMapper;
|
||||
@@ -42,6 +44,7 @@ public class LatentsyncPollingService {
|
||||
private final RedissonClient redissonClient;
|
||||
private final TikOssInitService ossInitService;
|
||||
private final cn.iocoder.yudao.module.infra.api.file.FileApi fileApi;
|
||||
private final TikUserFileMapper userFileMapper;
|
||||
|
||||
/**
|
||||
* Redis键前缀
|
||||
@@ -213,14 +216,14 @@ public class LatentsyncPollingService {
|
||||
}
|
||||
|
||||
// 保存视频到OSS(异步处理,轻量化逻辑)
|
||||
String savedVideoUrl = videoUrl;
|
||||
OssSaveResult saveResult = null;
|
||||
try {
|
||||
// 保存视频到OSS,避免临时URL过期
|
||||
savedVideoUrl = saveVideoToOss(task, videoUrl);
|
||||
log.info("[handleTaskCompleted][任务({})视频已保存到OSS][url={}]", taskId, savedVideoUrl);
|
||||
saveResult = saveVideoToOss(task, videoUrl);
|
||||
log.info("[handleTaskCompleted][任务({})视频已保存到OSS][url={}]", taskId, saveResult.getUrl());
|
||||
} catch (Exception e) {
|
||||
log.warn("[handleTaskCompleted][任务({})保存视频失败,使用原URL][error={}]", taskId, e.getMessage());
|
||||
savedVideoUrl = videoUrl; // 降级处理
|
||||
saveResult = new OssSaveResult(videoUrl, 0, null); // 降级处理
|
||||
}
|
||||
|
||||
// 更新任务状态为成功
|
||||
@@ -229,18 +232,21 @@ public class LatentsyncPollingService {
|
||||
updateObj.setStatus("SUCCESS");
|
||||
updateObj.setCurrentStep("finishing");
|
||||
updateObj.setProgress(100);
|
||||
updateObj.setResultVideoUrl(savedVideoUrl);
|
||||
updateObj.setResultVideoUrl(saveResult.getUrl());
|
||||
updateObj.setFinishTime(LocalDateTime.now());
|
||||
taskMapper.updateById(updateObj);
|
||||
|
||||
// 缓存结果到Redis(快速回显)
|
||||
try {
|
||||
String resultKey = "digital_human:task:result:" + taskId;
|
||||
stringRedisTemplate.opsForValue().set(resultKey, savedVideoUrl, Duration.ofHours(24));
|
||||
stringRedisTemplate.opsForValue().set(resultKey, saveResult.getUrl(), Duration.ofHours(24));
|
||||
} catch (Exception e) {
|
||||
log.warn("[handleTaskCompleted][任务({})缓存结果失败]", taskId, e);
|
||||
}
|
||||
|
||||
// 保存结果视频到用户文件表(这样用户可以在素材库中查看)
|
||||
saveResultVideoToUserFiles(task, saveResult);
|
||||
|
||||
// 从轮询队列中移除
|
||||
removeFromPollingQueue(taskId, requestId);
|
||||
|
||||
@@ -355,8 +361,9 @@ public class LatentsyncPollingService {
|
||||
|
||||
/**
|
||||
* 保存视频到OSS - 流式处理优化内存
|
||||
* 返回保存结果,包含URL和文件大小
|
||||
*/
|
||||
private String saveVideoToOss(TikDigitalHumanTaskDO task, String remoteVideoUrl) throws Exception {
|
||||
private OssSaveResult saveVideoToOss(TikDigitalHumanTaskDO task, String remoteVideoUrl) throws Exception {
|
||||
log.info("[saveVideoToOss][任务({})开始下载并保存视频到OSS][remoteUrl={}]", task.getId(), remoteVideoUrl);
|
||||
|
||||
try {
|
||||
@@ -382,14 +389,44 @@ public class LatentsyncPollingService {
|
||||
// 6. 移除预签名URL中的签名参数,获取基础URL
|
||||
String cleanOssUrl = HttpUtils.removeUrlQuery(ossUrl);
|
||||
|
||||
// 7. 生成文件路径(用于后续删除)
|
||||
String filePath = baseDirectory + "/" + fileName;
|
||||
|
||||
log.info("[saveVideoToOss][任务({})视频保存到OSS完成][directory={}, fileName={}, ossUrl={}]",
|
||||
task.getId(), baseDirectory, fileName, cleanOssUrl);
|
||||
return cleanOssUrl;
|
||||
return new OssSaveResult(cleanOssUrl, videoBytes.length, filePath);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[saveVideoToOss][任务({})保存视频到OSS失败][remoteUrl={}]", task.getId(), remoteVideoUrl, e);
|
||||
// 如果保存失败,返回原始URL(降级处理)
|
||||
return remoteVideoUrl;
|
||||
return new OssSaveResult(remoteVideoUrl, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OSS保存结果
|
||||
*/
|
||||
private static class OssSaveResult {
|
||||
private final String url;
|
||||
private final int fileSize;
|
||||
private final String filePath;
|
||||
|
||||
public OssSaveResult(String url, int fileSize, String filePath) {
|
||||
this.url = url;
|
||||
this.fileSize = fileSize;
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public int getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,4 +451,48 @@ public class LatentsyncPollingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存结果视频到用户文件表
|
||||
* 这样用户就可以在素材库中查看和管理生成的文件
|
||||
*/
|
||||
private void saveResultVideoToUserFiles(TikDigitalHumanTaskDO task, OssSaveResult saveResult) {
|
||||
try {
|
||||
Long userId = task.getUserId();
|
||||
if (userId == null) {
|
||||
log.warn("[saveResultVideoToUserFiles][任务({})用户ID为空,无法保存文件]", task.getId());
|
||||
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.setFileType("video/mp4");
|
||||
userFile.setFileCategory("generate"); // 文件分类:生成文件
|
||||
userFile.setFileUrl(resultVideoUrl);
|
||||
userFile.setFilePath(saveResult.getFilePath()); // 设置文件路径(用于后续删除)
|
||||
userFile.setFileSize((long) saveResult.getFileSize()); // 设置文件大小
|
||||
userFile.setDescription("数字人生成结果视频,任务ID:" + task.getId());
|
||||
|
||||
// 插入到用户文件表
|
||||
userFileMapper.insert(userFile);
|
||||
|
||||
log.info("[saveResultVideoToUserFiles][任务({})结果视频已保存到用户文件表,文件编号({})]",
|
||||
task.getId(), userFile.getId());
|
||||
} catch (Exception e) {
|
||||
// 保存失败不影响主流程,只记录日志
|
||||
log.error("[saveResultVideoToUserFiles][任务({})保存结果视频到用户文件表失败]", task.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user