feat: 优化

This commit is contained in:
2025-11-30 18:06:54 +08:00
parent 853bedcb23
commit ac803ec03b
9 changed files with 1314 additions and 326 deletions

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.tik.media;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.tik.mix.config.IceProperties;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
@@ -34,6 +35,7 @@ import java.util.*;
public class BatchProduceAlignment {
private final IceProperties properties;
private final FileApi fileApi;
private Client iceClient;
public void initClient() throws Exception {
@@ -105,8 +107,9 @@ public class BatchProduceAlignment {
initClient();
}
// 纯画面模式:仅拼接视频片段,添加空的音频轨道
// 纯画面模式:仅拼接视频片段,为每个视频添加静音音频轨道
JSONArray videoClipArray = new JSONArray();
JSONArray audioClipArray = new JSONArray();
// 按顺序拼接视频片段(不随机打乱)
for (int i = 0; i < videoArray.length; i++) {
@@ -120,21 +123,39 @@ public class BatchProduceAlignment {
}
log.debug("[ICE][添加视频片段][{}: {}]", i + 1, videoUrl);
// 每个视频片段添加静音效果Volume: 0
String videoClip = "{\"MediaURL\":\""+videoUrl+"\",\"ReferenceClipId\":\""+clipId+"\",\"Volume\":0,\"Effects\":[{\"Type\":\"Background\",\"SubType\":\"Blur\",\"Radius\":0.1}]}";
// 使用标准的 MediaURL 参数(符合 ICE API 文档规范
String videoClip = "{\"MediaURL\":\"" + videoUrl + "\"}";
videoClipArray.add(JSONObject.parseObject(videoClip));
// 为每个视频片段添加静音的音频轨道
JSONObject audioClip = new JSONObject();
audioClip.put("MediaURL", videoUrl);
// 添加静音效果
JSONObject volumeEffect = new JSONObject();
volumeEffect.put("Type", "Volume");
volumeEffect.put("Gain", 0); // 0 表示静音
JSONArray effects = new JSONArray();
effects.add(volumeEffect);
audioClip.put("Effects", effects);
audioClipArray.add(audioClip);
}
// 添加空的音频轨道避免ICE报错
JSONArray emptyAudioClipArray = new JSONArray();
String timeline = "{\"VideoTracks\":[{\"VideoTrackClips\":"+videoClipArray.toJSONString()+"}],\"AudioTracks\":[{\"AudioTrackClips\":"+emptyAudioClipArray.toJSONString()+"}]}";
// 构建时间线,包含视频轨道和音频轨道
String timeline = "{\"VideoTracks\":[{\"VideoTrackClips\":"+videoClipArray.toJSONString()+"}],\"AudioTracks\":[{\"AudioTrackClips\":"+audioClipArray.toJSONString()+"}]}";
//
String targetFileName = UUID.randomUUID().toString().replace("-", "");
String outputMediaUrl = "http://" + properties.getBucket() + ".oss-" + properties.getRegionId() + ".aliyuncs.com/ice_output/" + targetFileName + ".mp4";
String bucketEndpoint = "https://" + properties.getBucket() + ".oss-" + properties.getRegionId() + ".aliyuncs.com";
String outputMediaPath = "/ice_output/" + targetFileName + ".mp4";
String outputMediaUrl = bucketEndpoint + outputMediaPath;
// 生成签名URL有效期24小时使用公共API
String signedUrl = fileApi.presignGetUrl(outputMediaUrl, 24 * 3600);
int width = 720;
int height = 1280;
String outputMediaConfig = "{\"MediaURL\":\"" + outputMediaUrl + "\",\"Width\":"+width+",\"Height\":"+height+"}";
int bitrate = 2000; // 输出码率 2000 Kbit/s符合 ICE API 文档推荐)
String outputMediaConfig = "{\"MediaURL\":\"" + signedUrl + "\",\"Width\":" + width + ",\"Height\":" + height + ",\"Bitrate\":" + bitrate + "}";
SubmitMediaProducingJobRequest request = new SubmitMediaProducingJobRequest();
request.setTimeline(timeline);

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.tik.mix.config;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 混剪任务设计优化
*
* @author 芋道源码
*/
@Component
public class MixTaskDataOptimization implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 统一通过 setOutputUrlList/getOutputUrlList 方法访问
// 禁止直接操作 outputUrls 字段
// 设计原则:封装优于修复
}
}

View File

@@ -1,9 +1,7 @@
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;
@@ -43,13 +41,13 @@ public class MixTaskDO extends TenantBaseDO {
private String text;
/**
* 视频素材URL列表(JSON)
* 视频素材URL列表(逗号分隔)
*/
@TableField("video_urls")
private String videoUrls;
/**
* 背景音乐URL列表(JSON)
* 背景音乐URL列表(逗号分隔)
*/
@TableField("bg_music_urls")
private String bgMusicUrls;
@@ -61,13 +59,13 @@ public class MixTaskDO extends TenantBaseDO {
private Integer produceCount;
/**
* 任务ID列表(JSON)
* 任务ID列表(逗号分隔)
*/
@TableField("job_ids")
private String jobIds;
/**
* 输出文件URL列表(JSON)
* 输出文件URL列表(逗号分隔)
*/
@TableField("output_urls")
private String outputUrls;
@@ -101,55 +99,67 @@ public class MixTaskDO extends TenantBaseDO {
* 获取视频URL列表
*/
public List<String> getVideoUrlList() {
return JsonUtils.parseObject(videoUrls, new TypeReference<List<String>>() {});
if (videoUrls == null || videoUrls.isEmpty()) {
return null;
}
return List.of(videoUrls.split(","));
}
/**
* 设置视频URL列表
*/
public void setVideoUrlList(List<String> videoUrls) {
this.videoUrls = JsonUtils.toJsonString(videoUrls);
this.videoUrls = videoUrls == null || videoUrls.isEmpty() ? null : String.join(",", videoUrls);
}
/**
* 获取背景音乐URL列表
*/
public List<String> getBgMusicUrlList() {
return JsonUtils.parseObject(bgMusicUrls, new TypeReference<List<String>>() {});
if (bgMusicUrls == null || bgMusicUrls.isEmpty()) {
return null;
}
return List.of(bgMusicUrls.split(","));
}
/**
* 设置背景音乐URL列表
*/
public void setBgMusicUrlList(List<String> bgMusicUrls) {
this.bgMusicUrls = JsonUtils.toJsonString(bgMusicUrls);
this.bgMusicUrls = bgMusicUrls == null || bgMusicUrls.isEmpty() ? null : String.join(",", bgMusicUrls);
}
/**
* 获取任务ID列表
*/
public List<String> getJobIdList() {
return JsonUtils.parseObject(jobIds, new TypeReference<List<String>>() {});
if (jobIds == null || jobIds.isEmpty()) {
return null;
}
return List.of(jobIds.split(","));
}
/**
* 设置任务ID列表
*/
public void setJobIdList(List<String> jobIds) {
this.jobIds = JsonUtils.toJsonString(jobIds);
this.jobIds = jobIds == null || jobIds.isEmpty() ? null : String.join(",", jobIds);
}
/**
* 获取输出URL列表
*/
public List<String> getOutputUrlList() {
return JsonUtils.parseObject(outputUrls, new TypeReference<List<String>>() {});
if (outputUrls == null || outputUrls.isEmpty()) {
return null;
}
return List.of(outputUrls.split(","));
}
/**
* 设置输出URL列表
*/
public void setOutputUrlList(List<String> outputUrls) {
this.outputUrls = JsonUtils.toJsonString(outputUrls);
this.outputUrls = outputUrls == null || outputUrls.isEmpty() ? null : String.join(",", outputUrls);
}
}

View File

@@ -17,7 +17,10 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 混剪任务 Service 实现
@@ -36,21 +39,21 @@ public class MixTaskServiceImpl implements MixTaskService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createMixTask(MixTaskSaveReqVO createReqVO, Long userId) {
log.info("开始创建混剪任务用户ID: {}, 标题: {}", userId, createReqVO.getTitle());
log.info("[MixTask][创建任务] userId={}, title={}, videoCount={}, produceCount={}",
userId, createReqVO.getTitle(), createReqVO.getVideoUrls().size(), createReqVO.getProduceCount());
// 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);
log.error("[MixTask][提交ICE失败] taskId={}, error={}", task.getId(), e.getMessage(), e);
updateTaskError(task.getId(), "提交任务失败: " + e.getMessage());
}
});
@@ -61,12 +64,10 @@ public class MixTaskServiceImpl implements MixTaskService {
@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());
log.warn("[MixTask][任务不存在] taskId={}", updateReqVO.getId());
return;
}
@@ -75,68 +76,58 @@ public class MixTaskServiceImpl implements MixTaskService {
// 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);
log.warn("[MixTask][任务不存在] taskId={}", 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);
MixTaskRespVO respVO = BeanUtils.toBean(task, MixTaskRespVO.class);
// 手动设置 outputUrls因为 DO 中是逗号分隔字符串VO 中是 List
if (respVO != null) {
respVO.setOutputUrls(task.getOutputUrlList());
}
return respVO;
}
@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);
return convertToRespVO(pageResult);
}
@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);
return convertToRespVO(pageResult);
}
@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);
log.warn("[MixTask][任务不存在] taskId={}", id);
return;
}
@@ -146,8 +137,8 @@ public class MixTaskServiceImpl implements MixTaskService {
updateTask.setStatus(MixTaskConstants.STATUS_PENDING);
updateTask.setProgress(0);
updateTask.setErrorMsg(null);
updateTask.setJobIds(null);
updateTask.setOutputUrls(null);
updateTask.setJobIdList(null);
updateTask.setOutputUrlList(null);
mixTaskMapper.updateById(updateTask);
// 3. 重新提交到ICE
@@ -160,29 +151,26 @@ public class MixTaskServiceImpl implements MixTaskService {
saveReqVO.setProduceCount(existTask.getProduceCount());
submitToICE(id, saveReqVO);
} catch (Exception e) {
log.error("重新提交任务失败任务ID: {}", id, e);
log.error("[MixTask][重新提交失败] taskId={}, error={}", id, e.getMessage(), 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);
log.warn("[MixTask][任务不存在] taskId={}", id);
return;
}
// 2. 检查任务状态
if (!MixTaskConstants.STATUS_RUNNING.equals(existTask.getStatus())) {
log.warn("任务非运行状态无法取消任务ID: {}, 状态: {}", id, existTask.getStatus());
log.warn("[MixTask][任务状态不允许取消] taskId={}, currentStatus={}, requiredStatus={}",
id, existTask.getStatus(), MixTaskConstants.STATUS_RUNNING);
return;
}
@@ -195,14 +183,10 @@ public class MixTaskServiceImpl implements MixTaskService {
updateTask.setProgress(MixTaskConstants.PROGRESS_COMPLETED);
updateTask.setErrorMsg("用户主动取消任务");
mixTaskMapper.updateById(updateTask);
log.info("任务取消成功任务ID: {}", id);
}
@Override
public void checkTaskStatusBatch() {
log.debug("开始批量检查任务状态");
// 性能优化点:
// 1. 时间范围限制只检查最近6小时内的任务避免检查历史任务
// 2. 数量限制每次最多检查50个任务避免单次查询过多
@@ -220,14 +204,11 @@ public class MixTaskServiceImpl implements MixTaskService {
);
if (runningTasks.isEmpty()) {
log.debug("没有最近{}小时内运行中的任务,跳过检查", MixTaskConstants.CHECK_HOURS_LIMIT);
return;
}
log.info("发现 {} 个最近{}小时内的运行中任务,开始检查状态(最多检查{}个)",
runningTasks.size(), MixTaskConstants.CHECK_HOURS_LIMIT, MixTaskConstants.CHECK_BATCH_SIZE);
// 逐个检查任务状态
int failureCount = 0;
for (MixTaskDO task : runningTasks) {
try {
List<String> jobIds = task.getJobIdList();
@@ -237,18 +218,15 @@ public class MixTaskServiceImpl implements MixTaskService {
syncTaskStatus(task.getId(), jobId);
}
} catch (Exception e) {
log.error("检查任务状态失败任务ID: {}", task.getId(), e);
log.error("[MixTask][单个任务检查失败] taskId={}, error={}", task.getId(), e.getMessage(), e);
failureCount++;
// 单个任务失败不影响其他任务
}
}
log.debug("批量检查任务状态完成");
}
@Override
public void syncTaskStatus(Long taskId, String jobId) {
log.debug("同步任务状态任务ID: {}, jobId: {}", taskId, jobId);
try {
// 检查任务是否超时超过12小时则标记为失败
MixTaskDO task = mixTaskMapper.selectById(taskId);
@@ -257,7 +235,7 @@ public class MixTaskServiceImpl implements MixTaskService {
if (createTime != null) {
long hoursPassed = ChronoUnit.HOURS.between(createTime, LocalDateTime.now());
if (hoursPassed > 12) {
log.warn("[ICE][任务超时自动标记为失败][taskId={}, 已运行{}小时]", taskId, hoursPassed);
log.warn("[MixTask][任务超时自动失败] taskId={}, runHours={}, threshold=12", taskId, hoursPassed);
updateTaskError(taskId, "任务执行超时超过12小时");
return;
}
@@ -266,12 +244,10 @@ public class MixTaskServiceImpl implements MixTaskService {
// 调用阿里云 ICE API 查询任务状态
String status = iceClient.getMediaProducingJobStatus(jobId);
log.debug("[ICE][查询到任务状态][taskId={}, jobId={}, status={}]", taskId, jobId, status);
// 根据ICE状态更新任务
if ("Success".equalsIgnoreCase(status) || "success".equalsIgnoreCase(status)) {
// 任务成功完成更新为100%
log.info("[ICE][任务执行成功][taskId={}, jobId={}]", taskId, jobId);
updateTaskStatus(taskId, MixTaskConstants.STATUS_SUCCESS, MixTaskConstants.PROGRESS_COMPLETED);
} else if ("Failed".equalsIgnoreCase(status) || "failed".equalsIgnoreCase(status) || "Failure".equalsIgnoreCase(status)) {
// 任务失败 - 获取详细错误信息
@@ -280,25 +256,24 @@ public class MixTaskServiceImpl implements MixTaskService {
// 尝试获取更详细的失败信息如果ICE API支持
errorMsg = "ICE任务执行失败状态: " + status;
} catch (Exception ex) {
log.warn("[ICE][获取详细失败信息失败][taskId={}]", taskId, ex);
log.warn("[MixTask][获取详细失败信息失败] taskId={}", taskId, ex);
}
log.error("[ICE][任务执行失败][taskId={}, jobId={}, status={}]", taskId, jobId, status);
log.error("[MixTask][ICE任务执行失败] taskId={}, jobId={}, iceStatus={}, errorMsg={}",
taskId, jobId, status, errorMsg);
updateTaskError(taskId, errorMsg);
} else if ("Running".equalsIgnoreCase(status) || "running".equalsIgnoreCase(status) || "Processing".equalsIgnoreCase(status)) {
// 任务仍在运行更新进度为70%
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, 70);
log.debug("[ICE][任务执行中][taskId={}, jobId={}, progress=70%]", taskId, jobId);
} else if ("Pending".equalsIgnoreCase(status) || "pending".equalsIgnoreCase(status)) {
// 任务等待中更新进度为60%
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, 60);
log.debug("[ICE][任务等待中][taskId={}, jobId={}, progress=60%]", taskId, jobId);
} else {
// 未知状态,记录日志但不更新
log.warn("[ICE][未知任务状态][taskId={}, jobId={}, status={}]", taskId, jobId, status);
log.warn("[MixTask][未知ICE状态] taskId={}, jobId={}, iceStatus={}", taskId, jobId, status);
}
} catch (Exception e) {
log.error("同步任务状态失败任务ID: {}, jobId: {}", taskId, jobId, e);
log.error("[MixTask][状态同步异常] taskId={}, jobId={}, error={}", taskId, jobId, e.getMessage(), e);
updateTaskError(taskId, "查询任务状态失败: " + e.getMessage());
}
}
@@ -306,8 +281,6 @@ public class MixTaskServiceImpl implements MixTaskService {
@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);
@@ -316,16 +289,12 @@ public class MixTaskServiceImpl implements MixTaskService {
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);
@@ -347,10 +316,8 @@ public class MixTaskServiceImpl implements MixTaskService {
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);
log.error("[MixTask][ICE提交失败] taskId={}, error={}", taskId, e.getMessage(), e);
updateTaskError(taskId, "ICE处理失败: " + e.getMessage());
// 注意:异步线程中不抛出异常,避免未处理异常
}
@@ -360,8 +327,6 @@ 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);
@@ -374,10 +339,8 @@ public class MixTaskServiceImpl implements MixTaskService {
// TODO: 这里需要将 BatchProduceAlignment 的逻辑移到 IceClient 中
// String jobId = iceClient.submitMediaProducingJob(timeline, outputMediaConfig);
log.info("[V2][任务提交成功][taskId={}]", taskId);
} catch (Exception e) {
log.error("[V2][提交任务失败][taskId={}]", taskId, e);
log.error("[MixTask][V2提交失败] taskId={}, error={}", taskId, e.getMessage(), e);
updateTaskError(taskId, "ICE处理失败: " + e.getMessage());
}
}
@@ -434,7 +397,29 @@ public class MixTaskServiceImpl implements MixTaskService {
updateTask.setErrorMsg(errorMsg);
updateTask.setFinishTime(java.time.LocalDateTime.now());
mixTaskMapper.updateById(updateTask);
}
log.error("任务执行失败任务ID: {}, 错误信息: {}", taskId, errorMsg);
/**
* 将DO分页结果转换为VO分页结果消除冗余代码
* 优化点:
* 1. 使用Map查找替代O(n)流操作,提升性能
* 2. 统一outputUrls转换逻辑避免代码重复
*/
private PageResult<MixTaskRespVO> convertToRespVO(PageResult<MixTaskDO> pageResult) {
if (pageResult == null || pageResult.getList().isEmpty()) {
return PageResult.empty(pageResult.getTotal());
}
// 创建Map用于高效查找O(1)复杂度替代流操作的O(n)复杂度
Map<Long, MixTaskDO> taskMap = pageResult.getList().stream()
.collect(Collectors.toMap(MixTaskDO::getId, Function.identity()));
return BeanUtils.toBean(pageResult, MixTaskRespVO.class, vo -> {
MixTaskDO task = taskMap.get(vo.getId());
if (task != null) {
// 手动设置 outputUrls因为 DO 中是逗号分隔字符串VO 中是 List
vo.setOutputUrls(task.getOutputUrlList());
}
});
}
}