From 3a1a0c94c4c1c0cc4b35ac9b210780679929f193 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Tue, 12 May 2026 17:32:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0ai=E6=9C=8D=E5=8A=A1=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../freemarker/MyBatisPlusGenerator.java | 2 + .../boot/common/enums/LogModuleEnum.java | 4 +- .../controller/AiGenerationController.java | 64 +++ .../mapper/MiniAiGenerationTaskMapper.java | 14 + .../mini/mapper/MiniAiTaskMediaMapper.java | 14 + .../boot/mini/model/dto/GenerateResponse.java | 31 ++ .../boot/mini/model/dto/GenerationData.java | 48 +++ .../youlai/boot/mini/model/dto/ImageData.java | 26 ++ .../mini/model/dto/PhotoToComicRequest.java | 78 ++++ .../model/entity/MiniAiGenerationTask.java | 86 ++++ .../mini/model/entity/MiniAiTaskMedia.java | 102 +++++ .../boot/mini/model/vo/AiTaskCallbackVO.java | 25 ++ .../mini/service/AiGenerationService.java | 19 + .../service/impl/AiGenerationServiceImpl.java | 397 ++++++++++++++++++ .../mini/MiniAiGenerationTaskMapper.xml | 9 + .../mapper/mini/MiniAiTaskMediaMapper.xml | 9 + 16 files changed, 927 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java create mode 100644 src/main/java/com/youlai/boot/mini/mapper/MiniAiGenerationTaskMapper.java create mode 100644 src/main/java/com/youlai/boot/mini/mapper/MiniAiTaskMediaMapper.java create mode 100644 src/main/java/com/youlai/boot/mini/model/dto/GenerateResponse.java create mode 100644 src/main/java/com/youlai/boot/mini/model/dto/GenerationData.java create mode 100644 src/main/java/com/youlai/boot/mini/model/dto/ImageData.java create mode 100644 src/main/java/com/youlai/boot/mini/model/dto/PhotoToComicRequest.java create mode 100644 src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java create mode 100644 src/main/java/com/youlai/boot/mini/model/entity/MiniAiTaskMedia.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java create mode 100644 src/main/java/com/youlai/boot/mini/service/AiGenerationService.java create mode 100644 src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java create mode 100644 src/main/resources/mapper/mini/MiniAiGenerationTaskMapper.xml create mode 100644 src/main/resources/mapper/mini/MiniAiTaskMediaMapper.xml diff --git a/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java b/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java index be19bc6..50625c8 100644 --- a/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java +++ b/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") diff --git a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java index 83d8dab..124b1c2 100644 --- a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java @@ -32,7 +32,9 @@ public enum LogModuleEnum implements IBaseEnum { 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; diff --git a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java new file mode 100644 index 0000000..0615417 --- /dev/null +++ b/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> uploadReferenceFile( + @RequestPart(name = "images", required = false) List images, + @RequestPart(name = "videos", required = false) List videos + ) { + Long userId = SecurityUtils.getUserId(); + List 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 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 taskCallback(@Valid @RequestBody AiTaskCallbackVO vo) { + boolean success = aiGenerationService.handleTaskCallback(vo.getUuid(), vo.getStatus(), vo.getResultUrl()); + return Result.success(success); + } +} diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniAiGenerationTaskMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniAiGenerationTaskMapper.java new file mode 100644 index 0000000..197d131 --- /dev/null +++ b/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 { + +} diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniAiTaskMediaMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniAiTaskMediaMapper.java new file mode 100644 index 0000000..48cdfa0 --- /dev/null +++ b/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 { + +} diff --git a/src/main/java/com/youlai/boot/mini/model/dto/GenerateResponse.java b/src/main/java/com/youlai/boot/mini/model/dto/GenerateResponse.java new file mode 100644 index 0000000..388a89c --- /dev/null +++ b/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; +} diff --git a/src/main/java/com/youlai/boot/mini/model/dto/GenerationData.java b/src/main/java/com/youlai/boot/mini/model/dto/GenerationData.java new file mode 100644 index 0000000..36b4503 --- /dev/null +++ b/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 data; + + /** + * 错误信息,无错误则为null + */ + private Object error; + + /** + * Token使用统计 + */ + private Object usage; + + /** + * 创建时间戳 + */ + private Long created_at; + + /** + * 使用的工具 + */ + private String tool; + + /** + * 创建时间戳 + */ + private Long created; +} diff --git a/src/main/java/com/youlai/boot/mini/model/dto/ImageData.java b/src/main/java/com/youlai/boot/mini/model/dto/ImageData.java new file mode 100644 index 0000000..ab4e533 --- /dev/null +++ b/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; +} 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 new file mode 100644 index 0000000..9c9128f --- /dev/null +++ b/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 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 new file mode 100644 index 0000000..0a38229 --- /dev/null +++ b/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; + + +} diff --git a/src/main/java/com/youlai/boot/mini/model/entity/MiniAiTaskMedia.java b/src/main/java/com/youlai/boot/mini/model/entity/MiniAiTaskMedia.java new file mode 100644 index 0000000..7537ef5 --- /dev/null +++ b/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; + + +} 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 new file mode 100644 index 0000000..13aab1e --- /dev/null +++ b/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; +} diff --git a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java new file mode 100644 index 0000000..f201d08 --- /dev/null +++ b/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 uploadReferenceFile(List images, List videos, Long userId); + + String createAndGenerateImage(PhotoToComicRequest request, Long userId); + + boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl); + + 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 new file mode 100644 index 0000000..b3daaa1 --- /dev/null +++ b/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 uploadReferenceFile(List images, List 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() + .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 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() + .eq(MiniAiGenerationTask::getUuid, uuid) + .eq(MiniAiGenerationTask::getDeleted, false)); + } + + /** + * 更新任务状态 + */ + private boolean updateTaskStatus(String uuid, Byte status, String resultUrl) { + LambdaUpdateWrapper 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("下载生成结果失败"); + } + } +} diff --git a/src/main/resources/mapper/mini/MiniAiGenerationTaskMapper.xml b/src/main/resources/mapper/mini/MiniAiGenerationTaskMapper.xml new file mode 100644 index 0000000..d01050b --- /dev/null +++ b/src/main/resources/mapper/mini/MiniAiGenerationTaskMapper.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/main/resources/mapper/mini/MiniAiTaskMediaMapper.xml b/src/main/resources/mapper/mini/MiniAiTaskMediaMapper.xml new file mode 100644 index 0000000..2c91340 --- /dev/null +++ b/src/main/resources/mapper/mini/MiniAiTaskMediaMapper.xml @@ -0,0 +1,9 @@ + + + + + + +