Browse Source

增加ai服务调用接口

glx
glx 3 weeks ago
parent
commit
3a1a0c94c4
  1. 2
      src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java
  2. 4
      src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
  3. 64
      src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java
  4. 14
      src/main/java/com/youlai/boot/mini/mapper/MiniAiGenerationTaskMapper.java
  5. 14
      src/main/java/com/youlai/boot/mini/mapper/MiniAiTaskMediaMapper.java
  6. 31
      src/main/java/com/youlai/boot/mini/model/dto/GenerateResponse.java
  7. 48
      src/main/java/com/youlai/boot/mini/model/dto/GenerationData.java
  8. 26
      src/main/java/com/youlai/boot/mini/model/dto/ImageData.java
  9. 78
      src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java
  10. 86
      src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java
  11. 102
      src/main/java/com/youlai/boot/mini/model/entity/MiniAiTaskMedia.java
  12. 25
      src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java
  13. 19
      src/main/java/com/youlai/boot/mini/service/AiGenerationService.java
  14. 397
      src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java
  15. 9
      src/main/resources/mapper/mini/MiniAiGenerationTaskMapper.xml
  16. 9
      src/main/resources/mapper/mini/MiniAiTaskMediaMapper.xml

2
src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java

@ -99,6 +99,8 @@ public class MyBatisPlusGenerator {
,new TableConfig("mini_point_record", IdType.AUTO, "mini")
,new TableConfig("mini_point_rule", IdType.AUTO, "mini")
,new TableConfig("mini_sign_record", IdType.AUTO, "mini")
,new TableConfig("mini_ai_generation_task", IdType.AUTO, "mini")
,new TableConfig("mini_ai_task_media", IdType.AUTO, "mini")
// ,new TableConfig("mini_stray_animal", IdType.AUTO, "mini")
// ,new TableConfig("mini_stray_animal", IdType.INPUT, "minitest")

4
src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java

@ -32,7 +32,9 @@ public enum LogModuleEnum implements IBaseEnum<Integer> {
POINT_ACCOUNT(100, "积分账户"),
POINT_RECORD(101, "积分流水"),
POINT_RULE(102, "积分规则"),
SIGN_RECORD(103, "签到记录");
SIGN_RECORD(103, "签到记录"),
AI_TASK_MEDIA(104, "AI任务媒体"),
AI_GENERATION_TASK(105, "AI生成任务");
@EnumValue
private final Integer value;

64
src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java

@ -0,0 +1,64 @@
package com.youlai.boot.mini.controller;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.annotation.RepeatSubmit;
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.vo.AiTaskCallbackVO;
import com.youlai.boot.mini.service.AiGenerationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collections;
import java.util.List;
@Tag(name = "AI生成图片视频相关接口")
@RestController
@RequestMapping("/api/v1/mini/ai/generation")
@RequiredArgsConstructor
@Valid
public class AiGenerationController {
private final AiGenerationService aiGenerationService;
@Operation(summary = "上传AI生成参考文件", operationId = "AiReferenceSaveFile")
@PostMapping(value = "/upload-reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RepeatSubmit
@Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.INSERT)
public Result<List<String>> uploadReferenceFile(
@RequestPart(name = "images", required = false) List<MultipartFile> images,
@RequestPart(name = "videos", required = false) List<MultipartFile> videos
) {
Long userId = SecurityUtils.getUserId();
List<String> urlList = aiGenerationService.uploadReferenceFile(images, videos, userId);
return Result.success(urlList);
}
@Operation(summary = "提交单图漫画生成任务")
@PostMapping("/generate-single-image")
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT)
public Result<String> generateSingleImage(@Valid @RequestBody PhotoToComicRequest request) {
Long userId = SecurityUtils.getUserId();
String taskUuid = aiGenerationService.createAndGenerateImage(request, userId);
return Result.success(taskUuid);
}
@Operation(summary = "AI生成任务回调接口")
@PostMapping("/task/callback")
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE)
public Result<Boolean> taskCallback(@Valid @RequestBody AiTaskCallbackVO vo) {
boolean success = aiGenerationService.handleTaskCallback(vo.getUuid(), vo.getStatus(), vo.getResultUrl());
return Result.success(success);
}
}

14
src/main/java/com/youlai/boot/mini/mapper/MiniAiGenerationTaskMapper.java

@ -0,0 +1,14 @@
package com.youlai.boot.mini.mapper;
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* AI生成任务统一表 Mapper 接口
*
* @author jwy
* @since
*/
public interface MiniAiGenerationTaskMapper extends BaseMapper<MiniAiGenerationTask> {
}

14
src/main/java/com/youlai/boot/mini/mapper/MiniAiTaskMediaMapper.java

@ -0,0 +1,14 @@
package com.youlai.boot.mini.mapper;
import com.youlai.boot.mini.model.entity.MiniAiTaskMedia;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* AI任务媒体文件表 Mapper 接口
*
* @author jwy
* @since
*/
public interface MiniAiTaskMediaMapper extends BaseMapper<MiniAiTaskMedia> {
}

31
src/main/java/com/youlai/boot/mini/model/dto/GenerateResponse.java

@ -0,0 +1,31 @@
package com.youlai.boot.mini.model.dto;
import lombok.Data;
/**
* AI生成接口响应DTO
*
* @author youlai
*/
@Data
public class GenerateResponse {
/**
* 响应消息
*/
private String msg;
/**
* 响应码0表示成功
*/
private Integer code;
/**
* 生成结果数据
*/
private GenerationData data;
/**
* 请求ID
*/
private String request_id;
}

48
src/main/java/com/youlai/boot/mini/model/dto/GenerationData.java

@ -0,0 +1,48 @@
package com.youlai.boot.mini.model.dto;
import lombok.Data;
import java.util.List;
/**
* AI生成结果数据DTO
*
* @author youlai
*/
@Data
public class GenerationData {
/**
* 使用的模型ID
*/
private String model;
/**
* 生成的图片列表
*/
private List<ImageData> data;
/**
* 错误信息无错误则为null
*/
private Object error;
/**
* Token使用统计
*/
private Object usage;
/**
* 创建时间戳
*/
private Long created_at;
/**
* 使用的工具
*/
private String tool;
/**
* 创建时间戳
*/
private Long created;
}

26
src/main/java/com/youlai/boot/mini/model/dto/ImageData.java

@ -0,0 +1,26 @@
package com.youlai.boot.mini.model.dto;
import lombok.Data;
/**
* 生成的图片数据DTO
*
* @author youlai
*/
@Data
public class ImageData {
/**
* 图片URL地址
*/
private String url;
/**
* Base64编码的图片JSON
*/
private String b64_json;
/**
* 图片尺寸格式如 1664x2496
*/
private String size;
}

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

@ -0,0 +1,78 @@
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;
}

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

@ -0,0 +1,86 @@
package com.youlai.boot.mini.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
@Getter
@Setter
@ToString
@Accessors(chain = true)
@TableName("mini_ai_generation_task")
@Schema(description = "AI生成任务统一表")
public class MiniAiGenerationTask implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
@Schema(description = "笔记ID")
private Long id;
@TableField("uuid")
@Schema(description = "uuid唯一标识,前后端用这个进行数据交互")
private String uuid;
@TableField("mini_user_id")
@Schema(description = "作者用户ID")
private Long miniUserId;
@TableField("type")
@Schema(description = "生成类型:img_single, img_grid_4, video")
private String type;
@TableField("generate_params")
@Schema(description = "生成参数JSON,不同类型参数不同")
private String generateParams;
@TableField("status")
@Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消")
private Byte status;
@TableField("result_resource_url")
@Schema(description = "生成结果资源URL")
private String resultResourceUrl;
@TableField("points_consumed")
@Schema(description = "消耗积分")
private Integer pointsConsumed;
@TableField("create_time")
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@TableField("create_timestamp")
@Schema(description = "创建时间毫秒级时间戳")
private Long createTimestamp;
@TableField("create_by")
@Schema(description = "创建人ID")
private Long createBy;
@TableField("update_time")
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
@TableField("update_timestamp")
@Schema(description = "更新时间毫秒级时间戳")
private Long updateTimestamp;
@TableField("update_by")
@Schema(description = "修改人ID")
private Long updateBy;
@TableField("is_deleted")
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Boolean deleted;
}

102
src/main/java/com/youlai/boot/mini/model/entity/MiniAiTaskMedia.java

@ -0,0 +1,102 @@
package com.youlai.boot.mini.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
@Getter
@Setter
@ToString
@Accessors(chain = true)
@TableName("mini_ai_task_media")
@Schema(description = "AI任务媒体文件表")
public class MiniAiTaskMedia implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
@Schema(description = "")
private Long id;
@TableField("uuid")
@Schema(description = "uuid唯一标识,前后端用这个进行数据交互")
private String uuid;
@TableField("mini_user_id")
@Schema(description = "用户id")
private Long miniUserId;
@TableField("task_id")
@Schema(description = "AI生成任务id")
private Long taskId;
@TableField("file_source")
@Schema(description = "文件来源:user_upload(用户上传), ai_generated(AI生成)")
private String fileSource;
@TableField("media_type")
@Schema(description = "媒体类型,image-图片,video-视频")
private String mediaType;
@TableField("source_url")
@Schema(description = "资源URL")
private String sourceUrl;
@TableField("storage_key")
@Schema(description = "对象存储中的key")
private String storageKey;
@TableField("thumbnail_url")
@Schema(description = "缩略图URL(视频需要)")
private String thumbnailUrl;
@TableField("width")
@Schema(description = "宽度(像素)")
private Integer width;
@TableField("height")
@Schema(description = "高度(像素)")
private Integer height;
@TableField("duration")
@Schema(description = "时长(秒,视频用)")
private Integer duration;
@TableField("create_time")
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@TableField("create_timestamp")
@Schema(description = "创建时间毫秒级时间戳")
private Long createTimestamp;
@TableField("create_by")
@Schema(description = "创建人ID")
private Long createBy;
@TableField("update_time")
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
@TableField("update_timestamp")
@Schema(description = "更新时间毫秒级时间戳")
private Long updateTimestamp;
@TableField("update_by")
@Schema(description = "修改人ID")
private Long updateBy;
@TableField("is_deleted")
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Boolean deleted;
}

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

@ -0,0 +1,25 @@
package com.youlai.boot.mini.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* AI任务回调VO
*
* @author youlai
*/
@Data
@Schema(description = "AI任务回调VO")
public class AiTaskCallbackVO {
@NotBlank(message = "任务UUID不能为空")
@Schema(description = "任务UUID", requiredMode = Schema.RequiredMode.REQUIRED)
private String uuid;
@Schema(description = "生成结果URL")
private String resultUrl;
@Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消")
private Byte status;
}

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

@ -0,0 +1,19 @@
package com.youlai.boot.mini.service;
import com.youlai.boot.mini.model.dto.PhotoToComicRequest;
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface AiGenerationService {
List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId);
String createAndGenerateImage(PhotoToComicRequest request, Long userId);
boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl);
MiniAiGenerationTask getTaskByUuid(String uuid);
}

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

@ -0,0 +1,397 @@
package com.youlai.boot.mini.service.impl;
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.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.util.FileUtils;
import com.youlai.boot.common.util.JavaVCUtils;
import com.youlai.boot.common.util.RandomNumberUtils;
import com.youlai.boot.file.model.FileInfo;
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.service.AiGenerationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
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.util.concurrent.CompletableFuture;
@Slf4j
@Service
@RequiredArgsConstructor
public class AiGenerationServiceImpl implements AiGenerationService {
private final MiniAiGenerationTaskMapper aiGenerationTaskMapper;
private final MiniAiTaskMediaMapper aiTaskMediaMapper;
private final FileService fileService;
private final AliyunFileService aliyunFileService;
/**
* AI生成服务地址
*/
private static final String AI_GENERATE_URL = "http://127.0.0.1:8001/api/v1/photo-to-comic";
/**
* 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/";
@Override
public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) {
// 计算本次上传的文件总数
int uploadCount = (images != null ? images.size() : 0) + (videos != null ? videos.size() : 0);
if (uploadCount == 0) {
throw new BusinessException("请选择要上传的文件");
}
// 校验用户当前未关联任务的上传文件数量,最多5个
Long existCount = aiTaskMediaMapper.selectCount(new LambdaQueryWrapper<MiniAiTaskMedia>()
.eq(MiniAiTaskMedia::getCreateBy, userId)
.isNull(MiniAiTaskMedia::getTaskId)
.eq(MiniAiTaskMedia::getFileSource, "user_upload")
.eq(MiniAiTaskMedia::getDeleted, false));
if (existCount + uploadCount > 5) {
throw new BusinessException("最多只能上传5个待生成的参考文件");
}
List<String> urlList = new ArrayList<>();
long timestamp = System.currentTimeMillis();
// 处理图片
if (images != null && !images.isEmpty()) {
for (MultipartFile image : images) {
try {
String fileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8);
String ext = FileUtil.extName(image.getOriginalFilename());
String objectName = OSS_IMAGE_DIR + fileName + "." + ext;
String url = aliyunFileService.uploadFile(objectName, image.getInputStream());
// 获取图片信息
BufferedImage imageInfo = ImageIO.read(image.getInputStream());
// 保存媒体记录
MiniAiTaskMedia media = new MiniAiTaskMedia();
String uuid = UUID.randomUUID().toString().replace("-", "");
media.setUuid(uuid)
.setFileSource("user_upload")//TODO 待整理成枚举,参考项目已有内容
.setMediaType("image")
.setSourceUrl(url)
.setStorageKey(objectName)
.setWidth(imageInfo.getWidth())
.setHeight(imageInfo.getHeight())
.setCreateBy(userId)
.setCreateTimestamp(timestamp)
.setCreateTime(new Date(timestamp));
int result = aiTaskMediaMapper.insert(media);
if (result > 0) {
urlList.add(url);
}
} catch (Exception e) {
log.error("image upload failed", e);
}
}
}
// 处理视频
if (videos != null && !videos.isEmpty()) {
String tmpPath = System.getProperty("user.dir") + "/tmp";
// 确保临时目录存在
File tmpDir = new File(tmpPath);
if (!tmpDir.exists()) {
tmpDir.mkdirs();
}
for (MultipartFile video : videos) {
try {
String fileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8);
String ext = FileUtil.extName(video.getOriginalFilename());
String objectName = OSS_VIDEO_DIR + fileName + "." + ext;
String url = aliyunFileService.uploadFile(objectName, video.getInputStream());
// 保存媒体记录
MiniAiTaskMedia media = new MiniAiTaskMedia();
String uuid = UUID.randomUUID().toString().replace("-", "");
media.setUuid(uuid)
.setFileSource("user_upload")
.setMediaType("video")
.setSourceUrl(url)
.setStorageKey(objectName)
.setCreateBy(userId)
.setCreateTimestamp(timestamp)
.setCreateTime(new Date(timestamp));
// 获取视频时长
FileUtils.saveFile(video, tmpPath, fileName);
String videoPath = tmpPath + File.separator + fileName;
double duration = JavaVCUtils.getVideoDuration(videoPath);
media.setDuration((int) Math.ceil(duration));
// 生成并上传视频缩略图
BufferedImage thumbnail = JavaVCUtils.getVideoThumbnail(videoPath, 1);
String thumbnailFileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8);
String thumbnailObjectName = OSS_THUMBNAIL_DIR + thumbnailFileName + ".png";
String thumbnailUrl = aliyunFileService.uploadFile(thumbnailObjectName,
FileUtils.bufferedImageToInputStream(thumbnail, "png"));
media.setThumbnailUrl(thumbnailUrl);
int result = aiTaskMediaMapper.insert(media);
if (result > 0) {
urlList.add(url);
}
// 删除临时文件
FileUtils.delete(videoPath);
} catch (Exception e) {
log.error("video upload failed", e);
}
}
}
return urlList;
}
@Override
public String createAndGenerateImage(PhotoToComicRequest request, Long userId) {
// 创建生成任务
MiniAiGenerationTask task = new MiniAiGenerationTask();
String taskUuid = UUID.randomUUID().toString().replace("-", "");
Date now = new Date();
long timestamp = System.currentTimeMillis();
task.setUuid(taskUuid)
.setMiniUserId(userId)
.setType("img_single")
.setGenerateParams(JSONUtil.toJsonStr(request))
.setPointsConsumed(1) // 默认消耗1积分
.setStatus((byte) 0)
.setCreateBy(userId)
.setCreateTime(now)
.setCreateTimestamp(timestamp)
.setUpdateTime(now)
.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("-", "");
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);
// 更新任务状态为成功
updateTaskStatus(taskUuid, (byte) 1, ossUrl);
log.info("AI生成任务完成,任务UUID:{},结果URL:{}", taskUuid, ossUrl);
} catch (Exception e) {
log.error("AI生成任务异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e);
updateTaskStatus(taskUuid, (byte) 2, null);
}
});
return taskUuid;
}
@Override
public boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl) {
try {
// 查询任务是否存在
MiniAiGenerationTask task = getTaskByUuid(taskUuid);
if (task == null) {
log.error("回调任务不存在,UUID:{}", taskUuid);
return false;
}
// 如果生成成功,下载外部URL到OSS
String ossUrl = null;
if (status == 1 && externalResultUrl != null) {
ossUrl = downloadExternalUrlToOss(externalResultUrl);
// 保存生成的媒体记录
MiniAiTaskMedia media = new MiniAiTaskMedia();
String mediaUuid = UUID.randomUUID().toString().replace("-", "");
media.setUuid(mediaUuid)
.setTaskId(task.getId())
.setFileSource("ai_generated")
.setMediaType("image")
.setSourceUrl(ossUrl)
.setCreateBy(task.getCreateBy())
.setCreateTime(new Date())
.setCreateTimestamp(System.currentTimeMillis())
.setUpdateTime(new Date())
.setUpdateTimestamp(System.currentTimeMillis())
.setDeleted(false);
aiTaskMediaMapper.insert(media);
}
// 更新任务状态
return updateTaskStatus(taskUuid, status, ossUrl);
} catch (Exception e) {
log.error("处理AI任务回调异常,UUID:{},异常信息:{}", taskUuid, e.getMessage(), e);
return false;
}
}
@Override
public MiniAiGenerationTask getTaskByUuid(String uuid) {
return aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper<MiniAiGenerationTask>()
.eq(MiniAiGenerationTask::getUuid, uuid)
.eq(MiniAiGenerationTask::getDeleted, false));
}
/**
* 更新任务状态
*/
private boolean updateTaskStatus(String uuid, Byte status, String resultUrl) {
LambdaUpdateWrapper<MiniAiGenerationTask> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MiniAiGenerationTask::getUuid, uuid)
.eq(MiniAiGenerationTask::getDeleted, false)
.set(MiniAiGenerationTask::getStatus, status)
.set(resultUrl != null, MiniAiGenerationTask::getResultResourceUrl, resultUrl)
.set(MiniAiGenerationTask::getUpdateTime, new Date())
.set(MiniAiGenerationTask::getUpdateTimestamp, System.currentTimeMillis());
return aiGenerationTaskMapper.update(null, updateWrapper) > 0;
}
/**
* 下载外部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();
} catch (Exception e) {
log.error("下载外部URL到OSS失败,URL:{},异常信息:{}", externalUrl, e.getMessage(), e);
throw new BusinessException("下载生成结果失败");
}
}
}

9
src/main/resources/mapper/mini/MiniAiGenerationTaskMapper.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper">
</mapper>

9
src/main/resources/mapper/mini/MiniAiTaskMediaMapper.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper">
</mapper>
Loading…
Cancel
Save