16 changed files with 927 additions and 1 deletions
@ -0,0 +1,64 @@ |
|||||
|
package com.youlai.boot.mini.controller; |
||||
|
|
||||
|
import com.youlai.boot.common.annotation.Log; |
||||
|
import com.youlai.boot.common.annotation.RepeatSubmit; |
||||
|
import com.youlai.boot.common.enums.ActionTypeEnum; |
||||
|
import com.youlai.boot.common.enums.LogModuleEnum; |
||||
|
import com.youlai.boot.common.result.Result; |
||||
|
import com.youlai.boot.framework.security.util.SecurityUtils; |
||||
|
import com.youlai.boot.mini.model.dto.PhotoToComicRequest; |
||||
|
import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; |
||||
|
import com.youlai.boot.mini.service.AiGenerationService; |
||||
|
import io.swagger.v3.oas.annotations.Operation; |
||||
|
import io.swagger.v3.oas.annotations.Parameter; |
||||
|
import io.swagger.v3.oas.annotations.enums.ParameterIn; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import io.swagger.v3.oas.annotations.tags.Tag; |
||||
|
import jakarta.validation.Valid; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.http.MediaType; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Tag(name = "AI生成图片视频相关接口") |
||||
|
@RestController |
||||
|
@RequestMapping("/api/v1/mini/ai/generation") |
||||
|
@RequiredArgsConstructor |
||||
|
@Valid |
||||
|
public class AiGenerationController { |
||||
|
|
||||
|
private final AiGenerationService aiGenerationService; |
||||
|
|
||||
|
@Operation(summary = "上传AI生成参考文件", operationId = "AiReferenceSaveFile") |
||||
|
@PostMapping(value = "/upload-reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) |
||||
|
@RepeatSubmit |
||||
|
@Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.INSERT) |
||||
|
public Result<List<String>> uploadReferenceFile( |
||||
|
@RequestPart(name = "images", required = false) List<MultipartFile> images, |
||||
|
@RequestPart(name = "videos", required = false) List<MultipartFile> videos |
||||
|
) { |
||||
|
Long userId = SecurityUtils.getUserId(); |
||||
|
List<String> urlList = aiGenerationService.uploadReferenceFile(images, videos, userId); |
||||
|
return Result.success(urlList); |
||||
|
} |
||||
|
|
||||
|
@Operation(summary = "提交单图漫画生成任务") |
||||
|
@PostMapping("/generate-single-image") |
||||
|
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) |
||||
|
public Result<String> generateSingleImage(@Valid @RequestBody PhotoToComicRequest request) { |
||||
|
Long userId = SecurityUtils.getUserId(); |
||||
|
String taskUuid = aiGenerationService.createAndGenerateImage(request, userId); |
||||
|
return Result.success(taskUuid); |
||||
|
} |
||||
|
|
||||
|
@Operation(summary = "AI生成任务回调接口") |
||||
|
@PostMapping("/task/callback") |
||||
|
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) |
||||
|
public Result<Boolean> taskCallback(@Valid @RequestBody AiTaskCallbackVO vo) { |
||||
|
boolean success = aiGenerationService.handleTaskCallback(vo.getUuid(), vo.getStatus(), vo.getResultUrl()); |
||||
|
return Result.success(success); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
package com.youlai.boot.mini.mapper; |
||||
|
|
||||
|
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; |
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
|
||||
|
/** |
||||
|
* AI生成任务统一表 Mapper 接口 |
||||
|
* |
||||
|
* @author jwy |
||||
|
* @since |
||||
|
*/ |
||||
|
public interface MiniAiGenerationTaskMapper extends BaseMapper<MiniAiGenerationTask> { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
package com.youlai.boot.mini.mapper; |
||||
|
|
||||
|
import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; |
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
|
||||
|
/** |
||||
|
* AI任务媒体文件表 Mapper 接口 |
||||
|
* |
||||
|
* @author jwy |
||||
|
* @since |
||||
|
*/ |
||||
|
public interface MiniAiTaskMediaMapper extends BaseMapper<MiniAiTaskMedia> { |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
package com.youlai.boot.mini.model.dto; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* AI生成结果数据DTO |
||||
|
* |
||||
|
* @author youlai |
||||
|
*/ |
||||
|
@Data |
||||
|
public class GenerationData { |
||||
|
/** |
||||
|
* 使用的模型ID |
||||
|
*/ |
||||
|
private String model; |
||||
|
|
||||
|
/** |
||||
|
* 生成的图片列表 |
||||
|
*/ |
||||
|
private List<ImageData> data; |
||||
|
|
||||
|
/** |
||||
|
* 错误信息,无错误则为null |
||||
|
*/ |
||||
|
private Object error; |
||||
|
|
||||
|
/** |
||||
|
* Token使用统计 |
||||
|
*/ |
||||
|
private Object usage; |
||||
|
|
||||
|
/** |
||||
|
* 创建时间戳 |
||||
|
*/ |
||||
|
private Long created_at; |
||||
|
|
||||
|
/** |
||||
|
* 使用的工具 |
||||
|
*/ |
||||
|
private String tool; |
||||
|
|
||||
|
/** |
||||
|
* 创建时间戳 |
||||
|
*/ |
||||
|
private Long created; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
package com.youlai.boot.mini.model.dto; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 照片漫画化请求DTO |
||||
|
* |
||||
|
* @author youlai |
||||
|
*/ |
||||
|
@Data |
||||
|
public class PhotoToComicRequest { |
||||
|
/** |
||||
|
* 使用的模型ID |
||||
|
*/ |
||||
|
private String model; |
||||
|
|
||||
|
/** |
||||
|
* 参考图片URL数组 |
||||
|
*/ |
||||
|
private List<String> reference_image; |
||||
|
|
||||
|
/** |
||||
|
* 画风: 1=治愈系水彩, 2=Q版卡通, 3=日式漫画, 4=吉卜力风, 5=3D毛绒, 6=搞笑夸张 |
||||
|
*/ |
||||
|
private Integer style = 1; |
||||
|
|
||||
|
/** |
||||
|
* 比例: 1=1:1, 2=2:3, 3=3:4, 4=4:3, 5=5:4 |
||||
|
*/ |
||||
|
private Integer ratio = 1; |
||||
|
|
||||
|
/** |
||||
|
* 图片尺寸: 1=2K, 2=4K, 3=8K |
||||
|
*/ |
||||
|
private Integer img_size = 1; |
||||
|
|
||||
|
/** |
||||
|
* 图片格式: 1=png, 2=jpeg |
||||
|
*/ |
||||
|
private Integer img_type = 1; |
||||
|
|
||||
|
/** |
||||
|
* 宠物物种,如:cat, dog |
||||
|
*/ |
||||
|
private String species; |
||||
|
|
||||
|
/** |
||||
|
* 宠物品种,如:British Shorthair, Corgi |
||||
|
*/ |
||||
|
private String breed; |
||||
|
|
||||
|
/** |
||||
|
* 宠物毛色,如:blue-gray, orange |
||||
|
*/ |
||||
|
private String color; |
||||
|
|
||||
|
/** |
||||
|
* 眼睛颜色,如:amber, blue |
||||
|
*/ |
||||
|
private String eye_color; |
||||
|
|
||||
|
/** |
||||
|
* 体型,如:round chubby, slim and elegant |
||||
|
*/ |
||||
|
private String body_type; |
||||
|
|
||||
|
/** |
||||
|
* 特殊特征,如:flat face, thick coat |
||||
|
*/ |
||||
|
private String distinctive_features = ""; |
||||
|
|
||||
|
/** |
||||
|
* 场景描述,用户自定义场景 |
||||
|
*/ |
||||
|
private String scene_description; |
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.youlai.boot.mini.service; |
||||
|
|
||||
|
import com.youlai.boot.mini.model.dto.PhotoToComicRequest; |
||||
|
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface AiGenerationService { |
||||
|
|
||||
|
List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId); |
||||
|
|
||||
|
String createAndGenerateImage(PhotoToComicRequest request, Long userId); |
||||
|
|
||||
|
boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl); |
||||
|
|
||||
|
MiniAiGenerationTask getTaskByUuid(String uuid); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,397 @@ |
|||||
|
package com.youlai.boot.mini.service.impl; |
||||
|
|
||||
|
import cn.hutool.core.io.FileUtil; |
||||
|
import cn.hutool.core.util.IdUtil; |
||||
|
import cn.hutool.http.HttpRequest; |
||||
|
import cn.hutool.http.HttpResponse; |
||||
|
import cn.hutool.http.HttpUtil; |
||||
|
import cn.hutool.json.JSONUtil; |
||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
||||
|
import com.youlai.boot.common.exception.BusinessException; |
||||
|
import com.youlai.boot.common.util.FileUtils; |
||||
|
import com.youlai.boot.common.util.JavaVCUtils; |
||||
|
import com.youlai.boot.common.util.RandomNumberUtils; |
||||
|
import com.youlai.boot.file.model.FileInfo; |
||||
|
import com.youlai.boot.file.service.FileService; |
||||
|
import com.youlai.boot.file.service.impl.AliyunFileService; |
||||
|
import com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper; |
||||
|
import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; |
||||
|
import com.youlai.boot.mini.model.dto.GenerateResponse; |
||||
|
import com.youlai.boot.mini.model.dto.ImageData; |
||||
|
import com.youlai.boot.mini.model.dto.PhotoToComicRequest; |
||||
|
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; |
||||
|
import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; |
||||
|
import com.youlai.boot.mini.service.AiGenerationService; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import javax.imageio.ImageIO; |
||||
|
import java.awt.image.BufferedImage; |
||||
|
import java.io.File; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.CompletableFuture; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class AiGenerationServiceImpl implements AiGenerationService { |
||||
|
|
||||
|
private final MiniAiGenerationTaskMapper aiGenerationTaskMapper; |
||||
|
private final MiniAiTaskMediaMapper aiTaskMediaMapper; |
||||
|
private final FileService fileService; |
||||
|
private final AliyunFileService aliyunFileService; |
||||
|
|
||||
|
/** |
||||
|
* AI生成服务地址 |
||||
|
*/ |
||||
|
private static final String AI_GENERATE_URL = "http://127.0.0.1:8001/api/v1/photo-to-comic"; |
||||
|
|
||||
|
/** |
||||
|
* OSS存储目录配置 |
||||
|
*/ |
||||
|
private static final String OSS_IMAGE_DIR = "ai/image/"; |
||||
|
private static final String OSS_VIDEO_DIR = "ai/video/"; |
||||
|
private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/"; |
||||
|
|
||||
|
@Override |
||||
|
public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) { |
||||
|
// 计算本次上传的文件总数
|
||||
|
int uploadCount = (images != null ? images.size() : 0) + (videos != null ? videos.size() : 0); |
||||
|
if (uploadCount == 0) { |
||||
|
throw new BusinessException("请选择要上传的文件"); |
||||
|
} |
||||
|
|
||||
|
// 校验用户当前未关联任务的上传文件数量,最多5个
|
||||
|
Long existCount = aiTaskMediaMapper.selectCount(new LambdaQueryWrapper<MiniAiTaskMedia>() |
||||
|
.eq(MiniAiTaskMedia::getCreateBy, userId) |
||||
|
.isNull(MiniAiTaskMedia::getTaskId) |
||||
|
.eq(MiniAiTaskMedia::getFileSource, "user_upload") |
||||
|
.eq(MiniAiTaskMedia::getDeleted, false)); |
||||
|
|
||||
|
if (existCount + uploadCount > 5) { |
||||
|
throw new BusinessException("最多只能上传5个待生成的参考文件"); |
||||
|
} |
||||
|
|
||||
|
List<String> urlList = new ArrayList<>(); |
||||
|
long timestamp = System.currentTimeMillis(); |
||||
|
|
||||
|
// 处理图片
|
||||
|
if (images != null && !images.isEmpty()) { |
||||
|
for (MultipartFile image : images) { |
||||
|
try { |
||||
|
String fileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
||||
|
String ext = FileUtil.extName(image.getOriginalFilename()); |
||||
|
String objectName = OSS_IMAGE_DIR + fileName + "." + ext; |
||||
|
String url = aliyunFileService.uploadFile(objectName, image.getInputStream()); |
||||
|
|
||||
|
// 获取图片信息
|
||||
|
BufferedImage imageInfo = ImageIO.read(image.getInputStream()); |
||||
|
|
||||
|
// 保存媒体记录
|
||||
|
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
||||
|
String uuid = UUID.randomUUID().toString().replace("-", ""); |
||||
|
media.setUuid(uuid) |
||||
|
.setFileSource("user_upload")//TODO 待整理成枚举,参考项目已有内容
|
||||
|
.setMediaType("image") |
||||
|
.setSourceUrl(url) |
||||
|
.setStorageKey(objectName) |
||||
|
.setWidth(imageInfo.getWidth()) |
||||
|
.setHeight(imageInfo.getHeight()) |
||||
|
.setCreateBy(userId) |
||||
|
.setCreateTimestamp(timestamp) |
||||
|
.setCreateTime(new Date(timestamp)); |
||||
|
|
||||
|
int result = aiTaskMediaMapper.insert(media); |
||||
|
if (result > 0) { |
||||
|
urlList.add(url); |
||||
|
} |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("image upload failed", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理视频
|
||||
|
if (videos != null && !videos.isEmpty()) { |
||||
|
String tmpPath = System.getProperty("user.dir") + "/tmp"; |
||||
|
// 确保临时目录存在
|
||||
|
File tmpDir = new File(tmpPath); |
||||
|
if (!tmpDir.exists()) { |
||||
|
tmpDir.mkdirs(); |
||||
|
} |
||||
|
|
||||
|
for (MultipartFile video : videos) { |
||||
|
try { |
||||
|
String fileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
||||
|
String ext = FileUtil.extName(video.getOriginalFilename()); |
||||
|
String objectName = OSS_VIDEO_DIR + fileName + "." + ext; |
||||
|
String url = aliyunFileService.uploadFile(objectName, video.getInputStream()); |
||||
|
|
||||
|
// 保存媒体记录
|
||||
|
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
||||
|
String uuid = UUID.randomUUID().toString().replace("-", ""); |
||||
|
|
||||
|
media.setUuid(uuid) |
||||
|
.setFileSource("user_upload") |
||||
|
.setMediaType("video") |
||||
|
.setSourceUrl(url) |
||||
|
.setStorageKey(objectName) |
||||
|
.setCreateBy(userId) |
||||
|
.setCreateTimestamp(timestamp) |
||||
|
.setCreateTime(new Date(timestamp)); |
||||
|
|
||||
|
// 获取视频时长
|
||||
|
FileUtils.saveFile(video, tmpPath, fileName); |
||||
|
String videoPath = tmpPath + File.separator + fileName; |
||||
|
double duration = JavaVCUtils.getVideoDuration(videoPath); |
||||
|
media.setDuration((int) Math.ceil(duration)); |
||||
|
|
||||
|
// 生成并上传视频缩略图
|
||||
|
BufferedImage thumbnail = JavaVCUtils.getVideoThumbnail(videoPath, 1); |
||||
|
String thumbnailFileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
||||
|
String thumbnailObjectName = OSS_THUMBNAIL_DIR + thumbnailFileName + ".png"; |
||||
|
String thumbnailUrl = aliyunFileService.uploadFile(thumbnailObjectName, |
||||
|
FileUtils.bufferedImageToInputStream(thumbnail, "png")); |
||||
|
media.setThumbnailUrl(thumbnailUrl); |
||||
|
|
||||
|
int result = aiTaskMediaMapper.insert(media); |
||||
|
if (result > 0) { |
||||
|
urlList.add(url); |
||||
|
} |
||||
|
|
||||
|
// 删除临时文件
|
||||
|
FileUtils.delete(videoPath); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("video upload failed", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return urlList; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String createAndGenerateImage(PhotoToComicRequest request, Long userId) { |
||||
|
// 创建生成任务
|
||||
|
MiniAiGenerationTask task = new MiniAiGenerationTask(); |
||||
|
String taskUuid = UUID.randomUUID().toString().replace("-", ""); |
||||
|
Date now = new Date(); |
||||
|
long timestamp = System.currentTimeMillis(); |
||||
|
|
||||
|
task.setUuid(taskUuid) |
||||
|
.setMiniUserId(userId) |
||||
|
.setType("img_single") |
||||
|
.setGenerateParams(JSONUtil.toJsonStr(request)) |
||||
|
.setPointsConsumed(1) // 默认消耗1积分
|
||||
|
.setStatus((byte) 0) |
||||
|
.setCreateBy(userId) |
||||
|
.setCreateTime(now) |
||||
|
.setCreateTimestamp(timestamp) |
||||
|
.setUpdateTime(now) |
||||
|
.setUpdateTimestamp(timestamp) |
||||
|
.setDeleted(false); |
||||
|
|
||||
|
aiGenerationTaskMapper.insert(task); |
||||
|
|
||||
|
// 异步调用AI生成接口
|
||||
|
CompletableFuture.runAsync(() -> { |
||||
|
try { |
||||
|
log.info("开始调用AI生成接口,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(request)); |
||||
|
|
||||
|
// 发送HTTP请求
|
||||
|
HttpResponse response = HttpRequest.post(AI_GENERATE_URL) |
||||
|
.header("Content-Type", "application/json") |
||||
|
.body(JSONUtil.toJsonStr(request)) |
||||
|
.timeout(30000) // 超时30秒
|
||||
|
.execute(); |
||||
|
|
||||
|
if (!response.isOk()) { |
||||
|
log.error("AI生成接口调用失败,状态码:{},响应内容:{}", response.getStatus(), response.body()); |
||||
|
updateTaskStatus(taskUuid, (byte) 2, null); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 解析响应
|
||||
|
String responseBody = response.body(); |
||||
|
GenerateResponse generateResponse = JSONUtil.toBean(responseBody, GenerateResponse.class); |
||||
|
|
||||
|
if (generateResponse.getCode() != 0 || generateResponse.getData() == null || generateResponse.getData().getData().isEmpty()) { |
||||
|
log.error("AI生成接口返回错误,响应内容:{}", responseBody); |
||||
|
updateTaskStatus(taskUuid, (byte) 2, null); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 保存生成的图片
|
||||
|
ImageData imageData = generateResponse.getData().getData().get(0); |
||||
|
String externalResultUrl = imageData.getUrl(); |
||||
|
|
||||
|
// 下载外部图片到我们的OSS
|
||||
|
String ossUrl = downloadExternalUrlToOss(externalResultUrl); |
||||
|
|
||||
|
// 保存媒体记录
|
||||
|
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
||||
|
String mediaUuid = UUID.randomUUID().toString().replace("-", ""); |
||||
|
|
||||
|
media.setUuid(mediaUuid) |
||||
|
.setTaskId(task.getId()) |
||||
|
.setFileSource("ai_generated") |
||||
|
.setMediaType("image") |
||||
|
.setSourceUrl(ossUrl) |
||||
|
.setCreateBy(userId) |
||||
|
.setCreateTime(new Date()) |
||||
|
.setCreateTimestamp(System.currentTimeMillis()) |
||||
|
.setUpdateTime(new Date()) |
||||
|
.setUpdateTimestamp(System.currentTimeMillis()) |
||||
|
.setDeleted(false); |
||||
|
|
||||
|
aiTaskMediaMapper.insert(media); |
||||
|
|
||||
|
// 更新任务状态为成功
|
||||
|
updateTaskStatus(taskUuid, (byte) 1, ossUrl); |
||||
|
log.info("AI生成任务完成,任务UUID:{},结果URL:{}", taskUuid, ossUrl); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("AI生成任务异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); |
||||
|
updateTaskStatus(taskUuid, (byte) 2, null); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return taskUuid; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl) { |
||||
|
try { |
||||
|
// 查询任务是否存在
|
||||
|
MiniAiGenerationTask task = getTaskByUuid(taskUuid); |
||||
|
if (task == null) { |
||||
|
log.error("回调任务不存在,UUID:{}", taskUuid); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 如果生成成功,下载外部URL到OSS
|
||||
|
String ossUrl = null; |
||||
|
if (status == 1 && externalResultUrl != null) { |
||||
|
ossUrl = downloadExternalUrlToOss(externalResultUrl); |
||||
|
|
||||
|
// 保存生成的媒体记录
|
||||
|
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
||||
|
String mediaUuid = UUID.randomUUID().toString().replace("-", ""); |
||||
|
|
||||
|
media.setUuid(mediaUuid) |
||||
|
.setTaskId(task.getId()) |
||||
|
.setFileSource("ai_generated") |
||||
|
.setMediaType("image") |
||||
|
.setSourceUrl(ossUrl) |
||||
|
.setCreateBy(task.getCreateBy()) |
||||
|
.setCreateTime(new Date()) |
||||
|
.setCreateTimestamp(System.currentTimeMillis()) |
||||
|
.setUpdateTime(new Date()) |
||||
|
.setUpdateTimestamp(System.currentTimeMillis()) |
||||
|
.setDeleted(false); |
||||
|
|
||||
|
aiTaskMediaMapper.insert(media); |
||||
|
} |
||||
|
|
||||
|
// 更新任务状态
|
||||
|
return updateTaskStatus(taskUuid, status, ossUrl); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("处理AI任务回调异常,UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public MiniAiGenerationTask getTaskByUuid(String uuid) { |
||||
|
return aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper<MiniAiGenerationTask>() |
||||
|
.eq(MiniAiGenerationTask::getUuid, uuid) |
||||
|
.eq(MiniAiGenerationTask::getDeleted, false)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新任务状态 |
||||
|
*/ |
||||
|
private boolean updateTaskStatus(String uuid, Byte status, String resultUrl) { |
||||
|
LambdaUpdateWrapper<MiniAiGenerationTask> updateWrapper = new LambdaUpdateWrapper<>(); |
||||
|
updateWrapper.eq(MiniAiGenerationTask::getUuid, uuid) |
||||
|
.eq(MiniAiGenerationTask::getDeleted, false) |
||||
|
.set(MiniAiGenerationTask::getStatus, status) |
||||
|
.set(resultUrl != null, MiniAiGenerationTask::getResultResourceUrl, resultUrl) |
||||
|
.set(MiniAiGenerationTask::getUpdateTime, new Date()) |
||||
|
.set(MiniAiGenerationTask::getUpdateTimestamp, System.currentTimeMillis()); |
||||
|
|
||||
|
return aiGenerationTaskMapper.update(null, updateWrapper) > 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 下载外部URL到OSS,返回OSS访问地址 |
||||
|
*/ |
||||
|
private String downloadExternalUrlToOss(String externalUrl) { |
||||
|
try { |
||||
|
// 下载文件并获取输入流
|
||||
|
byte[] fileBytes = HttpUtil.downloadBytes(externalUrl); |
||||
|
String fileName = IdUtil.fastSimpleUUID() + "." + FileUtil.extName(externalUrl); |
||||
|
|
||||
|
// 创建临时的MultipartFile包装类
|
||||
|
MultipartFile multipartFile = new MultipartFile() { |
||||
|
@Override |
||||
|
public String getName() { |
||||
|
return "file"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getOriginalFilename() { |
||||
|
return fileName; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getContentType() { |
||||
|
return "image/png"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isEmpty() { |
||||
|
return fileBytes == null || fileBytes.length == 0; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public long getSize() { |
||||
|
return fileBytes.length; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public byte[] getBytes() { |
||||
|
return fileBytes; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public java.io.InputStream getInputStream() { |
||||
|
return new java.io.ByteArrayInputStream(fileBytes); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void transferTo(java.io.File dest) throws java.io.IOException, IllegalStateException { |
||||
|
FileUtil.writeBytes(fileBytes, dest); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 上传到OSS
|
||||
|
FileInfo fileInfo = fileService.uploadFile(multipartFile); |
||||
|
return fileInfo.getUrl(); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("下载外部URL到OSS失败,URL:{},异常信息:{}", externalUrl, e.getMessage(), e); |
||||
|
throw new BusinessException("下载生成结果失败"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE mapper |
||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
|
||||
|
<mapper namespace="com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper"> |
||||
|
|
||||
|
|
||||
|
</mapper> |
||||
@ -0,0 +1,9 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE mapper |
||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
|
||||
|
<mapper namespace="com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper"> |
||||
|
|
||||
|
|
||||
|
</mapper> |
||||
Loading…
Reference in new issue