优化
This commit is contained in:
@@ -45,15 +45,7 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<!-- 删除按钮 -->
|
<!-- 删除按钮 -->
|
||||||
<a-popconfirm
|
<a-button size="small" danger @click="handleDelete">删除</a-button>
|
||||||
v-if="canDelete"
|
|
||||||
title="确定删除这个任务吗?删除后无法恢复。"
|
|
||||||
@confirm="handleDelete"
|
|
||||||
>
|
|
||||||
<a-button size="small" danger>
|
|
||||||
删除
|
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
|
|
||||||
<!-- 插槽用于自定义操作 -->
|
<!-- 插槽用于自定义操作 -->
|
||||||
<slot name="extra" :task="task"></slot>
|
<slot name="extra" :task="task"></slot>
|
||||||
@@ -125,11 +117,6 @@ const canRetry = computed(() => {
|
|||||||
return props.task.status === 'failed'
|
return props.task.status === 'failed'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算属性:是否可删除
|
|
||||||
const canDelete = computed(() => {
|
|
||||||
return true // 所有任务都可以删除
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理预览
|
// 处理预览
|
||||||
const handlePreview = async () => {
|
const handlePreview = async () => {
|
||||||
if (getSignedUrlsApi) {
|
if (getSignedUrlsApi) {
|
||||||
@@ -162,8 +149,7 @@ const handleDelete = () => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
/* 确保按钮内的图标和文字对齐 */
|
|
||||||
:deep(.ant-btn .anticon) {
|
:deep(.ant-btn .anticon) {
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,11 +48,9 @@
|
|||||||
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
|
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
|
||||||
<a-alert :message="`已选中 ${selectedRowKeys.length} 项`" type="info" show-icon>
|
<a-alert :message="`已选中 ${selectedRowKeys.length} 项`" type="info" show-icon>
|
||||||
<template #action>
|
<template #action>
|
||||||
<a-popconfirm title="确定要删除选中的任务吗?" @confirm="handleBatchDelete">
|
<a-button size="small" danger @click="confirmBatchDelete">
|
||||||
<a-button size="small" danger>
|
<DeleteOutlined /> 批量删除
|
||||||
<DeleteOutlined /> 批量删除
|
</a-button>
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +81,7 @@
|
|||||||
<div class="progress-cell">
|
<div class="progress-cell">
|
||||||
<a-progress
|
<a-progress
|
||||||
:percent="record.progress"
|
:percent="record.progress"
|
||||||
:status="PROGRESS_STATUS[record.status]"
|
:status="PROGRESS_STATUS[record.status?.toLowerCase()] || 'normal'"
|
||||||
size="small"
|
size="small"
|
||||||
:show-info="false"
|
:show-info="false"
|
||||||
/>
|
/>
|
||||||
@@ -117,9 +115,7 @@
|
|||||||
取消
|
取消
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<a-popconfirm title="确定删除?删除后无法恢复。" @confirm="handleDelete(record.id)">
|
<a-button type="link" size="small" class="action-btn action-btn--danger" @click="handleDelete(record.id)">删除</a-button>
|
||||||
<a-button type="link" size="small" class="action-btn action-btn--danger">删除</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -131,7 +127,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { SearchOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
import { SearchOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
import { getDigitalHumanTaskPage, cancelTask, deleteTask } from '@/api/digitalHuman'
|
import { getDigitalHumanTaskPage, cancelTask, deleteTask } from '@/api/digitalHuman'
|
||||||
import { formatDate } from '@/utils/file'
|
import { formatDate } from '@/utils/file'
|
||||||
@@ -142,8 +138,11 @@ import TaskStatusTag from '@/views/system/task-management/components/TaskStatusT
|
|||||||
|
|
||||||
// 进度状态映射
|
// 进度状态映射
|
||||||
const PROGRESS_STATUS = {
|
const PROGRESS_STATUS = {
|
||||||
pending: 'normal', running: 'active', success: 'success', failed: 'exception', canceled: 'normal',
|
pending: 'normal',
|
||||||
PENDING: 'normal', RUNNING: 'active', SUCCESS: 'success', FAILED: 'exception', CANCELED: 'normal'
|
running: 'active',
|
||||||
|
success: 'success',
|
||||||
|
failed: 'exception',
|
||||||
|
canceled: 'normal'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
@@ -185,6 +184,18 @@ const handleBatchDelete = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确认批量删除
|
||||||
|
const confirmBatchDelete = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除选中的任务吗?',
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: handleBatchDelete
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, fixed: 'left' },
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, fixed: 'left' },
|
||||||
|
|||||||
@@ -124,9 +124,7 @@
|
|||||||
重试
|
重试
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<a-popconfirm title="确定删除?删除后无法恢复。" @confirm="handleDelete(record.id)">
|
<a-button type="link" size="small" class="action-btn action-btn--danger" @click="handleDelete(record.id)">删除</a-button>
|
||||||
<a-button type="link" size="small" class="action-btn action-btn--danger">删除</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -191,6 +189,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { SearchOutlined, PlayCircleOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
import { SearchOutlined, PlayCircleOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { Modal } from 'ant-design-vue'
|
||||||
import { MixTaskService } from '@/api/mixTask'
|
import { MixTaskService } from '@/api/mixTask'
|
||||||
import { formatDate } from '@/utils/file'
|
import { formatDate } from '@/utils/file'
|
||||||
import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
|
import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
|
||||||
@@ -214,17 +213,19 @@ const handleExpandedRowsChange = (keys) => { expandedRowKeys.value = keys }
|
|||||||
const preview = reactive({ visible: false, title: '', url: '' })
|
const preview = reactive({ visible: false, title: '', url: '' })
|
||||||
|
|
||||||
// 状态判断
|
// 状态判断
|
||||||
const isStatus = (status, target) => status === target || status === target.toUpperCase()
|
const isStatus = (status, target) => status?.toLowerCase() === target.toLowerCase()
|
||||||
|
|
||||||
const canOperate = (record, action) => {
|
const canOperate = (record, action) => {
|
||||||
const isSuccess = isStatus(record.status, 'success')
|
const isSuccess = isStatus(record.status, 'success')
|
||||||
const hasUrls = record.outputUrls?.length > 0
|
const hasOutput = record.outputUrls?.length > 0
|
||||||
const actions = {
|
|
||||||
preview: isSuccess && hasUrls,
|
switch (action) {
|
||||||
download: isSuccess && hasUrls,
|
case 'preview': return isSuccess && hasOutput
|
||||||
cancel: isStatus(record.status, 'running'),
|
case 'download': return isSuccess && hasOutput
|
||||||
retry: isStatus(record.status, 'failed')
|
case 'cancel': return isStatus(record.status, 'running')
|
||||||
|
case 'retry': return isStatus(record.status, 'failed')
|
||||||
|
default: return false
|
||||||
}
|
}
|
||||||
return actions[action]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预览视频
|
// 预览视频
|
||||||
@@ -261,7 +262,9 @@ const downloadVideo = async (taskId, index) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = (record) => {
|
const handleDownload = (record) => {
|
||||||
if (record.outputUrls?.length) handleBatchDownload([], MixTaskService.getSignedUrls, record.id)
|
if (record.outputUrls?.length) {
|
||||||
|
handleBatchDownload(record.outputUrls, MixTaskService.getSignedUrls, record.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
@@ -416,10 +419,4 @@ onMounted(fetchList)
|
|||||||
:deep(.ant-btn .anticon) {
|
:deep(.ant-btn .anticon) {
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修复 popconfirm 按钮对齐 */
|
|
||||||
:deep(.ant-popover .ant-popover-buttons) {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="user-name">{{ userStore.displayName }}</h2>
|
<h2 class="user-name">{{ userStore.displayName }}</h2>
|
||||||
<div class="user-id">ID: {{ userStore.userId || '未设置' }}</div>
|
|
||||||
<div class="user-role-badge">普通用户</div>
|
<div class="user-role-badge">普通用户</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -370,14 +369,8 @@ onMounted(async () => {
|
|||||||
.user-name {
|
.user-name {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-id {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-role-badge {
|
.user-role-badge {
|
||||||
|
|||||||
@@ -198,9 +198,8 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||||
// 1. 将 url 转换为 path
|
// 1. 将 url 转换为 path(支持 CDN 域名和 OSS 原始域名)
|
||||||
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
|
String path = extractPathFromUrl(url);
|
||||||
path = HttpUtils.removeUrlQuery(path);
|
|
||||||
String decodedPath = URLUtil.decode(path, StandardCharsets.UTF_8);
|
String decodedPath = URLUtil.decode(path, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// 2. 公开访问:无需签名
|
// 2. 公开访问:无需签名
|
||||||
@@ -272,6 +271,54 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||||||
return StrUtil.format("https://{}", config.getEndpoint());
|
return StrUtil.format("https://{}", config.getEndpoint());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 URL 中提取 path(支持 CDN 域名和 OSS 原始域名)
|
||||||
|
*
|
||||||
|
* @param url 完整的 URL
|
||||||
|
* @return 相对路径(不含查询参数)
|
||||||
|
*/
|
||||||
|
private String extractPathFromUrl(String url) {
|
||||||
|
if (StrUtil.isEmpty(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除查询参数
|
||||||
|
String cleanUrl = HttpUtils.removeUrlQuery(url);
|
||||||
|
|
||||||
|
// 1. 尝试使用配置的 domain 提取 path(CDN 域名)
|
||||||
|
if (StrUtil.isNotEmpty(config.getDomain()) && cleanUrl.startsWith(config.getDomain() + "/")) {
|
||||||
|
return StrUtil.removePrefix(cleanUrl, config.getDomain() + "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试从 OSS 原始域名提取 path(阿里云格式)
|
||||||
|
// 格式:https://{bucket}.oss-{region}.aliyuncs.com/{path}
|
||||||
|
if (cleanUrl.contains(".aliyuncs.com/")) {
|
||||||
|
int pathStart = cleanUrl.indexOf(".aliyuncs.com/") + ".aliyuncs.com/".length();
|
||||||
|
if (pathStart < cleanUrl.length()) {
|
||||||
|
return cleanUrl.substring(pathStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 尝试从 buildDomain() 格式提取 path
|
||||||
|
String builtDomain = buildDomain();
|
||||||
|
if (cleanUrl.startsWith(builtDomain + "/")) {
|
||||||
|
return StrUtil.removePrefix(cleanUrl, builtDomain + "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 兜底:如果 URL 包含路径分隔符,尝试提取路径部分
|
||||||
|
int slashIndex = cleanUrl.indexOf("://");
|
||||||
|
if (slashIndex > 0) {
|
||||||
|
String afterProtocol = cleanUrl.substring(slashIndex + 3);
|
||||||
|
int pathIndex = afterProtocol.indexOf('/');
|
||||||
|
if (pathIndex > 0 && pathIndex < afterProtocol.length() - 1) {
|
||||||
|
return afterProtocol.substring(pathIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 最后兜底:直接返回原始 URL(可能已经是 path)
|
||||||
|
return cleanUrl;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 endpoint 中提取 region
|
* 从 endpoint 中提取 region
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserOssInitDO;
|
|||||||
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserOssInitMapper;
|
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserOssInitMapper;
|
||||||
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserOssInitRespVO;
|
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserOssInitRespVO;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
@@ -45,35 +46,50 @@ public class TikOssInitServiceImpl implements TikOssInitService {
|
|||||||
return BeanUtils.toBean(existing, AppTikUserOssInitRespVO.class);
|
return BeanUtils.toBean(existing, AppTikUserOssInitRespVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户信息(获取手机号)
|
// 获取用户信息(优先使用手机号MD5,否则使用userId)
|
||||||
MemberUserRespDTO user = memberUserApi.getUser(userId);
|
|
||||||
if (user == null || StrUtil.isBlank(user.getMobile())) {
|
|
||||||
throw exception(OSS_INIT_FAILED, "用户手机号不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算手机号MD5和OSS路径
|
|
||||||
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
||||||
String mobileMd5 = DigestUtil.md5Hex(user.getMobile());
|
String pathIdentifier = getPathIdentifier(userId, tenantId);
|
||||||
OssPathInfo pathInfo = buildOssPaths(mobileMd5, tenantId);
|
OssPathInfo pathInfo = buildOssPaths(pathIdentifier, tenantId);
|
||||||
|
|
||||||
|
// 创建OSS初始化记录
|
||||||
|
TikUserOssInitDO ossInit = createOssInitDO(userId, pathIdentifier, pathInfo);
|
||||||
|
|
||||||
// 创建或更新OSS初始化记录
|
|
||||||
// 注意:OSS中目录是虚拟的,不需要显式创建,直接上传文件时包含路径即可自动创建
|
|
||||||
TikUserOssInitDO ossInit;
|
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
// 更新现有记录(补充缺失的字段或重新初始化)
|
// 更新现有记录
|
||||||
ossInit = existing;
|
ossInit.setId(existing.getId());
|
||||||
updateOssInitFields(ossInit, mobileMd5, pathInfo);
|
|
||||||
ossInitMapper.updateById(ossInit);
|
ossInitMapper.updateById(ossInit);
|
||||||
} else {
|
} else {
|
||||||
// 创建新记录
|
// 尝试插入,如果并发冲突则更新
|
||||||
ossInit = createOssInitDO(userId, mobileMd5, pathInfo);
|
try {
|
||||||
ossInitMapper.insert(ossInit);
|
ossInitMapper.insert(ossInit);
|
||||||
|
} catch (DuplicateKeyException e) {
|
||||||
|
log.info("[initOssDirectory][用户({})并发插入冲突,改为更新]", userId);
|
||||||
|
existing = ossInitMapper.selectByUserId(userId);
|
||||||
|
if (existing != null) {
|
||||||
|
ossInit.setId(existing.getId());
|
||||||
|
ossInitMapper.updateById(ossInit);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[initOssDirectory][用户({})OSS初始化成功,根路径({})]", userId, pathInfo.ossRootPath);
|
log.info("[initOssDirectory][用户({})OSS初始化成功,根路径({})]", userId, pathInfo.ossRootPath);
|
||||||
return BeanUtils.toBean(ossInit, AppTikUserOssInitRespVO.class);
|
return BeanUtils.toBean(ossInit, AppTikUserOssInitRespVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取路径标识符
|
||||||
|
* 优先使用手机号MD5,否则使用userId
|
||||||
|
*/
|
||||||
|
private String getPathIdentifier(Long userId, Long tenantId) {
|
||||||
|
MemberUserRespDTO user = memberUserApi.getUser(userId);
|
||||||
|
if (user != null && StrUtil.isNotBlank(user.getMobile())) {
|
||||||
|
return DigestUtil.md5Hex(user.getMobile());
|
||||||
|
}
|
||||||
|
// 无手机号时使用userId作为标识
|
||||||
|
log.info("[getPathIdentifier][用户({})无手机号,使用userId作为路径标识]", userId);
|
||||||
|
return "u" + userId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OSS路径信息
|
* OSS路径信息
|
||||||
*/
|
*/
|
||||||
@@ -88,9 +104,12 @@ public class TikOssInitServiceImpl implements TikOssInitService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建OSS路径信息
|
* 构建OSS路径信息
|
||||||
|
*
|
||||||
|
* @param pathIdentifier 路径标识符(手机号MD5或u{userId}格式)
|
||||||
|
* @param tenantId 租户ID
|
||||||
*/
|
*/
|
||||||
private OssPathInfo buildOssPaths(String mobileMd5, Long tenantId) {
|
private OssPathInfo buildOssPaths(String pathIdentifier, Long tenantId) {
|
||||||
String ossRootPath = mobileMd5 + "/" + tenantId;
|
String ossRootPath = pathIdentifier + "/" + tenantId;
|
||||||
return new OssPathInfo(
|
return new OssPathInfo(
|
||||||
ossRootPath,
|
ossRootPath,
|
||||||
ossRootPath + "/video",
|
ossRootPath + "/video",
|
||||||
@@ -103,25 +122,15 @@ public class TikOssInitServiceImpl implements TikOssInitService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建OSS初始化DO对象
|
* 创建OSS初始化DO对象
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param pathIdentifier 路径标识符(手机号MD5或u{userId}格式)
|
||||||
|
* @param pathInfo 路径信息
|
||||||
*/
|
*/
|
||||||
private TikUserOssInitDO createOssInitDO(Long userId, String mobileMd5, OssPathInfo pathInfo) {
|
private TikUserOssInitDO createOssInitDO(Long userId, String pathIdentifier, OssPathInfo pathInfo) {
|
||||||
return new TikUserOssInitDO()
|
return new TikUserOssInitDO()
|
||||||
.setUserId(userId)
|
.setUserId(userId)
|
||||||
.setMobileMd5(mobileMd5)
|
.setMobileMd5(pathIdentifier)
|
||||||
.setOssRootPath(pathInfo.ossRootPath)
|
|
||||||
.setVideoPath(pathInfo.videoPath)
|
|
||||||
.setGeneratePath(pathInfo.generatePath)
|
|
||||||
.setAudioPath(pathInfo.audioPath)
|
|
||||||
.setMixPath(pathInfo.mixPath)
|
|
||||||
.setVoicePath(pathInfo.voicePath)
|
|
||||||
.setInitStatus(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新OSS初始化DO对象的字段
|
|
||||||
*/
|
|
||||||
private void updateOssInitFields(TikUserOssInitDO ossInit, String mobileMd5, OssPathInfo pathInfo) {
|
|
||||||
ossInit.setMobileMd5(mobileMd5)
|
|
||||||
.setOssRootPath(pathInfo.ossRootPath)
|
.setOssRootPath(pathInfo.ossRootPath)
|
||||||
.setVideoPath(pathInfo.videoPath)
|
.setVideoPath(pathInfo.videoPath)
|
||||||
.setGeneratePath(pathInfo.generatePath)
|
.setGeneratePath(pathInfo.generatePath)
|
||||||
@@ -139,27 +148,27 @@ public class TikOssInitServiceImpl implements TikOssInitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public String getOssRootPath(Long userId) {
|
* 获取已初始化的OSS记录,未初始化则抛出异常
|
||||||
|
*/
|
||||||
|
private TikUserOssInitDO getRequiredOssInit(Long userId) {
|
||||||
TikUserOssInitDO ossInit = ossInitMapper.selectByUserId(userId);
|
TikUserOssInitDO ossInit = ossInitMapper.selectByUserId(userId);
|
||||||
if (ossInit == null || ossInit.getInitStatus() == 0) {
|
if (ossInit == null || ossInit.getInitStatus() == 0) {
|
||||||
throw exception(OSS_INIT_FAILED);
|
throw exception(OSS_INIT_FAILED);
|
||||||
}
|
}
|
||||||
return ossInit.getOssRootPath();
|
return ossInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOssRootPath(Long userId) {
|
||||||
|
return getRequiredOssInit(userId).getOssRootPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getOssDirectoryByCategory(Long userId, String fileCategory) {
|
public String getOssDirectoryByCategory(Long userId, String fileCategory) {
|
||||||
// 确保OSS已初始化
|
|
||||||
ensureOssInitialized(userId);
|
ensureOssInitialized(userId);
|
||||||
|
TikUserOssInitDO ossInit = getRequiredOssInit(userId);
|
||||||
|
|
||||||
// 获取OSS初始化记录
|
|
||||||
TikUserOssInitDO ossInit = ossInitMapper.selectByUserId(userId);
|
|
||||||
if (ossInit == null || ossInit.getInitStatus() == 0) {
|
|
||||||
throw exception(OSS_INIT_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据分类返回对应基础目录路径
|
|
||||||
return switch (fileCategory) {
|
return switch (fileCategory) {
|
||||||
case "video" -> ossInit.getVideoPath();
|
case "video" -> ossInit.getVideoPath();
|
||||||
case "generate" -> ossInit.getGeneratePath();
|
case "generate" -> ossInit.getGeneratePath();
|
||||||
@@ -173,7 +182,7 @@ public class TikOssInitServiceImpl implements TikOssInitService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于分类和分组获取OSS目录路径
|
* 基于分类和分组获取OSS目录路径
|
||||||
* 新路径格式:/user-files/{category}/{date}/{groupName}/
|
* 路径格式:{mobileMd5}/user-files/{category}/{date}/{groupName}
|
||||||
*
|
*
|
||||||
* @param userId 用户编号
|
* @param userId 用户编号
|
||||||
* @param category 分类:MIX 或 DIGITAL_HUMAN
|
* @param category 分类:MIX 或 DIGITAL_HUMAN
|
||||||
@@ -182,36 +191,18 @@ public class TikOssInitServiceImpl implements TikOssInitService {
|
|||||||
* @return OSS目录路径
|
* @return OSS目录路径
|
||||||
*/
|
*/
|
||||||
public String getOssDirectoryByCategoryAndGroup(Long userId, String category, String groupName, String dateStr) {
|
public String getOssDirectoryByCategoryAndGroup(Long userId, String category, String groupName, String dateStr) {
|
||||||
// 确保OSS已初始化
|
|
||||||
ensureOssInitialized(userId);
|
ensureOssInitialized(userId);
|
||||||
|
TikUserOssInitDO ossInit = getRequiredOssInit(userId);
|
||||||
|
|
||||||
// 构建新格式的路径
|
String path = ossInit.getMobileMd5() + "/user-files/" + category.toLowerCase() + "/" + dateStr;
|
||||||
// 路径格式:{mobileMd5}/{tenantId}/user-files/{category}/{date}/{groupName}
|
|
||||||
TikUserOssInitDO ossInit = ossInitMapper.selectByUserId(userId);
|
if (StrUtil.isNotBlank(groupName)) {
|
||||||
if (ossInit == null || ossInit.getInitStatus() == 0) {
|
// 对分组名进行URL安全处理:保留中文、字母、数字、下划线和连字符
|
||||||
throw exception(OSS_INIT_FAILED);
|
String safeGroupName = groupName.trim().replaceAll("[^a-zA-Z0-9一-鿿_-]", "_");
|
||||||
|
path += "/" + safeGroupName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取基础路径(去掉tenantId部分)
|
return path;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ public class MixTaskConstants {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时任务配置
|
* 定时任务配置
|
||||||
* 改为每30秒检查一次,提供更实时的进度更新
|
* 每1分钟检查一次,平衡响应速度和系统压力
|
||||||
*/
|
*/
|
||||||
public static final String CRON_CHECK_STATUS = "*/30 * * * * ?";
|
public static final String CRON_CHECK_STATUS = "0 */1 * * * ?";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务状态检查优化配置
|
* 任务状态检查优化配置
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package cn.iocoder.yudao.module.tik.mix.service;
|
|||||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.iocoder.yudao.module.infra.service.file.FileService;
|
import cn.iocoder.yudao.module.infra.service.file.FileService;
|
||||||
|
import com.alibaba.ttl.TtlRunnable;
|
||||||
import cn.iocoder.yudao.module.tik.mix.client.IceClient;
|
import cn.iocoder.yudao.module.tik.mix.client.IceClient;
|
||||||
import cn.iocoder.yudao.module.tik.mix.constants.MixTaskConstants;
|
import cn.iocoder.yudao.module.tik.mix.constants.MixTaskConstants;
|
||||||
import cn.iocoder.yudao.module.tik.mix.dal.dataobject.MixTaskDO;
|
import cn.iocoder.yudao.module.tik.mix.dal.dataobject.MixTaskDO;
|
||||||
@@ -65,15 +68,15 @@ public class MixTaskServiceImpl implements MixTaskService {
|
|||||||
// 2. 保存到数据库
|
// 2. 保存到数据库
|
||||||
mixTaskMapper.insert(task);
|
mixTaskMapper.insert(task);
|
||||||
|
|
||||||
// 3. 异步提交到阿里云 ICE
|
// 3. 异步提交到阿里云 ICE(使用 TTL 自动传递上下文)
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(TtlRunnable.get(() -> {
|
||||||
try {
|
try {
|
||||||
submitToICE(task.getId(), createReqVO, userId);
|
submitToICE(task.getId(), createReqVO, userId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[MixTask][提交ICE失败] taskId={}", task.getId(), e);
|
log.error("[MixTask][提交ICE失败] taskId={}", task.getId(), e);
|
||||||
updateTaskError(task.getId(), "提交任务失败: " + e.getMessage());
|
updateTaskError(task.getId(), "提交任务失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
return task.getId();
|
return task.getId();
|
||||||
}
|
}
|
||||||
@@ -175,8 +178,8 @@ public class MixTaskServiceImpl implements MixTaskService {
|
|||||||
updateTask.setOutputUrlList(null);
|
updateTask.setOutputUrlList(null);
|
||||||
mixTaskMapper.updateById(updateTask);
|
mixTaskMapper.updateById(updateTask);
|
||||||
|
|
||||||
// 3. 重新提交到ICE
|
// 3. 重新提交到ICE(使用 TTL 自动传递上下文)
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(TtlRunnable.get(() -> {
|
||||||
try {
|
try {
|
||||||
// 从 materialsJson 重建请求对象
|
// 从 materialsJson 重建请求对象
|
||||||
List<MixTaskSaveReqVO.MaterialItem> materials = null;
|
List<MixTaskSaveReqVO.MaterialItem> materials = null;
|
||||||
@@ -207,7 +210,7 @@ public class MixTaskServiceImpl implements MixTaskService {
|
|||||||
log.error("[MixTask][重新提交失败] taskId={}", id, e);
|
log.error("[MixTask][重新提交失败] taskId={}", id, e);
|
||||||
updateTaskError(id, "重新提交失败: " + e.getMessage());
|
updateTaskError(id, "重新提交失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -247,20 +250,22 @@ public class MixTaskServiceImpl implements MixTaskService {
|
|||||||
// 4. 频率优化:定时任务频率从30秒改为2分钟
|
// 4. 频率优化:定时任务频率从30秒改为2分钟
|
||||||
LocalDateTime startTime = LocalDateTime.now().minusHours(MixTaskConstants.CHECK_HOURS_LIMIT);
|
LocalDateTime startTime = LocalDateTime.now().minusHours(MixTaskConstants.CHECK_HOURS_LIMIT);
|
||||||
|
|
||||||
// 查询运行中的任务(限制时间和数量)
|
// 查询运行中的任务(忽略租户过滤,因为定时任务没有租户上下文)
|
||||||
List<MixTaskDO> runningTasks = mixTaskMapper.selectList(
|
List<MixTaskDO> runningTasks = TenantUtils.executeIgnore(() ->
|
||||||
new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<MixTaskDO>()
|
mixTaskMapper.selectList(
|
||||||
.eq(MixTaskDO::getStatus, MixTaskConstants.STATUS_RUNNING)
|
new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<MixTaskDO>()
|
||||||
.ge(MixTaskDO::getCreateTime, startTime)
|
.eq(MixTaskDO::getStatus, MixTaskConstants.STATUS_RUNNING)
|
||||||
.orderByDesc(MixTaskDO::getCreateTime)
|
.ge(MixTaskDO::getCreateTime, startTime)
|
||||||
.last("LIMIT " + MixTaskConstants.CHECK_BATCH_SIZE) // 限制数量
|
.orderByDesc(MixTaskDO::getCreateTime)
|
||||||
|
.last("LIMIT " + MixTaskConstants.CHECK_BATCH_SIZE)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (runningTasks.isEmpty()) {
|
if (runningTasks.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 逐个检查任务状态
|
// 逐个检查任务状态(每个任务使用自己的租户上下文)
|
||||||
int failureCount = 0;
|
int failureCount = 0;
|
||||||
for (MixTaskDO task : runningTasks) {
|
for (MixTaskDO task : runningTasks) {
|
||||||
try {
|
try {
|
||||||
@@ -268,7 +273,8 @@ public class MixTaskServiceImpl implements MixTaskService {
|
|||||||
if (jobIds != null && !jobIds.isEmpty()) {
|
if (jobIds != null && !jobIds.isEmpty()) {
|
||||||
// 每个任务可能有多个jobId,取第一个进行检查
|
// 每个任务可能有多个jobId,取第一个进行检查
|
||||||
String jobId = jobIds.get(0);
|
String jobId = jobIds.get(0);
|
||||||
syncTaskStatus(task.getId(), jobId);
|
// 使用任务的租户ID执行状态同步
|
||||||
|
TenantUtils.execute(task.getTenantId(), () -> syncTaskStatus(task.getId(), jobId));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[MixTask][单个任务检查失败] taskId={}", task.getId(), e);
|
log.error("[MixTask][单个任务检查失败] taskId={}", task.getId(), e);
|
||||||
|
|||||||
Reference in New Issue
Block a user