From 90ef9bd7b290a1d9964b6cc90b6901cde3c73d2f Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 14 May 2026 15:00:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=8D=95=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=94=9F=E6=88=90=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 21 +- .../mini/model/dto/PhotoToComicRequest.java | 78 ----- .../model/entity/MiniAiGenerationTask.java | 2 +- .../model/form/AiSingleImageGenerateForm.java | 54 ++++ .../mini/model/vo/AiTaskCallbackImage.java | 25 ++ .../mini/model/vo/AiTaskCallbackResult.java | 38 +++ .../mini/model/vo/AiTaskCallbackUsage.java | 31 ++ .../boot/mini/model/vo/AiTaskCallbackVO.java | 23 +- .../mini/service/AiGenerationService.java | 27 +- .../service/impl/AiGenerationServiceImpl.java | 277 ++++++++++-------- src/main/resources/application-dev.yml | 10 + 11 files changed, 359 insertions(+), 227 deletions(-) delete mode 100644 src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackResult.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackUsage.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 0615417..e5e1696 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -6,7 +6,7 @@ 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.dto.PhotoToComicRequest; +import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; import com.youlai.boot.mini.service.AiGenerationService; import io.swagger.v3.oas.annotations.Operation; @@ -45,20 +45,27 @@ public class AiGenerationController { return Result.success(urlList); } - @Operation(summary = "提交单图漫画生成任务") + @Operation(summary = "提交单图生成任务") @PostMapping("/generate-single-image") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) - public Result generateSingleImage(@Valid @RequestBody PhotoToComicRequest request) { + public Result generateSingleImage(@Valid @RequestBody AiSingleImageGenerateForm form) { Long userId = SecurityUtils.getUserId(); - String taskUuid = aiGenerationService.createAndGenerateImage(request, userId); + String taskUuid = aiGenerationService.createAndGenerateImage(form, userId); return Result.success(taskUuid); } - @Operation(summary = "AI生成任务回调接口") - @PostMapping("/task/callback") + //TODO: 后续增加用户小程序订阅通知 + @Operation(summary = "单图任务回调接口") + @PostMapping("/single-image/task/callback") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) public Result taskCallback(@Valid @RequestBody AiTaskCallbackVO vo) { - boolean success = aiGenerationService.handleTaskCallback(vo.getUuid(), vo.getStatus(), vo.getResultUrl()); + boolean success = aiGenerationService.handleTaskCallback(vo); return Result.success(success); } + + + // TODO: 添加 4宫格图片生成任务 接口 ,外部服务接口为:http://127.0.0.1:8001//api/v1/four-panel-comic + + + } diff --git a/src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java b/src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java deleted file mode 100644 index 9c9128f..0000000 --- a/src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.youlai.boot.mini.model.dto; - -import lombok.Data; - -import java.util.List; - -/** - * 照片漫画化请求DTO - * - * @author youlai - */ -@Data -public class PhotoToComicRequest { - /** - * 使用的模型ID - */ - private String model; - - /** - * 参考图片URL数组 - */ - private List reference_image; - - /** - * 画风: 1=治愈系水彩, 2=Q版卡通, 3=日式漫画, 4=吉卜力风, 5=3D毛绒, 6=搞笑夸张 - */ - private Integer style = 1; - - /** - * 比例: 1=1:1, 2=2:3, 3=3:4, 4=4:3, 5=5:4 - */ - private Integer ratio = 1; - - /** - * 图片尺寸: 1=2K, 2=4K, 3=8K - */ - private Integer img_size = 1; - - /** - * 图片格式: 1=png, 2=jpeg - */ - private Integer img_type = 1; - - /** - * 宠物物种,如:cat, dog - */ - private String species; - - /** - * 宠物品种,如:British Shorthair, Corgi - */ - private String breed; - - /** - * 宠物毛色,如:blue-gray, orange - */ - private String color; - - /** - * 眼睛颜色,如:amber, blue - */ - private String eye_color; - - /** - * 体型,如:round chubby, slim and elegant - */ - private String body_type; - - /** - * 特殊特征,如:flat face, thick coat - */ - private String distinctive_features = ""; - - /** - * 场景描述,用户自定义场景 - */ - private String scene_description; -} 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 0a38229..a8dacb2 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 @@ -42,7 +42,7 @@ public class MiniAiGenerationTask implements Serializable { @TableField("status") @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") - private Byte status; + private Integer status; @TableField("result_resource_url") @Schema(description = "生成结果资源URL") diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java new file mode 100644 index 0000000..ecaed29 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.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 AiSingleImageGenerateForm { + + @Schema(description = "参考图片URL列表,可选") + private List imageUrl; + + @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 = "场景描述:用户自定义生成场景") + private String sceneDescription; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java new file mode 100644 index 0000000..9e6a916 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java @@ -0,0 +1,25 @@ +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 AiTaskCallbackImage { + + @Schema(description = "图片URL地址") + private String url; + + @JsonProperty("b64_json") + @Schema(description = "Base64编码的图片JSON") + private String b64Json; + + @Schema(description = "图片尺寸,格式如 1664x2496") + private String size; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackResult.java b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackResult.java new file mode 100644 index 0000000..0f0b1c0 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackResult.java @@ -0,0 +1,38 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.util.List; + +/** + * AI任务生成结果 + * + * @author youlai + */ +@Data +@Schema(description = "AI任务生成结果") +public class AiTaskCallbackResult { + + @Schema(description = "使用的模型ID") + private String model; + + @Schema(description = "生成的图片列表") + private List data; + + @Schema(description = "错误信息,无错误则为null") + private Object error; + + @Schema(description = "Token使用统计") + private AiTaskCallbackUsage usage; + + @JsonProperty("created_at") + @Schema(description = "创建时间戳") + private Long createdAt; + + @Schema(description = "使用的工具") + private String tool; + + @Schema(description = "创建时间戳") + private Long created; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackUsage.java b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackUsage.java new file mode 100644 index 0000000..8fe587d --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackUsage.java @@ -0,0 +1,31 @@ +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 AiTaskCallbackUsage { + + @JsonProperty("generated_images") + @Schema(description = "生成的图片数量") + private Integer generatedImages; + + @JsonProperty("output_tokens") + @Schema(description = "输出token数") + private Integer outputTokens; + + @JsonProperty("total_tokens") + @Schema(description = "总token数") + private Integer totalTokens; + + @JsonProperty("tool_usage") + @Schema(description = "工具使用信息") + private Object toolUsage; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java index 13aab1e..bd93853 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java @@ -1,25 +1,34 @@ package com.youlai.boot.mini.model.vo; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; /** - * AI任务回调VO + * AI生成任务回调请求VO * * @author youlai */ @Data -@Schema(description = "AI任务回调VO") +@Schema(description = "AI生成任务回调请求") public class AiTaskCallbackVO { @NotBlank(message = "任务UUID不能为空") - @Schema(description = "任务UUID", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "任务唯一标识UUID", requiredMode = Schema.RequiredMode.REQUIRED) private String uuid; - @Schema(description = "生成结果URL") - private String resultUrl; + @Schema(description = "任务状态:succeeded=成功,failed=失败") + private String status; - @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") - private Byte status; + @Schema(description = "生成结果") + private AiTaskCallbackResult result; + + @JsonProperty("created_at") + @Schema(description = "创建时间戳") + private Long createdAt; + + @JsonProperty("updated_at") + @Schema(description = "更新时间戳") + private Long updatedAt; } 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 f201d08..f0f41b8 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -1,18 +1,37 @@ package com.youlai.boot.mini.service; -import com.youlai.boot.mini.model.dto.PhotoToComicRequest; +import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; +import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; 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); - String createAndGenerateImage(PhotoToComicRequest request, Long userId); - - boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl); + /** + * 创建AI生成任务并调用生成接口 + * @param form 生成请求表单 + * @param userId 用户ID + * @return 任务UUID + */ + String createAndGenerateImage(AiSingleImageGenerateForm form, Long userId); + + /** + * 处理AI生成任务回调 + * @param vo 回调请求参数 + * @return 是否处理成功 + */ + boolean handleTaskCallback(AiTaskCallbackVO 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 b3daaa1..abdbbc3 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 @@ -1,14 +1,18 @@ package com.youlai.boot.mini.service.impl; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.common.exception.MsgException; import com.youlai.boot.common.util.FileUtils; import com.youlai.boot.common.util.JavaVCUtils; import com.youlai.boot.common.util.RandomNumberUtils; @@ -17,25 +21,30 @@ import com.youlai.boot.file.service.FileService; import com.youlai.boot.file.service.impl.AliyunFileService; import com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper; import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; -import com.youlai.boot.mini.model.dto.GenerateResponse; -import com.youlai.boot.mini.model.dto.ImageData; -import com.youlai.boot.mini.model.dto.PhotoToComicRequest; + import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; +import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; +import com.youlai.boot.mini.model.form.MiniDeductPointForm; +import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.mini.service.AiGenerationService; +import com.youlai.boot.mini.service.MiniPointRecordService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.UUID; +import java.io.InputStream; +import java.net.URL; +import java.time.LocalDateTime; +import java.util.*; import java.util.concurrent.CompletableFuture; @Slf4j @@ -47,11 +56,22 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final MiniAiTaskMediaMapper aiTaskMediaMapper; private final FileService fileService; private final AliyunFileService aliyunFileService; + private final MiniPointRecordService pointRecordService; /** * AI生成服务地址 */ - private static final String AI_GENERATE_URL = "http://127.0.0.1:8001/api/v1/photo-to-comic"; + @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存储目录配置 @@ -59,6 +79,10 @@ public class AiGenerationServiceImpl implements AiGenerationService { 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单图生成积分规则编码 + */ + private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; @Override public List uploadReferenceFile(List images, List videos, Long userId) { @@ -180,97 +204,98 @@ public class AiGenerationServiceImpl implements AiGenerationService { } @Override - public String createAndGenerateImage(PhotoToComicRequest request, Long userId) { - // 创建生成任务 - MiniAiGenerationTask task = new MiniAiGenerationTask(); + @Transactional(rollbackFor = Exception.class) // 所有异常都触发事务回滚 + public String createAndGenerateImage(AiSingleImageGenerateForm 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_SINGLE_IMAGE_RULE); + deductForm.setBizId(taskUuid); + Integer deductPoint = pointRecordService.deductPoint(userId, deductForm.getRuleCode(), deductForm.getBizId()); + + // 创建生成任务(自动加入当前事务,失败会一起回滚) + MiniAiGenerationTask task = new MiniAiGenerationTask(); task.setUuid(taskUuid) .setMiniUserId(userId) - .setType("img_single") - .setGenerateParams(JSONUtil.toJsonStr(request)) - .setPointsConsumed(1) // 默认消耗1积分 - .setStatus((byte) 0) + .setType("img_single") // 单图 + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) // 生成中 .setCreateBy(userId) .setCreateTime(now) .setCreateTimestamp(timestamp) - .setUpdateTime(now) + .setUpdateTime(now) // 补全update相关字段 .setUpdateTimestamp(timestamp) .setDeleted(false); aiGenerationTaskMapper.insert(task); - // 异步调用AI生成接口 - CompletableFuture.runAsync(() -> { - try { - log.info("开始调用AI生成接口,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(request)); - - // 发送HTTP请求 - HttpResponse response = HttpRequest.post(AI_GENERATE_URL) - .header("Content-Type", "application/json") - .body(JSONUtil.toJsonStr(request)) - .timeout(30000) // 超时30秒 - .execute(); - - if (!response.isOk()) { - log.error("AI生成接口调用失败,状态码:{},响应内容:{}", response.getStatus(), response.body()); - updateTaskStatus(taskUuid, (byte) 2, null); - return; - } - - // 解析响应 - String responseBody = response.body(); - GenerateResponse generateResponse = JSONUtil.toBean(responseBody, GenerateResponse.class); - - if (generateResponse.getCode() != 0 || generateResponse.getData() == null || generateResponse.getData().getData().isEmpty()) { - log.error("AI生成接口返回错误,响应内容:{}", responseBody); - updateTaskStatus(taskUuid, (byte) 2, null); - return; - } - - // 保存生成的图片 - ImageData imageData = generateResponse.getData().getData().get(0); - String externalResultUrl = imageData.getUrl(); - - // 下载外部图片到我们的OSS - String ossUrl = downloadExternalUrlToOss(externalResultUrl); - - // 保存媒体记录 - MiniAiTaskMedia media = new MiniAiTaskMedia(); - String mediaUuid = UUID.randomUUID().toString().replace("-", ""); + // 组装第三方AI接口需要的参数 + Map aiRequest = new HashMap<>(); + aiRequest.put("model", form.getModel() == null ? aiDefaultModel : form.getModel()); + aiRequest.put("reference_image", form.getImageUrl()); + 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("scene_description", form.getSceneDescription()); + // 传递任务UUID和回调地址 + aiRequest.put("uuid", taskUuid); + aiRequest.put("callback_url", aiCallbackUrl); - media.setUuid(mediaUuid) - .setTaskId(task.getId()) - .setFileSource("ai_generated") - .setMediaType("image") - .setSourceUrl(ossUrl) - .setCreateBy(userId) - .setCreateTime(new Date()) - .setCreateTimestamp(System.currentTimeMillis()) - .setUpdateTime(new Date()) - .setUpdateTimestamp(System.currentTimeMillis()) - .setDeleted(false); - - aiTaskMediaMapper.insert(media); + try { + log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); + // 同步调用AI接口,超时1秒 + HttpResponse response = HttpRequest.post(aiGenerateServerUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(1000) + .execute(); + + // 先判断HTTP状态码 + if (!response.isOk()) { + log.error("AI生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body()); + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } - // 更新任务状态为成功 - updateTaskStatus(taskUuid, (byte) 1, ossUrl); - log.info("AI生成任务完成,任务UUID:{},结果URL:{}", taskUuid, ossUrl); + // 解析返回结果,校验业务状态码:只有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", "服务调用失败"); - } catch (Exception e) { - log.error("AI生成任务异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); - updateTaskStatus(taskUuid, (byte) 2, null); + log.error("AI生成任务提交失败,业务错误码:{},错误信息:{},request_id:{},完整响应:{}", + code, errMsg, resJson.getStr("request_id"), responseBody); + throw new MsgException("AI生成失败:" + errMsg); } - }); + } catch (JSONException e) { + log.error("AI生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } catch (Exception e) { + log.error("AI生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + // 抛出异常触发事务回滚 + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } return taskUuid; } @Override - public boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl) { + public boolean handleTaskCallback(AiTaskCallbackVO vo) { + log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid()); try { + String taskUuid = vo.getUuid(); // 查询任务是否存在 MiniAiGenerationTask task = getTaskByUuid(taskUuid); if (task == null) { @@ -278,9 +303,21 @@ public class AiGenerationServiceImpl implements AiGenerationService { return false; } + // 转换任务状态 + Integer status; + if ("succeeded".equals(vo.getStatus())) { + status = 1; // 成功 + } else if ("failed".equals(vo.getStatus())) { + status = 2; // 失败 + } else { + log.error("回调任务状态非法,UUID:{},status:{}", taskUuid, vo.getStatus()); + return false; + } + // 如果生成成功,下载外部URL到OSS String ossUrl = null; - if (status == 1 && externalResultUrl != null) { + if (status == 1 && vo.getResult() != null && !vo.getResult().getData().isEmpty()) { + String externalResultUrl = vo.getResult().getData().get(0).getUrl(); ossUrl = downloadExternalUrlToOss(externalResultUrl); // 保存生成的媒体记录 @@ -288,6 +325,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { String mediaUuid = UUID.randomUUID().toString().replace("-", ""); media.setUuid(mediaUuid) + .setMiniUserId(task.getMiniUserId()) .setTaskId(task.getId()) .setFileSource("ai_generated") .setMediaType("image") @@ -306,7 +344,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { return updateTaskStatus(taskUuid, status, ossUrl); } catch (Exception e) { - log.error("处理AI任务回调异常,UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + log.error("处理AI任务回调异常,UUID:{},异常信息:{}", vo.getUuid(), e.getMessage(), e); return false; } } @@ -321,7 +359,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { /** * 更新任务状态 */ - private boolean updateTaskStatus(String uuid, Byte status, String resultUrl) { + private boolean updateTaskStatus(String uuid, Integer status, String resultUrl) { LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(MiniAiGenerationTask::getUuid, uuid) .eq(MiniAiGenerationTask::getDeleted, false) @@ -337,61 +375,40 @@ public class AiGenerationServiceImpl implements AiGenerationService { * 下载外部URL到OSS,返回OSS访问地址 */ private String downloadExternalUrlToOss(String externalUrl) { - try { - // 下载文件并获取输入流 - byte[] fileBytes = HttpUtil.downloadBytes(externalUrl); - String fileName = IdUtil.fastSimpleUUID() + "." + FileUtil.extName(externalUrl); - - // 创建临时的MultipartFile包装类 - MultipartFile multipartFile = new MultipartFile() { - @Override - public String getName() { - return "file"; - } - - @Override - public String getOriginalFilename() { - return fileName; - } - - @Override - public String getContentType() { - return "image/png"; - } - - @Override - public boolean isEmpty() { - return fileBytes == null || fileBytes.length == 0; - } - - @Override - public long getSize() { - return fileBytes.length; - } - - @Override - public byte[] getBytes() { - return fileBytes; - } - - @Override - public java.io.InputStream getInputStream() { - return new java.io.ByteArrayInputStream(fileBytes); - } - - @Override - public void transferTo(java.io.File dest) throws java.io.IOException, IllegalStateException { - FileUtil.writeBytes(fileBytes, dest); - } - }; - - // 上传到OSS - FileInfo fileInfo = fileService.uploadFile(multipartFile); - return fileInfo.getUrl(); + try ( + // 直接打开URL输入流,自动关闭 + InputStream inputStream = new URL(externalUrl).openStream() + ) { + long currentTimestamp = System.currentTimeMillis(); + + // 生成OSS存储路径:按日期分类 + 唯一ID + 原文件后缀 +// String ext = FileUtil.extName(externalUrl); + String ext = null; + + // 1. 如果URL包含查询参数 '?',则只取问号前面的部分 + String path = externalUrl; + int queryIndex = externalUrl.indexOf('?'); + if (queryIndex > 0) { + path = externalUrl.substring(0, queryIndex); + } + // 2. 从纯净的路径中提取文件后缀 + int lastDotIndex = path.lastIndexOf('.'); + if (lastDotIndex > 0 && lastDotIndex < path.length() - 1) { + ext = path.substring(lastDotIndex + 1); + } + if (ext == null){ + log.error("无法从URL中提取文件后缀,URL:{}", externalUrl); + ext = "jpg"; + } + String objectName = "ai/generate/" + + currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8) + + "." + + ext; + return aliyunFileService.uploadFile(objectName, inputStream); } catch (Exception e) { log.error("下载外部URL到OSS失败,URL:{},异常信息:{}", externalUrl, e.getMessage(), e); - throw new BusinessException("下载生成结果失败"); + throw new MsgException("资源转存失败"); } } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index de3307d..7758041 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -91,6 +91,7 @@ security: - /api/v1/mini/public/** - /api/v1/mini/homePage/listByBounds - /healthcheck + - /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调 # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -225,3 +226,12 @@ wx: miniapp: appid: Your_AppId secret: Your_AppSecret + + +ai: + generate: + server-url: http://192.168.31.91:8001/api/v1/photo-to-comic + callback: + url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback + default: + model: doubao-seedream-5-0-260128