feat: 视频问题
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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("。");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 "{}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user