feat: 视频问题

This commit is contained in:
2025-11-28 20:26:47 +08:00
parent 46b98e78e5
commit d9f3103304
26 changed files with 1582 additions and 1021 deletions

View File

@@ -1,11 +1,16 @@
package cn.iocoder.yudao.module.tik.file.dal.mysql;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserFileDO;
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFilePageReqVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户文件 Mapper
@@ -16,12 +21,13 @@ import org.apache.ibatis.annotations.Mapper;
public interface TikUserFileMapper extends BaseMapperX<TikUserFileDO> {
default PageResult<TikUserFileDO> selectPage(AppTikUserFilePageReqVO reqVO) {
// 常规查询支持通过groupId查询
return selectPage(reqVO, new LambdaQueryWrapperX<TikUserFileDO>()
.eqIfPresent(TikUserFileDO::getUserId, reqVO.getUserId())
.eqIfPresent(TikUserFileDO::getFileCategory, reqVO.getFileCategory())
.eqIfPresent(TikUserFileDO::getGroupId, reqVO.getGroupId())
.likeIfPresent(TikUserFileDO::getFileName, reqVO.getFileName())
.eqIfPresent(TikUserFileDO::getFileType, reqVO.getFileType())
.eqIfPresent(TikUserFileDO::getGroupId, reqVO.getGroupId())
.betweenIfPresent(TikUserFileDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(TikUserFileDO::getId));
}

View File

@@ -6,9 +6,11 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikFileGroupDO;
import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserFileDO;
import cn.iocoder.yudao.module.tik.file.dal.dataobject.TikUserFileGroupDO;
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikFileGroupMapper;
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileGroupMapper;
import cn.iocoder.yudao.module.tik.file.dal.mysql.TikUserFileMapper;
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikFileGroupCreateReqVO;
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikFileGroupRespVO;
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikFileGroupUpdateReqVO;
@@ -39,6 +41,9 @@ public class TikFileGroupServiceImpl implements TikFileGroupService {
@Resource
private TikUserFileGroupMapper userFileGroupMapper;
@Resource
private TikUserFileMapper userFileMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createGroup(AppTikFileGroupCreateReqVO createReqVO) {
@@ -91,10 +96,6 @@ public class TikFileGroupServiceImpl implements TikFileGroupService {
// 校验分组存在且属于当前用户
validateGroupExists(groupId, userId);
// 删除分组关联关系
userFileGroupMapper.delete(new LambdaQueryWrapperX<TikUserFileGroupDO>()
.eq(TikUserFileGroupDO::getGroupId, groupId));
// 逻辑删除分组
fileGroupMapper.deleteById(groupId);
@@ -118,10 +119,11 @@ public class TikFileGroupServiceImpl implements TikFileGroupService {
// 转换为VO并查询文件数量
return CollectionUtils.convertList(groups, group -> {
AppTikFileGroupRespVO vo = BeanUtils.toBean(group, AppTikFileGroupRespVO.class);
// 查询分组下的文件数量
Long fileCount = userFileGroupMapper.selectCount(
new LambdaQueryWrapperX<TikUserFileGroupDO>()
.eq(TikUserFileGroupDO::getGroupId, group.getId())
// 查询分组下的文件数量使用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;

View File

@@ -51,8 +51,8 @@ import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.*;
@Slf4j
public class TikUserFileServiceImpl implements TikUserFileService {
/** 预签名URL过期时间1小时,单位:秒) */
private static final int PRESIGN_URL_EXPIRATION_SECONDS = 3600;
/** 预签名URL过期时间24小时,单位:秒) */
private static final int PRESIGN_URL_EXPIRATION_SECONDS = 24 * 60 * 60;
@Resource
private TikUserFileMapper userFileMapper;
@@ -321,31 +321,33 @@ public class TikUserFileServiceImpl implements TikUserFileService {
vo.setIsVideo(isVideo);
vo.setIsImage(isImage);
// 生成封面和缩略图的预签名URL避免重复调用
String coverUrlPresigned = StrUtil.isNotBlank(file.getCoverUrl())
? getCachedPresignUrl(file.getCoverUrl(), PRESIGN_URL_EXPIRATION_SECONDS)
: null;
String thumbnailUrlPresigned = StrUtil.isNotBlank(file.getThumbnailUrl())
? getCachedPresignUrl(file.getThumbnailUrl(), PRESIGN_URL_EXPIRATION_SECONDS)
: null;
// 视频文件不使用任何OSS预签名URL包括coverUrl和previewUrl
// 只返回coverBase64如果有前端从本地缓存获取
String coverUrlPresigned = null;
String thumbnailUrlPresigned = null;
String previewUrl = null;
// 非视频文件才生成预签名URL
if (!isVideo) {
// 图片文件生成缩略图URL
thumbnailUrlPresigned = StrUtil.isNotBlank(file.getThumbnailUrl())
? getCachedPresignUrl(file.getThumbnailUrl(), PRESIGN_URL_EXPIRATION_SECONDS)
: null;
// 生成预览URL
if (isImage) {
// 图片:优先使用缩略图,没有缩略图时使用原图
previewUrl = thumbnailUrlPresigned != null
? thumbnailUrlPresigned
: getCachedPresignUrl(file.getFileUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
}
}
// 设置封面和缩略图URL
vo.setCoverUrl(coverUrlPresigned);
vo.setThumbnailUrl(thumbnailUrlPresigned);
// 生成预览URL(优先使用封面/缩略图否则使用原文件URL
String previewUrl = null;
if (isVideo) {
// 视频优先使用封面没有封面时使用原视频URL
previewUrl = coverUrlPresigned != null
? coverUrlPresigned
: getCachedPresignUrl(file.getFileUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
} else if (isImage) {
// 图片:优先使用缩略图,没有缩略图时使用原图
previewUrl = thumbnailUrlPresigned != null
? thumbnailUrlPresigned
: getCachedPresignUrl(file.getFileUrl(), PRESIGN_URL_EXPIRATION_SECONDS);
}
// 生成预览URL
vo.setPreviewUrl(previewUrl);
return vo;
@@ -472,7 +474,13 @@ public class TikUserFileServiceImpl implements TikUserFileService {
* @return 预览URL
*/
private String determinePreviewUrl(TikUserFileDO file, String type) {
// 明确指定封面类型
// 视频文件不返回任何OSS URL前端使用coverBase64缓存
boolean isVideo = StrUtil.containsIgnoreCase(file.getFileType(), "video");
if (isVideo) {
return null;
}
// 明确指定封面类型(非视频文件)
if (StrUtil.equals(type, "cover") && StrUtil.isNotBlank(file.getCoverUrl())) {
return file.getCoverUrl();
}
@@ -480,20 +488,16 @@ public class TikUserFileServiceImpl implements TikUserFileService {
if (StrUtil.equals(type, "thumbnail") && StrUtil.isNotBlank(file.getThumbnailUrl())) {
return file.getThumbnailUrl();
}
// 根据文件类型自动选择
boolean isVideo = StrUtil.containsIgnoreCase(file.getFileType(), "video");
boolean isImage = FileTypeUtils.isImage(file.getFileType());
if (isVideo && StrUtil.isNotBlank(file.getCoverUrl())) {
return file.getCoverUrl();
}
if (isImage) {
return StrUtil.isNotBlank(file.getThumbnailUrl())
? file.getThumbnailUrl()
return StrUtil.isNotBlank(file.getThumbnailUrl())
? file.getThumbnailUrl()
: file.getFileUrl();
}
// 默认返回原文件URL
return file.getFileUrl();
}

View File

@@ -84,6 +84,7 @@ public class BatchProduceAlignment {
public void initClient() throws Exception {
log.info("初始化阿里云 ICE 客户端...");
// 阿里云账号AccessKey拥有所有API的访问权限建议您使用RAM用户进行API访问或日常运维。
// 本示例以将AccessKey ID和 AccessKey Secret保存在环境变量为例说明。配置方法请参见https://help.aliyun.com/zh/sdk/developer-reference/v2-manage-access-credentials?spm=a2c4g.11186623.0.0.423350fbOTFdOB#2a38e5c14b4em
com.aliyun.credentials.Client credentialClient = new com.aliyun.credentials.Client();
@@ -95,9 +96,14 @@ public class BatchProduceAlignment {
config.endpoint = "ice." + regionId + ".aliyuncs.com";
config.regionId = regionId;
iceClient = new Client(config);
log.info("ICE 客户端初始化成功");
}
public List<String> batchProduceAlignment(String title,String text,String[] videoArray,String[] bgMusicArray,int produceCount) throws Exception {
// 初始化 ICE 客户端
if (iceClient == null) {
initClient();
}
// 批量提交任务,返回 "jobId:url" 格式
List<String> jobIdWithUrls = new ArrayList<>();
@@ -110,6 +116,11 @@ public class BatchProduceAlignment {
}
public String produceSingleVideo(String title, String text, String[] videoArray, String[] bgMusicArray) throws Exception {
// 初始化 ICE 客户端
if (iceClient == null) {
initClient();
}
text = text.replace("", "");
text = text.replace("\n", "");
String[] sentenceArray = text.split("");

View File

@@ -0,0 +1,114 @@
package cn.iocoder.yudao.module.tik.mix.client;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.tik.mix.config.IceProperties;
import com.aliyun.ice20201109.Client;
import com.aliyun.ice20201109.models.*;
import com.aliyun.teaopenapi.models.Config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.module.tik.enums.ErrorCodeConstants.VOICE_TTS_FAILED;
/**
* 阿里云 ICE 客户端
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class IceClient {
private final IceProperties properties;
private volatile Client iceClient;
/**
* 获取 ICE 客户端(懒加载,线程安全)
*/
public Client getClient() {
if (iceClient == null) {
synchronized (this) {
if (iceClient == null) {
if (!properties.isEnabled()) {
throw exception0(VOICE_TTS_FAILED.getCode(), "未配置 ICE AccessKey");
}
iceClient = createClient();
}
}
}
return iceClient;
}
/**
* 创建 ICE 客户端
*/
private Client createClient() {
try {
log.info("[ICE][开始初始化客户端][regionId={}, bucket={}]",
properties.getRegionId(), properties.getBucket());
Config config = new Config();
config.accessKeyId = properties.getAccessKeyId();
config.accessKeySecret = properties.getAccessKeySecret();
config.endpoint = "ice." + properties.getRegionId() + ".aliyuncs.com";
config.regionId = properties.getRegionId();
Client client = new Client(config);
log.info("[ICE][客户端初始化成功]");
return client;
} catch (Exception e) {
log.error("[ICE][客户端初始化失败]", e);
throw exception0(VOICE_TTS_FAILED.getCode(), "ICE 客户端初始化失败: " + e.getMessage());
}
}
/**
* 提交媒体制作任务
*/
public String submitMediaProducingJob(String timeline, String outputMediaConfig) {
try {
Client client = getClient();
SubmitMediaProducingJobRequest request = new SubmitMediaProducingJobRequest();
request.setTimeline(timeline);
request.setOutputMediaConfig(outputMediaConfig);
SubmitMediaProducingJobResponse response = client.submitMediaProducingJob(request);
String jobId = response.getBody().getJobId();
log.info("[ICE][提交任务成功][jobId={}]", jobId);
return jobId;
} catch (Exception e) {
log.error("[ICE][提交任务失败]", e);
throw exception0(VOICE_TTS_FAILED.getCode(), "ICE 提交任务失败: " + e.getMessage());
}
}
/**
* 查询媒体制作任务状态
*/
public String getMediaProducingJobStatus(String jobId) {
try {
Client client = getClient();
GetMediaProducingJobRequest request = new GetMediaProducingJobRequest();
request.setJobId(jobId);
GetMediaProducingJobResponse response = client.getMediaProducingJob(request);
String status = response.getBody().getMediaProducingJob().getStatus();
log.debug("[ICE][查询任务状态][jobId={}, status={}]", jobId, status);
return status;
} catch (Exception e) {
log.error("[ICE][查询任务状态失败][jobId={}]", jobId, e);
throw exception0(VOICE_TTS_FAILED.getCode(), "ICE 查询任务状态失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.tik.mix.config;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 阿里云 ICE 配置属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "yudao.ice")
public class IceProperties {
/**
* AccessKey ID
*/
private String accessKeyId;
/**
* AccessKey Secret
*/
private String accessKeySecret;
/**
* 地域 ID
*/
private String regionId = "cn-hangzhou";
/**
* OSS Bucket 名称
*/
private String bucket = "muye-ai-chat";
/**
* 连接超时时间
*/
private Duration connectTimeout = Duration.ofSeconds(10);
/**
* 读取超时时间
*/
private Duration readTimeout = Duration.ofSeconds(120);
/**
* 是否启用
*/
private boolean enabled = true;
public boolean isEnabled() {
return enabled && StrUtil.isNotBlank(accessKeyId) && StrUtil.isNotBlank(accessKeySecret);
}
}

View File

@@ -303,6 +303,48 @@ public class MixTaskServiceImpl implements MixTaskService {
}
}
/**
* 提交任务到阿里云 ICE新版本使用 IceClient
*/
private void submitToIceV2(Long taskId, MixTaskSaveReqVO createReqVO) {
log.info("[V2][提交任务到ICE][taskId={}]", taskId);
try {
// 1. 更新任务状态
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, MixTaskConstants.PROGRESS_SUBMITTED);
// 2. 构建 timeline 和 outputMediaConfig
String timeline = buildTimeline(createReqVO);
String outputMediaConfig = buildOutputMediaConfig();
// 3. 提交任务
// TODO: 这里需要将 BatchProduceAlignment 的逻辑移到 IceClient 中
// String jobId = iceClient.submitMediaProducingJob(timeline, outputMediaConfig);
log.info("[V2][任务提交成功][taskId={}]", taskId);
} catch (Exception e) {
log.error("[V2][提交任务失败][taskId={}]", taskId, e);
updateTaskError(taskId, "ICE处理失败: " + e.getMessage());
}
}
/**
* 构建 ICE 时间线
*/
private String buildTimeline(MixTaskSaveReqVO reqVO) {
// TODO: 实现 timeline 构建逻辑
return "{}";
}
/**
* 构建输出媒体配置
*/
private String buildOutputMediaConfig() {
// TODO: 实现输出配置构建逻辑
return "{}";
}
/**
* 更新任务状态
*/