diff --git a/.gitignore b/.gitignore index c5abc96..84b9e58 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ docker/minio/config docker/xxljob/logs application-youlai.yml .claude +CLAUDE.md diff --git a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java index 5db1668..9210d1b 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -25,6 +25,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.boot.mini.model.query.AiTaskMediaQuery; import com.youlai.boot.mini.model.query.DiscoveryPublicWorkQuery; +import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; import com.youlai.boot.mini.model.vo.DiscoveryPublicWorkVO; import cn.hutool.core.util.StrUtil; @@ -179,4 +180,21 @@ public class AiGenerationController { return Result.success(result); } + @Operation(summary = "手动同步AI任务状态") + @PostMapping("/task/{taskUuid}/sync-status") + @RepeatSubmit(expire = 60) + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) + public Result syncTaskStatus(@PathVariable String taskUuid) { + Long userId = SecurityUtils.getUserId(); + MiniAiGenerationTask task = aiGenerationService.getTaskByUuid(taskUuid); + if (task == null) { + return Result.failed("任务不存在"); + } + if (!task.getMiniUserId().equals(userId)) { + return Result.failed("无权操作该任务"); + } + aiGenerationService.syncTaskStatus(taskUuid); + return Result.success(); + } + } diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java b/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java index 1f7a502..fd686cd 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java @@ -31,6 +31,9 @@ public class AiGenerationTaskVO { @Schema(description = "消耗积分") private Integer pointsConsumed; + @Schema(description = "可见范围:public(公开) / private(仅自己)") + private String visibility; + @Schema(description = "创建时间") private Date createTime; diff --git a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java index eeea838..d3974c1 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -51,4 +51,6 @@ public interface AiGenerationService { void updateTaskVisibility(Long userId, AiTaskVisibilityForm form); + void syncTaskStatus(String taskUuid); + } diff --git a/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java index f5d271e..1072025 100644 --- a/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java +++ b/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java @@ -123,6 +123,19 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Value("${ai.task.timeout-minutes:300}") private int taskTimeoutMinutes; + //AI任务状态查询接口 + @Value("${ai.status.single-image-url}") + private String aiSingleImageStatusUrl; + + @Value("${ai.status.four-panel-url}") + private String aiFourPanelStatusUrl; + + @Value("${ai.status.video-url}") + private String aiVideoStatusUrl; + + @Value("${ai.status.sync-min-interval-minutes:30}") + private int syncMinIntervalMinutes; + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; //AI四宫格生成积分规则编码 @@ -285,6 +298,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiGenerationTaskMapper.insert(task); + // 立即关联用户上传的参考文件到当前任务并软删除 + softDeleteUserUploads(userId, task.getId()); + // 组装第三方AI接口需要的参数 Map aiRequest = new HashMap<>(); aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); @@ -373,6 +389,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiGenerationTaskMapper.insert(task); + // 立即关联用户上传的参考文件到当前任务并软删除 + softDeleteUserUploads(userId, task.getId()); + // 组装第三方AI接口需要的参数 Map aiRequest = new HashMap<>(); aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); @@ -461,6 +480,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiGenerationTaskMapper.insert(task); + // 立即关联用户上传的参考文件到当前任务并软删除 + softDeleteUserUploads(userId, task.getId()); + // 组装第三方接口参数 Map aiRequest = new HashMap<>(); aiRequest.put("model", form.getModel() == null ? aiDefaultVideoModel : form.getModel()); @@ -564,18 +586,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); - // 更新用户上传的参考文件:关联当前任务ID并软删除 - LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); - updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); - aiTaskMediaMapper.update(null, updateMediaWrapper); - // 同步发送订阅消息通知 sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } @@ -654,7 +664,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { media.setUuid(mediaUuid) .setTaskId(task.getId()) .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") + .setFileSource("ai_generated") .setMediaType("image") .setSourceUrl(ossUrl) .setWidth(width) @@ -665,18 +675,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiTaskMediaMapper.insert(media); } - // 更新用户上传的参考文件:关联当前任务ID并软删除 - LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); - updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); - aiTaskMediaMapper.update(null, updateMediaWrapper); - // 同步发送订阅消息通知 sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } @@ -770,18 +768,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiTaskMediaMapper.insert(media); } - // 更新用户上传的参考文件:关联当前任务ID并软删除 - LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); - updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); - aiTaskMediaMapper.update(null, updateMediaWrapper); - // 同步发送订阅消息通知 sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } @@ -805,6 +791,77 @@ public class AiGenerationServiceImpl implements AiGenerationService { } } + @Override + @Transactional(rollbackFor = Exception.class) + public void syncTaskStatus(String taskUuid) { + MiniAiGenerationTask task = getTaskByUuid(taskUuid); + if (task == null) { + throw new BusinessException("任务不存在"); + } + + // 终态不允许手动同步 + if (task.getStatus() != null && (task.getStatus() == 1 || task.getStatus() == 2)) { + throw new BusinessException("任务已完成或已失败,无需同步"); + } + + // 创建后需等待配置的时间才能手动同步 + long elapsedMinutes = (System.currentTimeMillis() - task.getCreateTimestamp()) / 60000; + if (elapsedMinutes < syncMinIntervalMinutes) { + throw new BusinessException("任务创建不足" + syncMinIntervalMinutes + "分钟,请" + (syncMinIntervalMinutes - elapsedMinutes) + "分钟后再试"); + } + + String statusUrl = buildAiStatusUrl(task); + log.info("手动同步任务状态,taskUuid:{},type:{},statusUrl:{}", taskUuid, task.getType(), statusUrl); + + String responseBody; + try { + HttpResponse response = HttpRequest.get(statusUrl).timeout(10000).execute(); + if (!response.isOk()) { + log.error("AI状态查询返回非200,taskUuid:{},httpStatus:{}", taskUuid, response.getStatus()); + throw new BusinessException("查询AI任务状态失败,HTTP状态码: " + response.getStatus()); + } + responseBody = response.body(); + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + log.error("调用AI状态查询接口失败,taskUuid:{}", taskUuid, e); + throw new BusinessException("查询AI任务状态失败: " + e.getMessage()); + } + + log.info("AI状态查询响应,taskUuid:{},body:{}", taskUuid, responseBody); + processAiStatusResponse(task.getType(), responseBody); + } + + private String buildAiStatusUrl(MiniAiGenerationTask task) { + switch (task.getType()) { + case "img_single": + return aiSingleImageStatusUrl + task.getUuid(); + case "img_grid_4": + return aiFourPanelStatusUrl + task.getUuid(); + case "video": + if (StrUtil.isBlank(task.getVideoTaskUuid())) { + throw new BusinessException("视频任务尚未获取到第三方任务ID"); + } + return aiVideoStatusUrl + task.getVideoTaskUuid(); + default: + throw new BusinessException("不支持的任务类型: " + task.getType()); + } + } + + private void processAiStatusResponse(String type, String responseBody) { + switch (type) { + case "img_single": + handleTaskCallback(JSONUtil.toBean(responseBody, AiSingleImageCallbackForm.class)); + break; + case "img_grid_4": + handleFourPanelCallback(JSONUtil.toBean(responseBody, AiFourPanelCallbackForm.class)); + break; + case "video": + handleVideoTaskCallback(JSONUtil.toBean(responseBody, AiVideoCallbackVO.class)); + break; + } + } + @Override public MiniAiGenerationTask getTaskByUuid(String uuid) { return aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() @@ -1093,6 +1150,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { AiGenerationTaskVO vo = new AiGenerationTaskVO(); // vo.setId(task.getId()); vo.setUuid(task.getUuid()); + vo.setVisibility(task.getVisibility()); vo.setType(task.getType()); vo.setStatus(task.getStatus()); vo.setPointsConsumed(task.getPointsConsumed()); @@ -1181,6 +1239,23 @@ public class AiGenerationServiceImpl implements AiGenerationService { } } + /** + * 将用户当前未关联任务的上传文件关联到指定任务并软删除。 + * 在创建任务时调用,避免任务生成中用户仍能看到旧上传文件。 + */ + private void softDeleteUserUploads(Long userId, Long taskId) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(MiniAiTaskMedia::getMiniUserId, userId) + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, taskId) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + aiTaskMediaMapper.update(null, updateWrapper); + } + /** * 发送AI作品完成订阅消息通知 * @param userId 接收用户ID diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c45443c..05d52d6 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -233,20 +233,27 @@ wx: # AIGeneration 配置 ai: generate: - single-image-server-url: http://192.168.31.93:8001/api/v1/photo-to-comic - four-panel-server-url: http://192.168.31.93:8001/api/v1/four-panel-comic - video-server-url: http://192.168.31.93:8001/api/v1/video/submit + single-image-server-url: http://127.0.0.1:8001/api/v1/photo-to-comic + four-panel-server-url: http://127.0.0.1:8001/api/v1/four-panel-comic + video-server-url: http://127.0.0.1:8001/api/v1/video/submit callback: - single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/single-image/task/callback - four-panel-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/four-panel/task/callback + single-image-callback-url: https://0401-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/single-image/task/callback + four-panel-callback-url: https://0401-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/four-panel/task/callback #需要内网穿透工具 由火山方舟 api 回调,ngrok http 30101 替换为内网穿透工具地址 - video-url: http://101.34.78.57:30101/backend/api/v1/mini/ai/generation/video/task/callback + video-url: https://0401-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/video/task/callback default: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 task: # AI任务超时时间 单位分钟,默认300分钟 timeout-minutes: 300 + # AI任务状态查询接口 + status: + single-image-url: http://127.0.0.1:8001/api/v1/photo-to-comic/status/ + four-panel-url: http://127.0.0.1:8001/api/v1/four-panel-comic/status/ + video-url: http://127.0.0.1:8001/api/v1/video/status/ + # 手动同步最小间隔(分钟),任务创建后至少等待此时间才能手动同步 + sync-min-interval-minutes: 30 # 订阅模板配置 subscribe: