混剪功能

This commit is contained in:
2025-11-24 23:51:22 +08:00
parent 159eb835d6
commit cea43dd635
23 changed files with 2203 additions and 1470 deletions

View File

@@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.tik.media;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.ice20201109.Client;
import com.aliyun.ice20201109.models.*;
import com.aliyun.teaopenapi.models.Config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
@@ -26,6 +26,8 @@ import java.util.*;
* <version>1.2.9</version>
* </dependency>
*/
@Slf4j
@Component
public class BatchProduceAlignment {
static final String regionId = "cn-hangzhou";
@@ -97,36 +99,14 @@ public class BatchProduceAlignment {
public List<String> batchProduceAlignment(String title,String text,String[] videoArray,String[] bgMusicArray,int produceCount) throws Exception {
// 批量提交任务
List<String> jobIds = new ArrayList<>();
// 批量提交任务,返回 "jobId:url" 格式
List<String> jobIdWithUrls = new ArrayList<>();
for (int i = 0; i < produceCount; i++) {
String jobId = produceSingleVideo(title, text, videoArray, bgMusicArray);
jobIds.add(jobId);
String jobIdWithUrl = produceSingleVideo(title, text, videoArray, bgMusicArray);
jobIdWithUrls.add(jobIdWithUrl);
}
while (true) {
Thread.sleep(3000);
boolean allFinished = true;
for (int i = 0; i < jobIds.size(); i++) {
String jobId = jobIds.get(i);
GetMediaProducingJobRequest req = new GetMediaProducingJobRequest();
req.setJobId(jobId);
GetMediaProducingJobResponse response = iceClient.getMediaProducingJob(req);
GetMediaProducingJobResponseBody.GetMediaProducingJobResponseBodyMediaProducingJob mediaProducingJob = response.getBody().getMediaProducingJob();
String status = mediaProducingJob.getStatus();
System.out.println("jobId: " + mediaProducingJob.getJobId() + ", status:" + status);
if ("Failed".equalsIgnoreCase(status)) {
throw new Exception("Produce failed. jobid: " + mediaProducingJob.getJobId());
}
if (!"Success".equalsIgnoreCase(status)) {
allFinished = false;
break;
}
}
if (allFinished) {
break;
}
}
return jobIds;
// 改为异步模式,不在这里等待
return jobIdWithUrls;
}
public String produceSingleVideo(String title, String text, String[] videoArray, String[] bgMusicArray) throws Exception {
@@ -166,8 +146,41 @@ public class BatchProduceAlignment {
request.setTimeline(timeline);
request.setOutputMediaConfig(outputMediaConfig);
SubmitMediaProducingJobResponse response = iceClient.submitMediaProducingJob(request);
//ystem.out.println("start job. jobid: " + response.getBody().getJobId() + ", outputMediaUrl: " + outputMediaUrl);
log.info("start job. jobid: " + response.getBody().getJobId() + ", outputMediaUrl: " + outputMediaUrl);
return response.getBody().getJobId() + " : " + outputMediaUrl;
}
/**
* 检查单个任务状态
*
* @param jobId 任务ID
* @return 任务状态Pending/Running/Success/Failed
*/
public String checkJobStatus(String jobId) throws Exception {
if (iceClient == null) {
initClient();
}
GetMediaProducingJobRequest req = new GetMediaProducingJobRequest();
req.setJobId(jobId);
GetMediaProducingJobResponse response = iceClient.getMediaProducingJob(req);
GetMediaProducingJobResponseBody.GetMediaProducingJobResponseBodyMediaProducingJob mediaProducingJob = response.getBody().getMediaProducingJob();
String status = mediaProducingJob.getStatus();
log.debug("jobId: {}, status: {}", mediaProducingJob.getJobId(), status);
return status;
}
/**
* 从任务ID中获取输出URL
*
* @param jobIdWithUrl jobId : url 格式的字符串
* @return 输出URL
*/
public String extractOutputUrl(String jobIdWithUrl) {
if (jobIdWithUrl == null || !jobIdWithUrl.contains(" : ")) {
return null;
}
return jobIdWithUrl.split(" : ")[1];
}
}

View File

@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.tik.mix.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 混剪任务配置
*
* @author 芋道源码
*/
@Configuration
@EnableScheduling
public class MixTaskConfig {
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.tik.mix.constants;
/**
* 混剪任务常量
*
* @author 芋道源码
*/
public class MixTaskConstants {
/**
* 任务状态
*/
public static final String STATUS_PENDING = "pending";
public static final String STATUS_RUNNING = "running";
public static final String STATUS_SUCCESS = "success";
public static final String STATUS_FAILED = "failed";
/**
* 任务进度
*/
public static final int PROGRESS_SUBMITTED = 10;
public static final int PROGRESS_UPLOADED = 50;
public static final int PROGRESS_COMPLETED = 100;
/**
* 定时任务配置
*/
public static final String CRON_CHECK_STATUS = "*/30 * * * * ?";
private MixTaskConstants() {
// 防止实例化
}
}

View File

@@ -0,0 +1,101 @@
package cn.iocoder.yudao.module.tik.mix.controller;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.tik.mix.service.MixTaskService;
import cn.iocoder.yudao.module.tik.mix.vo.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
/**
* 混剪任务 Controller
*
* @author 芋道源码
*/
@Tag(name = "混剪任务", description = "混剪任务相关接口")
@RestController
@RequestMapping("/api/mix")
@RequiredArgsConstructor
@Slf4j
public class MixTaskController {
private final MixTaskService mixTaskService;
@PostMapping("/create")
@Operation(summary = "创建混剪任务")
public CommonResult<Long> createMixTask(@Valid @RequestBody MixTaskSaveReqVO createReqVO) {
// 从当前登录用户获取 userId
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (userId == null) {
log.warn("获取用户ID失败用户未登录");
return CommonResult.error(GlobalErrorCodeConstants.UNAUTHORIZED);
}
log.info("用户创建混剪任务, userId={}, title={}", userId, createReqVO.getTitle());
Long taskId = mixTaskService.createMixTask(createReqVO, userId);
return CommonResult.success(taskId);
}
@PutMapping("/update")
@Operation(summary = "更新混剪任务")
public CommonResult<Boolean> updateMixTask(@Valid @RequestBody MixTaskUpdateReqVO updateReqVO) {
mixTaskService.updateMixTask(updateReqVO);
return CommonResult.success(true);
}
@DeleteMapping("/delete/{id}")
@Operation(summary = "删除混剪任务")
public CommonResult<Boolean> deleteMixTask(@PathVariable Long id) {
mixTaskService.deleteMixTask(id);
return CommonResult.success(true);
}
@GetMapping("/get/{id}")
@Operation(summary = "获得混剪任务")
public CommonResult<MixTaskRespVO> getMixTask(@PathVariable Long id) {
MixTaskRespVO mixTaskVO = mixTaskService.getMixTask(id);
return CommonResult.success(mixTaskVO);
}
@GetMapping("/page")
@Operation(summary = "获得混剪任务分页")
public CommonResult<PageResult<MixTaskRespVO>> getMixTaskPage(MixTaskPageReqVO pageReqVO) {
// 从当前登录用户获取 userId
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (userId == null) {
log.warn("获取用户ID失败用户未登录");
return CommonResult.error(GlobalErrorCodeConstants.UNAUTHORIZED);
}
PageResult<MixTaskRespVO> pageResult = mixTaskService.getUserMixTaskPage(pageReqVO, userId);
return CommonResult.success(pageResult);
}
@GetMapping("/status/{id}")
@Operation(summary = "查询任务状态")
public CommonResult<MixTaskRespVO> getTaskStatus(@PathVariable Long id) {
MixTaskRespVO taskStatusVO = mixTaskService.getTaskStatus(id);
return CommonResult.success(taskStatusVO);
}
@PostMapping("/retry/{id}")
@Operation(summary = "重新生成失败的任务")
public CommonResult<Boolean> retryTask(@PathVariable Long id) {
mixTaskService.retryTask(id);
return CommonResult.success(true);
}
@PostMapping("/cancel/{id}")
@Operation(summary = "取消任务")
public CommonResult<Boolean> cancelTask(@PathVariable Long id) {
mixTaskService.cancelTask(id);
return CommonResult.success(true);
}
}

View File

@@ -0,0 +1,155 @@
package cn.iocoder.yudao.module.tik.mix.dal.dataobject;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.List;
/**
* 混剪任务 DO
*
* @author 芋道源码
*/
@TableName("tik_mix_task")
@KeySequence("tik_mix_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL可不写。
@Data
@EqualsAndHashCode(callSuper = true)
public class MixTaskDO extends TenantBaseDO {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("tenant_id")
private Long tenantId;
@TableField("user_id")
private Long userId;
/**
* 视频标题
*/
@TableField("title")
private String title;
/**
* 文案内容
*/
@TableField("text")
private String text;
/**
* 视频素材URL列表(JSON)
*/
@TableField("video_urls")
private String videoUrls;
/**
* 背景音乐URL列表(JSON)
*/
@TableField("bg_music_urls")
private String bgMusicUrls;
/**
* 生成数量
*/
@TableField("produce_count")
private Integer produceCount;
/**
* 任务ID列表(JSON)
*/
@TableField("job_ids")
private String jobIds;
/**
* 输出文件URL列表(JSON)
*/
@TableField("output_urls")
private String outputUrls;
/**
* 任务状态
* 枚举pending(待处理), running(处理中), success(成功), failed(失败)
*/
@TableField("status")
private String status;
/**
* 进度(0-100)
*/
@TableField("progress")
private Integer progress;
/**
* 错误信息
*/
@TableField("error_msg")
private String errorMsg;
/**
* 完成时间
*/
@TableField("finish_time")
private LocalDateTime finishTime;
/**
* 获取视频URL列表
*/
public List<String> getVideoUrlList() {
return JsonUtils.parseObject(videoUrls, new TypeReference<List<String>>() {});
}
/**
* 设置视频URL列表
*/
public void setVideoUrlList(List<String> videoUrls) {
this.videoUrls = JsonUtils.toJsonString(videoUrls);
}
/**
* 获取背景音乐URL列表
*/
public List<String> getBgMusicUrlList() {
return JsonUtils.parseObject(bgMusicUrls, new TypeReference<List<String>>() {});
}
/**
* 设置背景音乐URL列表
*/
public void setBgMusicUrlList(List<String> bgMusicUrls) {
this.bgMusicUrls = JsonUtils.toJsonString(bgMusicUrls);
}
/**
* 获取任务ID列表
*/
public List<String> getJobIdList() {
return JsonUtils.parseObject(jobIds, new TypeReference<List<String>>() {});
}
/**
* 设置任务ID列表
*/
public void setJobIdList(List<String> jobIds) {
this.jobIds = JsonUtils.toJsonString(jobIds);
}
/**
* 获取输出URL列表
*/
public List<String> getOutputUrlList() {
return JsonUtils.parseObject(outputUrls, new TypeReference<List<String>>() {});
}
/**
* 设置输出URL列表
*/
public void setOutputUrlList(List<String> outputUrls) {
this.outputUrls = JsonUtils.toJsonString(outputUrls);
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.tik.mix.dal.mysql;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.tik.mix.dal.dataobject.MixTaskDO;
import cn.iocoder.yudao.module.tik.mix.vo.MixTaskPageReqVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 混剪任务 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface MixTaskMapper extends BaseMapperX<MixTaskDO> {
/**
* 分页查询(所有任务)
*/
default PageResult<MixTaskDO> selectPage(MixTaskPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<MixTaskDO>()
.eqIfPresent(MixTaskDO::getStatus, reqVO.getStatus())
.likeIfPresent(MixTaskDO::getTitle, reqVO.getTitle())
.geIfPresent(MixTaskDO::getCreateTime, reqVO.getCreateTimeStart())
.leIfPresent(MixTaskDO::getCreateTime, reqVO.getCreateTimeEnd())
.orderByDesc(MixTaskDO::getId)
);
}
/**
* 分页查询(用户任务)
*/
default PageResult<MixTaskDO> selectPageByUserId(MixTaskPageReqVO reqVO, Long userId) {
return selectPage(reqVO, new LambdaQueryWrapperX<MixTaskDO>()
.eq(MixTaskDO::getUserId, userId)
.eqIfPresent(MixTaskDO::getStatus, reqVO.getStatus())
.likeIfPresent(MixTaskDO::getTitle, reqVO.getTitle())
.geIfPresent(MixTaskDO::getCreateTime, reqVO.getCreateTimeStart())
.leIfPresent(MixTaskDO::getCreateTime, reqVO.getCreateTimeEnd())
.orderByDesc(MixTaskDO::getId)
);
}
/**
* 查询用户的所有混剪任务
*/
@Select("SELECT * FROM tik_mix_task WHERE user_id = #{userId} ORDER BY create_time DESC")
List<MixTaskDO> selectListByUserId(@Param("userId") Long userId);
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.tik.mix.job;
import cn.iocoder.yudao.module.tik.mix.constants.MixTaskConstants;
import cn.iocoder.yudao.module.tik.mix.service.MixTaskService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 混剪任务状态同步定时任务
*
* @author 芋道源码
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class MixTaskStatusSyncJob {
private final MixTaskService mixTaskService;
/**
* 每30秒检查一次任务状态
*/
@Scheduled(cron = MixTaskConstants.CRON_CHECK_STATUS)
public void syncTaskStatus() {
log.debug("开始同步混剪任务状态");
try {
mixTaskService.checkTaskStatusBatch();
} catch (Exception e) {
log.error("同步混剪任务状态失败", e);
}
}
}

View File

@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.tik.mix.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.tik.mix.vo.*;
import java.util.List;
/**
* 混剪任务 Service 接口
*
* @author 芋道源码
*/
public interface MixTaskService {
/**
* 创建混剪任务
*/
Long createMixTask(MixTaskSaveReqVO createReqVO, Long userId);
/**
* 更新混剪任务
*/
void updateMixTask(MixTaskUpdateReqVO updateReqVO);
/**
* 删除混剪任务
*/
void deleteMixTask(Long id);
/**
* 获得混剪任务
*/
MixTaskRespVO getMixTask(Long id);
/**
* 获得混剪任务分页
*/
PageResult<MixTaskRespVO> getMixTaskPage(MixTaskPageReqVO pageReqVO, Long userId);
/**
* 查询用户混剪任务列表
*/
PageResult<MixTaskRespVO> getUserMixTaskPage(MixTaskPageReqVO pageReqVO, Long userId);
/**
* 查询任务状态
*/
MixTaskRespVO getTaskStatus(Long id);
/**
* 重新生成失败的任务
*/
void retryTask(Long id);
/**
* 取消任务
*/
void cancelTask(Long id);
/**
* 批量检查任务状态
*/
void checkTaskStatusBatch();
/**
* 同步任务状态(从阿里云 ICE 查询)
*/
void syncTaskStatus(Long taskId, String jobId);
/**
* 保存任务结果
*/
void saveTaskResult(Long taskId, List<String> outputUrls);
}

View File

@@ -0,0 +1,345 @@
package cn.iocoder.yudao.module.tik.mix.service;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
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.mysql.MixTaskMapper;
import cn.iocoder.yudao.module.tik.mix.util.MixTaskUtils;
import cn.iocoder.yudao.module.tik.mix.vo.*;
import cn.iocoder.yudao.module.tik.media.BatchProduceAlignment;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 混剪任务 Service 实现
*
* @author 芋道源码
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MixTaskServiceImpl implements MixTaskService {
private final MixTaskMapper mixTaskMapper;
private final BatchProduceAlignment batchProduceAlignment;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createMixTask(MixTaskSaveReqVO createReqVO, Long userId) {
log.info("开始创建混剪任务用户ID: {}, 标题: {}", userId, createReqVO.getTitle());
// 1. 创建初始任务对象
MixTaskDO task = MixTaskUtils.createInitialTask(createReqVO, userId);
// 2. 保存到数据库
mixTaskMapper.insert(task);
log.info("任务已创建任务ID: {}", task.getId());
// 3. 异步提交到阿里云 ICE
CompletableFuture.runAsync(() -> {
try {
submitToICE(task.getId(), createReqVO);
} catch (Exception e) {
log.error("提交任务到ICE失败任务ID: {}", task.getId(), e);
updateTaskError(task.getId(), "提交任务失败: " + e.getMessage());
}
});
return task.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMixTask(MixTaskUpdateReqVO updateReqVO) {
log.info("更新混剪任务任务ID: {}", updateReqVO.getId());
// 1. 检查任务是否存在
MixTaskDO existTask = mixTaskMapper.selectById(updateReqVO.getId());
if (existTask == null) {
log.error("任务不存在任务ID: {}", updateReqVO.getId());
return;
}
// 2. 创建更新对象
MixTaskDO updateTask = MixTaskUtils.createTaskUpdate(updateReqVO.getId(), updateReqVO);
// 3. 执行更新
mixTaskMapper.updateById(updateTask);
log.info("任务更新成功任务ID: {}", updateReqVO.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteMixTask(Long id) {
log.info("删除混剪任务任务ID: {}", id);
// 1. 检查任务是否存在
MixTaskDO existTask = mixTaskMapper.selectById(id);
if (existTask == null) {
log.error("任务不存在任务ID: {}", id);
return;
}
// 2. 执行删除
mixTaskMapper.deleteById(id);
log.info("任务删除成功任务ID: {}", id);
}
@Override
public MixTaskRespVO getMixTask(Long id) {
log.debug("查询混剪任务任务ID: {}", id);
MixTaskDO task = mixTaskMapper.selectById(id);
return BeanUtils.toBean(task, MixTaskRespVO.class);
}
@Override
public PageResult<MixTaskRespVO> getMixTaskPage(MixTaskPageReqVO pageReqVO, Long userId) {
log.debug("分页查询混剪任务用户ID: {}, 页码: {}, 页大小: {}",
userId, pageReqVO.getPageNo(), pageReqVO.getPageSize());
PageResult<MixTaskDO> pageResult = mixTaskMapper.selectPage(pageReqVO);
return BeanUtils.toBean(pageResult, MixTaskRespVO.class);
}
@Override
public PageResult<MixTaskRespVO> getUserMixTaskPage(MixTaskPageReqVO pageReqVO, Long userId) {
log.debug("分页查询用户混剪任务用户ID: {}, 页码: {}, 页大小: {}",
userId, pageReqVO.getPageNo(), pageReqVO.getPageSize());
// 使用用户ID过滤查询
PageResult<MixTaskDO> pageResult = mixTaskMapper.selectPageByUserId(pageReqVO, userId);
return BeanUtils.toBean(pageResult, MixTaskRespVO.class);
}
@Override
public MixTaskRespVO getTaskStatus(Long id) {
log.debug("查询任务状态任务ID: {}", id);
return getMixTask(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void retryTask(Long id) {
log.info("重新生成失败任务任务ID: {}", id);
// 1. 查询原任务
MixTaskDO existTask = mixTaskMapper.selectById(id);
if (existTask == null) {
log.error("任务不存在任务ID: {}", id);
return;
}
// 2. 重置任务状态
MixTaskDO updateTask = new MixTaskDO();
updateTask.setId(id);
updateTask.setStatus(MixTaskConstants.STATUS_PENDING);
updateTask.setProgress(0);
updateTask.setErrorMsg(null);
updateTask.setJobIds(null);
updateTask.setOutputUrls(null);
mixTaskMapper.updateById(updateTask);
// 3. 重新提交到ICE
CompletableFuture.runAsync(() -> {
try {
MixTaskSaveReqVO saveReqVO = BeanUtils.toBean(existTask, MixTaskSaveReqVO.class);
submitToICE(id, saveReqVO);
} catch (Exception e) {
log.error("重新提交任务失败任务ID: {}", id, e);
updateTaskError(id, "重新提交失败: " + e.getMessage());
}
});
log.info("任务重新提交成功任务ID: {}", id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelTask(Long id) {
log.info("取消任务任务ID: {}", id);
// 1. 查询任务
MixTaskDO existTask = mixTaskMapper.selectById(id);
if (existTask == null) {
log.error("任务不存在任务ID: {}", id);
return;
}
// 2. 检查任务状态
if (!MixTaskConstants.STATUS_RUNNING.equals(existTask.getStatus())) {
log.warn("任务非运行状态无法取消任务ID: {}, 状态: {}", id, existTask.getStatus());
return;
}
// 3. TODO: 调用阿里云 ICE 取消接口
// 4. 更新任务状态为失败
MixTaskDO updateTask = new MixTaskDO();
updateTask.setId(id);
updateTask.setStatus(MixTaskConstants.STATUS_FAILED);
updateTask.setProgress(MixTaskConstants.PROGRESS_COMPLETED);
updateTask.setErrorMsg("用户主动取消任务");
mixTaskMapper.updateById(updateTask);
log.info("任务取消成功任务ID: {}", id);
}
@Override
public void checkTaskStatusBatch() {
log.debug("开始批量检查任务状态");
// 查询所有运行中的任务
List<MixTaskDO> runningTasks = mixTaskMapper.selectList(
new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<MixTaskDO>()
.eq(MixTaskDO::getStatus, MixTaskConstants.STATUS_RUNNING)
);
if (runningTasks.isEmpty()) {
log.debug("没有运行中的任务,跳过检查");
return;
}
log.info("发现 {} 个运行中的任务,开始检查状态", runningTasks.size());
// 逐个检查任务状态
for (MixTaskDO task : runningTasks) {
try {
List<String> jobIds = task.getJobIdList();
if (jobIds != null && !jobIds.isEmpty()) {
// 每个任务可能有多个jobId取第一个进行检查
String jobId = jobIds.get(0);
syncTaskStatus(task.getId(), jobId);
}
} catch (Exception e) {
log.error("检查任务状态失败任务ID: {}", task.getId(), e);
}
}
log.debug("批量检查任务状态完成");
}
@Override
public void syncTaskStatus(Long taskId, String jobId) {
log.debug("同步任务状态任务ID: {}, jobId: {}", taskId, jobId);
try {
// TODO: 调用阿里云 ICE API 查询任务状态
// 这里需要集成具体的 ICE SDK 或 HTTP API
// 模拟状态检查逻辑
// String status = iceClient.getJobStatus(jobId);
// String progress = iceClient.getJobProgress(jobId);
// String outputUrl = iceClient.getJobOutput(jobId);
// 根据返回的状态更新任务
// updateTaskStatus(taskId, status, progress, outputUrl);
} catch (Exception e) {
log.error("同步任务状态失败任务ID: {}, jobId: {}", taskId, jobId, e);
updateTaskError(taskId, "查询任务状态失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveTaskResult(Long taskId, List<String> outputUrls) {
log.info("保存任务结果任务ID: {}, 结果数量: {}", taskId, outputUrls.size());
// 1. 更新任务输出URL
MixTaskDO updateTask = new MixTaskDO();
updateTask.setId(taskId);
updateTask.setOutputUrlList(outputUrls);
updateTask.setStatus(MixTaskConstants.STATUS_SUCCESS);
updateTask.setProgress(MixTaskConstants.PROGRESS_COMPLETED);
updateTask.setFinishTime(java.time.LocalDateTime.now());
mixTaskMapper.updateById(updateTask);
log.info("任务结果保存成功任务ID: {}", taskId);
}
/**
* 提交任务到阿里云 ICE
*/
private void submitToICE(Long taskId, MixTaskSaveReqVO createReqVO) {
log.info("提交任务到ICE任务ID: {}", taskId);
try {
// 1. 更新任务状态为运行中进度10%
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, MixTaskConstants.PROGRESS_SUBMITTED);
// 2. 转换为ICE需要的参数格式
String[] videoArray = createReqVO.getVideoUrls().toArray(new String[0]);
String[] bgMusicArray = createReqVO.getBgMusicUrls().toArray(new String[0]);
// 3. 调用ICE批量生成接口
List<String> jobIdWithUrls = batchProduceAlignment.batchProduceAlignment(
createReqVO.getTitle(),
createReqVO.getText(),
videoArray,
bgMusicArray,
createReqVO.getProduceCount()
);
// 4. 解析jobId和输出URL
MixTaskUtils.JobIdUrlPair jobIdUrlPair = MixTaskUtils.parseJobIdsAndUrls(jobIdWithUrls);
// 5. 更新任务信息
updateTaskWithResults(taskId, jobIdUrlPair.getJobIds(), jobIdUrlPair.getOutputUrls(),
MixTaskConstants.STATUS_RUNNING, MixTaskConstants.PROGRESS_UPLOADED);
log.info("任务提交到ICE成功任务ID: {}, jobId数量: {}", taskId, jobIdUrlPair.getJobIds().size());
} catch (Exception e) {
log.error("提交任务到ICE失败任务ID: {}", taskId, e);
updateTaskError(taskId, "ICE处理失败: " + e.getMessage());
// 注意:异步线程中不抛出异常,避免未处理异常
}
}
/**
* 更新任务状态
*/
private void updateTaskStatus(Long taskId, String status, Integer progress) {
MixTaskDO updateTask = new MixTaskDO();
updateTask.setId(taskId);
updateTask.setStatus(status);
updateTask.setProgress(progress);
mixTaskMapper.updateById(updateTask);
}
/**
* 更新任务结果
*/
private void updateTaskWithResults(Long taskId, List<String> jobIds, List<String> outputUrls,
String status, Integer progress) {
MixTaskDO updateTask = new MixTaskDO();
updateTask.setId(taskId);
updateTask.setStatus(status);
updateTask.setProgress(progress);
updateTask.setJobIdList(jobIds);
updateTask.setOutputUrlList(outputUrls);
mixTaskMapper.updateById(updateTask);
}
/**
* 更新任务错误信息
*/
private void updateTaskError(Long taskId, String errorMsg) {
MixTaskDO updateTask = new MixTaskDO();
updateTask.setId(taskId);
updateTask.setStatus(MixTaskConstants.STATUS_FAILED);
updateTask.setProgress(MixTaskConstants.PROGRESS_COMPLETED);
updateTask.setErrorMsg(errorMsg);
updateTask.setFinishTime(java.time.LocalDateTime.now());
mixTaskMapper.updateById(updateTask);
log.error("任务执行失败任务ID: {}, 错误信息: {}", taskId, errorMsg);
}
}

View File

@@ -0,0 +1,130 @@
package cn.iocoder.yudao.module.tik.mix.util;
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.vo.MixTaskSaveReqVO;
import cn.iocoder.yudao.module.tik.mix.vo.MixTaskUpdateReqVO;
import java.util.ArrayList;
import java.util.List;
/**
* 混剪任务工具类
*
* @author 芋道源码
*/
public class MixTaskUtils {
private MixTaskUtils() {
// 防止实例化
}
/**
* 创建初始任务对象
*/
public static MixTaskDO createInitialTask(MixTaskSaveReqVO reqVO, Long userId) {
MixTaskDO task = new MixTaskDO();
task.setUserId(userId);
task.setTitle(reqVO.getTitle());
task.setText(reqVO.getText());
task.setVideoUrlList(reqVO.getVideoUrls());
task.setBgMusicUrlList(reqVO.getBgMusicUrls());
task.setProduceCount(reqVO.getProduceCount());
task.setStatus(MixTaskConstants.STATUS_PENDING);
task.setProgress(0);
return task;
}
/**
* 创建任务更新对象
*/
public static MixTaskDO createTaskUpdate(Long taskId, MixTaskUpdateReqVO reqVO) {
MixTaskDO task = new MixTaskDO();
task.setId(taskId);
task.setTitle(reqVO.getTitle());
task.setText(reqVO.getText());
task.setVideoUrlList(reqVO.getVideoUrls());
task.setBgMusicUrlList(reqVO.getBgMusicUrls());
task.setProduceCount(reqVO.getProduceCount());
task.setJobIdList(reqVO.getJobIds());
task.setOutputUrlList(reqVO.getOutputUrls());
task.setStatus(reqVO.getStatus());
task.setProgress(reqVO.getProgress());
task.setErrorMsg(reqVO.getErrorMsg());
return task;
}
/**
* 解析 jobId 和输出 URL
*
* @param jobIdWithUrls jobId:url 格式的列表
* @return 解析结果
*/
public static JobIdUrlPair parseJobIdsAndUrls(List<String> jobIdWithUrls) {
List<String> jobIds = new ArrayList<>();
List<String> outputUrls = new ArrayList<>();
for (String jobIdWithUrl : jobIdWithUrls) {
String[] parts = jobIdWithUrl.split(" : ");
if (parts.length == 2) {
jobIds.add(parts[0]);
outputUrls.add(parts[1]);
}
}
return new JobIdUrlPair(jobIds, outputUrls);
}
/**
* 任务进度计算器
*/
public static class ProgressCalculator {
/**
* 计算任务进度
*
* @param totalCount 总数量
* @param completedCount 完成数量
* @param failedCount 失败数量
* @return 进度百分比
*/
public static int calculate(int totalCount, int completedCount, int failedCount) {
return (completedCount + failedCount) * MixTaskConstants.PROGRESS_COMPLETED / totalCount;
}
/**
* 判断任务是否完全失败
*/
public static boolean isAllFailed(int totalCount, int failedCount) {
return failedCount == totalCount;
}
/**
* 判断任务是否全部完成
*/
public static boolean isAllCompleted(int totalCount, int completedCount) {
return completedCount == totalCount;
}
}
/**
* jobId 和 URL 解析结果
*/
public static class JobIdUrlPair {
private final List<String> jobIds;
private final List<String> outputUrls;
public JobIdUrlPair(List<String> jobIds, List<String> outputUrls) {
this.jobIds = jobIds;
this.outputUrls = outputUrls;
}
public List<String> getJobIds() {
return jobIds;
}
public List<String> getOutputUrls() {
return outputUrls;
}
}
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.tik.mix.vo;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 混剪任务分页 Request VO")
@Data
public class MixTaskPageReqVO extends SortablePageParam {
@Schema(description = "任务状态", example = "running")
private String status;
@Schema(description = "视频标题", example = "美食")
private String title;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间开始")
private LocalDateTime createTimeStart;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间结束")
private LocalDateTime createTimeEnd;
}

View File

@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.tik.mix.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 混剪任务 Response VO")
@Data
public class MixTaskRespVO {
@Schema(description = "ID", example = "1")
private Long id;
@Schema(description = "用户ID", example = "1")
private Long userId;
@Schema(description = "视频标题", example = "美食纪录片")
private String title;
@Schema(description = "文案内容")
private String text;
@Schema(description = "视频素材URL列表")
private List<String> videoUrls;
@Schema(description = "背景音乐URL列表")
private List<String> bgMusicUrls;
@Schema(description = "生成数量", example = "3")
private Integer produceCount;
@Schema(description = "任务ID列表")
private List<String> jobIds;
@Schema(description = "输出文件URL列表")
private List<String> outputUrls;
@Schema(description = "任务状态", example = "running")
private String status;
@Schema(description = "进度(0-100)", example = "65")
private Integer progress;
@Schema(description = "错误信息")
private String errorMsg;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "完成时间")
private LocalDateTime finishTime;
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.tik.mix.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
@Schema(description = "管理后台 - 混剪任务创建 Request VO")
@Data
public class MixTaskSaveReqVO {
@Schema(description = "视频标题", required = true, example = "美食纪录片")
@NotBlank(message = "视频标题不能为空")
private String title;
@Schema(description = "文案内容", required = true, example = "人们懂得用五味杂陈形容人生...")
@NotBlank(message = "文案内容不能为空")
private String text;
@Schema(description = "视频素材URL列表", required = true)
@NotEmpty(message = "视频素材不能为空")
private List<String> videoUrls;
@Schema(description = "背景音乐URL列表", required = true)
@NotEmpty(message = "背景音乐不能为空")
private List<String> bgMusicUrls;
@Schema(description = "生成数量", required = true, example = "3")
@NotNull(message = "生成数量不能为空")
private Integer produceCount;
}

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.tik.mix.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 混剪任务更新 Request VO")
@Data
public class MixTaskUpdateReqVO {
@Schema(description = "ID", required = true, example = "1")
private Long id;
@Schema(description = "视频标题", example = "美食纪录片")
private String title;
@Schema(description = "文案内容")
private String text;
@Schema(description = "视频素材URL列表")
private List<String> videoUrls;
@Schema(description = "背景音乐URL列表")
private List<String> bgMusicUrls;
@Schema(description = "生成数量", example = "3")
private Integer produceCount;
@Schema(description = "任务ID列表")
private List<String> jobIds;
@Schema(description = "输出文件URL列表")
private List<String> outputUrls;
@Schema(description = "任务状态", example = "running")
private String status;
@Schema(description = "进度(0-100)", example = "65")
private Integer progress;
@Schema(description = "错误信息")
private String errorMsg;
}