From e5489fe290e1b872243915963bbcc29948f5d4b8 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 14 May 2026 17:26:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=86=E9=A2=91=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=BB=BB=E5=8A=A1=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 29 +- .../model/entity/MiniAiGenerationTask.java | 4 + .../model/form/AiFourPanelGenerateForm.java | 54 +++ .../mini/model/form/AiVideoGenerateForm.java | 28 ++ .../boot/mini/model/vo/AiVideoCallbackVO.java | 28 ++ .../mini/model/vo/VideoCallbackContent.java | 19 ++ .../boot/mini/model/vo/VideoCallbackData.java | 61 ++++ .../mini/model/vo/VideoCallbackUsage.java | 23 ++ .../mini/service/AiGenerationService.java | 27 +- .../service/impl/AiGenerationServiceImpl.java | 311 ++++++++++++++++-- src/main/resources/application-dev.yml | 13 +- 11 files changed, 548 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java 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 e5e1696..26f9544 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -6,8 +6,11 @@ import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; +import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.service.AiGenerationService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -63,9 +66,31 @@ public class AiGenerationController { return Result.success(success); } + @Operation(summary = "提交四宫格漫画生成任务") + @PostMapping("/generate-four-panel") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) + public Result generateFourPanelImage(@Valid @RequestBody AiFourPanelGenerateForm form) { + Long userId = SecurityUtils.getUserId(); + String taskUuid = aiGenerationService.createAndGenerateFourPanel(form, userId); + return Result.success(taskUuid); + } - // TODO: 添加 4宫格图片生成任务 接口 ,外部服务接口为:http://127.0.0.1:8001//api/v1/four-panel-comic - + @Operation(summary = "提交视频生成任务") + @PostMapping("/generate-video") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) + public Result generateVideo(@Valid @RequestBody AiVideoGenerateForm form) { + Long userId = SecurityUtils.getUserId(); + String taskUuid = aiGenerationService.createAndGenerateVideo(form, userId); + return Result.success(taskUuid); + } + //TODO: 后续增加用户小程序订阅通知 + @Operation(summary = "视频任务回调接口") + @PostMapping("/video/task/callback") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) + public Result videoTaskCallback(@Valid @RequestBody AiVideoCallbackVO vo) { + boolean success = aiGenerationService.handleVideoTaskCallback(vo); + return Result.success(success); + } } diff --git a/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java b/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java index a8dacb2..28af4d2 100644 --- a/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java +++ b/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java @@ -40,6 +40,10 @@ public class MiniAiGenerationTask implements Serializable { @Schema(description = "生成参数JSON,不同类型参数不同") private String generateParams; + @TableField("video_task_uuid") + @Schema(description = "调用视频接口时返回") + private String videoTaskUuid; + @TableField("status") @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") private Integer status; diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java new file mode 100644 index 0000000..3f6f0ad --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java @@ -0,0 +1,54 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.util.List; + +/** + * AI四宫格漫画生成请求表单 + * + * @author youlai + */ +@Data +@Schema(description = "AI四宫格漫画生成请求表单") +public class AiFourPanelGenerateForm { + + @Schema(description = "参考图片URL列表,可选") + private List referenceImage; + + @Schema(description = "使用的模型ID", defaultValue = "doubao-seedream-5-0-260128") + private String model; + + @Schema(description = "画风:1=治愈系水彩,2=Q版卡通,3=日式漫画,4=吉卜力风,5=3D毛绒,6=搞笑夸张", defaultValue = "1") + private Integer style = 1; + + @Schema(description = "比例:1=1:1,2=2:3,3=3:4,4=4:3,5=5:4", defaultValue = "1") + private Integer ratio = 1; + + @Schema(description = "图片尺寸:1=2K,2=4K,3=8K", defaultValue = "1") + private Integer imgSize = 1; + + @Schema(description = "图片格式:1=png,2=jpeg", defaultValue = "1") + private Integer imgType = 1; + + @Schema(description = "宠物物种:如cat、dog") + private String species; + + @Schema(description = "宠物品种:如British Shorthair、Corgi") + private String breed; + + @Schema(description = "宠物毛色:如blue-gray、orange") + private String color; + + @Schema(description = "眼睛颜色:如amber、blue") + private String eyeColor; + + @Schema(description = "体型:如round chubby、slim and elegant") + private String bodyType; + + @Schema(description = "特殊特征:如flat face、thick coat", defaultValue = "") + private String distinctiveFeatures = ""; + + @Schema(description = "故事梗概:AI会据此生成四格漫画脚本") + private String storyOutline; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java new file mode 100644 index 0000000..c21bc1a --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java @@ -0,0 +1,28 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.util.List; +import java.util.Map; + +/** + * AI视频生成请求表单 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成请求表单") +public class AiVideoGenerateForm { + + @Schema(description = "使用的模型ID", defaultValue = "doubao-seedance-2-0-260128") + private String model; + + @Schema(description = "内容数组,包含text、image_url等类型") + private List> content; + + @Schema(description = "分辨率: 480p, 720p, 1080p, 2K", defaultValue = "720p") + private String resolution = "720p"; + + @Schema(description = "视频时长,单位秒,最大15秒", defaultValue = "5") + private Integer duration = 5; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java b/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java new file mode 100644 index 0000000..76e6ff5 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java @@ -0,0 +1,28 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成任务回调请求 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成任务回调请求") +public class AiVideoCallbackVO { + + @Schema(description = "响应消息") + private String msg; + + @Schema(description = "响应码,0表示成功") + private Integer code; + + @Schema(description = "回调数据") + private VideoCallbackData data; + + @JsonProperty("request_id") + @Schema(description = "请求ID") + private String requestId; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java new file mode 100644 index 0000000..f66617d --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java @@ -0,0 +1,19 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成回调结果内容 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成回调结果内容") +public class VideoCallbackContent { + + @JsonProperty("video_url") + @Schema(description = "生成的视频地址") + private String videoUrl; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java new file mode 100644 index 0000000..706ffb1 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java @@ -0,0 +1,61 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成回调数据 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成回调数据") +public class VideoCallbackData { + + @Schema(description = "任务ID") + private String id; + + @Schema(description = "使用的模型ID") + private String model; + + @Schema(description = "任务状态: pending/running/succeeded/failed") + private String status; + + @Schema(description = "生成的结果内容") + private VideoCallbackContent content; + + @Schema(description = "资源消耗统计") + private VideoCallbackUsage usage; + + @JsonProperty("created_at") + @Schema(description = "创建时间戳") + private Long createdAt; + + @JsonProperty("updated_at") + @Schema(description = "更新时间戳") + private Long updatedAt; + + @Schema(description = "随机种子") + private Long seed; + + @Schema(description = "分辨率") + private String resolution; + + @Schema(description = "比例") + private String ratio; + + @Schema(description = "视频时长,单位秒") + private Integer duration; + + @Schema(description = "帧率") + private Integer framespersecond; + + @JsonProperty("service_tier") + @Schema(description = "服务等级") + private String serviceTier; + + @JsonProperty("execution_expires_after") + @Schema(description = "执行过期时间") + private Long executionExpiresAfter; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java new file mode 100644 index 0000000..2e4b752 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java @@ -0,0 +1,23 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成资源消耗统计 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成资源消耗统计") +public class VideoCallbackUsage { + + @JsonProperty("completion_tokens") + @Schema(description = "完成token数") + private Integer completionTokens; + + @JsonProperty("total_tokens") + @Schema(description = "总token数") + private Integer totalTokens; +} 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 f0f41b8..3837d54 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -1,38 +1,29 @@ package com.youlai.boot.mini.service; +import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; +import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import org.springframework.web.multipart.MultipartFile; import java.util.List; public interface AiGenerationService { - /** - * 上传参考文件(图片/视频) - * @param images 上传的图片列表 - * @param videos 上传的视频列表 - * @param userId 用户ID - * @return 文件访问URL列表 - */ List uploadReferenceFile(List images, List videos, Long userId); - /** - * 创建AI生成任务并调用生成接口 - * @param form 生成请求表单 - * @param userId 用户ID - * @return 任务UUID - */ String createAndGenerateImage(AiSingleImageGenerateForm form, Long userId); - /** - * 处理AI生成任务回调 - * @param vo 回调请求参数 - * @return 是否处理成功 - */ + String createAndGenerateFourPanel(AiFourPanelGenerateForm form, Long userId); + boolean handleTaskCallback(AiTaskCallbackVO vo); + String createAndGenerateVideo(AiVideoGenerateForm form, Long userId); + + boolean handleVideoTaskCallback(AiVideoCallbackVO vo); + MiniAiGenerationTask getTaskByUuid(String uuid); } 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 abdbbc3..6e1ff5c 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 @@ -24,9 +24,13 @@ import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; +import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; +import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.form.MiniDeductPointForm; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.youlai.boot.mini.model.vo.VideoCallbackData; import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.MiniPointRecordService; @@ -58,31 +62,41 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final AliyunFileService aliyunFileService; private final MiniPointRecordService pointRecordService; - /** - * AI生成服务地址 - */ - @Value("${ai.generate.server-url:http://127.0.0.1:8001/api/v1/photo-to-comic}") - private String aiGenerateServerUrl; - - /** - * AI任务回调地址 - */ - @Value("${ai.callback.url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/task/callback}") - private String aiCallbackUrl; - - @Value("${ai.default.model:doubao-seedream-5-0-260128}") - private String aiDefaultModel; - - /** - * OSS存储目录配置 - */ + //OSS存储目录配置 private static final String OSS_IMAGE_DIR = "ai/image/"; private static final String OSS_VIDEO_DIR = "ai/video/"; private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/"; - /** - * AI单图生成积分规则编码 - */ + + //AI单图生成服务地址 + @Value("${ai.generate.single-image-server-url:http://127.0.0.1:8001/api/v1/photo-to-comic}") + private String aiSingleImageServerUrl; + //AI四宫格图片生成服务地址 + @Value("${ai.generate.four-panel-server-url:http://127.0.0.1:8001/api/v1/four-panel-comic}") + private String aiFourPanelServerUrl; + //AI单图/四宫格图片生成任务回调地址 + @Value("${ai.callback.single-image-callback-url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/task/callback}") + private String aiSingleImageCallbackUrl; + //视频生成服务地址 + @Value("${ai.generate.video-server-url:http://127.0.0.1:8001/api/v1/video/submit}") + private String aiVideoServerUrl; + //视频任务回调地址 + @Value("${ai.callback.video-url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/video/task/callback}") + private String aiVideoCallbackUrl; + + //默认图片生成AI模型 + @Value("${ai.default.image-model:doubao-seedream-5-0-260128}") + private String aiDefaultImageModel; + + //默认视频生成AI模型 + @Value("${ai.default.video-model:doubao-seedance-2-0-260128}") + private String aiDefaultVideoModel; + + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; + //AI四宫格生成积分规则编码 + private static final String AI_GENERATE_QUAD_GRID_RULE = "AI_GENERATE_QUAD_GRID"; + //AI视频生成积分规则编码 + private static final String AI_GENERATE_VIDEO_RULE = "AI_GENERATE_VIDEO"; @Override public List uploadReferenceFile(List images, List videos, Long userId) { @@ -236,7 +250,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 组装第三方AI接口需要的参数 Map aiRequest = new HashMap<>(); - aiRequest.put("model", form.getModel() == null ? aiDefaultModel : form.getModel()); + aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); aiRequest.put("reference_image", form.getImageUrl()); aiRequest.put("style", form.getStyle()); aiRequest.put("ratio", form.getRatio()); @@ -251,12 +265,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiRequest.put("scene_description", form.getSceneDescription()); // 传递任务UUID和回调地址 aiRequest.put("uuid", taskUuid); - aiRequest.put("callback_url", aiCallbackUrl); + aiRequest.put("callback_url", aiSingleImageCallbackUrl); try { log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); // 同步调用AI接口,超时1秒 - HttpResponse response = HttpRequest.post(aiGenerateServerUrl) + HttpResponse response = HttpRequest.post(aiSingleImageServerUrl) .header("Content-Type", "application/json") .body(JSONUtil.toJsonStr(aiRequest)) .timeout(1000) @@ -291,6 +305,244 @@ public class AiGenerationServiceImpl implements AiGenerationService { return taskUuid; } + @Override + @Transactional(rollbackFor = Exception.class) // 所有异常都触发事务回滚 + public String createAndGenerateFourPanel(AiFourPanelGenerateForm form, Long userId) { + // 生成任务UUID + String taskUuid = UUID.randomUUID().toString().replace("-", ""); + Date now = new Date(); + long timestamp = System.currentTimeMillis(); + + // 扣减积分(自动加入当前事务,失败会一起回滚) + MiniDeductPointForm deductForm = new MiniDeductPointForm(); + deductForm.setRuleCode(AI_GENERATE_QUAD_GRID_RULE); + deductForm.setBizId(taskUuid); + Integer deductPoint = pointRecordService.deductPoint(userId, deductForm.getRuleCode(), deductForm.getBizId()); + + // 创建生成任务(自动加入当前事务,失败会一起回滚) + MiniAiGenerationTask task = new MiniAiGenerationTask(); + task.setUuid(taskUuid) + .setMiniUserId(userId) + .setType("img_grid_4") // 四宫格漫画 + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) // 生成中 + .setCreateBy(userId) + .setCreateTime(now) + .setCreateTimestamp(timestamp); + + aiGenerationTaskMapper.insert(task); + + // 组装第三方AI接口需要的参数 + Map aiRequest = new HashMap<>(); + aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); + aiRequest.put("reference_image", form.getReferenceImage()); + aiRequest.put("style", form.getStyle()); + aiRequest.put("ratio", form.getRatio()); + aiRequest.put("img_size", form.getImgSize()); + aiRequest.put("img_type", form.getImgType()); + aiRequest.put("species", form.getSpecies()); + aiRequest.put("breed", form.getBreed()); + aiRequest.put("color", form.getColor()); + aiRequest.put("eye_color", form.getEyeColor()); + aiRequest.put("body_type", form.getBodyType()); + aiRequest.put("distinctive_features", form.getDistinctiveFeatures()); + aiRequest.put("story_outline", form.getStoryOutline()); + // 传递任务UUID和回调地址 + aiRequest.put("uuid", taskUuid); + aiRequest.put("callback_url", aiSingleImageCallbackUrl); + + try { + log.info("提交四宫格漫画生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); + // 同步调用AI接口,超时1秒 + HttpResponse response = HttpRequest.post(aiFourPanelServerUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(1000) + .execute(); + + // 先判断HTTP状态码 + if (!response.isOk()) { + log.error("四宫格漫画生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body()); + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } + + // 解析返回结果,校验业务状态码:只有code=0才代表提交成功 + String responseBody = response.body(); + JSONObject resJson = JSONUtil.parseObj(responseBody); + Integer code = resJson.getInt("code"); + if (!Integer.valueOf(0).equals(code)) { + String errMsg = resJson.getStr("msg", "服务调用失败"); + + log.error("四宫格漫画生成任务提交失败,业务错误码:{},错误信息:{},request_id:{},完整响应:{}", + code, errMsg, resJson.getStr("request_id"), responseBody); + throw new MsgException("AI生成失败:" + errMsg); + } + } catch (JSONException e) { + log.error("四宫格漫画生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } catch (Exception e) { + log.error("四宫格漫画生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + // 抛出异常触发事务回滚 + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } + + return taskUuid; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String createAndGenerateVideo(AiVideoGenerateForm form, Long userId) { + // 生成任务UUID + String taskUuid = UUID.randomUUID().toString().replace("-", ""); + Date now = new Date(); + long timestamp = System.currentTimeMillis(); + + // 扣减积分 + MiniDeductPointForm deductForm = new MiniDeductPointForm(); + deductForm.setRuleCode(AI_GENERATE_VIDEO_RULE); + deductForm.setBizId(taskUuid); + Integer deductPoint = pointRecordService.deductPoint(userId, deductForm.getRuleCode(), deductForm.getBizId()); + + // 创建生成任务 + MiniAiGenerationTask task = new MiniAiGenerationTask(); + task.setUuid(taskUuid) + .setMiniUserId(userId) + .setType("video") + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) + .setCreateBy(userId) + .setCreateTime(now) + .setCreateTimestamp(timestamp); + + aiGenerationTaskMapper.insert(task); + + // 组装第三方接口参数 + Map aiRequest = new HashMap<>(); + aiRequest.put("model", form.getModel() == null ? aiDefaultVideoModel : form.getModel());//TODO写到上面 + aiRequest.put("content", form.getContent()); + aiRequest.put("resolution", form.getResolution()); + aiRequest.put("duration", form.getDuration()); + aiRequest.put("uuid", taskUuid); + aiRequest.put("callback_url", aiVideoCallbackUrl); //TODO 后续独立回调地址 + + try { + log.info("提交视频生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); + HttpResponse response = HttpRequest.post(aiVideoServerUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(1000) + .execute(); + + if (!response.isOk()) { + log.error("视频生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body()); + throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); + } + + String responseBody = response.body(); + JSONObject resJson = JSONUtil.parseObj(responseBody); + Integer code = resJson.getInt("code"); + if (!Integer.valueOf(0).equals(code)) { + String errMsg = resJson.getStr("msg", "服务调用失败"); + log.error("视频生成任务提交失败,业务错误码:{},错误信息:{},完整响应:{}", code, errMsg, responseBody); + throw new MsgException("AI视频生成失败:" + errMsg); + } + + // 提取第三方返回的视频任务uuid,保存到任务表中,后续回调通过这个uuid匹配任务 + JSONObject data = resJson.getJSONObject("data"); + String videoTaskUuid = data.getStr("uuid"); + task.setVideoTaskUuid(videoTaskUuid); + aiGenerationTaskMapper.updateById(task); + log.info("视频任务{}第三方返回uuid:{},已保存", task.getUuid(), videoTaskUuid); + + } catch (JSONException e) { + log.error("视频生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); + } catch (Exception e) { + log.error("视频生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); + } + + return taskUuid; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean handleVideoTaskCallback(AiVideoCallbackVO vo) { + log.info("处理AI视频生成任务回调,请求参数:{}", JSONUtil.toJsonStr(vo)); + boolean success = false; + try { + // 校验回调响应是否成功 + if (!Integer.valueOf(0).equals(vo.getCode())) { + log.error("视频生成任务回调失败,错误信息:{}", vo.getMsg()); + return false; + } + + VideoCallbackData data = vo.getData(); + if (data == null) { + log.error("视频回调数据为空"); + return false; + } + + String videoTaskUuid = data.getId(); + // 根据第三方返回的视频任务uuid查询对应的任务 + MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) + .eq(MiniAiGenerationTask::getDeleted, false)); + if (task == null) { + log.error("视频回调任务不存在,第三方视频任务UUID:{}", videoTaskUuid); + return false; + } + + // 转换任务状态 + Integer status; + if ("succeeded".equals(data.getStatus())) { + status = 1; // 成功 + } else if ("failed".equals(data.getStatus())) { + status = 2; // 失败 + } else { + log.info("视频任务{}处于中间状态:{},不处理", task.getUuid(), data.getStatus()); + return true; // 中间状态直接返回成功,不更新任务 + } + + // 更新任务状态 + task.setStatus(status); + task.setUpdateTime(new Date()); + task.setUpdateTimestamp(System.currentTimeMillis()); + aiGenerationTaskMapper.updateById(task); + + // 如果生成成功,下载外部视频到OSS + if (status == 1 && data.getContent() != null && data.getContent().getVideoUrl() != null) { + String externalVideoUrl = data.getContent().getVideoUrl(); + // 调用下载方法,和图片下载逻辑一致,存储到视频目录 + String ossUrl = downloadExternalUrlToOss(externalVideoUrl, OSS_VIDEO_DIR); + task.setResultResourceUrl(ossUrl); + aiGenerationTaskMapper.updateById(task); + + // 保存视频媒体记录 + MiniAiTaskMedia media = new MiniAiTaskMedia(); + String mediaUuid = UUID.randomUUID().toString().replace("-", ""); + media.setUuid(mediaUuid) + .setTaskId(task.getId()) + .setFileSource("ai_generate") + .setMediaType("video") + .setSourceUrl(ossUrl) + .setDuration(data.getDuration()) + .setCreateBy(task.getCreateBy()) + .setCreateTimestamp(System.currentTimeMillis()) + .setCreateTime(new Date()); + aiTaskMediaMapper.insert(media); + } + + success = true; + log.info("视频任务{}回调处理完成,状态:{}", task.getUuid(), status); + } catch (Exception e) { + log.error("视频任务回调处理异常,异常信息:{}", e.getMessage(), e); + } + return success; + } + @Override public boolean handleTaskCallback(AiTaskCallbackVO vo) { log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid()); @@ -375,6 +627,13 @@ public class AiGenerationServiceImpl implements AiGenerationService { * 下载外部URL到OSS,返回OSS访问地址 */ private String downloadExternalUrlToOss(String externalUrl) { + return downloadExternalUrlToOss(externalUrl, "ai/generate/"); + } + + /** + * 下载外部URL到指定OSS目录,返回OSS访问地址 + */ + private String downloadExternalUrlToOss(String externalUrl, String dir) { try ( // 直接打开URL输入流,自动关闭 InputStream inputStream = new URL(externalUrl).openStream() @@ -401,7 +660,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { ext = "jpg"; } - String objectName = "ai/generate/" + String objectName = dir + currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8) + "." + ext; @@ -411,4 +670,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { throw new MsgException("资源转存失败"); } } + + } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 7758041..59b78c3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -92,6 +92,7 @@ security: - /api/v1/mini/homePage/listByBounds - /healthcheck - /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调 + - /api/v1/mini/ai/generation/video/task/callback # AIGeneration 单图任务回调 # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -227,11 +228,15 @@ wx: appid: Your_AppId secret: Your_AppSecret - +# AIGeneration 配置 ai: generate: - server-url: http://192.168.31.91:8001/api/v1/photo-to-comic + single-image-server-url: http://192.168.31.91:8001/api/v1/photo-to-comic + four-panel-server-url: http://192.168.31.91:8001/api/v1/four-panel-comic + video-server-url: http://192.168.31.91:8001/api/v1/video/submit callback: - url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback + single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback + video-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/video/task/callback default: - model: doubao-seedream-5-0-260128 + image-model: doubao-seedream-5-0-260128 + video-model: doubao-seedance-2-0-260128