Browse Source

修改单图片生成任务

glx
glx 2 weeks ago
parent
commit
90ef9bd7b2
  1. 21
      src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java
  2. 78
      src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java
  3. 2
      src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java
  4. 54
      src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java
  5. 25
      src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java
  6. 38
      src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackResult.java
  7. 31
      src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackUsage.java
  8. 23
      src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java
  9. 27
      src/main/java/com/youlai/boot/mini/service/AiGenerationService.java
  10. 277
      src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java
  11. 10
      src/main/resources/application-dev.yml

21
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.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.framework.security.util.SecurityUtils; 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.model.vo.AiTaskCallbackVO;
import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.AiGenerationService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -45,20 +45,27 @@ public class AiGenerationController {
return Result.success(urlList); return Result.success(urlList);
} }
@Operation(summary = "提交单图漫画生成任务") @Operation(summary = "提交单图生成任务")
@PostMapping("/generate-single-image") @PostMapping("/generate-single-image")
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT)
public Result<String> generateSingleImage(@Valid @RequestBody PhotoToComicRequest request) { public Result<String> generateSingleImage(@Valid @RequestBody AiSingleImageGenerateForm form) {
Long userId = SecurityUtils.getUserId(); Long userId = SecurityUtils.getUserId();
String taskUuid = aiGenerationService.createAndGenerateImage(request, userId); String taskUuid = aiGenerationService.createAndGenerateImage(form, userId);
return Result.success(taskUuid); return Result.success(taskUuid);
} }
@Operation(summary = "AI生成任务回调接口") //TODO: 后续增加用户小程序订阅通知
@PostMapping("/task/callback") @Operation(summary = "单图任务回调接口")
@PostMapping("/single-image/task/callback")
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE)
public Result<Boolean> taskCallback(@Valid @RequestBody AiTaskCallbackVO vo) { public Result<Boolean> taskCallback(@Valid @RequestBody AiTaskCallbackVO vo) {
boolean success = aiGenerationService.handleTaskCallback(vo.getUuid(), vo.getStatus(), vo.getResultUrl()); boolean success = aiGenerationService.handleTaskCallback(vo);
return Result.success(success); return Result.success(success);
} }
// TODO: 添加 4宫格图片生成任务 接口 ,外部服务接口为:http://127.0.0.1:8001//api/v1/four-panel-comic
} }

78
src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java

@ -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<String> 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;
}

2
src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java

@ -42,7 +42,7 @@ public class MiniAiGenerationTask implements Serializable {
@TableField("status") @TableField("status")
@Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消")
private Byte status; private Integer status;
@TableField("result_resource_url") @TableField("result_resource_url")
@Schema(description = "生成结果资源URL") @Schema(description = "生成结果资源URL")

54
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<String> 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;
}

25
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;
}

38
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<AiTaskCallbackImage> 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;
}

31
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;
}

23
src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java

@ -1,25 +1,34 @@
package com.youlai.boot.mini.model.vo; package com.youlai.boot.mini.model.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
/** /**
* AI任务回调VO * AI生成任务回调请求VO
* *
* @author youlai * @author youlai
*/ */
@Data @Data
@Schema(description = "AI任务回调VO") @Schema(description = "AI生成任务回调请求")
public class AiTaskCallbackVO { public class AiTaskCallbackVO {
@NotBlank(message = "任务UUID不能为空") @NotBlank(message = "任务UUID不能为空")
@Schema(description = "任务UUID", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "任务唯一标识UUID", requiredMode = Schema.RequiredMode.REQUIRED)
private String uuid; private String uuid;
@Schema(description = "生成结果URL") @Schema(description = "任务状态:succeeded=成功,failed=失败")
private String resultUrl; private String status;
@Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") @Schema(description = "生成结果")
private Byte status; private AiTaskCallbackResult result;
@JsonProperty("created_at")
@Schema(description = "创建时间戳")
private Long createdAt;
@JsonProperty("updated_at")
@Schema(description = "更新时间戳")
private Long updatedAt;
} }

27
src/main/java/com/youlai/boot/mini/service/AiGenerationService.java

@ -1,18 +1,37 @@
package com.youlai.boot.mini.service; 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.entity.MiniAiGenerationTask;
import com.youlai.boot.mini.model.vo.AiTaskCallbackVO;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
public interface AiGenerationService { public interface AiGenerationService {
/**
* 上传参考文件图片/视频
* @param images 上传的图片列表
* @param videos 上传的视频列表
* @param userId 用户ID
* @return 文件访问URL列表
*/
List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId); List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId);
String createAndGenerateImage(PhotoToComicRequest request, Long userId); /**
* 创建AI生成任务并调用生成接口
boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl); * @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); MiniAiGenerationTask getTaskByUuid(String uuid);

277
src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java

@ -1,14 +1,18 @@
package com.youlai.boot.mini.service.impl; package com.youlai.boot.mini.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.youlai.boot.common.exception.BusinessException; 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.FileUtils;
import com.youlai.boot.common.util.JavaVCUtils; import com.youlai.boot.common.util.JavaVCUtils;
import com.youlai.boot.common.util.RandomNumberUtils; 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.file.service.impl.AliyunFileService;
import com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper; import com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper;
import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; 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.MiniAiGenerationTask;
import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; 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.AiGenerationService;
import com.youlai.boot.mini.service.MiniPointRecordService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.io.InputStream;
import java.util.Date; import java.net.URL;
import java.util.List; import java.time.LocalDateTime;
import java.util.UUID; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@Slf4j @Slf4j
@ -47,11 +56,22 @@ public class AiGenerationServiceImpl implements AiGenerationService {
private final MiniAiTaskMediaMapper aiTaskMediaMapper; private final MiniAiTaskMediaMapper aiTaskMediaMapper;
private final FileService fileService; private final FileService fileService;
private final AliyunFileService aliyunFileService; private final AliyunFileService aliyunFileService;
private final MiniPointRecordService pointRecordService;
/** /**
* AI生成服务地址 * 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存储目录配置 * OSS存储目录配置
@ -59,6 +79,10 @@ public class AiGenerationServiceImpl implements AiGenerationService {
private static final String OSS_IMAGE_DIR = "ai/image/"; private static final String OSS_IMAGE_DIR = "ai/image/";
private static final String OSS_VIDEO_DIR = "ai/video/"; private static final String OSS_VIDEO_DIR = "ai/video/";
private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/"; private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/";
/**
* AI单图生成积分规则编码
*/
private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE";
@Override @Override
public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) { public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) {
@ -180,97 +204,98 @@ public class AiGenerationServiceImpl implements AiGenerationService {
} }
@Override @Override
public String createAndGenerateImage(PhotoToComicRequest request, Long userId) { @Transactional(rollbackFor = Exception.class) // 所有异常都触发事务回滚
// 创建生成任务 public String createAndGenerateImage(AiSingleImageGenerateForm form, Long userId) {
MiniAiGenerationTask task = new MiniAiGenerationTask(); // 生成任务UUID
String taskUuid = UUID.randomUUID().toString().replace("-", ""); String taskUuid = UUID.randomUUID().toString().replace("-", "");
Date now = new Date(); Date now = new Date();
long timestamp = System.currentTimeMillis(); 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) task.setUuid(taskUuid)
.setMiniUserId(userId) .setMiniUserId(userId)
.setType("img_single") .setType("img_single") // 单图
.setGenerateParams(JSONUtil.toJsonStr(request)) .setGenerateParams(JSONUtil.toJsonStr(form))
.setPointsConsumed(1) // 默认消耗1积分 .setPointsConsumed(Math.abs(deductPoint))
.setStatus((byte) 0) .setStatus(0) // 生成中
.setCreateBy(userId) .setCreateBy(userId)
.setCreateTime(now) .setCreateTime(now)
.setCreateTimestamp(timestamp) .setCreateTimestamp(timestamp)
.setUpdateTime(now) .setUpdateTime(now) // 补全update相关字段
.setUpdateTimestamp(timestamp) .setUpdateTimestamp(timestamp)
.setDeleted(false); .setDeleted(false);
aiGenerationTaskMapper.insert(task); aiGenerationTaskMapper.insert(task);
// 异步调用AI生成接口 // 组装第三方AI接口需要的参数
CompletableFuture.runAsync(() -> { Map<String, Object> aiRequest = new HashMap<>();
try { aiRequest.put("model", form.getModel() == null ? aiDefaultModel : form.getModel());
log.info("开始调用AI生成接口,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(request)); aiRequest.put("reference_image", form.getImageUrl());
aiRequest.put("style", form.getStyle());
// 发送HTTP请求 aiRequest.put("ratio", form.getRatio());
HttpResponse response = HttpRequest.post(AI_GENERATE_URL) aiRequest.put("img_size", form.getImgSize());
.header("Content-Type", "application/json") aiRequest.put("img_type", form.getImgType());
.body(JSONUtil.toJsonStr(request)) aiRequest.put("species", form.getSpecies());
.timeout(30000) // 超时30秒 aiRequest.put("breed", form.getBreed());
.execute(); aiRequest.put("color", form.getColor());
aiRequest.put("eye_color", form.getEyeColor());
if (!response.isOk()) { aiRequest.put("body_type", form.getBodyType());
log.error("AI生成接口调用失败,状态码:{},响应内容:{}", response.getStatus(), response.body()); aiRequest.put("distinctive_features", form.getDistinctiveFeatures());
updateTaskStatus(taskUuid, (byte) 2, null); aiRequest.put("scene_description", form.getSceneDescription());
return; // 传递任务UUID和回调地址
} aiRequest.put("uuid", taskUuid);
aiRequest.put("callback_url", aiCallbackUrl);
// 解析响应
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("-", "");
media.setUuid(mediaUuid) try {
.setTaskId(task.getId()) log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest));
.setFileSource("ai_generated") // 同步调用AI接口,超时1秒
.setMediaType("image") HttpResponse response = HttpRequest.post(aiGenerateServerUrl)
.setSourceUrl(ossUrl) .header("Content-Type", "application/json")
.setCreateBy(userId) .body(JSONUtil.toJsonStr(aiRequest))
.setCreateTime(new Date()) .timeout(1000)
.setCreateTimestamp(System.currentTimeMillis()) .execute();
.setUpdateTime(new Date())
.setUpdateTimestamp(System.currentTimeMillis()) // 先判断HTTP状态码
.setDeleted(false); if (!response.isOk()) {
log.error("AI生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body());
aiTaskMediaMapper.insert(media); throw new MsgException("AI生成服务暂时不可用,请稍后重试");
}
// 更新任务状态为成功 // 解析返回结果,校验业务状态码:只有code=0才代表提交成功
updateTaskStatus(taskUuid, (byte) 1, ossUrl); String responseBody = response.body();
log.info("AI生成任务完成,任务UUID:{},结果URL:{}", taskUuid, ossUrl); 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生成任务提交失败,业务错误码:{},错误信息:{},request_id:{},完整响应:{}",
log.error("AI生成任务异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); code, errMsg, resJson.getStr("request_id"), responseBody);
updateTaskStatus(taskUuid, (byte) 2, null); 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; return taskUuid;
} }
@Override @Override
public boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl) { public boolean handleTaskCallback(AiTaskCallbackVO vo) {
log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid());
try { try {
String taskUuid = vo.getUuid();
// 查询任务是否存在 // 查询任务是否存在
MiniAiGenerationTask task = getTaskByUuid(taskUuid); MiniAiGenerationTask task = getTaskByUuid(taskUuid);
if (task == null) { if (task == null) {
@ -278,9 +303,21 @@ public class AiGenerationServiceImpl implements AiGenerationService {
return false; 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 // 如果生成成功,下载外部URL到OSS
String ossUrl = null; 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); ossUrl = downloadExternalUrlToOss(externalResultUrl);
// 保存生成的媒体记录 // 保存生成的媒体记录
@ -288,6 +325,7 @@ public class AiGenerationServiceImpl implements AiGenerationService {
String mediaUuid = UUID.randomUUID().toString().replace("-", ""); String mediaUuid = UUID.randomUUID().toString().replace("-", "");
media.setUuid(mediaUuid) media.setUuid(mediaUuid)
.setMiniUserId(task.getMiniUserId())
.setTaskId(task.getId()) .setTaskId(task.getId())
.setFileSource("ai_generated") .setFileSource("ai_generated")
.setMediaType("image") .setMediaType("image")
@ -306,7 +344,7 @@ public class AiGenerationServiceImpl implements AiGenerationService {
return updateTaskStatus(taskUuid, status, ossUrl); return updateTaskStatus(taskUuid, status, ossUrl);
} catch (Exception e) { } catch (Exception e) {
log.error("处理AI任务回调异常,UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); log.error("处理AI任务回调异常,UUID:{},异常信息:{}", vo.getUuid(), e.getMessage(), e);
return false; 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<MiniAiGenerationTask> updateWrapper = new LambdaUpdateWrapper<>(); LambdaUpdateWrapper<MiniAiGenerationTask> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MiniAiGenerationTask::getUuid, uuid) updateWrapper.eq(MiniAiGenerationTask::getUuid, uuid)
.eq(MiniAiGenerationTask::getDeleted, false) .eq(MiniAiGenerationTask::getDeleted, false)
@ -337,61 +375,40 @@ public class AiGenerationServiceImpl implements AiGenerationService {
* 下载外部URL到OSS返回OSS访问地址 * 下载外部URL到OSS返回OSS访问地址
*/ */
private String downloadExternalUrlToOss(String externalUrl) { private String downloadExternalUrlToOss(String externalUrl) {
try { try (
// 下载文件并获取输入流 // 直接打开URL输入流,自动关闭
byte[] fileBytes = HttpUtil.downloadBytes(externalUrl); InputStream inputStream = new URL(externalUrl).openStream()
String fileName = IdUtil.fastSimpleUUID() + "." + FileUtil.extName(externalUrl); ) {
long currentTimestamp = System.currentTimeMillis();
// 创建临时的MultipartFile包装类
MultipartFile multipartFile = new MultipartFile() { // 生成OSS存储路径:按日期分类 + 唯一ID + 原文件后缀
@Override // String ext = FileUtil.extName(externalUrl);
public String getName() { String ext = null;
return "file";
} // 1. 如果URL包含查询参数 '?',则只取问号前面的部分
String path = externalUrl;
@Override int queryIndex = externalUrl.indexOf('?');
public String getOriginalFilename() { if (queryIndex > 0) {
return fileName; path = externalUrl.substring(0, queryIndex);
} }
// 2. 从纯净的路径中提取文件后缀
@Override int lastDotIndex = path.lastIndexOf('.');
public String getContentType() { if (lastDotIndex > 0 && lastDotIndex < path.length() - 1) {
return "image/png"; ext = path.substring(lastDotIndex + 1);
} }
if (ext == null){
@Override log.error("无法从URL中提取文件后缀,URL:{}", externalUrl);
public boolean isEmpty() { ext = "jpg";
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();
String objectName = "ai/generate/"
+ currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8)
+ "."
+ ext;
return aliyunFileService.uploadFile(objectName, inputStream);
} catch (Exception e) { } catch (Exception e) {
log.error("下载外部URL到OSS失败,URL:{},异常信息:{}", externalUrl, e.getMessage(), e); log.error("下载外部URL到OSS失败,URL:{},异常信息:{}", externalUrl, e.getMessage(), e);
throw new BusinessException("下载生成结果失败"); throw new MsgException("资源转存失败");
} }
} }
} }

10
src/main/resources/application-dev.yml

@ -91,6 +91,7 @@ security:
- /api/v1/mini/public/** - /api/v1/mini/public/**
- /api/v1/mini/homePage/listByBounds - /api/v1/mini/homePage/listByBounds
- /healthcheck - /healthcheck
- /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调
# 非安全端点路径,完全绕过 Spring Security 的过滤器 # 非安全端点路径,完全绕过 Spring Security 的过滤器
unsecured-urls: unsecured-urls:
- ${springdoc.swagger-ui.path} - ${springdoc.swagger-ui.path}
@ -225,3 +226,12 @@ wx:
miniapp: miniapp:
appid: Your_AppId appid: Your_AppId
secret: Your_AppSecret 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

Loading…
Cancel
Save