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 01/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0ai=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E8=B0=83=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 @@ + + + + + + + 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 02/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=8D=95=E5=9B=BE?= =?UTF-8?q?=E7=89=87=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 From e5489fe290e1b872243915963bbcc29948f5d4b8 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 14 May 2026 17:26:07 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=94=9F=E6=88=90=E4=BB=BB=E5=8A=A1=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 29 +- .../model/entity/MiniAiGenerationTask.java | 4 + .../model/form/AiFourPanelGenerateForm.java | 54 +++ .../mini/model/form/AiVideoGenerateForm.java | 28 ++ .../boot/mini/model/vo/AiVideoCallbackVO.java | 28 ++ .../mini/model/vo/VideoCallbackContent.java | 19 ++ .../boot/mini/model/vo/VideoCallbackData.java | 61 ++++ .../mini/model/vo/VideoCallbackUsage.java | 23 ++ .../mini/service/AiGenerationService.java | 27 +- .../service/impl/AiGenerationServiceImpl.java | 311 ++++++++++++++++-- src/main/resources/application-dev.yml | 13 +- 11 files changed, 548 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java diff --git a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java index e5e1696..26f9544 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -6,8 +6,11 @@ import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; +import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.service.AiGenerationService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -63,9 +66,31 @@ public class AiGenerationController { return Result.success(success); } + @Operation(summary = "提交四宫格漫画生成任务") + @PostMapping("/generate-four-panel") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) + public Result generateFourPanelImage(@Valid @RequestBody AiFourPanelGenerateForm form) { + Long userId = SecurityUtils.getUserId(); + String taskUuid = aiGenerationService.createAndGenerateFourPanel(form, userId); + return Result.success(taskUuid); + } - // TODO: 添加 4宫格图片生成任务 接口 ,外部服务接口为:http://127.0.0.1:8001//api/v1/four-panel-comic - + @Operation(summary = "提交视频生成任务") + @PostMapping("/generate-video") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) + public Result generateVideo(@Valid @RequestBody AiVideoGenerateForm form) { + Long userId = SecurityUtils.getUserId(); + String taskUuid = aiGenerationService.createAndGenerateVideo(form, userId); + return Result.success(taskUuid); + } + //TODO: 后续增加用户小程序订阅通知 + @Operation(summary = "视频任务回调接口") + @PostMapping("/video/task/callback") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) + public Result videoTaskCallback(@Valid @RequestBody AiVideoCallbackVO vo) { + boolean success = aiGenerationService.handleVideoTaskCallback(vo); + return Result.success(success); + } } diff --git a/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java b/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java index a8dacb2..28af4d2 100644 --- a/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java +++ b/src/main/java/com/youlai/boot/mini/model/entity/MiniAiGenerationTask.java @@ -40,6 +40,10 @@ public class MiniAiGenerationTask implements Serializable { @Schema(description = "生成参数JSON,不同类型参数不同") private String generateParams; + @TableField("video_task_uuid") + @Schema(description = "调用视频接口时返回") + private String videoTaskUuid; + @TableField("status") @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") private Integer status; diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java new file mode 100644 index 0000000..3f6f0ad --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java @@ -0,0 +1,54 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.util.List; + +/** + * AI四宫格漫画生成请求表单 + * + * @author youlai + */ +@Data +@Schema(description = "AI四宫格漫画生成请求表单") +public class AiFourPanelGenerateForm { + + @Schema(description = "参考图片URL列表,可选") + private List referenceImage; + + @Schema(description = "使用的模型ID", defaultValue = "doubao-seedream-5-0-260128") + private String model; + + @Schema(description = "画风:1=治愈系水彩,2=Q版卡通,3=日式漫画,4=吉卜力风,5=3D毛绒,6=搞笑夸张", defaultValue = "1") + private Integer style = 1; + + @Schema(description = "比例:1=1:1,2=2:3,3=3:4,4=4:3,5=5:4", defaultValue = "1") + private Integer ratio = 1; + + @Schema(description = "图片尺寸:1=2K,2=4K,3=8K", defaultValue = "1") + private Integer imgSize = 1; + + @Schema(description = "图片格式:1=png,2=jpeg", defaultValue = "1") + private Integer imgType = 1; + + @Schema(description = "宠物物种:如cat、dog") + private String species; + + @Schema(description = "宠物品种:如British Shorthair、Corgi") + private String breed; + + @Schema(description = "宠物毛色:如blue-gray、orange") + private String color; + + @Schema(description = "眼睛颜色:如amber、blue") + private String eyeColor; + + @Schema(description = "体型:如round chubby、slim and elegant") + private String bodyType; + + @Schema(description = "特殊特征:如flat face、thick coat", defaultValue = "") + private String distinctiveFeatures = ""; + + @Schema(description = "故事梗概:AI会据此生成四格漫画脚本") + private String storyOutline; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java new file mode 100644 index 0000000..c21bc1a --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java @@ -0,0 +1,28 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.util.List; +import java.util.Map; + +/** + * AI视频生成请求表单 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成请求表单") +public class AiVideoGenerateForm { + + @Schema(description = "使用的模型ID", defaultValue = "doubao-seedance-2-0-260128") + private String model; + + @Schema(description = "内容数组,包含text、image_url等类型") + private List> content; + + @Schema(description = "分辨率: 480p, 720p, 1080p, 2K", defaultValue = "720p") + private String resolution = "720p"; + + @Schema(description = "视频时长,单位秒,最大15秒", defaultValue = "5") + private Integer duration = 5; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java b/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java new file mode 100644 index 0000000..76e6ff5 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java @@ -0,0 +1,28 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成任务回调请求 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成任务回调请求") +public class AiVideoCallbackVO { + + @Schema(description = "响应消息") + private String msg; + + @Schema(description = "响应码,0表示成功") + private Integer code; + + @Schema(description = "回调数据") + private VideoCallbackData data; + + @JsonProperty("request_id") + @Schema(description = "请求ID") + private String requestId; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java new file mode 100644 index 0000000..f66617d --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java @@ -0,0 +1,19 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成回调结果内容 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成回调结果内容") +public class VideoCallbackContent { + + @JsonProperty("video_url") + @Schema(description = "生成的视频地址") + private String videoUrl; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java new file mode 100644 index 0000000..706ffb1 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java @@ -0,0 +1,61 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成回调数据 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成回调数据") +public class VideoCallbackData { + + @Schema(description = "任务ID") + private String id; + + @Schema(description = "使用的模型ID") + private String model; + + @Schema(description = "任务状态: pending/running/succeeded/failed") + private String status; + + @Schema(description = "生成的结果内容") + private VideoCallbackContent content; + + @Schema(description = "资源消耗统计") + private VideoCallbackUsage usage; + + @JsonProperty("created_at") + @Schema(description = "创建时间戳") + private Long createdAt; + + @JsonProperty("updated_at") + @Schema(description = "更新时间戳") + private Long updatedAt; + + @Schema(description = "随机种子") + private Long seed; + + @Schema(description = "分辨率") + private String resolution; + + @Schema(description = "比例") + private String ratio; + + @Schema(description = "视频时长,单位秒") + private Integer duration; + + @Schema(description = "帧率") + private Integer framespersecond; + + @JsonProperty("service_tier") + @Schema(description = "服务等级") + private String serviceTier; + + @JsonProperty("execution_expires_after") + @Schema(description = "执行过期时间") + private Long executionExpiresAfter; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java new file mode 100644 index 0000000..2e4b752 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java @@ -0,0 +1,23 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI视频生成资源消耗统计 + * + * @author youlai + */ +@Data +@Schema(description = "AI视频生成资源消耗统计") +public class VideoCallbackUsage { + + @JsonProperty("completion_tokens") + @Schema(description = "完成token数") + private Integer completionTokens; + + @JsonProperty("total_tokens") + @Schema(description = "总token数") + private Integer totalTokens; +} diff --git a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java index f0f41b8..3837d54 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -1,38 +1,29 @@ package com.youlai.boot.mini.service; +import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; +import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import org.springframework.web.multipart.MultipartFile; import java.util.List; public interface AiGenerationService { - /** - * 上传参考文件(图片/视频) - * @param images 上传的图片列表 - * @param videos 上传的视频列表 - * @param userId 用户ID - * @return 文件访问URL列表 - */ List uploadReferenceFile(List images, List videos, Long userId); - /** - * 创建AI生成任务并调用生成接口 - * @param form 生成请求表单 - * @param userId 用户ID - * @return 任务UUID - */ String createAndGenerateImage(AiSingleImageGenerateForm form, Long userId); - /** - * 处理AI生成任务回调 - * @param vo 回调请求参数 - * @return 是否处理成功 - */ + String createAndGenerateFourPanel(AiFourPanelGenerateForm form, Long userId); + boolean handleTaskCallback(AiTaskCallbackVO vo); + String createAndGenerateVideo(AiVideoGenerateForm form, Long userId); + + boolean handleVideoTaskCallback(AiVideoCallbackVO vo); + MiniAiGenerationTask getTaskByUuid(String uuid); } diff --git a/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java index abdbbc3..6e1ff5c 100644 --- a/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java +++ b/src/main/java/com/youlai/boot/mini/service/impl/AiGenerationServiceImpl.java @@ -24,9 +24,13 @@ import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; +import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; +import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.form.MiniDeductPointForm; import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.youlai.boot.mini.model.vo.VideoCallbackData; import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.MiniPointRecordService; @@ -58,31 +62,41 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final AliyunFileService aliyunFileService; private final MiniPointRecordService pointRecordService; - /** - * AI生成服务地址 - */ - @Value("${ai.generate.server-url:http://127.0.0.1:8001/api/v1/photo-to-comic}") - private String aiGenerateServerUrl; - - /** - * AI任务回调地址 - */ - @Value("${ai.callback.url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/task/callback}") - private String aiCallbackUrl; - - @Value("${ai.default.model:doubao-seedream-5-0-260128}") - private String aiDefaultModel; - - /** - * OSS存储目录配置 - */ + //OSS存储目录配置 private static final String OSS_IMAGE_DIR = "ai/image/"; private static final String OSS_VIDEO_DIR = "ai/video/"; private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/"; - /** - * AI单图生成积分规则编码 - */ + + //AI单图生成服务地址 + @Value("${ai.generate.single-image-server-url:http://127.0.0.1:8001/api/v1/photo-to-comic}") + private String aiSingleImageServerUrl; + //AI四宫格图片生成服务地址 + @Value("${ai.generate.four-panel-server-url:http://127.0.0.1:8001/api/v1/four-panel-comic}") + private String aiFourPanelServerUrl; + //AI单图/四宫格图片生成任务回调地址 + @Value("${ai.callback.single-image-callback-url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/task/callback}") + private String aiSingleImageCallbackUrl; + //视频生成服务地址 + @Value("${ai.generate.video-server-url:http://127.0.0.1:8001/api/v1/video/submit}") + private String aiVideoServerUrl; + //视频任务回调地址 + @Value("${ai.callback.video-url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/video/task/callback}") + private String aiVideoCallbackUrl; + + //默认图片生成AI模型 + @Value("${ai.default.image-model:doubao-seedream-5-0-260128}") + private String aiDefaultImageModel; + + //默认视频生成AI模型 + @Value("${ai.default.video-model:doubao-seedance-2-0-260128}") + private String aiDefaultVideoModel; + + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; + //AI四宫格生成积分规则编码 + private static final String AI_GENERATE_QUAD_GRID_RULE = "AI_GENERATE_QUAD_GRID"; + //AI视频生成积分规则编码 + private static final String AI_GENERATE_VIDEO_RULE = "AI_GENERATE_VIDEO"; @Override public List uploadReferenceFile(List images, List videos, Long userId) { @@ -236,7 +250,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 组装第三方AI接口需要的参数 Map aiRequest = new HashMap<>(); - aiRequest.put("model", form.getModel() == null ? aiDefaultModel : form.getModel()); + aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); aiRequest.put("reference_image", form.getImageUrl()); aiRequest.put("style", form.getStyle()); aiRequest.put("ratio", form.getRatio()); @@ -251,12 +265,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiRequest.put("scene_description", form.getSceneDescription()); // 传递任务UUID和回调地址 aiRequest.put("uuid", taskUuid); - aiRequest.put("callback_url", aiCallbackUrl); + aiRequest.put("callback_url", aiSingleImageCallbackUrl); try { log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); // 同步调用AI接口,超时1秒 - HttpResponse response = HttpRequest.post(aiGenerateServerUrl) + HttpResponse response = HttpRequest.post(aiSingleImageServerUrl) .header("Content-Type", "application/json") .body(JSONUtil.toJsonStr(aiRequest)) .timeout(1000) @@ -291,6 +305,244 @@ public class AiGenerationServiceImpl implements AiGenerationService { return taskUuid; } + @Override + @Transactional(rollbackFor = Exception.class) // 所有异常都触发事务回滚 + public String createAndGenerateFourPanel(AiFourPanelGenerateForm form, Long userId) { + // 生成任务UUID + String taskUuid = UUID.randomUUID().toString().replace("-", ""); + Date now = new Date(); + long timestamp = System.currentTimeMillis(); + + // 扣减积分(自动加入当前事务,失败会一起回滚) + MiniDeductPointForm deductForm = new MiniDeductPointForm(); + deductForm.setRuleCode(AI_GENERATE_QUAD_GRID_RULE); + deductForm.setBizId(taskUuid); + Integer deductPoint = pointRecordService.deductPoint(userId, deductForm.getRuleCode(), deductForm.getBizId()); + + // 创建生成任务(自动加入当前事务,失败会一起回滚) + MiniAiGenerationTask task = new MiniAiGenerationTask(); + task.setUuid(taskUuid) + .setMiniUserId(userId) + .setType("img_grid_4") // 四宫格漫画 + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) // 生成中 + .setCreateBy(userId) + .setCreateTime(now) + .setCreateTimestamp(timestamp); + + aiGenerationTaskMapper.insert(task); + + // 组装第三方AI接口需要的参数 + Map aiRequest = new HashMap<>(); + aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); + aiRequest.put("reference_image", form.getReferenceImage()); + aiRequest.put("style", form.getStyle()); + aiRequest.put("ratio", form.getRatio()); + aiRequest.put("img_size", form.getImgSize()); + aiRequest.put("img_type", form.getImgType()); + aiRequest.put("species", form.getSpecies()); + aiRequest.put("breed", form.getBreed()); + aiRequest.put("color", form.getColor()); + aiRequest.put("eye_color", form.getEyeColor()); + aiRequest.put("body_type", form.getBodyType()); + aiRequest.put("distinctive_features", form.getDistinctiveFeatures()); + aiRequest.put("story_outline", form.getStoryOutline()); + // 传递任务UUID和回调地址 + aiRequest.put("uuid", taskUuid); + aiRequest.put("callback_url", aiSingleImageCallbackUrl); + + try { + log.info("提交四宫格漫画生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); + // 同步调用AI接口,超时1秒 + HttpResponse response = HttpRequest.post(aiFourPanelServerUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(1000) + .execute(); + + // 先判断HTTP状态码 + if (!response.isOk()) { + log.error("四宫格漫画生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body()); + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } + + // 解析返回结果,校验业务状态码:只有code=0才代表提交成功 + String responseBody = response.body(); + JSONObject resJson = JSONUtil.parseObj(responseBody); + Integer code = resJson.getInt("code"); + if (!Integer.valueOf(0).equals(code)) { + String errMsg = resJson.getStr("msg", "服务调用失败"); + + log.error("四宫格漫画生成任务提交失败,业务错误码:{},错误信息:{},request_id:{},完整响应:{}", + code, errMsg, resJson.getStr("request_id"), responseBody); + throw new MsgException("AI生成失败:" + errMsg); + } + } catch (JSONException e) { + log.error("四宫格漫画生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } catch (Exception e) { + log.error("四宫格漫画生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + // 抛出异常触发事务回滚 + throw new MsgException("AI生成服务暂时不可用,请稍后重试"); + } + + return taskUuid; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String createAndGenerateVideo(AiVideoGenerateForm form, Long userId) { + // 生成任务UUID + String taskUuid = UUID.randomUUID().toString().replace("-", ""); + Date now = new Date(); + long timestamp = System.currentTimeMillis(); + + // 扣减积分 + MiniDeductPointForm deductForm = new MiniDeductPointForm(); + deductForm.setRuleCode(AI_GENERATE_VIDEO_RULE); + deductForm.setBizId(taskUuid); + Integer deductPoint = pointRecordService.deductPoint(userId, deductForm.getRuleCode(), deductForm.getBizId()); + + // 创建生成任务 + MiniAiGenerationTask task = new MiniAiGenerationTask(); + task.setUuid(taskUuid) + .setMiniUserId(userId) + .setType("video") + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) + .setCreateBy(userId) + .setCreateTime(now) + .setCreateTimestamp(timestamp); + + aiGenerationTaskMapper.insert(task); + + // 组装第三方接口参数 + Map aiRequest = new HashMap<>(); + aiRequest.put("model", form.getModel() == null ? aiDefaultVideoModel : form.getModel());//TODO写到上面 + aiRequest.put("content", form.getContent()); + aiRequest.put("resolution", form.getResolution()); + aiRequest.put("duration", form.getDuration()); + aiRequest.put("uuid", taskUuid); + aiRequest.put("callback_url", aiVideoCallbackUrl); //TODO 后续独立回调地址 + + try { + log.info("提交视频生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); + HttpResponse response = HttpRequest.post(aiVideoServerUrl) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(1000) + .execute(); + + if (!response.isOk()) { + log.error("视频生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body()); + throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); + } + + String responseBody = response.body(); + JSONObject resJson = JSONUtil.parseObj(responseBody); + Integer code = resJson.getInt("code"); + if (!Integer.valueOf(0).equals(code)) { + String errMsg = resJson.getStr("msg", "服务调用失败"); + log.error("视频生成任务提交失败,业务错误码:{},错误信息:{},完整响应:{}", code, errMsg, responseBody); + throw new MsgException("AI视频生成失败:" + errMsg); + } + + // 提取第三方返回的视频任务uuid,保存到任务表中,后续回调通过这个uuid匹配任务 + JSONObject data = resJson.getJSONObject("data"); + String videoTaskUuid = data.getStr("uuid"); + task.setVideoTaskUuid(videoTaskUuid); + aiGenerationTaskMapper.updateById(task); + log.info("视频任务{}第三方返回uuid:{},已保存", task.getUuid(), videoTaskUuid); + + } catch (JSONException e) { + log.error("视频生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); + } catch (Exception e) { + log.error("视频生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); + } + + return taskUuid; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean handleVideoTaskCallback(AiVideoCallbackVO vo) { + log.info("处理AI视频生成任务回调,请求参数:{}", JSONUtil.toJsonStr(vo)); + boolean success = false; + try { + // 校验回调响应是否成功 + if (!Integer.valueOf(0).equals(vo.getCode())) { + log.error("视频生成任务回调失败,错误信息:{}", vo.getMsg()); + return false; + } + + VideoCallbackData data = vo.getData(); + if (data == null) { + log.error("视频回调数据为空"); + return false; + } + + String videoTaskUuid = data.getId(); + // 根据第三方返回的视频任务uuid查询对应的任务 + MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) + .eq(MiniAiGenerationTask::getDeleted, false)); + if (task == null) { + log.error("视频回调任务不存在,第三方视频任务UUID:{}", videoTaskUuid); + return false; + } + + // 转换任务状态 + Integer status; + if ("succeeded".equals(data.getStatus())) { + status = 1; // 成功 + } else if ("failed".equals(data.getStatus())) { + status = 2; // 失败 + } else { + log.info("视频任务{}处于中间状态:{},不处理", task.getUuid(), data.getStatus()); + return true; // 中间状态直接返回成功,不更新任务 + } + + // 更新任务状态 + task.setStatus(status); + task.setUpdateTime(new Date()); + task.setUpdateTimestamp(System.currentTimeMillis()); + aiGenerationTaskMapper.updateById(task); + + // 如果生成成功,下载外部视频到OSS + if (status == 1 && data.getContent() != null && data.getContent().getVideoUrl() != null) { + String externalVideoUrl = data.getContent().getVideoUrl(); + // 调用下载方法,和图片下载逻辑一致,存储到视频目录 + String ossUrl = downloadExternalUrlToOss(externalVideoUrl, OSS_VIDEO_DIR); + task.setResultResourceUrl(ossUrl); + aiGenerationTaskMapper.updateById(task); + + // 保存视频媒体记录 + MiniAiTaskMedia media = new MiniAiTaskMedia(); + String mediaUuid = UUID.randomUUID().toString().replace("-", ""); + media.setUuid(mediaUuid) + .setTaskId(task.getId()) + .setFileSource("ai_generate") + .setMediaType("video") + .setSourceUrl(ossUrl) + .setDuration(data.getDuration()) + .setCreateBy(task.getCreateBy()) + .setCreateTimestamp(System.currentTimeMillis()) + .setCreateTime(new Date()); + aiTaskMediaMapper.insert(media); + } + + success = true; + log.info("视频任务{}回调处理完成,状态:{}", task.getUuid(), status); + } catch (Exception e) { + log.error("视频任务回调处理异常,异常信息:{}", e.getMessage(), e); + } + return success; + } + @Override public boolean handleTaskCallback(AiTaskCallbackVO vo) { log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid()); @@ -375,6 +627,13 @@ public class AiGenerationServiceImpl implements AiGenerationService { * 下载外部URL到OSS,返回OSS访问地址 */ private String downloadExternalUrlToOss(String externalUrl) { + return downloadExternalUrlToOss(externalUrl, "ai/generate/"); + } + + /** + * 下载外部URL到指定OSS目录,返回OSS访问地址 + */ + private String downloadExternalUrlToOss(String externalUrl, String dir) { try ( // 直接打开URL输入流,自动关闭 InputStream inputStream = new URL(externalUrl).openStream() @@ -401,7 +660,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { ext = "jpg"; } - String objectName = "ai/generate/" + String objectName = dir + currentTimestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8) + "." + ext; @@ -411,4 +670,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { throw new MsgException("资源转存失败"); } } + + } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 7758041..59b78c3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -92,6 +92,7 @@ security: - /api/v1/mini/homePage/listByBounds - /healthcheck - /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调 + - /api/v1/mini/ai/generation/video/task/callback # AIGeneration 单图任务回调 # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -227,11 +228,15 @@ wx: appid: Your_AppId secret: Your_AppSecret - +# AIGeneration 配置 ai: generate: - server-url: http://192.168.31.91:8001/api/v1/photo-to-comic + single-image-server-url: http://192.168.31.91:8001/api/v1/photo-to-comic + four-panel-server-url: http://192.168.31.91:8001/api/v1/four-panel-comic + video-server-url: http://192.168.31.91:8001/api/v1/video/submit callback: - url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback + single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback + video-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/video/task/callback default: - model: doubao-seedream-5-0-260128 + image-model: doubao-seedream-5-0-260128 + video-model: doubao-seedance-2-0-260128 From d138d40869d5c5740c88c41e2bde1bede01dbc89 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Fri, 15 May 2026 17:37:08 +0800 Subject: [PATCH 04/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 27 +- .../AiCallbackImage.java} | 10 +- .../model/form/AiFourPanelCallbackForm.java | 35 ++ .../model/form/AiFourPanelGenerateForm.java | 12 +- .../AiSingleImageCallbackForm.java} | 13 +- .../model/form/AiSingleImageGenerateForm.java | 12 +- .../mini/model/form/AiVideoGenerateForm.java | 5 +- .../youlai/boot/mini/model/form/MediaUrl.java | 19 ++ .../mini/model/form/VideoContentItem.java | 34 ++ .../mini/model/vo/AiTaskCallbackResult.java | 38 --- .../mini/model/vo/AiTaskCallbackUsage.java | 31 -- .../mini/service/AiGenerationService.java | 7 +- .../service/impl/AiGenerationServiceImpl.java | 323 ++++++++++++------ src/main/resources/application-dev.yml | 34 +- 14 files changed, 380 insertions(+), 220 deletions(-) rename src/main/java/com/youlai/boot/mini/model/{vo/AiTaskCallbackImage.java => form/AiCallbackImage.java} (63%) create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AiFourPanelCallbackForm.java rename src/main/java/com/youlai/boot/mini/model/{vo/AiTaskCallbackVO.java => form/AiSingleImageCallbackForm.java} (70%) create mode 100644 src/main/java/com/youlai/boot/mini/model/form/MediaUrl.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/VideoContentItem.java delete mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackResult.java delete 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 26f9544..0b69114 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -7,15 +7,13 @@ import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; import com.youlai.boot.framework.security.util.SecurityUtils; import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; +import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.form.AiVideoGenerateForm; -import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.service.AiGenerationService; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -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; @@ -23,7 +21,6 @@ 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生成图片视频相关接口") @@ -48,6 +45,10 @@ public class AiGenerationController { return Result.success(urlList); } + //TODO: 查询用户最近上传的图片视频接口 + + //TODO:用户删除上次的图片视频接口 + @Operation(summary = "提交单图生成任务") @PostMapping("/generate-single-image") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) @@ -57,12 +58,11 @@ public class AiGenerationController { return Result.success(taskUuid); } - //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); + public Result singleImageTaskCallback(@Valid @RequestBody AiSingleImageCallbackForm form) { + boolean success = aiGenerationService.handleTaskCallback(form); return Result.success(success); } @@ -75,6 +75,14 @@ public class AiGenerationController { return Result.success(taskUuid); } + @Operation(summary = "四宫格漫画任务回调接口") + @PostMapping("/four-panel/task/callback") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) + public Result fourPanelTaskCallback(@Valid @RequestBody AiFourPanelCallbackForm form) { + boolean success = aiGenerationService.handleFourPanelCallback(form); + return Result.success(success); + } + @Operation(summary = "提交视频生成任务") @PostMapping("/generate-video") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) @@ -84,7 +92,6 @@ public class AiGenerationController { return Result.success(taskUuid); } - //TODO: 后续增加用户小程序订阅通知 @Operation(summary = "视频任务回调接口") @PostMapping("/video/task/callback") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) @@ -93,4 +100,6 @@ public class AiGenerationController { return Result.success(success); } + //TODO: 后续在回调中 增加用户小程序订阅通知 + } diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java b/src/main/java/com/youlai/boot/mini/model/form/AiCallbackImage.java similarity index 63% rename from src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java rename to src/main/java/com/youlai/boot/mini/model/form/AiCallbackImage.java index 9e6a916..e080aa9 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackImage.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiCallbackImage.java @@ -1,17 +1,17 @@ -package com.youlai.boot.mini.model.vo; +package com.youlai.boot.mini.model.form; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** - * AI生成图片信息 + * AI回调图片通用结构 * * @author youlai */ @Data -@Schema(description = "AI生成图片信息") -public class AiTaskCallbackImage { +@Schema(description = "AI回调图片通用结构") +public class AiCallbackImage { @Schema(description = "图片URL地址") private String url; @@ -20,6 +20,6 @@ public class AiTaskCallbackImage { @Schema(description = "Base64编码的图片JSON") private String b64Json; - @Schema(description = "图片尺寸,格式如 1664x2496") + @Schema(description = "图片尺寸,格式如 2048x2048") private String size; } diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelCallbackForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelCallbackForm.java new file mode 100644 index 0000000..15d0131 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelCallbackForm.java @@ -0,0 +1,35 @@ +package com.youlai.boot.mini.model.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import java.util.List; + +/** + * 四宫格漫画任务回调请求表单 + * + * @author youlai + */ +@Data +@Schema(description = "四宫格漫画任务回调请求表单") +public class AiFourPanelCallbackForm { + + @NotBlank(message = "任务UUID不能为空") + @Schema(description = "任务唯一标识UUID", requiredMode = Schema.RequiredMode.REQUIRED) + private String uuid; + + @Schema(description = "任务状态:succeeded=成功,failed=失败") + private String status; + + @Schema(description = "生成的图片列表") + private List 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/model/form/AiFourPanelGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java index 3f6f0ad..d8cf303 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java @@ -20,16 +20,16 @@ public class AiFourPanelGenerateForm { private String model; @Schema(description = "画风:1=治愈系水彩,2=Q版卡通,3=日式漫画,4=吉卜力风,5=3D毛绒,6=搞笑夸张", defaultValue = "1") - private Integer style = 1; + private Integer style; @Schema(description = "比例:1=1:1,2=2:3,3=3:4,4=4:3,5=5:4", defaultValue = "1") - private Integer ratio = 1; + private Integer ratio; @Schema(description = "图片尺寸:1=2K,2=4K,3=8K", defaultValue = "1") - private Integer imgSize = 1; + private Integer imgSize; @Schema(description = "图片格式:1=png,2=jpeg", defaultValue = "1") - private Integer imgType = 1; + private Integer imgType; @Schema(description = "宠物物种:如cat、dog") private String species; @@ -47,8 +47,8 @@ public class AiFourPanelGenerateForm { private String bodyType; @Schema(description = "特殊特征:如flat face、thick coat", defaultValue = "") - private String distinctiveFeatures = ""; + private String distinctiveFeatures; @Schema(description = "故事梗概:AI会据此生成四格漫画脚本") - private String storyOutline; + private String description; } diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java b/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageCallbackForm.java similarity index 70% rename from src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java rename to src/main/java/com/youlai/boot/mini/model/form/AiSingleImageCallbackForm.java index bd93853..9fe2d06 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackVO.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageCallbackForm.java @@ -1,18 +1,19 @@ -package com.youlai.boot.mini.model.vo; +package com.youlai.boot.mini.model.form; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; +import java.util.List; /** - * AI生成任务回调请求VO + * 单图生成任务回调请求表单 * * @author youlai */ @Data -@Schema(description = "AI生成任务回调请求") -public class AiTaskCallbackVO { +@Schema(description = "单图生成任务回调请求表单") +public class AiSingleImageCallbackForm { @NotBlank(message = "任务UUID不能为空") @Schema(description = "任务唯一标识UUID", requiredMode = Schema.RequiredMode.REQUIRED) @@ -21,8 +22,8 @@ public class AiTaskCallbackVO { @Schema(description = "任务状态:succeeded=成功,failed=失败") private String status; - @Schema(description = "生成结果") - private AiTaskCallbackResult result; + @Schema(description = "生成的图片列表") + private List result; @JsonProperty("created_at") @Schema(description = "创建时间戳") 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 index ecaed29..2d09d56 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java @@ -20,16 +20,16 @@ public class AiSingleImageGenerateForm { private String model; @Schema(description = "画风:1=治愈系水彩,2=Q版卡通,3=日式漫画,4=吉卜力风,5=3D毛绒,6=搞笑夸张", defaultValue = "1") - private Integer style = 1; + private Integer style; @Schema(description = "比例:1=1:1,2=2:3,3=3:4,4=4:3,5=5:4", defaultValue = "1") - private Integer ratio = 1; + private Integer ratio; @Schema(description = "图片尺寸:1=2K,2=4K,3=8K", defaultValue = "1") - private Integer imgSize = 1; + private Integer imgSize; @Schema(description = "图片格式:1=png,2=jpeg", defaultValue = "1") - private Integer imgType = 1; + private Integer imgType; @Schema(description = "宠物物种:如cat、dog") private String species; @@ -47,8 +47,8 @@ public class AiSingleImageGenerateForm { private String bodyType; @Schema(description = "特殊特征:如flat face、thick coat", defaultValue = "") - private String distinctiveFeatures = ""; + private String distinctiveFeatures; @Schema(description = "场景描述:用户自定义生成场景") - private String sceneDescription; + private String description; } diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java index c21bc1a..5ad08ec 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java @@ -3,7 +3,6 @@ package com.youlai.boot.mini.model.form; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; -import java.util.Map; /** * AI视频生成请求表单 @@ -17,8 +16,8 @@ public class AiVideoGenerateForm { @Schema(description = "使用的模型ID", defaultValue = "doubao-seedance-2-0-260128") private String model; - @Schema(description = "内容数组,包含text、image_url等类型") - private List> content; + @Schema(description = "内容数组,包含text、image_url、video_url、audio_url类型") + private List content; @Schema(description = "分辨率: 480p, 720p, 1080p, 2K", defaultValue = "720p") private String resolution = "720p"; diff --git a/src/main/java/com/youlai/boot/mini/model/form/MediaUrl.java b/src/main/java/com/youlai/boot/mini/model/form/MediaUrl.java new file mode 100644 index 0000000..9b028a1 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/MediaUrl.java @@ -0,0 +1,19 @@ +package com.youlai.boot.mini.model.form; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 媒体URL通用结构 + * + * @author youlai + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "媒体URL通用结构") +public class MediaUrl { + + @Schema(description = "资源URL地址") + private String url; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/VideoContentItem.java b/src/main/java/com/youlai/boot/mini/model/form/VideoContentItem.java new file mode 100644 index 0000000..cddb30a --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/VideoContentItem.java @@ -0,0 +1,34 @@ +package com.youlai.boot.mini.model.form; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 视频生成内容项 + * + * @author youlai + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "视频生成内容项") +public class VideoContentItem { + + @Schema(description = "内容类型:text/image_url/video_url/audio_url", allowableValues = {"text", "image_url", "video_url", "audio_url"}) + private String type; + + @Schema(description = "文本内容,type=text时必填") + private String text; + + @Schema(description = "图片URL结构,type=image_url时必填") + private MediaUrl image_url; + + @Schema(description = "视频URL结构,type=video_url时必填") + private MediaUrl video_url; + + @Schema(description = "音频URL结构,type=audio_url时必填") + private MediaUrl audio_url; + + @Schema(description = "内容角色标识,比如reference_image/reference_video/reference_audio") + private String role; +} 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 deleted file mode 100644 index 0f0b1c0..0000000 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackResult.java +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 8fe587d..0000000 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiTaskCallbackUsage.java +++ /dev/null @@ -1,31 +0,0 @@ -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/service/AiGenerationService.java b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java index 3837d54..316f3ef 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -1,10 +1,11 @@ package com.youlai.boot.mini.service; import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; +import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; -import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; +import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import org.springframework.web.multipart.MultipartFile; @@ -18,12 +19,14 @@ public interface AiGenerationService { String createAndGenerateFourPanel(AiFourPanelGenerateForm form, Long userId); - boolean handleTaskCallback(AiTaskCallbackVO vo); + boolean handleTaskCallback(AiSingleImageCallbackForm form); String createAndGenerateVideo(AiVideoGenerateForm form, Long userId); boolean handleVideoTaskCallback(AiVideoCallbackVO vo); + boolean handleFourPanelCallback(AiFourPanelCallbackForm form); + 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 6e1ff5c..0309e49 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 @@ -25,11 +25,13 @@ import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; +import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.form.MiniDeductPointForm; -import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.youlai.boot.mini.model.form.AiCallbackImage; +import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; import com.youlai.boot.mini.model.vo.VideoCallbackData; import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.mini.service.AiGenerationService; @@ -76,6 +78,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { //AI单图/四宫格图片生成任务回调地址 @Value("${ai.callback.single-image-callback-url:http://127.0.0.1:30101/backend/api/v1/mini/ai/generation/task/callback}") private String aiSingleImageCallbackUrl; + //四宫格图片生成任务回调地址 + @Value("${ai.callback.four-panel-callback-url:http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/four-panel/task/callback}") + private String aiFourPanelCallbackUrl; //视频生成服务地址 @Value("${ai.generate.video-server-url:http://127.0.0.1:8001/api/v1/video/submit}") private String aiVideoServerUrl; @@ -108,10 +113,10 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 校验用户当前未关联任务的上传文件数量,最多5个 Long existCount = aiTaskMediaMapper.selectCount(new LambdaQueryWrapper() - .eq(MiniAiTaskMedia::getCreateBy, userId) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false)); + .eq(MiniAiTaskMedia::getCreateBy, userId) + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false)); if (existCount + uploadCount > 5) { throw new BusinessException("最多只能上传5个待生成的参考文件"); @@ -129,22 +134,23 @@ public class AiGenerationServiceImpl implements AiGenerationService { 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)); + .setMiniUserId(userId) + .setFileSource("user_upload") + .setMediaType("image") + .setSourceUrl(url) + .setStorageKey(objectName) + .setCreateBy(userId) + .setCreateTimestamp(timestamp) + .setCreateTime(new Date(timestamp)); + + // 获取图片信息 + BufferedImage imageInfo = ImageIO.read(image.getInputStream()); + media.setWidth(imageInfo.getWidth()) + .setHeight(imageInfo.getHeight()); int result = aiTaskMediaMapper.insert(media); if (result > 0) { @@ -178,13 +184,14 @@ public class AiGenerationServiceImpl implements AiGenerationService { 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)); + .setMiniUserId(userId) + .setFileSource("user_upload") + .setMediaType("video") + .setSourceUrl(url) + .setStorageKey(objectName) + .setCreateBy(userId) + .setCreateTimestamp(timestamp) + .setCreateTime(new Date(timestamp)); // 获取视频时长 FileUtils.saveFile(video, tmpPath, fileName); @@ -197,7 +204,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { String thumbnailFileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); String thumbnailObjectName = OSS_THUMBNAIL_DIR + thumbnailFileName + ".png"; String thumbnailUrl = aliyunFileService.uploadFile(thumbnailObjectName, - FileUtils.bufferedImageToInputStream(thumbnail, "png")); + FileUtils.bufferedImageToInputStream(thumbnail, "png")); media.setThumbnailUrl(thumbnailUrl); int result = aiTaskMediaMapper.insert(media); @@ -234,17 +241,17 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 创建生成任务(自动加入当前事务,失败会一起回滚) MiniAiGenerationTask task = new MiniAiGenerationTask(); task.setUuid(taskUuid) - .setMiniUserId(userId) - .setType("img_single") // 单图 - .setGenerateParams(JSONUtil.toJsonStr(form)) - .setPointsConsumed(Math.abs(deductPoint)) - .setStatus(0) // 生成中 - .setCreateBy(userId) - .setCreateTime(now) - .setCreateTimestamp(timestamp) - .setUpdateTime(now) // 补全update相关字段 - .setUpdateTimestamp(timestamp) - .setDeleted(false); + .setMiniUserId(userId) + .setType("img_single") // 单图 + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) // 生成中 + .setCreateBy(userId) + .setCreateTime(now) + .setCreateTimestamp(timestamp) + .setUpdateTime(now) // 补全update相关字段 + .setUpdateTimestamp(timestamp) + .setDeleted(false); aiGenerationTaskMapper.insert(task); @@ -262,7 +269,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiRequest.put("eye_color", form.getEyeColor()); aiRequest.put("body_type", form.getBodyType()); aiRequest.put("distinctive_features", form.getDistinctiveFeatures()); - aiRequest.put("scene_description", form.getSceneDescription()); + aiRequest.put("description", form.getDescription()); // 传递任务UUID和回调地址 aiRequest.put("uuid", taskUuid); aiRequest.put("callback_url", aiSingleImageCallbackUrl); @@ -271,10 +278,10 @@ public class AiGenerationServiceImpl implements AiGenerationService { log.info("提交AI生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); // 同步调用AI接口,超时1秒 HttpResponse response = HttpRequest.post(aiSingleImageServerUrl) - .header("Content-Type", "application/json") - .body(JSONUtil.toJsonStr(aiRequest)) - .timeout(1000) - .execute(); + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(1000) + .execute(); // 先判断HTTP状态码 if (!response.isOk()) { @@ -290,7 +297,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { String errMsg = resJson.getStr("msg", "服务调用失败"); log.error("AI生成任务提交失败,业务错误码:{},错误信息:{},request_id:{},完整响应:{}", - code, errMsg, resJson.getStr("request_id"), responseBody); + code, errMsg, resJson.getStr("request_id"), responseBody); throw new MsgException("AI生成失败:" + errMsg); } } catch (JSONException e) { @@ -322,14 +329,14 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 创建生成任务(自动加入当前事务,失败会一起回滚) MiniAiGenerationTask task = new MiniAiGenerationTask(); task.setUuid(taskUuid) - .setMiniUserId(userId) - .setType("img_grid_4") // 四宫格漫画 - .setGenerateParams(JSONUtil.toJsonStr(form)) - .setPointsConsumed(Math.abs(deductPoint)) - .setStatus(0) // 生成中 - .setCreateBy(userId) - .setCreateTime(now) - .setCreateTimestamp(timestamp); + .setMiniUserId(userId) + .setType("img_grid_4") // 四宫格漫画 + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) // 生成中 + .setCreateBy(userId) + .setCreateTime(now) + .setCreateTimestamp(timestamp); aiGenerationTaskMapper.insert(task); @@ -347,19 +354,19 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiRequest.put("eye_color", form.getEyeColor()); aiRequest.put("body_type", form.getBodyType()); aiRequest.put("distinctive_features", form.getDistinctiveFeatures()); - aiRequest.put("story_outline", form.getStoryOutline()); + aiRequest.put("description", form.getDescription()); // 传递任务UUID和回调地址 aiRequest.put("uuid", taskUuid); - aiRequest.put("callback_url", aiSingleImageCallbackUrl); + aiRequest.put("callback_url", aiFourPanelCallbackUrl); try { log.info("提交四宫格漫画生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); // 同步调用AI接口,超时1秒 HttpResponse response = HttpRequest.post(aiFourPanelServerUrl) - .header("Content-Type", "application/json") - .body(JSONUtil.toJsonStr(aiRequest)) - .timeout(1000) - .execute(); + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(1000) + .execute(); // 先判断HTTP状态码 if (!response.isOk()) { @@ -375,7 +382,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { String errMsg = resJson.getStr("msg", "服务调用失败"); log.error("四宫格漫画生成任务提交失败,业务错误码:{},错误信息:{},request_id:{},完整响应:{}", - code, errMsg, resJson.getStr("request_id"), responseBody); + code, errMsg, resJson.getStr("request_id"), responseBody); throw new MsgException("AI生成失败:" + errMsg); } } catch (JSONException e) { @@ -407,14 +414,14 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 创建生成任务 MiniAiGenerationTask task = new MiniAiGenerationTask(); task.setUuid(taskUuid) - .setMiniUserId(userId) - .setType("video") - .setGenerateParams(JSONUtil.toJsonStr(form)) - .setPointsConsumed(Math.abs(deductPoint)) - .setStatus(0) - .setCreateBy(userId) - .setCreateTime(now) - .setCreateTimestamp(timestamp); + .setMiniUserId(userId) + .setType("video") + .setGenerateParams(JSONUtil.toJsonStr(form)) + .setPointsConsumed(Math.abs(deductPoint)) + .setStatus(0) + .setCreateBy(userId) + .setCreateTime(now) + .setCreateTimestamp(timestamp); aiGenerationTaskMapper.insert(task); @@ -425,15 +432,15 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiRequest.put("resolution", form.getResolution()); aiRequest.put("duration", form.getDuration()); aiRequest.put("uuid", taskUuid); - aiRequest.put("callback_url", aiVideoCallbackUrl); //TODO 后续独立回调地址 + aiRequest.put("callback_url", aiVideoCallbackUrl); try { log.info("提交视频生成任务,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(aiRequest)); HttpResponse response = HttpRequest.post(aiVideoServerUrl) - .header("Content-Type", "application/json") - .body(JSONUtil.toJsonStr(aiRequest)) - .timeout(1000) - .execute(); + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(aiRequest)) + .timeout(30000) //超时30秒 + .execute(); if (!response.isOk()) { log.error("视频生成任务提交失败,HTTP状态码:{},响应内容:{}", response.getStatus(), response.body()); @@ -488,8 +495,8 @@ public class AiGenerationServiceImpl implements AiGenerationService { String videoTaskUuid = data.getId(); // 根据第三方返回的视频任务uuid查询对应的任务 MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() - .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) - .eq(MiniAiGenerationTask::getDeleted, false)); + .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) + .eq(MiniAiGenerationTask::getDeleted, false)); if (task == null) { log.error("视频回调任务不存在,第三方视频任务UUID:{}", videoTaskUuid); return false; @@ -525,6 +532,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { String mediaUuid = UUID.randomUUID().toString().replace("-", ""); media.setUuid(mediaUuid) .setTaskId(task.getId()) + .setMiniUserId(task.getMiniUserId()) .setFileSource("ai_generate") .setMediaType("video") .setSourceUrl(ossUrl) @@ -544,59 +552,176 @@ public class AiGenerationServiceImpl implements AiGenerationService { } @Override - public boolean handleTaskCallback(AiTaskCallbackVO vo) { - log.info("处理AI生成任务回调,任务UUID:{}", vo.getUuid()); + @Transactional(rollbackFor = Exception.class) + public boolean handleFourPanelCallback(AiFourPanelCallbackForm form) { try { - String taskUuid = vo.getUuid(); + log.info("处理四宫格漫画生成任务回调,任务UUID:{}", form.getUuid()); + String taskUuid = form.getUuid(); // 查询任务是否存在 MiniAiGenerationTask task = getTaskByUuid(taskUuid); if (task == null) { - log.error("回调任务不存在,UUID:{}", taskUuid); + log.error("四宫格回调任务不存在,UUID:{}", taskUuid); return false; } // 转换任务状态 Integer status; - if ("succeeded".equals(vo.getStatus())) { + if ("succeeded".equals(form.getStatus())) { status = 1; // 成功 - } else if ("failed".equals(vo.getStatus())) { + } else if ("failed".equals(form.getStatus())) { status = 2; // 失败 } else { - log.error("回调任务状态非法,UUID:{},status:{}", taskUuid, vo.getStatus()); - return false; + log.info("四宫格任务{}处于中间状态:{},不处理", taskUuid, form.getStatus()); + return true; // 中间状态直接返回成功,不更新任务 } + // 更新任务状态 + task.setStatus(status); + task.setUpdateTime(new Date()); + task.setUpdateTimestamp(System.currentTimeMillis()); + aiGenerationTaskMapper.updateById(task); + // 如果生成成功,下载外部URL到OSS - String ossUrl = null; - if (status == 1 && vo.getResult() != null && !vo.getResult().getData().isEmpty()) { - String externalResultUrl = vo.getResult().getData().get(0).getUrl(); - ossUrl = downloadExternalUrlToOss(externalResultUrl); + if (status == 1 && form.getResult() != null && !form.getResult().isEmpty()) { + String externalImageUrl = form.getResult().get(0).getUrl(); + // 调用下载方法,存储到图片目录 + String ossUrl = downloadExternalUrlToOss(externalImageUrl); + task.setResultResourceUrl(ossUrl); + aiGenerationTaskMapper.updateById(task); // 保存生成的媒体记录 MiniAiTaskMedia media = new MiniAiTaskMedia(); String mediaUuid = UUID.randomUUID().toString().replace("-", ""); + AiCallbackImage image = form.getResult().get(0); + // 解析尺寸 + Integer width = null; + Integer height = null; + if (image.getSize() != null && image.getSize().contains("x")) { + String[] sizeArr = image.getSize().split("x"); + try { + width = Integer.parseInt(sizeArr[0]); + height = Integer.parseInt(sizeArr[1]); + } catch (Exception e) { + log.warn("解析四宫格图片尺寸失败,size:{}", image.getSize(), e); + } + } media.setUuid(mediaUuid) - .setMiniUserId(task.getMiniUserId()) .setTaskId(task.getId()) - .setFileSource("ai_generated") + .setMiniUserId(task.getMiniUserId()) + .setFileSource("ai_generate") .setMediaType("image") .setSourceUrl(ossUrl) + .setWidth(width) + .setHeight(height) .setCreateBy(task.getCreateBy()) - .setCreateTime(new Date()) .setCreateTimestamp(System.currentTimeMillis()) - .setUpdateTime(new Date()) - .setUpdateTimestamp(System.currentTimeMillis()) - .setDeleted(false); + .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); + + // 更新用户上传的参考文件:关联当前任务ID并软删除 + LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); + updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + aiTaskMediaMapper.update(null, updateMediaWrapper); + } + + log.info("四宫格任务{}回调处理完成,状态:{}", taskUuid, status); + return true; + } catch (Exception e) { + log.error("四宫格任务回调处理异常,任务UUID:{},异常信息:{}", form.getUuid(), e.getMessage(), e); + return false; + } + } + + @Override + public boolean handleTaskCallback(AiSingleImageCallbackForm form) { + log.info("处理单图生成任务回调,任务UUID:{}", form.getUuid()); + try { + String taskUuid = form.getUuid(); + // 查询任务是否存在 + MiniAiGenerationTask task = getTaskByUuid(taskUuid); + if (task == null) { + log.error("单图回调任务不存在,UUID:{}", taskUuid); + return false; + } + + // 转换任务状态 + Integer status; + if ("succeeded".equals(form.getStatus())) { + status = 1; // 成功 + } else if ("failed".equals(form.getStatus())) { + status = 2; // 失败 + } else { + log.error("单图回调任务状态非法,UUID:{},status:{}", taskUuid, form.getStatus()); + return false; + } + + // 如果生成成功,下载外部URL到OSS + String ossUrl = null; + if (status == 1 && form.getResult() != null && !form.getResult().isEmpty()) { + String externalResultUrl = form.getResult().get(0).getUrl(); + ossUrl = downloadExternalUrlToOss(externalResultUrl); + + // 保存生成的媒体记录 + MiniAiTaskMedia media = new MiniAiTaskMedia(); + String mediaUuid = UUID.randomUUID().toString().replace("-", ""); + AiCallbackImage image = form.getResult().get(0); + // 解析尺寸 + Integer width = null; + Integer height = null; + if (image.getSize() != null && image.getSize().contains("x")) { + String[] sizeArr = image.getSize().split("x"); + try { + width = Integer.parseInt(sizeArr[0]); + height = Integer.parseInt(sizeArr[1]); + } catch (Exception e) { + log.warn("解析单图图片尺寸失败,size:{}", image.getSize(), e); + } + } + + media.setUuid(mediaUuid) + .setMiniUserId(task.getMiniUserId()) + .setTaskId(task.getId()) + .setFileSource("ai_generated") + .setMediaType("image") + .setSourceUrl(ossUrl) + .setWidth(width) + .setHeight(height) + .setCreateBy(task.getCreateBy()) + .setCreateTime(new Date()) + .setCreateTimestamp(System.currentTimeMillis()) + .setUpdateTime(new Date()) + .setUpdateTimestamp(System.currentTimeMillis()) + .setDeleted(false); + + aiTaskMediaMapper.insert(media); + + // 更新用户上传的参考文件:关联当前任务ID并软删除 + LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); + updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + aiTaskMediaMapper.update(null, updateMediaWrapper); } // 更新任务状态 return updateTaskStatus(taskUuid, status, ossUrl); } catch (Exception e) { - log.error("处理AI任务回调异常,UUID:{},异常信息:{}", vo.getUuid(), e.getMessage(), e); + log.error("处理AI任务回调异常,UUID:{},异常信息:{}", form.getUuid(), e.getMessage(), e); return false; } } @@ -604,8 +729,8 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Override public MiniAiGenerationTask getTaskByUuid(String uuid) { return aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() - .eq(MiniAiGenerationTask::getUuid, uuid) - .eq(MiniAiGenerationTask::getDeleted, false)); + .eq(MiniAiGenerationTask::getUuid, uuid) + .eq(MiniAiGenerationTask::getDeleted, false)); } /** @@ -614,11 +739,11 @@ public class AiGenerationServiceImpl implements AiGenerationService { private boolean updateTaskStatus(String uuid, Integer 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()); + .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; } @@ -655,7 +780,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { if (lastDotIndex > 0 && lastDotIndex < path.length() - 1) { ext = path.substring(lastDotIndex + 1); } - if (ext == null){ + if (ext == null) { log.error("无法从URL中提取文件后缀,URL:{}", externalUrl); ext = "jpg"; } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 59b78c3..51667a9 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -82,17 +82,18 @@ security: allow-multi-login: true # 是否允许多设备登录 # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等) ignore-urls: - - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) - - /api/v1/auth/captcha # 验证码获取接口 - - /api/v1/auth/sms/code # 发送登录短信验证码 - - /api/v1/auth/refresh-token # 刷新令牌接口 - - /api/v1/wxma/auth/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) - - /api/v1/logs/** # 日志接口(访问日志列表) - - /api/v1/mini/public/** - - /api/v1/mini/homePage/listByBounds - - /healthcheck - - /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调 - - /api/v1/mini/ai/generation/video/task/callback # AIGeneration 单图任务回调 + - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) + - /api/v1/auth/captcha # 验证码获取接口 + - /api/v1/auth/sms/code # 发送登录短信验证码 + - /api/v1/auth/refresh-token # 刷新令牌接口 + - /api/v1/wxma/auth/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) + - /api/v1/logs/** # 日志接口(访问日志列表) + - /api/v1/mini/public/** + - /api/v1/mini/homePage/listByBounds + - /healthcheck + - /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调 + - /api/v1/mini/ai/generation/video/task/callback # AIGeneration 视频任务回调 + - /api/v1/mini/ai/generation/four-panel/task/callback # AIGeneration 四宫格任务回调 # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -231,12 +232,15 @@ wx: # AIGeneration 配置 ai: generate: - single-image-server-url: http://192.168.31.91:8001/api/v1/photo-to-comic - four-panel-server-url: http://192.168.31.91:8001/api/v1/four-panel-comic - video-server-url: http://192.168.31.91:8001/api/v1/video/submit + single-image-server-url: http://192.168.31.93:8001/api/v1/photo-to-comic + four-panel-server-url: http://192.168.31.93:8001/api/v1/four-panel-comic + video-server-url: http://192.168.31.93:8001/api/v1/video/submit callback: - single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/task/callback + single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/single-image/task/callback + four-panel-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/four-panel/task/callback + #需要内网穿透工具 由火山方舟 api 回调,ngrok http 30101 替换为内网穿透工具地址 video-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/video/task/callback default: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 + From 65fc5f0a81fc3a0e18ed3f47cde3b02d0a7a77f6 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Fri, 15 May 2026 18:24:01 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 1 - .../boot/mini/model/vo/AiVideoCallbackVO.java | 60 +++++++++++++++--- .../mini/model/vo/VideoCallbackContent.java | 2 + .../boot/mini/model/vo/VideoCallbackData.java | 61 ------------------- .../mini/model/vo/VideoCallbackUsage.java | 6 +- .../service/impl/AiGenerationServiceImpl.java | 52 ++++++++-------- src/main/resources/application-dev.yml | 2 +- 7 files changed, 82 insertions(+), 102 deletions(-) delete mode 100644 src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.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 0b69114..fe0bb3e 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -101,5 +101,4 @@ public class AiGenerationController { } //TODO: 后续在回调中 增加用户小程序订阅通知 - } diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java b/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java index 76e6ff5..63798b8 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiVideoCallbackVO.java @@ -1,5 +1,6 @@ package com.youlai.boot.mini.model.vo; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -10,19 +11,60 @@ import lombok.Data; * @author youlai */ @Data +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "AI视频生成任务回调请求") public class AiVideoCallbackVO { - @Schema(description = "响应消息") - private String msg; + @Schema(description = "第三方视频任务唯一ID") + private String id; - @Schema(description = "响应码,0表示成功") - private Integer code; + @Schema(description = "使用的模型ID") + private String model; - @Schema(description = "回调数据") - private VideoCallbackData data; + @Schema(description = "任务状态:queued排队中/running处理中/succeeded成功/failed失败") + private String status; - @JsonProperty("request_id") - @Schema(description = "请求ID") - private String requestId; + @JsonProperty("created_at") + @Schema(description = "创建时间戳") + private Long createdAt; + + @JsonProperty("updated_at") + @Schema(description = "更新时间戳") + private Long updatedAt; + + @JsonProperty("service_tier") + @Schema(description = "服务等级") + private String serviceTier; + + @JsonProperty("execution_expires_after") + @Schema(description = "执行过期时间") + private Long executionExpiresAfter; + + @JsonProperty("generate_audio") + @Schema(description = "是否生成音频") + private Boolean generateAudio; + + @Schema(description = "是否为草稿") + private Boolean draft; + + @Schema(description = "生成结果,status=succeeded时返回") + private VideoCallbackContent content; + + @Schema(description = "资源消耗统计") + private VideoCallbackUsage usage; + + @Schema(description = "随机种子") + private Long seed; + + @Schema(description = "分辨率,比如720p") + private String resolution; + + @Schema(description = "比例,比如16:9") + private String ratio; + + @Schema(description = "视频时长,单位秒") + private Integer duration; + + @Schema(description = "帧率") + private Integer framespersecond; } diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java index f66617d..34a321b 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackContent.java @@ -1,5 +1,6 @@ package com.youlai.boot.mini.model.vo; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -10,6 +11,7 @@ import lombok.Data; * @author youlai */ @Data +@JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "AI视频生成回调结果内容") public class VideoCallbackContent { diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java deleted file mode 100644 index 706ffb1..0000000 --- a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackData.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.youlai.boot.mini.model.vo; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -/** - * AI视频生成回调数据 - * - * @author youlai - */ -@Data -@Schema(description = "AI视频生成回调数据") -public class VideoCallbackData { - - @Schema(description = "任务ID") - private String id; - - @Schema(description = "使用的模型ID") - private String model; - - @Schema(description = "任务状态: pending/running/succeeded/failed") - private String status; - - @Schema(description = "生成的结果内容") - private VideoCallbackContent content; - - @Schema(description = "资源消耗统计") - private VideoCallbackUsage usage; - - @JsonProperty("created_at") - @Schema(description = "创建时间戳") - private Long createdAt; - - @JsonProperty("updated_at") - @Schema(description = "更新时间戳") - private Long updatedAt; - - @Schema(description = "随机种子") - private Long seed; - - @Schema(description = "分辨率") - private String resolution; - - @Schema(description = "比例") - private String ratio; - - @Schema(description = "视频时长,单位秒") - private Integer duration; - - @Schema(description = "帧率") - private Integer framespersecond; - - @JsonProperty("service_tier") - @Schema(description = "服务等级") - private String serviceTier; - - @JsonProperty("execution_expires_after") - @Schema(description = "执行过期时间") - private Long executionExpiresAfter; -} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java index 2e4b752..c6c0d3e 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java +++ b/src/main/java/com/youlai/boot/mini/model/vo/VideoCallbackUsage.java @@ -1,16 +1,18 @@ package com.youlai.boot.mini.model.vo; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** - * AI视频生成资源消耗统计 + * 视频生成资源消耗统计 * * @author youlai */ @Data -@Schema(description = "AI视频生成资源消耗统计") +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "视频生成资源消耗统计") public class VideoCallbackUsage { @JsonProperty("completion_tokens") 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 0309e49..be37209 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 @@ -32,13 +32,10 @@ import com.youlai.boot.mini.model.form.MiniDeductPointForm; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.model.form.AiCallbackImage; import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; -import com.youlai.boot.mini.model.vo.VideoCallbackData; -import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.MiniPointRecordService; 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; @@ -49,9 +46,7 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; import java.net.URL; -import java.time.LocalDateTime; import java.util.*; -import java.util.concurrent.CompletableFuture; @Slf4j @Service @@ -480,23 +475,11 @@ public class AiGenerationServiceImpl implements AiGenerationService { log.info("处理AI视频生成任务回调,请求参数:{}", JSONUtil.toJsonStr(vo)); boolean success = false; try { - // 校验回调响应是否成功 - if (!Integer.valueOf(0).equals(vo.getCode())) { - log.error("视频生成任务回调失败,错误信息:{}", vo.getMsg()); - return false; - } - - VideoCallbackData data = vo.getData(); - if (data == null) { - log.error("视频回调数据为空"); - return false; - } - - String videoTaskUuid = data.getId(); + String videoTaskUuid = vo.getId(); // 根据第三方返回的视频任务uuid查询对应的任务 MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() - .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) - .eq(MiniAiGenerationTask::getDeleted, false)); + .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) + .eq(MiniAiGenerationTask::getDeleted, false)); if (task == null) { log.error("视频回调任务不存在,第三方视频任务UUID:{}", videoTaskUuid); return false; @@ -504,13 +487,14 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 转换任务状态 Integer status; - if ("succeeded".equals(data.getStatus())) { + if ("succeeded".equals(vo.getStatus())) { status = 1; // 成功 - } else if ("failed".equals(data.getStatus())) { + } else if ("failed".equals(vo.getStatus())) { status = 2; // 失败 } else { - log.info("视频任务{}处于中间状态:{},不处理", task.getUuid(), data.getStatus()); - return true; // 中间状态直接返回成功,不更新任务 + // queued/running中间状态直接返回成功,不更新任务 + log.info("视频任务{}处于中间状态:{},不处理", task.getUuid(), vo.getStatus()); + return true; } // 更新任务状态 @@ -520,9 +504,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiGenerationTaskMapper.updateById(task); // 如果生成成功,下载外部视频到OSS - if (status == 1 && data.getContent() != null && data.getContent().getVideoUrl() != null) { - String externalVideoUrl = data.getContent().getVideoUrl(); - // 调用下载方法,和图片下载逻辑一致,存储到视频目录 + if (status == 1 && vo.getContent() != null && vo.getContent().getVideoUrl() != null) { + String externalVideoUrl = vo.getContent().getVideoUrl(); + // 调用下载方法,存储到视频目录 String ossUrl = downloadExternalUrlToOss(externalVideoUrl, OSS_VIDEO_DIR); task.setResultResourceUrl(ossUrl); aiGenerationTaskMapper.updateById(task); @@ -536,11 +520,23 @@ public class AiGenerationServiceImpl implements AiGenerationService { .setFileSource("ai_generate") .setMediaType("video") .setSourceUrl(ossUrl) - .setDuration(data.getDuration()) + .setDuration(vo.getDuration()) .setCreateBy(task.getCreateBy()) .setCreateTimestamp(System.currentTimeMillis()) .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); + + // 更新用户上传的参考文件:关联当前任务ID并软删除 + LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); + updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + aiTaskMediaMapper.update(null, updateMediaWrapper); } success = true; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 51667a9..6c512aa 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -239,7 +239,7 @@ ai: single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/single-image/task/callback four-panel-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/four-panel/task/callback #需要内网穿透工具 由火山方舟 api 回调,ngrok http 30101 替换为内网穿透工具地址 - video-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/video/task/callback + video-url: https://2dd0-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/video/task/callback default: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 From 3b73dc21350160f98071f9e6d303a808c7c392bf Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Mon, 18 May 2026 15:36:25 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E8=AE=A2=E9=98=85=E6=B6=88=E6=81=AF=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../freemarker/MyBatisPlusGenerator.java | 1 + .../boot/common/enums/LogModuleEnum.java | 3 +- .../controller/AiGenerationController.java | 63 ++++-- .../mini/mapper/MiniUserSubscribeMapper.java | 17 ++ .../mini/model/entity/MiniUserSubscribe.java | 74 +++++++ .../mini/model/enums/SubscribeStatusEnum.java | 46 ++++ .../mini/model/form/WxSubscribeAuthForm.java | 26 +++ .../mini/model/form/WxSubscribeSendForm.java | 33 +++ .../boot/mini/model/vo/UserUploadMediaVO.java | 43 ++++ .../boot/mini/model/vo/WxApiResponse.java | 16 ++ .../mini/service/AiGenerationService.java | 6 + .../boot/mini/service/WxSubscribeService.java | 14 ++ .../service/impl/AiGenerationServiceImpl.java | 209 +++++++++++++----- .../service/impl/WxSubscribeServiceImpl.java | 147 ++++++++++++ src/main/resources/application-dev.yml | 9 +- .../mapper/mini/MiniUserSubscribeMapper.xml | 16 ++ 16 files changed, 652 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java create mode 100644 src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java create mode 100644 src/main/java/com/youlai/boot/mini/model/enums/SubscribeStatusEnum.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java create mode 100644 src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java create mode 100644 src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java create mode 100644 src/main/resources/mapper/mini/MiniUserSubscribeMapper.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 50625c8..5aef6e4 100644 --- a/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java +++ b/src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java @@ -101,6 +101,7 @@ public class MyBatisPlusGenerator { ,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_user_subscribe", 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 124b1c2..7c9bbea 100644 --- a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java @@ -34,7 +34,8 @@ public enum LogModuleEnum implements IBaseEnum { POINT_RULE(102, "积分规则"), SIGN_RECORD(103, "签到记录"), AI_TASK_MEDIA(104, "AI任务媒体"), - AI_GENERATION_TASK(105, "AI生成任务"); + AI_GENERATION_TASK(105, "AI生成任务"), + USER_SUBSCRIBE(106, "用户订阅模板消息"); @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 index fe0bb3e..abdf9cc 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -6,14 +6,15 @@ import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; import com.youlai.boot.framework.security.util.SecurityUtils; -import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; -import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; -import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; -import com.youlai.boot.mini.model.form.AiVideoGenerateForm; -import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; +import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; +import com.youlai.boot.mini.model.form.*; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.service.AiGenerationService; +import com.youlai.boot.mini.service.WxSubscribeService; 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; @@ -22,8 +23,9 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; +import com.youlai.boot.mini.model.vo.UserUploadMediaVO; -@Tag(name = "AI生成图片视频相关接口") +@Tag(name = "AI生成相关接口") @RestController @RequestMapping("/api/v1/mini/ai/generation") @RequiredArgsConstructor @@ -31,24 +33,21 @@ import java.util.List; public class AiGenerationController { private final AiGenerationService aiGenerationService; + private final WxSubscribeService wxSubscribeService; - @Operation(summary = "上传AI生成参考文件", operationId = "AiReferenceSaveFile") - @PostMapping(value = "/upload-reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "上传AI生成参考文件") + @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 + @RequestParam(name = "images", required = false) List images, + @RequestParam(name = "videos", required = false) List videos ) { Long userId = SecurityUtils.getUserId(); List urlList = aiGenerationService.uploadReferenceFile(images, videos, userId); return Result.success(urlList); } - //TODO: 查询用户最近上传的图片视频接口 - - //TODO:用户删除上次的图片视频接口 - @Operation(summary = "提交单图生成任务") @PostMapping("/generate-single-image") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) @@ -100,5 +99,39 @@ public class AiGenerationController { return Result.success(success); } - //TODO: 后续在回调中 增加用户小程序订阅通知 + @Operation(summary = "上报微信订阅消息授权状态") + @PostMapping("/subscribe/auth/report") + @Log(module = LogModuleEnum.USER_SUBSCRIBE, value = ActionTypeEnum.UPDATE) + public Result reportSubscribeAuth(@Valid @RequestBody WxSubscribeAuthForm form) { + form.setUserId(SecurityUtils.getUserId()); + wxSubscribeService.reportAuth(form); + return Result.success(); + } + + @Operation(summary = "发送微信订阅消息") + @PostMapping("/subscribe/send") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) + public Result sendSubscribeMessage(@Valid @RequestBody WxSubscribeSendForm form) { + form.setUserId(SecurityUtils.getUserId()); + Boolean result = wxSubscribeService.sendMessage(form); + return Result.success(result); + } + + @Operation(summary = "查询当前用户最近上传的图片视频") + @GetMapping("/my/recent-upload") + public Result> getMyRecentUpload() { + Long userId = SecurityUtils.getUserId(); + List voList = aiGenerationService.getRecentUploadVO(userId); + return Result.success(voList); + } + + @Operation(summary = "删除用户上传的图片/视频") + @DeleteMapping("/my/upload/delete") + @Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.DELETE) + public Result deleteMyUpload(@RequestParam String uuid) { + Long userId = SecurityUtils.getUserId(); + boolean success = aiGenerationService.deleteUploadMedia(userId, uuid); + return Result.success(success); + } + } diff --git a/src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java b/src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java new file mode 100644 index 0000000..bb7b331 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/mapper/MiniUserSubscribeMapper.java @@ -0,0 +1,17 @@ +package com.youlai.boot.mini.mapper; + +import com.youlai.boot.mini.model.entity.MiniUserSubscribe; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +/** +* 用户订阅状态表 Mapper 接口 +* +* @author jwy +* @since +*/ +public interface MiniUserSubscribeMapper extends BaseMapper { + + String getOpenidByUserId(Long aLong); +} diff --git a/src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java b/src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java new file mode 100644 index 0000000..eaceff0 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/entity/MiniUserSubscribe.java @@ -0,0 +1,74 @@ +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_user_subscribe") +@Schema(description = "用户订阅状态表") +public class MiniUserSubscribe implements Serializable { + + @TableId(value = "id", type = IdType.AUTO) + @Schema(description = "") + private Long id; + + + @TableField("uuid") + @Schema(description = "uuid唯一标识,前后端用这个进行数据交互") + private String uuid; + + @TableField("user_id") + @Schema(description = "用户id") + private Long userId; + + @TableField("template_id") + @Schema(description = "订阅消息模板ID") + private String templateId; + + @TableField("status") + @Schema(description = "授权状态:0=拒绝 1=同意一次 2=总是同意") + private Integer status; + + @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/enums/SubscribeStatusEnum.java b/src/main/java/com/youlai/boot/mini/model/enums/SubscribeStatusEnum.java new file mode 100644 index 0000000..28bbd7c --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/enums/SubscribeStatusEnum.java @@ -0,0 +1,46 @@ +package com.youlai.boot.mini.model.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; + + +@Schema(description = "微信订阅消息授权状态") +public enum SubscribeStatusEnum { + + REJECT(0, "拒绝授权"), + ONCE(1, "同意一次"), + ALWAYS(2, "总是同意"); + + private final Integer value; + private final String desc; + + SubscribeStatusEnum(Integer value, String desc) { + this.value = value; + this.desc = desc; + } + + @JsonValue + public Integer getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + @JsonCreator + public static SubscribeStatusEnum from(Integer value) { + if (value == null) return null; + for (SubscribeStatusEnum e : values()) { + if (e.value.equals(value)) { + return e; + } + } + return null; + } + + public static boolean contains(Integer value) { + return from(value) != null; + } +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java new file mode 100644 index 0000000..e6c15d4 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java @@ -0,0 +1,26 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 上报用户订阅授权请求 + */ +@Data +@Schema(description = "上报用户订阅授权请求") +public class WxSubscribeAuthForm { + + @NotBlank(message = "用户ID不能为空") + @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long userId; + + @NotBlank(message = "模板ID不能为空") + @Schema(description = "订阅消息模板ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String templateId; + + @NotNull(message = "授权状态不能为空") + @Schema(description = "授权状态:0=拒绝 1=同意一次 2=总是同意", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer status; +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java new file mode 100644 index 0000000..c51ad18 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java @@ -0,0 +1,33 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import java.util.Map; + +/** + * 发送订阅消息请求 + */ +@Data +@Schema(description = "发送订阅消息请求") +public class WxSubscribeSendForm { + + @NotBlank(message = "用户ID不能为空") + @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long userId; + + @NotBlank(message = "模板ID不能为空") + @Schema(description = "订阅消息模板ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String templateId; + + @Schema(description = "跳转页面,例:/pages/result/index?id=123") + private String page; + + @Schema(description = "跳转版本:developer=开发版 trial=体验版 formal=正式版,默认formal") + private String miniProgramState = "developer"; + + @NotNull(message = "模板参数不能为空") + @Schema(description = "模板参数,key是模板字段名,value是对应的值", requiredMode = Schema.RequiredMode.REQUIRED) + private Map templateParams; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java b/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java new file mode 100644 index 0000000..f0ef7f1 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java @@ -0,0 +1,43 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 用户上传媒体返回VO + * + * @author youlai + */ +@Data +@Schema(description = "用户上传媒体返回VO") +public class UserUploadMediaVO { + + @Schema(description = "媒体ID") + private Long id; + + @Schema(description = "媒体类型:image-图片,video-视频") + private String mediaType; + + @Schema(description = "媒体访问地址") + private String sourceUrl; + + @Schema(description = "缩略图地址(视频才有)") + private String thumbnailUrl; + + @Schema(description = "视频时长(秒,视频才有)") + private Integer duration; + + @Schema(description = "图片宽度") + private Integer width; + + @Schema(description = "图片高度") + private Integer height; + + @Schema(description = "上传时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java b/src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java new file mode 100644 index 0000000..9b903b4 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/WxApiResponse.java @@ -0,0 +1,16 @@ +package com.youlai.boot.mini.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 微信接口通用返回 + */ +@Data +public class WxApiResponse { + @JsonProperty("errcode") + private Integer errcode; + + @JsonProperty("errmsg") + private String errmsg; +} 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 316f3ef..9634936 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -1,5 +1,6 @@ package com.youlai.boot.mini.service; +import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; @@ -7,6 +8,7 @@ import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.youlai.boot.mini.model.vo.UserUploadMediaVO; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -29,4 +31,8 @@ public interface AiGenerationService { MiniAiGenerationTask getTaskByUuid(String uuid); + List getRecentUploadVO(Long userId); + + boolean deleteUploadMedia(Long userId, String uuid); + } diff --git a/src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java b/src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java new file mode 100644 index 0000000..99945d2 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/WxSubscribeService.java @@ -0,0 +1,14 @@ +package com.youlai.boot.mini.service; + +import com.youlai.boot.mini.model.form.WxSubscribeAuthForm; +import com.youlai.boot.mini.model.form.WxSubscribeSendForm; + +/** + * 微信订阅消息服务 + */ +public interface WxSubscribeService { + + void reportAuth(WxSubscribeAuthForm form); + + Boolean sendMessage(WxSubscribeSendForm form); +} 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 be37209..9678429 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,11 +1,10 @@ package com.youlai.boot.mini.service.impl; -import cn.hutool.core.date.DateUtil; +import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.io.FileUtil; -import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; 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; @@ -16,7 +15,6 @@ 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; -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; @@ -24,18 +22,15 @@ import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; -import com.youlai.boot.mini.model.form.AiFourPanelGenerateForm; -import com.youlai.boot.mini.model.form.AiSingleImageCallbackForm; -import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; -import com.youlai.boot.mini.model.form.AiVideoGenerateForm; -import com.youlai.boot.mini.model.form.MiniDeductPointForm; +import com.youlai.boot.mini.model.form.*; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; -import com.youlai.boot.mini.model.form.AiCallbackImage; -import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; +import com.youlai.boot.mini.model.vo.UserUploadMediaVO; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.MiniPointRecordService; +import com.youlai.boot.mini.service.WxSubscribeService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,7 +41,11 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; import java.net.URL; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.stream.Collectors; +import java.util.concurrent.CompletableFuture; @Slf4j @Service @@ -58,6 +57,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final FileService fileService; private final AliyunFileService aliyunFileService; private final MiniPointRecordService pointRecordService; + private final WxSubscribeService wxSubscribeService; //OSS存储目录配置 private static final String OSS_IMAGE_DIR = "ai/image/"; @@ -91,6 +91,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Value("${ai.default.video-model:doubao-seedance-2-0-260128}") private String aiDefaultVideoModel; + @Value("${subscribe.template}") + private String subscribeTemplate; + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; //AI四宫格生成积分规则编码 @@ -478,8 +481,8 @@ public class AiGenerationServiceImpl implements AiGenerationService { String videoTaskUuid = vo.getId(); // 根据第三方返回的视频任务uuid查询对应的任务 MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() - .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) - .eq(MiniAiGenerationTask::getDeleted, false)); + .eq(MiniAiGenerationTask::getVideoTaskUuid, videoTaskUuid) + .eq(MiniAiGenerationTask::getDeleted, false)); if (task == null) { log.error("视频回调任务不存在,第三方视频任务UUID:{}", videoTaskUuid); return false; @@ -515,28 +518,46 @@ public class AiGenerationServiceImpl implements AiGenerationService { MiniAiTaskMedia media = new MiniAiTaskMedia(); String mediaUuid = UUID.randomUUID().toString().replace("-", ""); media.setUuid(mediaUuid) - .setTaskId(task.getId()) - .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") - .setMediaType("video") - .setSourceUrl(ossUrl) - .setDuration(vo.getDuration()) - .setCreateBy(task.getCreateBy()) - .setCreateTimestamp(System.currentTimeMillis()) - .setCreateTime(new Date()); + .setTaskId(task.getId()) + .setMiniUserId(task.getMiniUserId()) + .setFileSource("ai_generate") + .setMediaType("video") + .setSourceUrl(ossUrl) + .setDuration(vo.getDuration()) + .setCreateBy(task.getCreateBy()) + .setCreateTimestamp(System.currentTimeMillis()) + .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); aiTaskMediaMapper.update(null, updateMediaWrapper); + + // 同步发送订阅消息通知 + if (StrUtil.isNotBlank(subscribeTemplate)) { + try { + WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); + sendForm.setUserId(task.getMiniUserId()); + sendForm.setTemplateId(subscribeTemplate); + sendForm.setPage("pages/index/index"); + sendForm.setTemplateParams(Map.of( + "thing1", "您的AI视频作品已完成", + "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + "phrase3", "点击查看状态" + )); + wxSubscribeService.sendMessage(sendForm); + } catch (Exception e) { + log.error("视频任务{}发送订阅消息失败", task.getId(), e); + } + } } success = true; @@ -603,30 +624,49 @@ public class AiGenerationServiceImpl implements AiGenerationService { } media.setUuid(mediaUuid) - .setTaskId(task.getId()) - .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") - .setMediaType("image") - .setSourceUrl(ossUrl) - .setWidth(width) - .setHeight(height) - .setCreateBy(task.getCreateBy()) - .setCreateTimestamp(System.currentTimeMillis()) - .setCreateTime(new Date()); + .setTaskId(task.getId()) + .setMiniUserId(task.getMiniUserId()) + .setFileSource("ai_generate") + .setMediaType("image") + .setSourceUrl(ossUrl) + .setWidth(width) + .setHeight(height) + .setCreateBy(task.getCreateBy()) + .setCreateTimestamp(System.currentTimeMillis()) + .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); aiTaskMediaMapper.update(null, updateMediaWrapper); + + // 同步发送订阅消息通知 + if (StrUtil.isNotBlank(subscribeTemplate)) { + try { + WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); + sendForm.setUserId(task.getMiniUserId()); + sendForm.setTemplateId(subscribeTemplate); + sendForm.setPage("pages/index/index"); + // 根据实际模板字段调整参数 + sendForm.setTemplateParams(Map.of( + "thing1", "您的AI绘画作品已完成", + "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + "phrase3", "点击查看状态" + )); + wxSubscribeService.sendMessage(sendForm); + } catch (Exception e) { + log.error("单图任务{}发送订阅消息失败", task.getId(), e); + } + } } log.info("四宫格任务{}回调处理完成,状态:{}", taskUuid, status); @@ -703,14 +743,33 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, task.getId()) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); aiTaskMediaMapper.update(null, updateMediaWrapper); + + // 同步发送订阅消息通知 + if (StrUtil.isNotBlank(subscribeTemplate)) { + try { + WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); + sendForm.setUserId(task.getMiniUserId()); + sendForm.setTemplateId(subscribeTemplate); + sendForm.setPage("pages/index/index"); + // 根据实际模板字段调整参数 + sendForm.setTemplateParams(Map.of( + "thing1", "您的AI绘画作品已完成", + "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + "phrase3", "点击查看状态" + )); + wxSubscribeService.sendMessage(sendForm); + } catch (Exception e) { + log.error("单图任务{}发送订阅消息失败", task.getId(), e); + } + } } // 更新任务状态 @@ -793,4 +852,48 @@ public class AiGenerationServiceImpl implements AiGenerationService { } + @Override + public List getRecentUploadVO(Long userId) { + List mediaList = aiTaskMediaMapper.selectList(new LambdaQueryWrapper() + .eq(MiniAiTaskMedia::getMiniUserId, userId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .orderByDesc(MiniAiTaskMedia::getCreateTime) + .last("limit 20")); + + if (mediaList == null || mediaList.isEmpty()) { + return Collections.emptyList(); + } + + return mediaList.stream() + .map(this::convertToVO) + .toList(); + } + + private UserUploadMediaVO convertToVO(MiniAiTaskMedia media) { + UserUploadMediaVO vo = new UserUploadMediaVO(); + BeanUtil.copyProperties(media, vo); + return vo; + } + + @Override + public boolean deleteUploadMedia(Long userId, String uuid) { + // 校验是否是当前用户的上传媒体 + MiniAiTaskMedia media = aiTaskMediaMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniAiTaskMedia::getUuid, uuid) + .eq(MiniAiTaskMedia::getMiniUserId, userId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .last("limit 1")); + if (media == null) { + log.warn("用户{}删除媒体失败,媒体{}不存在或无权限", userId, uuid); + return false; + } + // 软删除 + media.setDeleted(true); + media.setUpdateTime(new Date()); + media.setUpdateTimestamp(System.currentTimeMillis()); + return aiTaskMediaMapper.updateById(media) > 0; + } + } diff --git a/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java new file mode 100644 index 0000000..6d0f2a0 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java @@ -0,0 +1,147 @@ +package com.youlai.boot.mini.service.impl; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.common.exception.MsgException; +import com.youlai.boot.mini.model.enums.SubscribeStatusEnum; +import com.youlai.boot.mini.mapper.MiniUserSubscribeMapper; +import com.youlai.boot.mini.model.entity.MiniUserSubscribe; +import com.youlai.boot.mini.model.form.WxSubscribeAuthForm; +import com.youlai.boot.mini.model.form.WxSubscribeSendForm; +import com.youlai.boot.mini.model.vo.WxApiResponse; +import com.youlai.boot.mini.service.WxSubscribeService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * 微信订阅消息服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WxSubscribeServiceImpl implements WxSubscribeService { + + private final WxMaService wxMaService; + private final MiniUserSubscribeMapper userSubscribeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void reportAuth(WxSubscribeAuthForm form) { + try { + // 查询用户openid + String openid = userSubscribeMapper.getOpenidByUserId(form.getUserId()); + if (StrUtil.isBlank(openid)) { + throw new MsgException("用户未绑定微信"); + } + long currentTime = System.currentTimeMillis(); + + // 查询是否已有授权记录 + MiniUserSubscribe exist = userSubscribeMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniUserSubscribe::getUserId, form.getUserId()) + .eq(MiniUserSubscribe::getTemplateId, form.getTemplateId()) + .eq(MiniUserSubscribe::getDeleted, 0) + .last("limit 1")); + + if (exist != null) { + exist.setStatus(form.getStatus()); + exist.setUpdateTime(new Date()); + exist.setUpdateTimestamp(currentTime); + userSubscribeMapper.updateById(exist); + } else { + MiniUserSubscribe subscribe = new MiniUserSubscribe(); + subscribe.setUserId(form.getUserId()); + subscribe.setTemplateId(form.getTemplateId()); + subscribe.setStatus(form.getStatus()); + subscribe.setCreateTime(new Date()); + subscribe.setCreateTimestamp(currentTime); + subscribe.setUpdateTime(new Date()); + subscribe.setUpdateTimestamp(currentTime); + subscribe.setDeleted(false); + userSubscribeMapper.insert(subscribe); + } + log.info("用户{}订阅授权上报成功,模板ID:{},状态:{}", form.getUserId(), form.getTemplateId(), form.getStatus()); + } catch (Exception e) { + log.error("上报用户订阅授权失败,请求参数:{}", JSONUtil.toJsonStr(form), e); + throw new MsgException("上报授权状态失败"); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean sendMessage(WxSubscribeSendForm form) { + log.info("开始发送订阅消息,请求参数:{}", JSONUtil.toJsonStr(form)); + try { + // 1. 查询用户openid + String openid = userSubscribeMapper.getOpenidByUserId(form.getUserId()); + if (StrUtil.isBlank(openid)) { + log.error("用户{}未绑定微信openid,无法发送订阅消息", form.getUserId()); + return false; + } + + // 2. 构建微信订阅消息参数 + List> dataList = new ArrayList<>(); + for (Map.Entry entry : form.getTemplateParams().entrySet()) { + Map dataMap = new HashMap<>(); + dataMap.put("name", entry.getKey()); + dataMap.put("value", entry.getValue()); + dataList.add(dataMap); + } + + Map messageMap = new HashMap<>(); + messageMap.put("touser", openid); + messageMap.put("template_id", form.getTemplateId()); + messageMap.put("page", form.getPage()); + messageMap.put("miniprogram_state", form.getMiniProgramState()); + messageMap.put("lang", "zh_CN"); + messageMap.put("data", dataList); + + // 3. 调用微信接口发送 + String responseStr = wxMaService.post("https://api.weixin.qq.com/cgi-bin/message/subscribe/send", JSONUtil.toJsonStr(messageMap)); + WxApiResponse response = JSONUtil.toBean(responseStr, WxApiResponse.class); + log.info("微信订阅消息发送结果,用户:{},返回:{}", form.getUserId(), JSONUtil.toJsonStr(response)); + + // 4. 根据返回结果更新用户订阅状态 + MiniUserSubscribe subscribe = userSubscribeMapper.selectOne(new LambdaQueryWrapper() + .eq(MiniUserSubscribe::getUserId, form.getUserId()) + .eq(MiniUserSubscribe::getTemplateId, form.getTemplateId()) + .eq(MiniUserSubscribe::getDeleted, 0) + .last("limit 1") + ); + + if (response.getErrcode() == 0) { + log.info("订阅消息发送成功,用户:{},模板ID:{}", form.getUserId(), form.getTemplateId()); + // 如果是同意一次的状态,发送后更新为未授权 + if (subscribe != null && SubscribeStatusEnum.ONCE.getValue().equals(subscribe.getStatus())) { + subscribe.setStatus(SubscribeStatusEnum.REJECT.getValue()); + subscribe.setUpdateTime(new Date()); + userSubscribeMapper.updateById(subscribe); + } + return true; + } else if (response.getErrcode() == 43101) { + // 用户未订阅,更新本地状态为拒绝 + log.warn("用户{}未订阅该模板消息,更新本地状态为拒绝", form.getUserId()); + if (subscribe != null) { + subscribe.setStatus(SubscribeStatusEnum.REJECT.getValue()); + subscribe.setUpdateTime(new Date()); + userSubscribeMapper.updateById(subscribe); + } + return false; + } else { + log.error("订阅消息发送失败,错误码:{},错误信息:{}", response.getErrcode(), response.getErrmsg()); + return false; + } + } catch (Exception e) { + log.error("发送订阅消息异常,请求参数:{}", JSONUtil.toJsonStr(form), e); + return false; + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6c512aa..35c102f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -226,8 +226,8 @@ captcha: # 微信小程序配置 wx: miniapp: - appid: Your_AppId - secret: Your_AppSecret + appid: wx56425c3301f5c6df + secret: 7c4060199f49b0ab872b06b97cef2ac4 # AIGeneration 配置 ai: @@ -244,3 +244,8 @@ ai: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 +# 订阅模板配置 +subscribe: + template: "7m5Vu4gaCo2zY6hzID4yqEv94y1guuOGxdPOJYV_xHE" + + diff --git a/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml b/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml new file mode 100644 index 0000000..c418984 --- /dev/null +++ b/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + From 4d1a22f5dea296a10890071faec6e797a7532902 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Mon, 18 May 2026 16:03:09 +0800 Subject: [PATCH 07/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/youlai/boot/mini/model/vo/UserUploadMediaVO.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java b/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java index f0ef7f1..3ebb0ee 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java +++ b/src/main/java/com/youlai/boot/mini/model/vo/UserUploadMediaVO.java @@ -1,5 +1,6 @@ package com.youlai.boot.mini.model.vo; +import com.baomidou.mybatisplus.annotation.TableField; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -16,8 +17,8 @@ import java.util.Date; @Schema(description = "用户上传媒体返回VO") public class UserUploadMediaVO { - @Schema(description = "媒体ID") - private Long id; + @Schema(description = "媒体UUID") + private String uuid; @Schema(description = "媒体类型:image-图片,video-视频") private String mediaType; @@ -40,4 +41,8 @@ public class UserUploadMediaVO { @Schema(description = "上传时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; + + @TableField("create_timestamp") + @Schema(description = "创建时间毫秒级时间戳") + private Long createTimestamp; } From 9fdb7d1900676d9744437024d2f6d044fbad69d6 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Wed, 20 May 2026 16:33:12 +0800 Subject: [PATCH 08/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0ai=E7=94=9F=E6=88=90?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=8E=86=E5=8F=B2=E6=9F=A5=E8=AF=A2=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 12 +++ .../mini/model/query/AiTaskMediaQuery.java | 21 +++++ .../mini/model/vo/AiGenerationTaskVO.java | 39 ++++++++++ .../boot/mini/model/vo/MiniAiTaskMediaVO.java | 39 ++++++++++ .../mini/service/AiGenerationService.java | 5 ++ .../service/impl/AiGenerationServiceImpl.java | 77 +++++++++++++++++++ 6 files changed, 193 insertions(+) create mode 100644 src/main/java/com/youlai/boot/mini/model/query/AiTaskMediaQuery.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/MiniAiTaskMediaVO.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 abdf9cc..8fa65b1 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -22,6 +22,9 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.youlai.boot.mini.model.query.AiTaskMediaQuery; +import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; import java.util.List; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; @@ -134,4 +137,13 @@ public class AiGenerationController { return Result.success(success); } + @Operation(summary = "查询我的AI生成任务历史") + @GetMapping("/my/history") + @Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.LIST) + public Result> getMyAiGenerateHistory(@Valid AiTaskMediaQuery query) { + Long userId = SecurityUtils.getUserId(); + IPage result = aiGenerationService.getMyAiGenerateHistory(query, userId); + return Result.success(result); + } + } diff --git a/src/main/java/com/youlai/boot/mini/model/query/AiTaskMediaQuery.java b/src/main/java/com/youlai/boot/mini/model/query/AiTaskMediaQuery.java new file mode 100644 index 0000000..171b7aa --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/query/AiTaskMediaQuery.java @@ -0,0 +1,21 @@ +package com.youlai.boot.mini.model.query; + +import com.youlai.boot.common.base.BaseQuery; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 用户AI生成任务历史查询参数 + */ +@Getter +@Setter +@Schema(description = "用户AI生成任务历史查询参数") +public class AiTaskMediaQuery extends BaseQuery { + + @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消,不传查全部") + private Integer status; + + @Schema(description = "生成类型:img_single(单图), img_grid_4(四宫格), video(视频),不传查全部") + private String type; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java b/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java new file mode 100644 index 0000000..1f7a502 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java @@ -0,0 +1,39 @@ +package com.youlai.boot.mini.model.vo; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import java.util.Date; +import java.util.List; + +/** + * AI生成任务列表VO + */ +@Getter +@Setter +@Schema(description = "AI生成任务列表VO") +public class AiGenerationTaskVO { + +// @Hidden +// @Schema(description = "任务ID",hidden = true) +// private Long id; + + @Schema(description = "任务唯一标识") + private String uuid; + + @Schema(description = "生成类型:img_single(单图), img_grid_4(四宫格), video(视频)") + private String type; + + @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") + private Integer status; + + @Schema(description = "消耗积分") + private Integer pointsConsumed; + + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "生成内容列表") + private List generateContent; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/MiniAiTaskMediaVO.java b/src/main/java/com/youlai/boot/mini/model/vo/MiniAiTaskMediaVO.java new file mode 100644 index 0000000..1ff32ff --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/MiniAiTaskMediaVO.java @@ -0,0 +1,39 @@ +package com.youlai.boot.mini.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import java.util.Date; + +/** + * AI生成媒体内容VO + */ +@Getter +@Setter +@Schema(description = "AI生成媒体内容VO") +public class MiniAiTaskMediaVO { + + @Schema(description = "唯一标识") + private String uuid; + + @Schema(description = "媒体类型,image-图片,video-视频") + private String mediaType; + + @Schema(description = "资源URL") + private String sourceUrl; + + @Schema(description = "缩略图URL(视频需要)") + private String thumbnailUrl; + + @Schema(description = "宽度(像素)") + private Integer width; + + @Schema(description = "高度(像素)") + private Integer height; + + @Schema(description = "时长(秒,视频用)") + private Integer duration; + + @Schema(description = "创建时间") + private Date createTime; +} 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 9634936..f2f971e 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -8,6 +8,9 @@ import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.youlai.boot.mini.model.query.AiTaskMediaQuery; +import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; import org.springframework.web.multipart.MultipartFile; @@ -35,4 +38,6 @@ public interface AiGenerationService { boolean deleteUploadMedia(Long userId, String uuid); + IPage getMyAiGenerateHistory(AiTaskMediaQuery query, Long userId); + } 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 9678429..cc7fae9 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 @@ -10,6 +10,8 @@ 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.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.exception.MsgException; import com.youlai.boot.common.util.FileUtils; @@ -23,7 +25,10 @@ import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; import com.youlai.boot.mini.model.form.*; +import com.youlai.boot.mini.model.query.AiTaskMediaQuery; +import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.youlai.boot.mini.model.vo.MiniAiTaskMediaVO; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.MiniPointRecordService; @@ -43,6 +48,8 @@ import java.io.InputStream; import java.net.URL; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; import java.util.*; import java.util.stream.Collectors; import java.util.concurrent.CompletableFuture; @@ -896,4 +903,74 @@ public class AiGenerationServiceImpl implements AiGenerationService { return aiTaskMediaMapper.updateById(media) > 0; } + + @Override + public IPage getMyAiGenerateHistory(AiTaskMediaQuery query, Long userId) { + // 1. 分页查询当前用户的生成任务 + LambdaQueryWrapper taskQueryWrapper = new LambdaQueryWrapper<>(); + taskQueryWrapper.eq(MiniAiGenerationTask::getMiniUserId, userId); + // 任务状态筛选 + if (query.getStatus() != null) { + taskQueryWrapper.eq(MiniAiGenerationTask::getStatus, query.getStatus()); + } + // 生成类型筛选 + if (query.getType() != null) { + taskQueryWrapper.eq(MiniAiGenerationTask::getType, query.getType()); + } + taskQueryWrapper.orderByDesc(MiniAiGenerationTask::getCreateTime); + IPage taskPage = aiGenerationTaskMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), taskQueryWrapper); + + if (taskPage.getRecords().isEmpty()) { + return taskPage.convert(task -> new AiGenerationTaskVO()); + } + + // 2. 提取所有任务ID + List taskIds = taskPage.getRecords().stream() + .map(MiniAiGenerationTask::getId) + .collect(Collectors.toList()); + + // 3. 批量查询这些任务对应的AI生成媒体内容 + LambdaQueryWrapper mediaQueryWrapper = new LambdaQueryWrapper<>(); + mediaQueryWrapper.in(MiniAiTaskMedia::getTaskId, taskIds); + mediaQueryWrapper.eq(MiniAiTaskMedia::getFileSource, "ai_generate"); + mediaQueryWrapper.eq(MiniAiTaskMedia::getDeleted, false); + + List mediaList = aiTaskMediaMapper.selectList(mediaQueryWrapper); + + // 4. 先按任务ID分组媒体内容,再批量转换为VO + Map> rawMediaGroupMap = mediaList.stream() + .collect(Collectors.groupingBy(MiniAiTaskMedia::getTaskId)); + + // 转换为VO分组 + Map> mediaGroupMap = rawMediaGroupMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().stream().map(media -> { + MiniAiTaskMediaVO mediaVO = new MiniAiTaskMediaVO(); + mediaVO.setUuid(media.getUuid()); + mediaVO.setMediaType(media.getMediaType()); + mediaVO.setSourceUrl(media.getSourceUrl()); + mediaVO.setThumbnailUrl(media.getThumbnailUrl()); + mediaVO.setWidth(media.getWidth()); + mediaVO.setHeight(media.getHeight()); + mediaVO.setDuration(media.getDuration()); + mediaVO.setCreateTime(media.getCreateTime()); + return mediaVO; + }).collect(Collectors.toList()) + )); + + // 5. 组装VO + return taskPage.convert(task -> { + AiGenerationTaskVO vo = new AiGenerationTaskVO(); +// vo.setId(task.getId()); + vo.setUuid(task.getUuid()); + vo.setType(task.getType()); + vo.setStatus(task.getStatus()); + vo.setPointsConsumed(task.getPointsConsumed()); + vo.setCreateTime(task.getCreateTime()); + vo.setGenerateContent(mediaGroupMap.getOrDefault(task.getId(), Collections.emptyList())); + return vo; + }); + } + } From 784b9b4a13c8dc6a4eea4ee839f8621f53c13c6f Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 21 May 2026 10:17:45 +0800 Subject: [PATCH 09/16] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 6 +- .../mini/controller/MiniUserController.java | 2 +- .../mini/model/form/WxSubscribeAuthForm.java | 2 +- .../mini/model/form/WxSubscribeSendForm.java | 2 +- .../service/impl/AiGenerationServiceImpl.java | 68 +++++++++++-------- .../service/impl/MiniUserServiceImpl.java | 1 + .../service/impl/WxSubscribeServiceImpl.java | 15 ++-- src/main/resources/application-dev.yml | 3 +- .../mapper/mini/MiniUserSubscribeMapper.xml | 1 - 9 files changed, 56 insertions(+), 44 deletions(-) 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 8fa65b1..1b6277c 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -6,15 +6,12 @@ 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.entity.MiniAiTaskMedia; import com.youlai.boot.mini.model.form.*; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.WxSubscribeService; +import io.swagger.v3.oas.annotations.Hidden; 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; @@ -111,6 +108,7 @@ public class AiGenerationController { return Result.success(); } + @Hidden @Operation(summary = "发送微信订阅消息") @PostMapping("/subscribe/send") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) diff --git a/src/main/java/com/youlai/boot/mini/controller/MiniUserController.java b/src/main/java/com/youlai/boot/mini/controller/MiniUserController.java index 173645b..e62f335 100644 --- a/src/main/java/com/youlai/boot/mini/controller/MiniUserController.java +++ b/src/main/java/com/youlai/boot/mini/controller/MiniUserController.java @@ -46,7 +46,7 @@ public class MiniUserController { @Operation(summary = "修改当前登录用户基本信息") @PostMapping(value = "/updateInfo") @Log(module = LogModuleEnum.USER, value = ActionTypeEnum.UPDATE) - public Result updateCurrentUserInfo(@Valid MiniUserUpdateForm form) { + public Result updateCurrentUserInfo(@Valid @RequestBody MiniUserUpdateForm form) { Long userId = SecurityUtils.getUserId(); miniUserService.updateCurrentUserInfo(userId, form); return Result.success(); diff --git a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java index e6c15d4..a7e90e3 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeAuthForm.java @@ -12,7 +12,7 @@ import lombok.Data; @Schema(description = "上报用户订阅授权请求") public class WxSubscribeAuthForm { - @NotBlank(message = "用户ID不能为空") +// @NotBlank(message = "用户ID不能为空") @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) private Long userId; diff --git a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java index c51ad18..04b11aa 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java @@ -13,7 +13,7 @@ import java.util.Map; @Schema(description = "发送订阅消息请求") public class WxSubscribeSendForm { - @NotBlank(message = "用户ID不能为空") +// @NotBlank(message = "用户ID不能为空") @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) private Long userId; 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 cc7fae9..fa4e12f 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 @@ -554,11 +554,11 @@ public class AiGenerationServiceImpl implements AiGenerationService { WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); sendForm.setUserId(task.getMiniUserId()); sendForm.setTemplateId(subscribeTemplate); - sendForm.setPage("pages/index/index"); + sendForm.setPage("pages/creation/list"); sendForm.setTemplateParams(Map.of( - "thing1", "您的AI视频作品已完成", - "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), - "phrase3", "点击查看状态" + "thing5", "您的AI视频作品已完成", + "time3", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm")), + "phrase4", "待查看" )); wxSubscribeService.sendMessage(sendForm); } catch (Exception e) { @@ -657,23 +657,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiTaskMediaMapper.update(null, updateMediaWrapper); // 同步发送订阅消息通知 - if (StrUtil.isNotBlank(subscribeTemplate)) { - try { - WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); - sendForm.setUserId(task.getMiniUserId()); - sendForm.setTemplateId(subscribeTemplate); - sendForm.setPage("pages/index/index"); - // 根据实际模板字段调整参数 - sendForm.setTemplateParams(Map.of( - "thing1", "您的AI绘画作品已完成", - "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), - "phrase3", "点击查看状态" - )); - wxSubscribeService.sendMessage(sendForm); - } catch (Exception e) { - log.error("单图任务{}发送订阅消息失败", task.getId(), e); - } - } + sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } log.info("四宫格任务{}回调处理完成,状态:{}", taskUuid, status); @@ -765,12 +749,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); sendForm.setUserId(task.getMiniUserId()); sendForm.setTemplateId(subscribeTemplate); - sendForm.setPage("pages/index/index"); + sendForm.setPage("pages/creation/list"); // 根据实际模板字段调整参数 sendForm.setTemplateParams(Map.of( - "thing1", "您的AI绘画作品已完成", - "time2", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), - "phrase3", "点击查看状态" + "thing5", "您的AI绘画作品已完成", + "time3", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm")), + "phrase4", "待查看" )); wxSubscribeService.sendMessage(sendForm); } catch (Exception e) { @@ -917,7 +901,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { if (query.getType() != null) { taskQueryWrapper.eq(MiniAiGenerationTask::getType, query.getType()); } - taskQueryWrapper.orderByDesc(MiniAiGenerationTask::getCreateTime); + taskQueryWrapper.orderByDesc(MiniAiGenerationTask::getCreateTimestamp); IPage taskPage = aiGenerationTaskMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), taskQueryWrapper); if (taskPage.getRecords().isEmpty()) { @@ -932,7 +916,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 3. 批量查询这些任务对应的AI生成媒体内容 LambdaQueryWrapper mediaQueryWrapper = new LambdaQueryWrapper<>(); mediaQueryWrapper.in(MiniAiTaskMedia::getTaskId, taskIds); - mediaQueryWrapper.eq(MiniAiTaskMedia::getFileSource, "ai_generate"); + mediaQueryWrapper.eq(MiniAiTaskMedia::getFileSource, "ai_generated"); mediaQueryWrapper.eq(MiniAiTaskMedia::getDeleted, false); List mediaList = aiTaskMediaMapper.selectList(mediaQueryWrapper); @@ -960,7 +944,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { )); // 5. 组装VO - return taskPage.convert(task -> { + IPage convert = taskPage.convert(task -> { AiGenerationTaskVO vo = new AiGenerationTaskVO(); // vo.setId(task.getId()); vo.setUuid(task.getUuid()); @@ -971,6 +955,34 @@ public class AiGenerationServiceImpl implements AiGenerationService { vo.setGenerateContent(mediaGroupMap.getOrDefault(task.getId(), Collections.emptyList())); return vo; }); + log.info("用户{}查询AI生成任务历史成功,结果:{}", userId, convert.getRecords().toString()); + return convert; + } + + /** + * 发送AI绘画完成订阅消息通知 + * @param userId 接收用户ID + * @param templateId 订阅消息模板ID + * @param taskId 任务ID(仅用于日志) + */ + private void sendAiGenerateSuccessNotify(Long userId, String templateId, Long taskId) { + if (StrUtil.isBlank(templateId)) { + return; + } + try { + WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); + sendForm.setUserId(userId); + sendForm.setTemplateId(templateId); + sendForm.setPage("pages/creation/list"); + sendForm.setTemplateParams(Map.of( + "thing5", "您的AI绘画作品已完成", + "time3", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm")), + "phrase4", "待查看" + )); + wxSubscribeService.sendMessage(sendForm); + } catch (Exception e) { + log.error("四宫格图片任务{}发送订阅消息失败", taskId, e); + } } } diff --git a/src/main/java/com/youlai/boot/mini/service/impl/MiniUserServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/MiniUserServiceImpl.java index 3a6daea..2aa35a1 100644 --- a/src/main/java/com/youlai/boot/mini/service/impl/MiniUserServiceImpl.java +++ b/src/main/java/com/youlai/boot/mini/service/impl/MiniUserServiceImpl.java @@ -132,6 +132,7 @@ public class MiniUserServiceImpl implements MiniUserService { log.warn("删除用户旧头像失败,userId={}, oldAvatar={}", userId, oldAvatar, e); } } + sysUserService.update(updateWrapper); } catch (Exception e) { log.error("user avatar upload failed", e); throw new MsgException("头像上传失败"); diff --git a/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java index 6d0f2a0..fc7c4d0 100644 --- a/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java +++ b/src/main/java/com/youlai/boot/mini/service/impl/WxSubscribeServiceImpl.java @@ -1,6 +1,7 @@ package com.youlai.boot.mini.service.impl; import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -58,6 +59,7 @@ public class WxSubscribeServiceImpl implements WxSubscribeService { userSubscribeMapper.updateById(exist); } else { MiniUserSubscribe subscribe = new MiniUserSubscribe(); + subscribe.setUuid(IdUtil.fastSimpleUUID()); subscribe.setUserId(form.getUserId()); subscribe.setTemplateId(form.getTemplateId()); subscribe.setStatus(form.getStatus()); @@ -87,13 +89,12 @@ public class WxSubscribeServiceImpl implements WxSubscribeService { return false; } - // 2. 构建微信订阅消息参数 - List> dataList = new ArrayList<>(); + // 2. 构建微信订阅消息参数(新版小程序订阅消息格式) + Map> dataMap = new HashMap<>(); for (Map.Entry entry : form.getTemplateParams().entrySet()) { - Map dataMap = new HashMap<>(); - dataMap.put("name", entry.getKey()); - dataMap.put("value", entry.getValue()); - dataList.add(dataMap); + Map valueMap = new HashMap<>(); + valueMap.put("value", entry.getValue()); + dataMap.put(entry.getKey(), valueMap); } Map messageMap = new HashMap<>(); @@ -102,7 +103,7 @@ public class WxSubscribeServiceImpl implements WxSubscribeService { messageMap.put("page", form.getPage()); messageMap.put("miniprogram_state", form.getMiniProgramState()); messageMap.put("lang", "zh_CN"); - messageMap.put("data", dataList); + messageMap.put("data", dataMap); // 3. 调用微信接口发送 String responseStr = wxMaService.post("https://api.weixin.qq.com/cgi-bin/message/subscribe/send", JSONUtil.toJsonStr(messageMap)); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 35c102f..b20e303 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -94,6 +94,7 @@ security: - /api/v1/mini/ai/generation/single-image/task/callback # AIGeneration 单图任务回调 - /api/v1/mini/ai/generation/video/task/callback # AIGeneration 视频任务回调 - /api/v1/mini/ai/generation/four-panel/task/callback # AIGeneration 四宫格任务回调 + - /api/v1/mini/ai/generation/subscribe/send # AIGeneration 发送订阅消息 # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -239,7 +240,7 @@ ai: single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/single-image/task/callback four-panel-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/four-panel/task/callback #需要内网穿透工具 由火山方舟 api 回调,ngrok http 30101 替换为内网穿透工具地址 - video-url: https://2dd0-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/video/task/callback + video-url: http://101.34.78.57:30101/backend/api/v1/mini/ai/generation/video/task/callback default: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 diff --git a/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml b/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml index c418984..3018fa3 100644 --- a/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml +++ b/src/main/resources/mapper/mini/MiniUserSubscribeMapper.xml @@ -9,7 +9,6 @@ SELECT openid FROM sys_user_social WHERE user_id = #{userId} - AND is_deleted = 0 LIMIT 1 From 1b665c2f2c6a781ebada28ed0a7d91c396fe7f6e Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Mon, 25 May 2026 15:05:01 +0800 Subject: [PATCH 10/16] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../youlai/boot/mini/service/impl/AiGenerationServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fa4e12f..3099ef4 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 @@ -527,7 +527,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { media.setUuid(mediaUuid) .setTaskId(task.getId()) .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") + .setFileSource("ai_generated") .setMediaType("video") .setSourceUrl(ossUrl) .setDuration(vo.getDuration()) From e76df77ce5c0d761cb1dd65a5a1a8c7081848b02 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Tue, 26 May 2026 15:56:32 +0800 Subject: [PATCH 11/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 15 ++++ .../mini/model/form/WxSubscribeSendForm.java | 2 +- .../mini/service/AiGenerationService.java | 2 + .../service/impl/AiGenerationServiceImpl.java | 74 ++++++++++--------- src/main/resources/application-dev.yml | 2 + 5 files changed, 58 insertions(+), 37 deletions(-) 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 1b6277c..9b554a1 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -18,10 +18,14 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import org.springframework.security.access.prepost.PreAuthorize; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.boot.mini.model.query.AiTaskMediaQuery; import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; +import cn.hutool.core.util.StrUtil; import java.util.List; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; @@ -144,4 +148,15 @@ public class AiGenerationController { return Result.success(result); } + @Operation(summary = "设置微信订阅消息跳转版本") + @PostMapping("/subscribe/mini-program-state/set") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) + public Result setMiniProgramState( + @Schema(description = "跳转版本:developer=开发版 trial=体验版 formal=正式版,为空时删除配置") + @Pattern(regexp = "^(developer|trial|formal|)$", message = "参数只能是developer/trial/formal或为空") + @RequestParam(required = false) String miniProgramState + ) { + aiGenerationService.setMiniProgramState(miniProgramState); + return Result.success(); + } } diff --git a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java index 04b11aa..468861b 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/WxSubscribeSendForm.java @@ -25,7 +25,7 @@ public class WxSubscribeSendForm { private String page; @Schema(description = "跳转版本:developer=开发版 trial=体验版 formal=正式版,默认formal") - private String miniProgramState = "developer"; + private String miniProgramState; @NotNull(message = "模板参数不能为空") @Schema(description = "模板参数,key是模板字段名,value是对应的值", requiredMode = Schema.RequiredMode.REQUIRED) 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 f2f971e..d429011 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -40,4 +40,6 @@ public interface AiGenerationService { IPage getMyAiGenerateHistory(AiTaskMediaQuery query, Long userId); + void setMiniProgramState(String miniProgramState); + } 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 3099ef4..7e4bb17 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 @@ -37,6 +37,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -65,6 +66,10 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final AliyunFileService aliyunFileService; private final MiniPointRecordService pointRecordService; private final WxSubscribeService wxSubscribeService; + private final StringRedisTemplate stringRedisTemplate; + + // Redis key 存储微信订阅消息跳转版本 + private static final String WX_MINI_PROGRAM_STATE_KEY = "wx:subscribe:mini_program_state"; //OSS存储目录配置 private static final String OSS_IMAGE_DIR = "ai/image/"; @@ -101,6 +106,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Value("${subscribe.template}") private String subscribeTemplate; + @Value("${subscribe.miniProgramState}") + private String miniProgramState; + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; //AI四宫格生成积分规则编码 @@ -549,22 +557,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiTaskMediaMapper.update(null, updateMediaWrapper); // 同步发送订阅消息通知 - if (StrUtil.isNotBlank(subscribeTemplate)) { - try { - WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); - sendForm.setUserId(task.getMiniUserId()); - sendForm.setTemplateId(subscribeTemplate); - sendForm.setPage("pages/creation/list"); - sendForm.setTemplateParams(Map.of( - "thing5", "您的AI视频作品已完成", - "time3", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm")), - "phrase4", "待查看" - )); - wxSubscribeService.sendMessage(sendForm); - } catch (Exception e) { - log.error("视频任务{}发送订阅消息失败", task.getId(), e); - } - } + sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } success = true; @@ -744,23 +737,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiTaskMediaMapper.update(null, updateMediaWrapper); // 同步发送订阅消息通知 - if (StrUtil.isNotBlank(subscribeTemplate)) { - try { - WxSubscribeSendForm sendForm = new WxSubscribeSendForm(); - sendForm.setUserId(task.getMiniUserId()); - sendForm.setTemplateId(subscribeTemplate); - sendForm.setPage("pages/creation/list"); - // 根据实际模板字段调整参数 - sendForm.setTemplateParams(Map.of( - "thing5", "您的AI绘画作品已完成", - "time3", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm")), - "phrase4", "待查看" - )); - wxSubscribeService.sendMessage(sendForm); - } catch (Exception e) { - log.error("单图任务{}发送订阅消息失败", task.getId(), e); - } - } + sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } // 更新任务状态 @@ -794,6 +771,30 @@ public class AiGenerationServiceImpl implements AiGenerationService { return aiGenerationTaskMapper.update(null, updateWrapper) > 0; } + /** + * 获取微信订阅消息跳转版本 + * 优先级: 配置文件配置 > Redis配置 > 默认formal + */ + private String getMiniProgramState() { + if (StrUtil.isNotBlank(miniProgramState)) { + return miniProgramState; + } + String redisState = stringRedisTemplate.opsForValue().get(WX_MINI_PROGRAM_STATE_KEY); + if (StrUtil.isNotBlank(redisState)) { + return redisState; + } + return "formal"; + } + + @Override + public void setMiniProgramState(String miniProgramState) { + if (StrUtil.isBlank(miniProgramState)) { + stringRedisTemplate.delete(WX_MINI_PROGRAM_STATE_KEY); + } else { + stringRedisTemplate.opsForValue().set(WX_MINI_PROGRAM_STATE_KEY, miniProgramState); + } + } + /** * 下载外部URL到OSS,返回OSS访问地址 */ @@ -960,7 +961,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { } /** - * 发送AI绘画完成订阅消息通知 + * 发送AI作品完成订阅消息通知 * @param userId 接收用户ID * @param templateId 订阅消息模板ID * @param taskId 任务ID(仅用于日志) @@ -974,14 +975,15 @@ public class AiGenerationServiceImpl implements AiGenerationService { sendForm.setUserId(userId); sendForm.setTemplateId(templateId); sendForm.setPage("pages/creation/list"); + sendForm.setMiniProgramState(getMiniProgramState()); sendForm.setTemplateParams(Map.of( - "thing5", "您的AI绘画作品已完成", + "thing5", "您的AI作品已完成", "time3", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm")), "phrase4", "待查看" )); wxSubscribeService.sendMessage(sendForm); } catch (Exception e) { - log.error("四宫格图片任务{}发送订阅消息失败", taskId, e); + log.error("AI任务{}发送订阅消息失败", taskId, e); } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b20e303..4b44a9c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -248,5 +248,7 @@ ai: # 订阅模板配置 subscribe: template: "7m5Vu4gaCo2zY6hzID4yqEv94y1guuOGxdPOJYV_xHE" + # developer=开发版 trial=体验版 formal=正式版 + miniProgramState: "developer" From 89b0e34eb009cbe7cefbe3f0e910338c431cc08f Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Wed, 27 May 2026 17:39:05 +0800 Subject: [PATCH 12/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=86=85=E5=AE=B9=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../controller/AiGenerationController.java | 14 ++ .../model/entity/MiniAiGenerationTask.java | 4 + .../model/form/AiFourPanelGenerateForm.java | 3 + .../model/form/AiSingleImageGenerateForm.java | 3 + .../mini/model/form/AiVideoGenerateForm.java | 3 + .../service/impl/AiGenerationServiceImpl.java | 162 +++++++++--------- 7 files changed, 105 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index 18f34bd..c5abc96 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ docker/*/data/ docker/minio/config docker/xxljob/logs application-youlai.yml +.claude 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 9b554a1..db2a621 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -159,4 +159,18 @@ public class AiGenerationController { aiGenerationService.setMiniProgramState(miniProgramState); return Result.success(); } + + //需要在三个回调接口中控制状态,更新其他状态; 更新状态时需要判断 当前状态已是成功或失败时不能再更新状态,当状态是失败时需要退还补偿积分; + // mini_point_rule积分规则表,每个回调回退的积分是对应的规则扣减积分的负数值;mini_point_record积分记录表;mini_point_account用户积分账户 + + + // 当外部服务挂掉,不回调我的接口时,需要根据时间段判断,如果超过一定时间未回调,则更新状态为失败,并退还积分; + + //TODO 现在 用户生成内容时,需要增加一个参数,同意或不同意公开生成的作品内容 + //TODO mini 中增加 查询 用户公开生成作品的接口,考虑设计一下查询规则 满足平台前中后期运营需求 + + //TODO admin 中 后台管理中增加 查询用户生成任务历史的接口 + //TODO admin 中 编辑用户生成作品历史接口 + //TODO admin 中 增加 手动查询生成任务接口,手动跳转生成失败任务,或做个接口给用户手动刷新查看,需要注意状态流转,只有超时未完成任务可出现刷新按钮 + } 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 28af4d2..38d7dea 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 @@ -48,6 +48,10 @@ public class MiniAiGenerationTask implements Serializable { @Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") private Integer status; + @TableField("visibility") + @Schema(description = "可见范围:public-公开,private-仅自己可见,friends-好友可见") + private String visibility; + @TableField("result_resource_url") @Schema(description = "生成结果资源URL") private String resultResourceUrl; diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java index d8cf303..899e794 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiFourPanelGenerateForm.java @@ -51,4 +51,7 @@ public class AiFourPanelGenerateForm { @Schema(description = "故事梗概:AI会据此生成四格漫画脚本") private String description; + + @Schema(description = "可见范围: public公开 private仅自己", defaultValue = "private") + private String visibility; } 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 index 2d09d56..bb9f866 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiSingleImageGenerateForm.java @@ -51,4 +51,7 @@ public class AiSingleImageGenerateForm { @Schema(description = "场景描述:用户自定义生成场景") private String description; + + @Schema(description = "可见范围: public公开 private仅自己", defaultValue = "private") + private String visibility; } diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java index 5ad08ec..1673eda 100644 --- a/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java +++ b/src/main/java/com/youlai/boot/mini/model/form/AiVideoGenerateForm.java @@ -24,4 +24,7 @@ public class AiVideoGenerateForm { @Schema(description = "视频时长,单位秒,最大15秒", defaultValue = "5") private Integer duration = 5; + + @Schema(description = "可见范围: public公开 private仅自己", defaultValue = "private") + private String visibility; } 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 7e4bb17..2430fa7 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 @@ -75,6 +75,8 @@ 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/"; + private static final String OSS_GENERATE_IMAGE_DIR = "ai/generate/image"; + private static final String OSS_GENERATE_VIDEO_DIR = "ai/generate/video"; //AI单图生成服务地址 @Value("${ai.generate.single-image-server-url:http://127.0.0.1:8001/api/v1/photo-to-comic}") @@ -255,6 +257,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { MiniAiGenerationTask task = new MiniAiGenerationTask(); task.setUuid(taskUuid) .setMiniUserId(userId) + .setVisibility(form.getVisibility()) .setType("img_single") // 单图 .setGenerateParams(JSONUtil.toJsonStr(form)) .setPointsConsumed(Math.abs(deductPoint)) @@ -343,6 +346,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { MiniAiGenerationTask task = new MiniAiGenerationTask(); task.setUuid(taskUuid) .setMiniUserId(userId) + .setVisibility(form.getVisibility()) .setType("img_grid_4") // 四宫格漫画 .setGenerateParams(JSONUtil.toJsonStr(form)) .setPointsConsumed(Math.abs(deductPoint)) @@ -427,6 +431,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 创建生成任务 MiniAiGenerationTask task = new MiniAiGenerationTask(); task.setUuid(taskUuid) + .setVisibility(form.getVisibility()) .setMiniUserId(userId) .setType("video") .setGenerateParams(JSONUtil.toJsonStr(form)) @@ -440,7 +445,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 组装第三方接口参数 Map aiRequest = new HashMap<>(); - aiRequest.put("model", form.getModel() == null ? aiDefaultVideoModel : form.getModel());//TODO写到上面 + aiRequest.put("model", form.getModel() == null ? aiDefaultVideoModel : form.getModel()); aiRequest.put("content", form.getContent()); aiRequest.put("resolution", form.getResolution()); aiRequest.put("duration", form.getDuration()); @@ -524,12 +529,8 @@ public class AiGenerationServiceImpl implements AiGenerationService { // 如果生成成功,下载外部视频到OSS if (status == 1 && vo.getContent() != null && vo.getContent().getVideoUrl() != null) { String externalVideoUrl = vo.getContent().getVideoUrl(); - // 调用下载方法,存储到视频目录 - String ossUrl = downloadExternalUrlToOss(externalVideoUrl, OSS_VIDEO_DIR); - task.setResultResourceUrl(ossUrl); - aiGenerationTaskMapper.updateById(task); + String ossUrl = downloadExternalUrlToOss(externalVideoUrl, OSS_GENERATE_VIDEO_DIR); - // 保存视频媒体记录 MiniAiTaskMedia media = new MiniAiTaskMedia(); String mediaUuid = UUID.randomUUID().toString().replace("-", ""); media.setUuid(mediaUuid) @@ -598,44 +599,41 @@ public class AiGenerationServiceImpl implements AiGenerationService { task.setUpdateTimestamp(System.currentTimeMillis()); aiGenerationTaskMapper.updateById(task); - // 如果生成成功,下载外部URL到OSS + // 如果生成成功,下载外部URL到OSS,存储所有结果图片 if (status == 1 && form.getResult() != null && !form.getResult().isEmpty()) { - String externalImageUrl = form.getResult().get(0).getUrl(); - // 调用下载方法,存储到图片目录 - String ossUrl = downloadExternalUrlToOss(externalImageUrl); - task.setResultResourceUrl(ossUrl); - aiGenerationTaskMapper.updateById(task); - - // 保存生成的媒体记录 - MiniAiTaskMedia media = new MiniAiTaskMedia(); - String mediaUuid = UUID.randomUUID().toString().replace("-", ""); - AiCallbackImage image = form.getResult().get(0); - // 解析尺寸 - Integer width = null; - Integer height = null; - if (image.getSize() != null && image.getSize().contains("x")) { - String[] sizeArr = image.getSize().split("x"); - try { - width = Integer.parseInt(sizeArr[0]); - height = Integer.parseInt(sizeArr[1]); - } catch (Exception e) { - log.warn("解析四宫格图片尺寸失败,size:{}", image.getSize(), e); + long now = System.currentTimeMillis(); + + for (AiCallbackImage image : form.getResult()) { + String ossUrl = downloadExternalUrlToOss(image.getUrl(), OSS_GENERATE_IMAGE_DIR); + + // 解析尺寸 + Integer width = null; + Integer height = null; + if (image.getSize() != null && image.getSize().contains("x")) { + String[] sizeArr = image.getSize().split("x"); + try { + width = Integer.parseInt(sizeArr[0]); + height = Integer.parseInt(sizeArr[1]); + } catch (Exception e) { + log.warn("解析四宫格图片尺寸失败,size:{}", image.getSize(), e); + } } - } - media.setUuid(mediaUuid) - .setTaskId(task.getId()) - .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") - .setMediaType("image") - .setSourceUrl(ossUrl) - .setWidth(width) - .setHeight(height) - .setCreateBy(task.getCreateBy()) - .setCreateTimestamp(System.currentTimeMillis()) - .setCreateTime(new Date()); - - aiTaskMediaMapper.insert(media); + MiniAiTaskMedia media = new MiniAiTaskMedia(); + String mediaUuid = UUID.randomUUID().toString().replace("-", ""); + media.setUuid(mediaUuid) + .setTaskId(task.getId()) + .setMiniUserId(task.getMiniUserId()) + .setFileSource("ai_generate") + .setMediaType("image") + .setSourceUrl(ossUrl) + .setWidth(width) + .setHeight(height) + .setCreateBy(task.getCreateBy()) + .setCreateTimestamp(now) + .setCreateTime(new Date(now)); + aiTaskMediaMapper.insert(media); + } // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); @@ -662,6 +660,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { } @Override + @Transactional(rollbackFor = Exception.class) public boolean handleTaskCallback(AiSingleImageCallbackForm form) { log.info("处理单图生成任务回调,任务UUID:{}", form.getUuid()); try { @@ -684,45 +683,45 @@ public class AiGenerationServiceImpl implements AiGenerationService { return false; } - // 如果生成成功,下载外部URL到OSS - String ossUrl = null; + // 如果生成成功,下载外部URL到OSS,存储所有结果图片 if (status == 1 && form.getResult() != null && !form.getResult().isEmpty()) { - String externalResultUrl = form.getResult().get(0).getUrl(); - ossUrl = downloadExternalUrlToOss(externalResultUrl); - - // 保存生成的媒体记录 - MiniAiTaskMedia media = new MiniAiTaskMedia(); - String mediaUuid = UUID.randomUUID().toString().replace("-", ""); - AiCallbackImage image = form.getResult().get(0); - // 解析尺寸 - Integer width = null; - Integer height = null; - if (image.getSize() != null && image.getSize().contains("x")) { - String[] sizeArr = image.getSize().split("x"); - try { - width = Integer.parseInt(sizeArr[0]); - height = Integer.parseInt(sizeArr[1]); - } catch (Exception e) { - log.warn("解析单图图片尺寸失败,size:{}", image.getSize(), e); + long now = System.currentTimeMillis(); + + for (AiCallbackImage image : form.getResult()) { + String ossUrl = downloadExternalUrlToOss(image.getUrl(), OSS_GENERATE_IMAGE_DIR); + + // 解析尺寸 + Integer width = null; + Integer height = null; + if (image.getSize() != null && image.getSize().contains("x")) { + String[] sizeArr = image.getSize().split("x"); + try { + width = Integer.parseInt(sizeArr[0]); + height = Integer.parseInt(sizeArr[1]); + } catch (Exception e) { + log.warn("解析单图图片尺寸失败,size:{}", image.getSize(), e); + } } - } - - media.setUuid(mediaUuid) - .setMiniUserId(task.getMiniUserId()) - .setTaskId(task.getId()) - .setFileSource("ai_generated") - .setMediaType("image") - .setSourceUrl(ossUrl) - .setWidth(width) - .setHeight(height) - .setCreateBy(task.getCreateBy()) - .setCreateTime(new Date()) - .setCreateTimestamp(System.currentTimeMillis()) - .setUpdateTime(new Date()) - .setUpdateTimestamp(System.currentTimeMillis()) - .setDeleted(false); - aiTaskMediaMapper.insert(media); + MiniAiTaskMedia media = new MiniAiTaskMedia(); + String mediaUuid = UUID.randomUUID().toString().replace("-", ""); + media.setUuid(mediaUuid) + .setMiniUserId(task.getMiniUserId()) + .setTaskId(task.getId()) + .setFileSource("ai_generated") + .setMediaType("image") + .setSourceUrl(ossUrl) + .setWidth(width) + .setHeight(height) + .setCreateBy(task.getCreateBy()) + .setCreateTime(new Date(now)) + .setCreateTimestamp(now) + .setUpdateTime(new Date(now)) + .setUpdateTimestamp(now) + .setDeleted(false); + + aiTaskMediaMapper.insert(media); + } // 更新用户上传的参考文件:关联当前任务ID并软删除 LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); @@ -741,7 +740,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { } // 更新任务状态 - return updateTaskStatus(taskUuid, status, ossUrl); + return updateTaskStatus(taskUuid, status, null); } catch (Exception e) { log.error("处理AI任务回调异常,UUID:{},异常信息:{}", form.getUuid(), e.getMessage(), e); @@ -795,13 +794,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { } } - /** - * 下载外部URL到OSS,返回OSS访问地址 - */ - private String downloadExternalUrlToOss(String externalUrl) { - return downloadExternalUrlToOss(externalUrl, "ai/generate/"); - } - /** * 下载外部URL到指定OSS目录,返回OSS访问地址 */ From 9aa59d888ad4779acde26dea67d2649e13ae97a9 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 28 May 2026 11:12:32 +0800 Subject: [PATCH 13/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E8=B6=85=E6=97=B6=E9=80=80=E8=BF=98=E7=A7=AF?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../youlai/boot/YouLaiBootApplication.java | 2 + .../boot/mini/job/AiTaskTimeoutJob.java | 28 +++ .../model/entity/MiniAiGenerationTask.java | 8 +- .../mini/service/AiGenerationService.java | 2 + .../service/impl/AiGenerationServiceImpl.java | 165 ++++++++++++++++-- src/main/resources/application-dev.yml | 3 + 6 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/youlai/boot/mini/job/AiTaskTimeoutJob.java diff --git a/src/main/java/com/youlai/boot/YouLaiBootApplication.java b/src/main/java/com/youlai/boot/YouLaiBootApplication.java index c9d4252..9d583d8 100644 --- a/src/main/java/com/youlai/boot/YouLaiBootApplication.java +++ b/src/main/java/com/youlai/boot/YouLaiBootApplication.java @@ -3,6 +3,7 @@ package com.youlai.boot; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; /** * 应用启动类 @@ -12,6 +13,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; */ @SpringBootApplication @MapperScan("com.youlai.boot.**.mapper") +@EnableScheduling public class YouLaiBootApplication { public static void main(String[] args) { diff --git a/src/main/java/com/youlai/boot/mini/job/AiTaskTimeoutJob.java b/src/main/java/com/youlai/boot/mini/job/AiTaskTimeoutJob.java new file mode 100644 index 0000000..a164b00 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/job/AiTaskTimeoutJob.java @@ -0,0 +1,28 @@ +package com.youlai.boot.mini.job; + +import com.youlai.boot.mini.service.AiGenerationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class AiTaskTimeoutJob { + + private final AiGenerationService aiGenerationService; + + /** + * 每10分钟扫描一次超时的AI生成任务,标记为超时并退还积分 + */ + @Scheduled(cron = "0 */10 * * * ?") + public void execute() { + log.info("开始扫描超时AI生成任务"); + try { + aiGenerationService.handleTimeoutTasks(); + } catch (Exception e) { + log.error("扫描超时AI生成任务异常", e); + } + } +} 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 38d7dea..1f34d7f 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 @@ -28,6 +28,10 @@ public class MiniAiGenerationTask implements Serializable { @Schema(description = "uuid唯一标识,前后端用这个进行数据交互") private String uuid; + @TableField("refund_uuid") + @Schema(description = "退积分唯一uuid") + private String refundUuid; + @TableField("mini_user_id") @Schema(description = "作者用户ID") private Long miniUserId; @@ -52,10 +56,6 @@ public class MiniAiGenerationTask implements Serializable { @Schema(description = "可见范围:public-公开,private-仅自己可见,friends-好友可见") private String visibility; - @TableField("result_resource_url") - @Schema(description = "生成结果资源URL") - private String resultResourceUrl; - @TableField("points_consumed") @Schema(description = "消耗积分") private Integer pointsConsumed; 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 d429011..22306b3 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -42,4 +42,6 @@ public interface AiGenerationService { void setMiniProgramState(String miniProgramState); + void handleTimeoutTasks(); + } 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 2430fa7..8921c81 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 @@ -30,6 +30,8 @@ import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.youlai.boot.mini.model.vo.MiniAiTaskMediaVO; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; +import com.youlai.boot.admin.model.form.AdjustUserPointForm; +import com.youlai.boot.admin.service.PointManageService; import com.youlai.boot.mini.service.AiGenerationService; import com.youlai.boot.mini.service.MiniPointRecordService; import com.youlai.boot.mini.service.WxSubscribeService; @@ -67,6 +69,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final MiniPointRecordService pointRecordService; private final WxSubscribeService wxSubscribeService; private final StringRedisTemplate stringRedisTemplate; + private final PointManageService pointManageService; // Redis key 存储微信订阅消息跳转版本 private static final String WX_MINI_PROGRAM_STATE_KEY = "wx:subscribe:mini_program_state"; @@ -111,12 +114,18 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Value("${subscribe.miniProgramState}") private String miniProgramState; + //AI任务超时时间(分钟),超过此时间的生成中任务将被标记为超时 + @Value("${ai.task.timeout-minutes:300}") + private int taskTimeoutMinutes; + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; //AI四宫格生成积分规则编码 private static final String AI_GENERATE_QUAD_GRID_RULE = "AI_GENERATE_QUAD_GRID"; //AI视频生成积分规则编码 private static final String AI_GENERATE_VIDEO_RULE = "AI_GENERATE_VIDEO"; + //未知类型 + private static final String AI_GENERATE_UNKNOWN_TYPE= "AI_GENERATE_UNKNOWN_TYPE"; @Override public List uploadReferenceFile(List images, List videos, Long userId) { @@ -318,9 +327,11 @@ public class AiGenerationServiceImpl implements AiGenerationService { } } catch (JSONException e) { log.error("AI生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + log.warn("单图任务{}提交异常,事务即将回滚,但AI服务可能已接收请求并在处理中,请关注AI侧状态", taskUuid); throw new MsgException("AI生成服务暂时不可用,请稍后重试"); } catch (Exception e) { log.error("AI生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + log.warn("单图任务{}提交异常,事务即将回滚,但AI服务可能已接收请求并在处理中,请关注AI侧状态", taskUuid); // 抛出异常触发事务回滚 throw new MsgException("AI生成服务暂时不可用,请稍后重试"); } @@ -404,9 +415,11 @@ public class AiGenerationServiceImpl implements AiGenerationService { } } catch (JSONException e) { log.error("四宫格漫画生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + log.warn("四宫格任务{}提交异常,事务即将回滚,但AI服务可能已接收请求并在处理中,请关注AI侧状态", taskUuid); throw new MsgException("AI生成服务暂时不可用,请稍后重试"); } catch (Exception e) { log.error("四宫格漫画生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + log.warn("四宫格任务{}提交异常,事务即将回滚,但AI服务可能已接收请求并在处理中,请关注AI侧状态", taskUuid); // 抛出异常触发事务回滚 throw new MsgException("AI生成服务暂时不可用,请稍后重试"); } @@ -483,9 +496,11 @@ public class AiGenerationServiceImpl implements AiGenerationService { } catch (JSONException e) { log.error("视频生成任务返回结果解析失败,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + log.warn("视频任务{}提交异常,事务即将回滚,但AI服务可能已接收请求并在处理中,请关注AI侧状态", taskUuid); throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); } catch (Exception e) { log.error("视频生成任务提交异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); + log.warn("视频任务{}提交异常,事务即将回滚,但AI服务可能已接收请求并在处理中,请关注AI侧状态", taskUuid); throw new MsgException("AI视频生成服务暂时不可用,请稍后重试"); } @@ -496,7 +511,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Transactional(rollbackFor = Exception.class) public boolean handleVideoTaskCallback(AiVideoCallbackVO vo) { log.info("处理AI视频生成任务回调,请求参数:{}", JSONUtil.toJsonStr(vo)); - boolean success = false; try { String videoTaskUuid = vo.getId(); // 根据第三方返回的视频任务uuid查询对应的任务 @@ -508,6 +522,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { return false; } + // 终态保护:已成功或已失败的任务不允许再次更新状态 + if (task.getStatus() != null && (task.getStatus() == 1 || task.getStatus() == 2)) { + log.warn("视频任务{}已是终态(status={}),忽略本次回调", task.getUuid(), task.getStatus()); + return true; + } + // 转换任务状态 Integer status; if ("succeeded".equals(vo.getStatus())) { @@ -520,12 +540,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { return true; } - // 更新任务状态 - task.setStatus(status); - task.setUpdateTime(new Date()); - task.setUpdateTimestamp(System.currentTimeMillis()); - aiGenerationTaskMapper.updateById(task); - // 如果生成成功,下载外部视频到OSS if (status == 1 && vo.getContent() != null && vo.getContent().getVideoUrl() != null) { String externalVideoUrl = vo.getContent().getVideoUrl(); @@ -561,12 +575,23 @@ public class AiGenerationServiceImpl implements AiGenerationService { sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } - success = true; + // 失败时退还积分 + if (status == 2) { + refundPoints(task); + } + + // 更新任务状态 + if (!updateTaskStatus(task.getUuid(), status)) { + log.error("视频任务{}更新状态失败,可能已被并发更新", task.getUuid()); + return false; + } + log.info("视频任务{}回调处理完成,状态:{}", task.getUuid(), status); + return true; } catch (Exception e) { log.error("视频任务回调处理异常,异常信息:{}", e.getMessage(), e); + return false; } - return success; } @Override @@ -582,6 +607,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { return false; } + // 终态保护:已成功或已失败的任务不允许再次更新状态 + if (task.getStatus() != null && (task.getStatus() == 1 || task.getStatus() == 2)) { + log.warn("四宫格任务{}已是终态(status={}),忽略本次回调", taskUuid, task.getStatus()); + return true; + } + // 转换任务状态 Integer status; if ("succeeded".equals(form.getStatus())) { @@ -593,12 +624,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { return true; // 中间状态直接返回成功,不更新任务 } - // 更新任务状态 - task.setStatus(status); - task.setUpdateTime(new Date()); - task.setUpdateTimestamp(System.currentTimeMillis()); - aiGenerationTaskMapper.updateById(task); - // 如果生成成功,下载外部URL到OSS,存储所有结果图片 if (status == 1 && form.getResult() != null && !form.getResult().isEmpty()) { long now = System.currentTimeMillis(); @@ -651,6 +676,17 @@ public class AiGenerationServiceImpl implements AiGenerationService { sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } + // 失败时退还积分 + if (status == 2) { + refundPoints(task); + } + + // 更新任务状态 + if (!updateTaskStatus(taskUuid, status)) { + log.error("四宫格任务{}更新状态失败,可能已被并发更新", taskUuid); + return false; + } + log.info("四宫格任务{}回调处理完成,状态:{}", taskUuid, status); return true; } catch (Exception e) { @@ -672,6 +708,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { return false; } + // 终态保护:已成功或已失败的任务不允许再次更新状态 + if (task.getStatus() != null && (task.getStatus() == 1 || task.getStatus() == 2)) { + log.warn("单图任务{}已是终态(status={}),忽略本次回调", taskUuid, task.getStatus()); + return true; + } + // 转换任务状态 Integer status; if ("succeeded".equals(form.getStatus())) { @@ -739,9 +781,19 @@ public class AiGenerationServiceImpl implements AiGenerationService { sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } + // 失败时退还积分 + if (status == 2) { + refundPoints(task); + } + // 更新任务状态 - return updateTaskStatus(taskUuid, status, null); + if (!updateTaskStatus(task.getUuid(), status)) { + log.error("单图任务{}更新状态失败,可能已被并发更新", task.getUuid()); + return false; + } + log.info("单图任务{}回调处理完成,状态:{}", task.getUuid(), status); + return true; } catch (Exception e) { log.error("处理AI任务回调异常,UUID:{},异常信息:{}", form.getUuid(), e.getMessage(), e); return false; @@ -758,12 +810,12 @@ public class AiGenerationServiceImpl implements AiGenerationService { /** * 更新任务状态 */ - private boolean updateTaskStatus(String uuid, Integer status, String resultUrl) { + private boolean updateTaskStatus(String uuid, Integer status) { LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(MiniAiGenerationTask::getUuid, uuid) .eq(MiniAiGenerationTask::getDeleted, false) + .in(MiniAiGenerationTask::getStatus, 0, 3) // 只有生成中或超时状态允许更新,终态(成功/失败)不可覆盖 .set(MiniAiGenerationTask::getStatus, status) - .set(resultUrl != null, MiniAiGenerationTask::getResultResourceUrl, resultUrl) .set(MiniAiGenerationTask::getUpdateTime, new Date()) .set(MiniAiGenerationTask::getUpdateTimestamp, System.currentTimeMillis()); @@ -952,6 +1004,83 @@ public class AiGenerationServiceImpl implements AiGenerationService { return convert; } + /** + * 退还任务消耗的积分。refundUuid 是幂等唯一标识,adjustPoint 内部唯一索引兜底。 + */ + private void refundPoints(MiniAiGenerationTask task) { + if (StrUtil.isNotBlank(task.getRefundUuid())) { + log.info("任务{}已退过款(refundUuid={}),跳过", task.getUuid(), task.getRefundUuid()); + return; + } + if (task.getPointsConsumed() == null || task.getPointsConsumed() <= 0) { + log.warn("任务{}消耗积分为0或null,跳过退款", task.getUuid()); + return; + } + + String refundUuid = UUID.randomUUID().toString().replace("-", ""); + String ruleCode = getRuleCodeByTaskType(task.getType()); + + AdjustUserPointForm adjustForm = new AdjustUserPointForm(); + adjustForm.setUserId(task.getMiniUserId()); + adjustForm.setBizType(ruleCode); + adjustForm.setChangeAmount(task.getPointsConsumed()); + adjustForm.setBizId(refundUuid); + + pointManageService.adjustPoint(adjustForm); + + // adjustPoint 成功后回填 refundUuid,下次直接跳过 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(MiniAiGenerationTask::getUuid, task.getUuid()) + .set(MiniAiGenerationTask::getRefundUuid, refundUuid) + .set(MiniAiGenerationTask::getUpdateTime, new Date()) + .set(MiniAiGenerationTask::getUpdateTimestamp, System.currentTimeMillis()); + aiGenerationTaskMapper.update(null, updateWrapper); + + log.info("任务{}退款成功,退还{}积分,refundUuid={}", task.getUuid(), task.getPointsConsumed(), refundUuid); + } + + private String getRuleCodeByTaskType(String type) { + return switch (type) { + case "img_single" -> AI_GENERATE_SINGLE_IMAGE_RULE; + case "img_grid_4" -> AI_GENERATE_QUAD_GRID_RULE; + case "video" -> AI_GENERATE_VIDEO_RULE; + default -> { + log.error("未知任务类型: {}", type); + yield AI_GENERATE_UNKNOWN_TYPE; + } + }; + } + + @Override + public void handleTimeoutTasks() { + LocalDateTime timeoutThreshold = LocalDateTime.now().minusMinutes(taskTimeoutMinutes); + + List timeoutTasks = aiGenerationTaskMapper.selectList( + new LambdaQueryWrapper() + .in(MiniAiGenerationTask::getStatus, 0,3) // 0:生成中 3:超时 + .lt(MiniAiGenerationTask::getCreateTime, timeoutThreshold) + .eq(MiniAiGenerationTask::getDeleted, false)); + + if (timeoutTasks.isEmpty()) { + return; + } + + log.info("发现{}个超时任务,开始处理", timeoutTasks.size()); + + for (MiniAiGenerationTask task : timeoutTasks) { + // 先改状态,再退款; + if (!updateTaskStatus(task.getUuid(), 2)) { + log.warn("超时任务{}更新状态失败,可能已被其他流程处理", task.getUuid()); + continue; + } + try { + refundPoints(task); + } catch (Exception e) { + log.error("超时任务{}退款失败,需人工处理", task.getUuid(), e); + } + } + } + /** * 发送AI作品完成订阅消息通知 * @param userId 接收用户ID diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 4b44a9c..cf789ce 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -244,6 +244,9 @@ ai: default: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 + task: + # AI任务超时时间 单位分钟,默认300分钟 + timeout: 300 # 订阅模板配置 subscribe: From 481d72888dffa05af60762f54557322d0ca2f661 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 28 May 2026 13:46:51 +0800 Subject: [PATCH 14/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=9C=E5=93=81=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiGenerationController.java | 30 +++--- .../mini/model/form/AiTaskVisibilityForm.java | 23 ++++ .../model/query/DiscoveryPublicWorkQuery.java | 15 +++ .../mini/model/vo/DiscoveryPublicWorkVO.java | 38 +++++++ .../mini/service/AiGenerationService.java | 7 ++ .../service/impl/AiGenerationServiceImpl.java | 100 ++++++++++++++++++ src/main/resources/application-dev.yml | 2 +- 7 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AiTaskVisibilityForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/query/DiscoveryPublicWorkQuery.java create mode 100644 src/main/java/com/youlai/boot/mini/model/vo/DiscoveryPublicWorkVO.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 db2a621..7f4c75d 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -24,7 +24,9 @@ import org.springframework.security.access.prepost.PreAuthorize; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.boot.mini.model.query.AiTaskMediaQuery; +import com.youlai.boot.mini.model.query.DiscoveryPublicWorkQuery; import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; +import com.youlai.boot.mini.model.vo.DiscoveryPublicWorkVO; import cn.hutool.core.util.StrUtil; import java.util.List; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; @@ -160,17 +162,21 @@ public class AiGenerationController { return Result.success(); } - //需要在三个回调接口中控制状态,更新其他状态; 更新状态时需要判断 当前状态已是成功或失败时不能再更新状态,当状态是失败时需要退还补偿积分; - // mini_point_rule积分规则表,每个回调回退的积分是对应的规则扣减积分的负数值;mini_point_record积分记录表;mini_point_account用户积分账户 - - - // 当外部服务挂掉,不回调我的接口时,需要根据时间段判断,如果超过一定时间未回调,则更新状态为失败,并退还积分; - - //TODO 现在 用户生成内容时,需要增加一个参数,同意或不同意公开生成的作品内容 - //TODO mini 中增加 查询 用户公开生成作品的接口,考虑设计一下查询规则 满足平台前中后期运营需求 - - //TODO admin 中 后台管理中增加 查询用户生成任务历史的接口 - //TODO admin 中 编辑用户生成作品历史接口 - //TODO admin 中 增加 手动查询生成任务接口,手动跳转生成失败任务,或做个接口给用户手动刷新查看,需要注意状态流转,只有超时未完成任务可出现刷新按钮 + @Operation(summary = "更新任务可见范围") + @PutMapping("/my/task/visibility") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) + public Result updateTaskVisibility(@Valid @RequestBody AiTaskVisibilityForm form) { + Long userId = SecurityUtils.getUserId(); + aiGenerationService.updateTaskVisibility(userId, form); + return Result.success(); + } + @Operation(summary = "查询公开作品发现页") + @GetMapping("/discovery/feed") + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.LIST) + public Result> getPublicDiscoveryFeed(@Valid DiscoveryPublicWorkQuery query) { + IPage result = aiGenerationService.getPublicDiscoveryFeed(query); + return Result.success(result); + } + } diff --git a/src/main/java/com/youlai/boot/mini/model/form/AiTaskVisibilityForm.java b/src/main/java/com/youlai/boot/mini/model/form/AiTaskVisibilityForm.java new file mode 100644 index 0000000..8c150db --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AiTaskVisibilityForm.java @@ -0,0 +1,23 @@ +package com.youlai.boot.mini.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "更新任务可见范围表单") +public class AiTaskVisibilityForm { + + @NotBlank(message = "任务UUID不能为空") + @Schema(description = "任务UUID") + private String uuid; + + @NotBlank(message = "可见范围不能为空") + @Pattern(regexp = "^(public|private|friends)$", message = "可见范围只能是public/private/friends") + @Schema(description = "可见范围:public-公开,private-仅自己可见") + private String visibility; + +} diff --git a/src/main/java/com/youlai/boot/mini/model/query/DiscoveryPublicWorkQuery.java b/src/main/java/com/youlai/boot/mini/model/query/DiscoveryPublicWorkQuery.java new file mode 100644 index 0000000..3428ff3 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/query/DiscoveryPublicWorkQuery.java @@ -0,0 +1,15 @@ +package com.youlai.boot.mini.model.query; + +import com.youlai.boot.common.base.BaseQuery; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "发现页公开作品查询参数") +public class DiscoveryPublicWorkQuery extends BaseQuery { + + @Schema(description = "生成类型:img_single(单图), img_grid_4(四宫格), video(视频),不传查全部") + private String type; +} diff --git a/src/main/java/com/youlai/boot/mini/model/vo/DiscoveryPublicWorkVO.java b/src/main/java/com/youlai/boot/mini/model/vo/DiscoveryPublicWorkVO.java new file mode 100644 index 0000000..e11f0e1 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/vo/DiscoveryPublicWorkVO.java @@ -0,0 +1,38 @@ +package com.youlai.boot.mini.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.List; + +@Getter +@Setter +@Schema(description = "发现页公开作品VO") +public class DiscoveryPublicWorkVO { + + @Schema(description = "任务唯一标识") + private String uuid; + + @Schema(description = "生成类型:img_single(单图), img_grid_4(四宫格), video(视频)") + private String type; + + @Schema(description = "消耗积分") + private Integer pointsConsumed; + + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "生成内容列表") + private List generateContent; + + @Schema(description = "创作者用户UUID") + private String userUuid; + + @Schema(description = "创作者昵称") + private String nickname; + + @Schema(description = "创作者头像") + private String avatar; +} 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 22306b3..eeea838 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -7,10 +7,13 @@ import com.youlai.boot.mini.model.form.AiSingleImageGenerateForm; import com.youlai.boot.mini.model.form.AiVideoGenerateForm; import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.form.AiFourPanelCallbackForm; +import com.youlai.boot.mini.model.form.AiTaskVisibilityForm; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.boot.mini.model.query.AiTaskMediaQuery; +import com.youlai.boot.mini.model.query.DiscoveryPublicWorkQuery; import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; +import com.youlai.boot.mini.model.vo.DiscoveryPublicWorkVO; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; import org.springframework.web.multipart.MultipartFile; @@ -44,4 +47,8 @@ public interface AiGenerationService { void handleTimeoutTasks(); + IPage getPublicDiscoveryFeed(DiscoveryPublicWorkQuery query); + + void updateTaskVisibility(Long userId, AiTaskVisibilityForm form); + } 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 8921c81..f5d271e 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 @@ -26,10 +26,14 @@ import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; import com.youlai.boot.mini.model.form.*; import com.youlai.boot.mini.model.query.AiTaskMediaQuery; +import com.youlai.boot.mini.model.query.DiscoveryPublicWorkQuery; import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; import com.youlai.boot.mini.model.vo.AiVideoCallbackVO; +import com.youlai.boot.mini.model.vo.DiscoveryPublicWorkVO; import com.youlai.boot.mini.model.vo.MiniAiTaskMediaVO; import com.youlai.boot.mini.model.vo.UserUploadMediaVO; +import com.youlai.boot.system.mapper.UserMapper; +import com.youlai.boot.system.model.entity.SysUser; import com.youlai.boot.admin.model.form.AdjustUserPointForm; import com.youlai.boot.admin.service.PointManageService; import com.youlai.boot.mini.service.AiGenerationService; @@ -70,6 +74,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { private final WxSubscribeService wxSubscribeService; private final StringRedisTemplate stringRedisTemplate; private final PointManageService pointManageService; + private final UserMapper userMapper; // Redis key 存储微信订阅消息跳转版本 private static final String WX_MINI_PROGRAM_STATE_KEY = "wx:subscribe:mini_program_state"; @@ -912,6 +917,24 @@ public class AiGenerationServiceImpl implements AiGenerationService { return vo; } + @Override + public void updateTaskVisibility(Long userId, AiTaskVisibilityForm form) { + MiniAiGenerationTask task = aiGenerationTaskMapper.selectOne( + new LambdaQueryWrapper() + .eq(MiniAiGenerationTask::getUuid, form.getUuid()) + .eq(MiniAiGenerationTask::getMiniUserId, userId) + .eq(MiniAiGenerationTask::getDeleted, false)); + + if (task == null) { + throw new MsgException("任务不存在或无权限"); + } + + task.setVisibility(form.getVisibility()); + task.setUpdateTime(new Date()); + task.setUpdateTimestamp(System.currentTimeMillis()); + aiGenerationTaskMapper.updateById(task); + } + @Override public boolean deleteUploadMedia(Long userId, String uuid) { // 校验是否是当前用户的上传媒体 @@ -933,6 +956,83 @@ public class AiGenerationServiceImpl implements AiGenerationService { } + @Override + public IPage getPublicDiscoveryFeed(DiscoveryPublicWorkQuery query) { + // 1. 分页查询公开且已生成成功的任务 + LambdaQueryWrapper taskWrapper = new LambdaQueryWrapper<>(); + taskWrapper.eq(MiniAiGenerationTask::getVisibility, "public") + .eq(MiniAiGenerationTask::getStatus, 1) + .eq(MiniAiGenerationTask::getDeleted, false); + if (query.getType() != null) { + taskWrapper.eq(MiniAiGenerationTask::getType, query.getType()); + } + taskWrapper.orderByDesc(MiniAiGenerationTask::getCreateTimestamp); + + IPage taskPage = aiGenerationTaskMapper.selectPage( + new Page<>(query.getPageNum(), query.getPageSize()), taskWrapper); + + if (taskPage.getRecords().isEmpty()) { + return new Page<>(query.getPageNum(), query.getPageSize()); + } + + // 2. 批量查询媒体和用户 + List taskIds = taskPage.getRecords().stream() + .map(MiniAiGenerationTask::getId) + .toList(); + + List mediaList = aiTaskMediaMapper.selectList( + new LambdaQueryWrapper() + .in(MiniAiTaskMedia::getTaskId, taskIds) + .eq(MiniAiTaskMedia::getFileSource, "ai_generated") + .eq(MiniAiTaskMedia::getDeleted, false)); + + Map> mediaGroupMap = mediaList.stream() + .collect(Collectors.groupingBy( + MiniAiTaskMedia::getTaskId, + Collectors.mapping(media -> { + MiniAiTaskMediaVO vo = new MiniAiTaskMediaVO(); + vo.setUuid(media.getUuid()); + vo.setMediaType(media.getMediaType()); + vo.setSourceUrl(media.getSourceUrl()); + vo.setThumbnailUrl(media.getThumbnailUrl()); + vo.setWidth(media.getWidth()); + vo.setHeight(media.getHeight()); + vo.setDuration(media.getDuration()); + vo.setCreateTime(media.getCreateTime()); + return vo; + }, Collectors.toList()) + )); + + Set userIds = taskPage.getRecords().stream() + .map(MiniAiGenerationTask::getMiniUserId) + .collect(Collectors.toSet()); + + Map userMap = userMapper.selectList( + new LambdaQueryWrapper() + .in(SysUser::getId, userIds) + .eq(SysUser::getIsDeleted, 0)) + .stream() + .collect(Collectors.toMap(SysUser::getId, u -> u)); + + // 3. 组装VO + return taskPage.convert(task -> { + DiscoveryPublicWorkVO vo = new DiscoveryPublicWorkVO(); + vo.setUuid(task.getUuid()); + vo.setType(task.getType()); + vo.setPointsConsumed(task.getPointsConsumed()); + vo.setCreateTime(task.getCreateTime()); + vo.setGenerateContent(mediaGroupMap.getOrDefault(task.getId(), Collections.emptyList())); + + SysUser user = userMap.get(task.getMiniUserId()); + if (user != null) { + vo.setUserUuid(user.getUuid()); + vo.setNickname(user.getNickname()); + vo.setAvatar(user.getAvatar()); + } + return vo; + }); + } + @Override public IPage getMyAiGenerateHistory(AiTaskMediaQuery query, Long userId) { // 1. 分页查询当前用户的生成任务 diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index cf789ce..c45443c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -246,7 +246,7 @@ ai: video-model: doubao-seedance-2-0-260128 task: # AI任务超时时间 单位分钟,默认300分钟 - timeout: 300 + timeout-minutes: 300 # 订阅模板配置 subscribe: From 98e4be38269984ee0215bd4a23f6a7527c5996b5 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 28 May 2026 14:07:43 +0800 Subject: [PATCH 15/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../youlai/boot/mini/controller/AiGenerationController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 7f4c75d..5db1668 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -133,7 +133,7 @@ public class AiGenerationController { } @Operation(summary = "删除用户上传的图片/视频") - @DeleteMapping("/my/upload/delete") + @PostMapping("/my/upload/delete") @Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.DELETE) public Result deleteMyUpload(@RequestParam String uuid) { Long userId = SecurityUtils.getUserId(); @@ -163,7 +163,7 @@ public class AiGenerationController { } @Operation(summary = "更新任务可见范围") - @PutMapping("/my/task/visibility") + @PostMapping("/my/task/visibility") @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) public Result updateTaskVisibility(@Valid @RequestBody AiTaskVisibilityForm form) { Long userId = SecurityUtils.getUserId(); @@ -178,5 +178,5 @@ public class AiGenerationController { IPage result = aiGenerationService.getPublicDiscoveryFeed(query); return Result.success(result); } - + } From b4ca8a6e1251456cdd226b056a8e2c01072fde32 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Fri, 29 May 2026 17:22:06 +0800 Subject: [PATCH 16/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E4=BB=BB=E5=8A=A1=E7=8A=B6=E6=80=81=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../controller/AiGenerationController.java | 18 +++ .../mini/model/vo/AiGenerationTaskVO.java | 3 + .../mini/service/AiGenerationService.java | 2 + .../service/impl/AiGenerationServiceImpl.java | 149 +++++++++++++----- src/main/resources/application-dev.yml | 19 ++- 6 files changed, 149 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index c5abc96..84b9e58 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ docker/minio/config docker/xxljob/logs application-youlai.yml .claude +CLAUDE.md 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 5db1668..9210d1b 100644 --- a/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java +++ b/src/main/java/com/youlai/boot/mini/controller/AiGenerationController.java @@ -25,6 +25,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.boot.mini.model.query.AiTaskMediaQuery; import com.youlai.boot.mini.model.query.DiscoveryPublicWorkQuery; +import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; import com.youlai.boot.mini.model.vo.AiGenerationTaskVO; import com.youlai.boot.mini.model.vo.DiscoveryPublicWorkVO; import cn.hutool.core.util.StrUtil; @@ -179,4 +180,21 @@ public class AiGenerationController { return Result.success(result); } + @Operation(summary = "手动同步AI任务状态") + @PostMapping("/task/{taskUuid}/sync-status") + @RepeatSubmit(expire = 60) + @Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) + public Result syncTaskStatus(@PathVariable String taskUuid) { + Long userId = SecurityUtils.getUserId(); + MiniAiGenerationTask task = aiGenerationService.getTaskByUuid(taskUuid); + if (task == null) { + return Result.failed("任务不存在"); + } + if (!task.getMiniUserId().equals(userId)) { + return Result.failed("无权操作该任务"); + } + aiGenerationService.syncTaskStatus(taskUuid); + return Result.success(); + } + } diff --git a/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java b/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java index 1f7a502..fd686cd 100644 --- a/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java +++ b/src/main/java/com/youlai/boot/mini/model/vo/AiGenerationTaskVO.java @@ -31,6 +31,9 @@ public class AiGenerationTaskVO { @Schema(description = "消耗积分") private Integer pointsConsumed; + @Schema(description = "可见范围:public(公开) / private(仅自己)") + private String visibility; + @Schema(description = "创建时间") private Date createTime; 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 eeea838..d3974c1 100644 --- a/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java +++ b/src/main/java/com/youlai/boot/mini/service/AiGenerationService.java @@ -51,4 +51,6 @@ public interface AiGenerationService { void updateTaskVisibility(Long userId, AiTaskVisibilityForm form); + void syncTaskStatus(String taskUuid); + } 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 f5d271e..1072025 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 @@ -123,6 +123,19 @@ public class AiGenerationServiceImpl implements AiGenerationService { @Value("${ai.task.timeout-minutes:300}") private int taskTimeoutMinutes; + //AI任务状态查询接口 + @Value("${ai.status.single-image-url}") + private String aiSingleImageStatusUrl; + + @Value("${ai.status.four-panel-url}") + private String aiFourPanelStatusUrl; + + @Value("${ai.status.video-url}") + private String aiVideoStatusUrl; + + @Value("${ai.status.sync-min-interval-minutes:30}") + private int syncMinIntervalMinutes; + //AI单图生成积分规则编码 private static final String AI_GENERATE_SINGLE_IMAGE_RULE = "AI_GENERATE_SINGLE_IMAGE"; //AI四宫格生成积分规则编码 @@ -285,6 +298,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiGenerationTaskMapper.insert(task); + // 立即关联用户上传的参考文件到当前任务并软删除 + softDeleteUserUploads(userId, task.getId()); + // 组装第三方AI接口需要的参数 Map aiRequest = new HashMap<>(); aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); @@ -373,6 +389,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiGenerationTaskMapper.insert(task); + // 立即关联用户上传的参考文件到当前任务并软删除 + softDeleteUserUploads(userId, task.getId()); + // 组装第三方AI接口需要的参数 Map aiRequest = new HashMap<>(); aiRequest.put("model", form.getModel() == null ? aiDefaultImageModel : form.getModel()); @@ -461,6 +480,9 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiGenerationTaskMapper.insert(task); + // 立即关联用户上传的参考文件到当前任务并软删除 + softDeleteUserUploads(userId, task.getId()); + // 组装第三方接口参数 Map aiRequest = new HashMap<>(); aiRequest.put("model", form.getModel() == null ? aiDefaultVideoModel : form.getModel()); @@ -564,18 +586,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { .setCreateTime(new Date()); aiTaskMediaMapper.insert(media); - // 更新用户上传的参考文件:关联当前任务ID并软删除 - LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); - updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); - aiTaskMediaMapper.update(null, updateMediaWrapper); - // 同步发送订阅消息通知 sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } @@ -654,7 +664,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { media.setUuid(mediaUuid) .setTaskId(task.getId()) .setMiniUserId(task.getMiniUserId()) - .setFileSource("ai_generate") + .setFileSource("ai_generated") .setMediaType("image") .setSourceUrl(ossUrl) .setWidth(width) @@ -665,18 +675,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiTaskMediaMapper.insert(media); } - // 更新用户上传的参考文件:关联当前任务ID并软删除 - LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); - updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); - aiTaskMediaMapper.update(null, updateMediaWrapper); - // 同步发送订阅消息通知 sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } @@ -770,18 +768,6 @@ public class AiGenerationServiceImpl implements AiGenerationService { aiTaskMediaMapper.insert(media); } - // 更新用户上传的参考文件:关联当前任务ID并软删除 - LambdaUpdateWrapper updateMediaWrapper = new LambdaUpdateWrapper<>(); - updateMediaWrapper.eq(MiniAiTaskMedia::getMiniUserId, task.getMiniUserId()) - .isNull(MiniAiTaskMedia::getTaskId) - .eq(MiniAiTaskMedia::getFileSource, "user_upload") - .eq(MiniAiTaskMedia::getDeleted, false) - .set(MiniAiTaskMedia::getTaskId, task.getId()) - .set(MiniAiTaskMedia::getDeleted, true) - .set(MiniAiTaskMedia::getUpdateTime, new Date()) - .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); - aiTaskMediaMapper.update(null, updateMediaWrapper); - // 同步发送订阅消息通知 sendAiGenerateSuccessNotify(task.getMiniUserId(), subscribeTemplate, task.getId()); } @@ -805,6 +791,77 @@ public class AiGenerationServiceImpl implements AiGenerationService { } } + @Override + @Transactional(rollbackFor = Exception.class) + public void syncTaskStatus(String taskUuid) { + MiniAiGenerationTask task = getTaskByUuid(taskUuid); + if (task == null) { + throw new BusinessException("任务不存在"); + } + + // 终态不允许手动同步 + if (task.getStatus() != null && (task.getStatus() == 1 || task.getStatus() == 2)) { + throw new BusinessException("任务已完成或已失败,无需同步"); + } + + // 创建后需等待配置的时间才能手动同步 + long elapsedMinutes = (System.currentTimeMillis() - task.getCreateTimestamp()) / 60000; + if (elapsedMinutes < syncMinIntervalMinutes) { + throw new BusinessException("任务创建不足" + syncMinIntervalMinutes + "分钟,请" + (syncMinIntervalMinutes - elapsedMinutes) + "分钟后再试"); + } + + String statusUrl = buildAiStatusUrl(task); + log.info("手动同步任务状态,taskUuid:{},type:{},statusUrl:{}", taskUuid, task.getType(), statusUrl); + + String responseBody; + try { + HttpResponse response = HttpRequest.get(statusUrl).timeout(10000).execute(); + if (!response.isOk()) { + log.error("AI状态查询返回非200,taskUuid:{},httpStatus:{}", taskUuid, response.getStatus()); + throw new BusinessException("查询AI任务状态失败,HTTP状态码: " + response.getStatus()); + } + responseBody = response.body(); + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + log.error("调用AI状态查询接口失败,taskUuid:{}", taskUuid, e); + throw new BusinessException("查询AI任务状态失败: " + e.getMessage()); + } + + log.info("AI状态查询响应,taskUuid:{},body:{}", taskUuid, responseBody); + processAiStatusResponse(task.getType(), responseBody); + } + + private String buildAiStatusUrl(MiniAiGenerationTask task) { + switch (task.getType()) { + case "img_single": + return aiSingleImageStatusUrl + task.getUuid(); + case "img_grid_4": + return aiFourPanelStatusUrl + task.getUuid(); + case "video": + if (StrUtil.isBlank(task.getVideoTaskUuid())) { + throw new BusinessException("视频任务尚未获取到第三方任务ID"); + } + return aiVideoStatusUrl + task.getVideoTaskUuid(); + default: + throw new BusinessException("不支持的任务类型: " + task.getType()); + } + } + + private void processAiStatusResponse(String type, String responseBody) { + switch (type) { + case "img_single": + handleTaskCallback(JSONUtil.toBean(responseBody, AiSingleImageCallbackForm.class)); + break; + case "img_grid_4": + handleFourPanelCallback(JSONUtil.toBean(responseBody, AiFourPanelCallbackForm.class)); + break; + case "video": + handleVideoTaskCallback(JSONUtil.toBean(responseBody, AiVideoCallbackVO.class)); + break; + } + } + @Override public MiniAiGenerationTask getTaskByUuid(String uuid) { return aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper() @@ -1093,6 +1150,7 @@ public class AiGenerationServiceImpl implements AiGenerationService { AiGenerationTaskVO vo = new AiGenerationTaskVO(); // vo.setId(task.getId()); vo.setUuid(task.getUuid()); + vo.setVisibility(task.getVisibility()); vo.setType(task.getType()); vo.setStatus(task.getStatus()); vo.setPointsConsumed(task.getPointsConsumed()); @@ -1181,6 +1239,23 @@ public class AiGenerationServiceImpl implements AiGenerationService { } } + /** + * 将用户当前未关联任务的上传文件关联到指定任务并软删除。 + * 在创建任务时调用,避免任务生成中用户仍能看到旧上传文件。 + */ + private void softDeleteUserUploads(Long userId, Long taskId) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(MiniAiTaskMedia::getMiniUserId, userId) + .isNull(MiniAiTaskMedia::getTaskId) + .eq(MiniAiTaskMedia::getFileSource, "user_upload") + .eq(MiniAiTaskMedia::getDeleted, false) + .set(MiniAiTaskMedia::getTaskId, taskId) + .set(MiniAiTaskMedia::getDeleted, true) + .set(MiniAiTaskMedia::getUpdateTime, new Date()) + .set(MiniAiTaskMedia::getUpdateTimestamp, System.currentTimeMillis()); + aiTaskMediaMapper.update(null, updateWrapper); + } + /** * 发送AI作品完成订阅消息通知 * @param userId 接收用户ID diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c45443c..05d52d6 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -233,20 +233,27 @@ wx: # AIGeneration 配置 ai: generate: - single-image-server-url: http://192.168.31.93:8001/api/v1/photo-to-comic - four-panel-server-url: http://192.168.31.93:8001/api/v1/four-panel-comic - video-server-url: http://192.168.31.93:8001/api/v1/video/submit + single-image-server-url: http://127.0.0.1:8001/api/v1/photo-to-comic + four-panel-server-url: http://127.0.0.1:8001/api/v1/four-panel-comic + video-server-url: http://127.0.0.1:8001/api/v1/video/submit callback: - single-image-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/single-image/task/callback - four-panel-callback-url: http://192.168.31.197:30101/backend/api/v1/mini/ai/generation/four-panel/task/callback + single-image-callback-url: https://0401-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/single-image/task/callback + four-panel-callback-url: https://0401-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/four-panel/task/callback #需要内网穿透工具 由火山方舟 api 回调,ngrok http 30101 替换为内网穿透工具地址 - video-url: http://101.34.78.57:30101/backend/api/v1/mini/ai/generation/video/task/callback + video-url: https://0401-153-34-180-144.ngrok-free.app/backend/api/v1/mini/ai/generation/video/task/callback default: image-model: doubao-seedream-5-0-260128 video-model: doubao-seedance-2-0-260128 task: # AI任务超时时间 单位分钟,默认300分钟 timeout-minutes: 300 + # AI任务状态查询接口 + status: + single-image-url: http://127.0.0.1:8001/api/v1/photo-to-comic/status/ + four-panel-url: http://127.0.0.1:8001/api/v1/four-panel-comic/status/ + video-url: http://127.0.0.1:8001/api/v1/video/status/ + # 手动同步最小间隔(分钟),任务创建后至少等待此时间才能手动同步 + sync-min-interval-minutes: 30 # 订阅模板配置 subscribe: