tryon first commit
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -42,6 +42,12 @@
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--minio-->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>8.0.3</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- MyBatis-Plus -->
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.aida.lanecarford.common.response;
|
||||
|
||||
/**
|
||||
* @ClassName ResultEnum
|
||||
* @Description 响应结果枚举
|
||||
* @Author dwjian
|
||||
* @Date 2019/9/8 21:58
|
||||
*/
|
||||
public enum ResultEnum {
|
||||
|
||||
SUCCESS(true, 0, "SUCCESS", "操作成功"),
|
||||
FAIL(false, -1, "FAIL", "操作失败"),
|
||||
ERROR(false, -1, "System error", "系统错误"),
|
||||
PARAMETER_ERROR(false, -2, "Parameter error", "参数错误"),
|
||||
|
||||
NO_LOGIN(false, -100, "User not logged in", "用户未登录"),
|
||||
NO_PERMISSION(false, -200, "No permission", "无权限访问"),
|
||||
ACCOUNT_LOCK(false, -300, "Account locked", "账户已锁定"),
|
||||
|
||||
PROMPT(false, 1, "Prompt", "提示"),
|
||||
WARNING(false, 2, "Warning", "警告"),
|
||||
|
||||
;
|
||||
private int code;
|
||||
private String msg; // 英文消息,返回给前端
|
||||
private String msgCn; // 中文消息,用于日志
|
||||
private boolean isOK;
|
||||
|
||||
ResultEnum(boolean isOK, int code, String msg, String msgCn) {
|
||||
this.isOK = isOK;
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.msgCn = msgCn;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public String getMsgCn() {
|
||||
return msgCn;
|
||||
}
|
||||
|
||||
public void setMsgCn(String msgCn) {
|
||||
this.msgCn = msgCn;
|
||||
}
|
||||
|
||||
public boolean isOK() {
|
||||
return isOK;
|
||||
}
|
||||
|
||||
public void setOK(boolean OK) {
|
||||
isOK = OK;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
package com.aida.lanecarford.controller;
|
||||
|
||||
import com.aida.lanecarford.common.ApiResponse;
|
||||
import com.aida.lanecarford.entity.TryOnEffect;
|
||||
import com.aida.lanecarford.service.TryOnEffectService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -14,8 +22,18 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RestController
|
||||
@RequestMapping("/api/try-on-effects")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "试穿效果管理", description = "试穿效果生成和管理相关API")
|
||||
public class TryOnEffectController {
|
||||
|
||||
private final TryOnEffectService tryOnEffectService;
|
||||
|
||||
@Operation(summary = "生成试穿效果", description = "根据顾客照片和模特照片生成试穿效果")
|
||||
@PostMapping("/generate")
|
||||
public ApiResponse<String> generateTryOnEffect(
|
||||
@Parameter(description = "试穿效果请求参数", required = true)
|
||||
@Valid @RequestBody TryOnEffect tryOnEffectDto) {
|
||||
String taskId = tryOnEffectService.generateTryOnEffect(tryOnEffectDto);
|
||||
return ApiResponse.success(taskId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -64,4 +64,10 @@ public class CustomerPhoto {
|
||||
*/
|
||||
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -60,10 +60,10 @@ public class Style {
|
||||
private String pythonRequestId;
|
||||
|
||||
/**
|
||||
* 生成状态(0-处理中,1-已完成,2-失败)
|
||||
* 生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)
|
||||
*/
|
||||
@TableField("generation_status")
|
||||
private Integer generationStatus;
|
||||
private String generationStatus;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
|
||||
@@ -41,6 +41,12 @@ public class TryOnEffect {
|
||||
@TableField("visit_record_id")
|
||||
private Long visitRecordId;
|
||||
|
||||
/**
|
||||
* 风格ID
|
||||
*/
|
||||
@TableField("style_id")
|
||||
private Long styleId;
|
||||
|
||||
/**
|
||||
* 顾客照片ID
|
||||
*/
|
||||
@@ -53,12 +59,6 @@ public class TryOnEffect {
|
||||
@TableField("model_photo_id")
|
||||
private Long modelPhotoId;
|
||||
|
||||
/**
|
||||
* 试穿效果图URL
|
||||
*/
|
||||
@TableField("try_on_image_url")
|
||||
private String tryOnImageUrl;
|
||||
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
@@ -66,16 +66,34 @@ public class TryOnEffect {
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* Python请求ID
|
||||
* 原试穿效果ID,当is_regenerated为1时才会有值
|
||||
*/
|
||||
@TableField("python_request_id")
|
||||
private String pythonRequestId;
|
||||
@TableField("original_try_on_id")
|
||||
private Long originalTryOnId;
|
||||
|
||||
/**
|
||||
* 生成状态(0-处理中,1-已完成,2-失败)
|
||||
* 是否由生成结果重新生成(0-否,1-是)
|
||||
*/
|
||||
@TableField("is_regenerated")
|
||||
private Integer isRegenerated;
|
||||
|
||||
/**
|
||||
* 试穿结果图片URL
|
||||
*/
|
||||
@TableField("result_image_url")
|
||||
private String resultImageUrl;
|
||||
|
||||
/**
|
||||
* 请求ID
|
||||
*/
|
||||
@TableField("request_id")
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)
|
||||
*/
|
||||
@TableField("generation_status")
|
||||
private Integer generationStatus;
|
||||
private String generationStatus;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
@@ -84,17 +102,11 @@ public class TryOnEffect {
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 是否收藏(0-否,1-是)
|
||||
* 是否喜欢的最终造型(0-否,1-是)
|
||||
*/
|
||||
@TableField("is_favorite")
|
||||
private Integer isFavorite;
|
||||
|
||||
/**
|
||||
* 原始试穿效果ID(用于重新生成)
|
||||
*/
|
||||
@TableField("original_try_on_id")
|
||||
private Long originalTryOnId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
|
||||
@@ -1,121 +1,126 @@
|
||||
package com.aida.lanecarford.exception;
|
||||
|
||||
import com.aida.lanecarford.common.response.ResultEnum;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 业务异常类
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private String code;
|
||||
private String message;
|
||||
private Integer code;
|
||||
private String msg; // 英文消息,返回给前端
|
||||
private String msgCn; // 中文消息,用于日志
|
||||
|
||||
public BusinessException(String code, String message) {
|
||||
super(message);
|
||||
public BusinessException(String msg) {
|
||||
this.code = ResultEnum.FAIL.getCode();
|
||||
this.msg = msg;
|
||||
this.msgCn = msg;
|
||||
}
|
||||
|
||||
public BusinessException(String msg, Integer code) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.msg = msg;
|
||||
this.msgCn = msg;
|
||||
}
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = "BUSINESS_ERROR";
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BusinessException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
public BusinessException(String msg, String msgCn, Integer code) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.msg = msg;
|
||||
this.msgCn = msgCn;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
public BusinessException(ResultEnum resultEnum) {
|
||||
this.code = resultEnum.getCode();
|
||||
this.msg = resultEnum.getMsg();
|
||||
this.msgCn = resultEnum.getMsgCn();
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
public BusinessException(ResultEnum resultEnum, String customMessage, String customMessageCn) {
|
||||
this.code = resultEnum.getCode();
|
||||
this.msg = customMessage;
|
||||
this.msgCn = customMessageCn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
/**
|
||||
* 创建原始试穿ID必填异常
|
||||
*/
|
||||
public static BusinessException originalTryOnIdRequired() {
|
||||
return new BusinessException(ResultEnum.PARAMETER_ERROR, "Original try-on ID is required", "原始试穿ID不能为空");
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
/**
|
||||
* 创建风格ID必填异常
|
||||
*/
|
||||
public static BusinessException styleIdRequired() {
|
||||
return new BusinessException(ResultEnum.PARAMETER_ERROR, "Style ID is required", "风格ID不能为空");
|
||||
}
|
||||
|
||||
// 常用的业务异常静态方法
|
||||
public static BusinessException customerNotFound() {
|
||||
return new BusinessException("CUSTOMER_NOT_FOUND", "客户不存在");
|
||||
/**
|
||||
* 创建通用参数为空异常
|
||||
*/
|
||||
public static BusinessException parameterRequired(String parameterName) {
|
||||
return new BusinessException(ResultEnum.PARAMETER_ERROR,
|
||||
parameterName + " is required",
|
||||
parameterName + "不能为空");
|
||||
}
|
||||
|
||||
public static BusinessException styleOutfitNotFound() {
|
||||
return new BusinessException("STYLE_OUTFIT_NOT_FOUND", "风格搭配不存在");
|
||||
}
|
||||
|
||||
public static BusinessException modelPhotoNotFound() {
|
||||
return new BusinessException("MODEL_PHOTO_NOT_FOUND", "模特照片不存在");
|
||||
}
|
||||
|
||||
public static BusinessException virtualTryOnNotFound() {
|
||||
return new BusinessException("VIRTUAL_TRYON_NOT_FOUND", "虚拟试穿记录不存在");
|
||||
}
|
||||
|
||||
public static BusinessException favoriteNotFound() {
|
||||
return new BusinessException("FAVORITE_NOT_FOUND", "收藏记录不存在");
|
||||
}
|
||||
|
||||
public static BusinessException fileUploadFailed() {
|
||||
return new BusinessException("FILE_UPLOAD_FAILED", "文件上传失败");
|
||||
}
|
||||
|
||||
public static BusinessException invalidFileFormat() {
|
||||
return new BusinessException("INVALID_FILE_FORMAT", "文件格式不支持");
|
||||
}
|
||||
|
||||
public static BusinessException fileSizeExceeded() {
|
||||
return new BusinessException("FILE_SIZE_EXCEEDED", "文件大小超过限制");
|
||||
}
|
||||
|
||||
public static BusinessException emailAlreadyExists() {
|
||||
return new BusinessException("EMAIL_ALREADY_EXISTS", "邮箱已存在");
|
||||
}
|
||||
|
||||
public static BusinessException phoneAlreadyExists() {
|
||||
return new BusinessException("PHONE_ALREADY_EXISTS", "手机号已存在");
|
||||
}
|
||||
|
||||
public static BusinessException invalidCredentials() {
|
||||
return new BusinessException("INVALID_CREDENTIALS", "用户名或密码错误");
|
||||
/**
|
||||
* 创建资源不存在异常
|
||||
*/
|
||||
public static BusinessException resourceNotFound(String resourceName) {
|
||||
return new BusinessException(ResultEnum.FAIL,
|
||||
resourceName + " not found",
|
||||
resourceName + "不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建权限不足异常
|
||||
*/
|
||||
public static BusinessException accessDenied() {
|
||||
return new BusinessException("ACCESS_DENIED", "访问被拒绝");
|
||||
return new BusinessException(ResultEnum.NO_PERMISSION);
|
||||
}
|
||||
|
||||
public static BusinessException operationFailed() {
|
||||
return new BusinessException("OPERATION_FAILED", "操作失败");
|
||||
/**
|
||||
* 创建操作失败异常
|
||||
*/
|
||||
public static BusinessException operationFailed(String operation) {
|
||||
return new BusinessException(ResultEnum.ERROR,
|
||||
operation + " operation failed",
|
||||
operation + "操作失败");
|
||||
}
|
||||
|
||||
public static BusinessException dataNotFound() {
|
||||
return new BusinessException("DATA_NOT_FOUND", "数据不存在");
|
||||
/**
|
||||
* 创建用户未登录异常
|
||||
*/
|
||||
public static BusinessException notLogin() {
|
||||
return new BusinessException(ResultEnum.NO_LOGIN);
|
||||
}
|
||||
|
||||
public static BusinessException duplicateData() {
|
||||
return new BusinessException("DUPLICATE_DATA", "数据重复");
|
||||
/**
|
||||
* 创建账户锁定异常
|
||||
*/
|
||||
public static BusinessException accountLocked() {
|
||||
return new BusinessException(ResultEnum.ACCOUNT_LOCK);
|
||||
}
|
||||
|
||||
public static BusinessException invalidParameter(String paramName) {
|
||||
return new BusinessException("INVALID_PARAMETER", "参数 " + paramName + " 无效");
|
||||
/**
|
||||
* 创建提示异常
|
||||
*/
|
||||
public static BusinessException prompt(String message, String messageCn) {
|
||||
return new BusinessException(ResultEnum.PROMPT, message, messageCn);
|
||||
}
|
||||
|
||||
public static BusinessException serviceUnavailable() {
|
||||
return new BusinessException("SERVICE_UNAVAILABLE", "服务暂时不可用");
|
||||
}
|
||||
|
||||
public static BusinessException externalServiceError() {
|
||||
return new BusinessException("EXTERNAL_SERVICE_ERROR", "外部服务调用失败");
|
||||
/**
|
||||
* 创建警告异常
|
||||
*/
|
||||
public static BusinessException warning(String message, String messageCn) {
|
||||
return new BusinessException(ResultEnum.WARNING, message, messageCn);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,23 @@
|
||||
package com.aida.lanecarford.exception;
|
||||
|
||||
import com.aida.lanecarford.common.ApiResponse;
|
||||
import com.aida.lanecarford.common.response.ResultEnum;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
* 统一处理应用程序中的各种异常,提供一致的错误响应格式
|
||||
*
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@@ -39,246 +25,102 @@ import java.util.stream.Collectors;
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
// 错误代码常量
|
||||
private static final String BUSINESS_ERROR = "BUSINESS_ERROR";
|
||||
private static final String VALIDATION_ERROR = "VALIDATION_ERROR";
|
||||
private static final String BIND_ERROR = "BIND_ERROR";
|
||||
private static final String FILE_SIZE_EXCEEDED = "FILE_SIZE_EXCEEDED";
|
||||
private static final String ILLEGAL_ARGUMENT = "ILLEGAL_ARGUMENT";
|
||||
private static final String NULL_POINTER = "NULL_POINTER";
|
||||
private static final String RUNTIME_ERROR = "RUNTIME_ERROR";
|
||||
private static final String UNKNOWN_ERROR = "UNKNOWN_ERROR";
|
||||
private static final String DATABASE_ERROR = "DATABASE_ERROR";
|
||||
private static final String METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
|
||||
private static final String NOT_FOUND = "NOT_FOUND";
|
||||
private static final String MEDIA_TYPE_NOT_SUPPORTED = "MEDIA_TYPE_NOT_SUPPORTED";
|
||||
private static final String MESSAGE_NOT_READABLE = "MESSAGE_NOT_READABLE";
|
||||
private static final String MISSING_PARAMETER = "MISSING_PARAMETER";
|
||||
private static final String TYPE_MISMATCH = "TYPE_MISMATCH";
|
||||
private static final String CONSTRAINT_VIOLATION = "CONSTRAINT_VIOLATION";
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||
logger.warn("业务异常 [{}]: {} - 请求路径: {}", e.getCode(), e.getMessage(), request.getRequestURI());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(BUSINESS_ERROR, e.getMessage(), request.getRequestURI(), null));
|
||||
public ApiResponse<Object> handleBusinessException(BusinessException e) {
|
||||
logger.warn("业务异常: {}", e.getMsgCn() != null ? e.getMsgCn() : e.getMsg());
|
||||
return ApiResponse.error(String.valueOf(e.getCode()), e.getMsg());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数验证异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||
logger.warn("参数验证异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
Map<String, String> errors = e.getBindingResult().getAllErrors().stream()
|
||||
.collect(Collectors.toMap(
|
||||
error -> ((FieldError) error).getField(),
|
||||
error -> error.getDefaultMessage(),
|
||||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(VALIDATION_ERROR, "参数验证失败", request.getRequestURI(), errors));
|
||||
public ApiResponse<Object> handleValidationException(MethodArgumentNotValidException e) {
|
||||
|
||||
logger.warn("参数验证异常: {}", e.getMessage());
|
||||
|
||||
// 构建英文错误消息返回给前端
|
||||
String errorMessageEn = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||
.reduce((msg1, msg2) -> msg1 + "; " + msg2)
|
||||
.orElse("Parameter validation failed");
|
||||
|
||||
return ApiResponse.error(String.valueOf(ResultEnum.PARAMETER_ERROR.getCode()), errorMessageEn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理绑定异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleBindException(BindException e, HttpServletRequest request) {
|
||||
logger.warn("绑定异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
Map<String, String> errors = e.getBindingResult().getAllErrors().stream()
|
||||
.collect(Collectors.toMap(
|
||||
error -> ((FieldError) error).getField(),
|
||||
error -> error.getDefaultMessage(),
|
||||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(BIND_ERROR, "数据绑定失败", request.getRequestURI(), errors));
|
||||
}
|
||||
public ApiResponse<Object> handleBindException(BindException e) {
|
||||
|
||||
/**
|
||||
* 处理约束违反异常
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
|
||||
logger.warn("约束违反异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
|
||||
Map<String, String> errors = violations.stream()
|
||||
.collect(Collectors.toMap(
|
||||
violation -> violation.getPropertyPath().toString(),
|
||||
ConstraintViolation::getMessage,
|
||||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(CONSTRAINT_VIOLATION, "约束验证失败", request.getRequestURI(), errors));
|
||||
logger.warn("绑定异常: {}", e.getMessage());
|
||||
|
||||
// 构建英文错误消息返回给前端
|
||||
String errorMessageEn = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||
.reduce((msg1, msg2) -> msg1 + "; " + msg2)
|
||||
.orElse("Data binding failed");
|
||||
|
||||
return ApiResponse.error(String.valueOf(ResultEnum.PARAMETER_ERROR.getCode()), errorMessageEn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件上传大小超限异常
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) {
|
||||
logger.warn("文件上传大小超限: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
public ApiResponse<Object> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
|
||||
|
||||
logger.warn("文件上传大小超限: {}", e.getMessage());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(FILE_SIZE_EXCEEDED, "上传文件大小超过限制", request.getRequestURI(), null));
|
||||
return ApiResponse.error(String.valueOf(ResultEnum.PARAMETER_ERROR.getCode()), "File size exceeds limit");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理非法参数异常
|
||||
* 处理MinIO异常
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
|
||||
logger.warn("非法参数异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
@ExceptionHandler(MinioException.class)
|
||||
public ApiResponse<Object> handleMinioException(MinioException e) {
|
||||
|
||||
logger.error("MinIO异常: {}", e.getMessage(), e);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(ILLEGAL_ARGUMENT, "参数错误:" + e.getMessage(), request.getRequestURI(), null));
|
||||
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "File storage service error");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缺少请求参数异常
|
||||
* 处理SQL异常
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleMissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
|
||||
logger.warn("缺少请求参数异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
String message = String.format("缺少必需的请求参数: %s", e.getParameterName());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(MISSING_PARAMETER, message, request.getRequestURI(), null));
|
||||
}
|
||||
@ExceptionHandler(SQLException.class)
|
||||
public ApiResponse<Object> handleSQLException(SQLException e) {
|
||||
|
||||
/**
|
||||
* 处理参数类型不匹配异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||
logger.warn("参数类型不匹配异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
String message = String.format("参数 %s 的值 %s 类型不正确", e.getName(), e.getValue());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(TYPE_MISMATCH, message, request.getRequestURI(), null));
|
||||
}
|
||||
logger.error("数据库异常: {}", e.getMessage(), e);
|
||||
|
||||
/**
|
||||
* 处理HTTP请求方法不支持异常
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
|
||||
logger.warn("HTTP请求方法不支持异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
String message = String.format("不支持的请求方法: %s", e.getMethod());
|
||||
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.body(createErrorResponse(METHOD_NOT_ALLOWED, message, request.getRequestURI(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理媒体类型不支持异常
|
||||
*/
|
||||
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e, HttpServletRequest request) {
|
||||
logger.warn("媒体类型不支持异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
.body(createErrorResponse(MEDIA_TYPE_NOT_SUPPORTED, "不支持的媒体类型", request.getRequestURI(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTTP消息不可读异常
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
|
||||
logger.warn("HTTP消息不可读异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(MESSAGE_NOT_READABLE, "请求体格式错误", request.getRequestURI(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理404异常
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
|
||||
logger.warn("404异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(createErrorResponse(NOT_FOUND, "请求的资源不存在", request.getRequestURI(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理数据库相关异常
|
||||
*/
|
||||
@ExceptionHandler({DataAccessException.class, SQLException.class, DataIntegrityViolationException.class})
|
||||
public ResponseEntity<Map<String, Object>> handleDatabaseException(Exception e, HttpServletRequest request) {
|
||||
logger.error("数据库异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI(), e);
|
||||
|
||||
String message = "数据库操作失败";
|
||||
if (e instanceof DataIntegrityViolationException) {
|
||||
message = "数据完整性约束违反";
|
||||
}
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(createErrorResponse(DATABASE_ERROR, message, request.getRequestURI(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理空指针异常
|
||||
*/
|
||||
@ExceptionHandler(NullPointerException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
|
||||
logger.error("空指针异常 - 请求路径: {}", request.getRequestURI(), e);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(createErrorResponse(NULL_POINTER, "系统内部错误", request.getRequestURI(), null));
|
||||
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "Database operation failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理运行时异常
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||
logger.error("运行时异常 - 请求路径: {}", request.getRequestURI(), e);
|
||||
public ApiResponse<Object> handleRuntimeException(RuntimeException e) {
|
||||
|
||||
logger.error("运行时异常: {}", e.getMessage(), e);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(createErrorResponse(RUNTIME_ERROR, "系统运行时错误", request.getRequestURI(), null));
|
||||
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "System runtime error");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有其他异常
|
||||
* 处理其他异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(Exception e, HttpServletRequest request) {
|
||||
logger.error("未知异常 - 请求路径: {}", request.getRequestURI(), e);
|
||||
public ApiResponse<Object> handleException(Exception e) {
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(createErrorResponse(UNKNOWN_ERROR, "系统内部错误", request.getRequestURI(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建统一的错误响应格式
|
||||
*/
|
||||
private Map<String, Object> createErrorResponse(String code, String message, String path, Map<String, String> errors) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("code", code);
|
||||
response.put("message", message);
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
response.put("path", path);
|
||||
logger.error("未知异常: {}", e.getMessage(), e);
|
||||
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
response.put("errors", errors);
|
||||
}
|
||||
|
||||
return response;
|
||||
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "System internal error");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.aida.lanecarford.exception;
|
||||
|
||||
/**
|
||||
* MinIO 操作异常类
|
||||
* 用于处理 MinIO 相关的业务异常
|
||||
*
|
||||
* @author Aida
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public class MinioException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param message 异常信息
|
||||
*/
|
||||
public MinioException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param message 异常信息
|
||||
* @param cause 原因
|
||||
*/
|
||||
public MinioException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
* @param message 异常信息
|
||||
*/
|
||||
public MinioException(String errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
* @param message 异常信息
|
||||
* @param cause 原因
|
||||
*/
|
||||
public MinioException(String errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误码
|
||||
*
|
||||
* @return 错误码
|
||||
*/
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误码
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
*/
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.aida.lanecarford.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片合成服务接口
|
||||
*
|
||||
* @author Aida
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public interface ImageCompositionService {
|
||||
|
||||
/**
|
||||
* 合成图片并上传到MinIO
|
||||
*
|
||||
* @param imageUrls 图片URL列表(1-3张)
|
||||
* @return 合成后图片的MinIO访问URL
|
||||
*/
|
||||
String composeAndUploadImages(List<String> imageUrls);
|
||||
|
||||
/**
|
||||
* 合成图片并上传到指定存储桶
|
||||
*
|
||||
* @param imageUrls 图片URL列表(1-3张)
|
||||
* @param bucketName 存储桶名称
|
||||
* @return 合成后图片的MinIO访问URL
|
||||
*/
|
||||
String composeAndUploadImages(List<String> imageUrls, String bucketName);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.aida.lanecarford.service;
|
||||
|
||||
import com.aida.lanecarford.entity.TryOnEffect;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 试穿效果服务接口
|
||||
@@ -11,4 +12,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
*/
|
||||
public interface TryOnEffectService extends IService<TryOnEffect> {
|
||||
|
||||
String generateTryOnEffect(@Valid TryOnEffect tryOnEffectDto);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.aida.lanecarford.service.impl;
|
||||
|
||||
import com.aida.lanecarford.service.ImageCompositionService;
|
||||
import com.aida.lanecarford.util.ImageCompositionUtil;
|
||||
import com.aida.lanecarford.util.MinioUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片合成服务实现类
|
||||
*
|
||||
* @author Aida
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImageCompositionServiceImpl implements ImageCompositionService {
|
||||
|
||||
private final ImageCompositionUtil imageCompositionUtil;
|
||||
private final MinioUtil minioUtil;
|
||||
|
||||
@Override
|
||||
public String composeAndUploadImages(List<String> imageUrls) {
|
||||
return composeAndUploadImages(imageUrls, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String composeAndUploadImages(List<String> imageUrls, String bucketName) {
|
||||
try {
|
||||
log.info("开始合成并上传图片,图片数量: {}, 存储桶: {}",
|
||||
imageUrls != null ? imageUrls.size() : 0, bucketName);
|
||||
|
||||
// 参数验证
|
||||
if (imageUrls == null || imageUrls.isEmpty()) {
|
||||
throw new IllegalArgumentException("图片URL列表不能为空");
|
||||
}
|
||||
|
||||
// 过滤有效的URL
|
||||
List<String> validUrls = imageUrls.stream()
|
||||
.filter(url -> url != null && !url.trim().isEmpty())
|
||||
.toList();
|
||||
|
||||
if (validUrls.isEmpty()) {
|
||||
throw new IllegalArgumentException("没有有效的图片URL");
|
||||
}
|
||||
|
||||
log.debug("有效图片URL数量: {}", validUrls.size());
|
||||
|
||||
// 如果只有一张图片,直接返回原URL
|
||||
if (validUrls.size() == 1) {
|
||||
log.info("只有一张图片,直接返回原URL: {}", validUrls.get(0));
|
||||
return validUrls.get(0);
|
||||
}
|
||||
|
||||
// 合成图片
|
||||
byte[] composedImageBytes = imageCompositionUtil.composeImages(validUrls);
|
||||
|
||||
// 生成文件名
|
||||
String fileName = imageCompositionUtil.generateComposedFileName(validUrls);
|
||||
|
||||
// 上传到MinIO
|
||||
String uploadedUrl;
|
||||
if (bucketName != null && !bucketName.trim().isEmpty()) {
|
||||
uploadedUrl = minioUtil.uploadBytes(composedImageBytes, fileName, "image/jpeg", bucketName);
|
||||
} else {
|
||||
uploadedUrl = minioUtil.uploadBytes(composedImageBytes, fileName, "image/jpeg");
|
||||
}
|
||||
|
||||
log.info("图片合成并上传成功,访问URL: {}", uploadedUrl);
|
||||
return uploadedUrl;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("图片合成并上传失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("图片合成并上传失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,117 @@
|
||||
package com.aida.lanecarford.service.impl;
|
||||
|
||||
import com.aida.lanecarford.entity.CustomerPhoto;
|
||||
import com.aida.lanecarford.entity.ModelPhoto;
|
||||
import com.aida.lanecarford.entity.Style;
|
||||
import com.aida.lanecarford.entity.TryOnEffect;
|
||||
import com.aida.lanecarford.mapper.TryOnEffectMapper;
|
||||
import com.aida.lanecarford.service.CustomerPhotoService;
|
||||
import com.aida.lanecarford.service.ImageCompositionService;
|
||||
import com.aida.lanecarford.service.ModelPhotoService;
|
||||
import com.aida.lanecarford.service.StyleService;
|
||||
import com.aida.lanecarford.service.TryOnEffectService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 试穿效果服务实现类
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOnEffect> implements TryOnEffectService {
|
||||
private final StyleService styleService;
|
||||
private final ModelPhotoService modelPhotoService;
|
||||
private final CustomerPhotoService customerPhotoService;
|
||||
private final TryOnEffectService tryOnEffectService;
|
||||
private final ImageCompositionService imageCompositionService;
|
||||
|
||||
@Override
|
||||
public String generateTryOnEffect(TryOnEffect tryOnEffectDto) {
|
||||
Integer isRegenerated = tryOnEffectDto.getIsRegenerated();
|
||||
String toAIUrl = null;
|
||||
// 收集图片URL
|
||||
List<String> imageUrls = new ArrayList<>();
|
||||
if (isRegenerated == 1) {
|
||||
String prompt = tryOnEffectDto.getPrompt();
|
||||
Long originalTryOnId = tryOnEffectDto.getOriginalTryOnId();
|
||||
// 验证originalTryOnId不能为空
|
||||
if (originalTryOnId == null) {
|
||||
throw new RuntimeException("originalTryOnId cannot be null");
|
||||
}
|
||||
TryOnEffect originalTryOn = tryOnEffectService.getById(originalTryOnId);
|
||||
String resultImageUrl = originalTryOn.getResultImageUrl();
|
||||
imageUrls.add(resultImageUrl);
|
||||
} else {
|
||||
Long styleId = tryOnEffectDto.getStyleId();
|
||||
// 验证styleId不能为空
|
||||
if (styleId == null) {
|
||||
throw new RuntimeException("styleId cannot be null");
|
||||
}
|
||||
|
||||
|
||||
|
||||
//根据id查到对应styleurl
|
||||
Style style = styleService.getById(styleId);
|
||||
String styleImageUrl = style.getStyleImageUrl();
|
||||
if (styleImageUrl != null && !styleImageUrl.trim().isEmpty()) {
|
||||
imageUrls.add(styleImageUrl);
|
||||
}
|
||||
|
||||
Long modelPhotoId = tryOnEffectDto.getModelPhotoId();
|
||||
if (modelPhotoId != null) {
|
||||
//根据id查到对应modelurl
|
||||
ModelPhoto modelPhoto = modelPhotoService.getById(modelPhotoId);
|
||||
String modelPhotoUrl = modelPhoto.getPhotoUrl();
|
||||
if (modelPhotoUrl != null && !modelPhotoUrl.trim().isEmpty()) {
|
||||
imageUrls.add(modelPhotoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Long customerPhotoId = tryOnEffectDto.getCustomerPhotoId();
|
||||
if (customerPhotoId != null) {
|
||||
//根据id查到对应customerurl
|
||||
CustomerPhoto customerPhoto = customerPhotoService.getById(customerPhotoId);
|
||||
String customerPhotoUrl = customerPhoto.getPhotoUrl();
|
||||
if (customerPhotoUrl != null && !customerPhotoUrl.trim().isEmpty()) {
|
||||
imageUrls.add(customerPhotoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// 合成图片
|
||||
if (!imageUrls.isEmpty()) {
|
||||
log.info("开始合成图片,图片数量: {}", imageUrls.size());
|
||||
try {
|
||||
toAIUrl = imageCompositionService.composeAndUploadImages(imageUrls);
|
||||
log.info("图片合成成功,合成图片URL: {}", toAIUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("图片合成失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("image error " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.warn("没有找到有效的图片URL进行合成");
|
||||
throw new RuntimeException("image cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
//调用模型生成试穿效果
|
||||
log.info("准备调用第三方AI服务,输入图片URL: {}", toAIUrl);
|
||||
String AIRreultUrl = AITryOnEffect(tryOnEffectDto.getPrompt(), toAIUrl);
|
||||
|
||||
|
||||
return "taskId";
|
||||
}
|
||||
|
||||
public String AITryOnEffect(String prompt,String url) {
|
||||
|
||||
//调用模型生成试穿效果
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package com.aida.lanecarford.util;
|
||||
|
||||
import com.aida.lanecarford.exception.BusinessException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 文件工具类
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public class FileUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
|
||||
|
||||
/**
|
||||
* 支持的图片格式
|
||||
*/
|
||||
private static final List<String> SUPPORTED_IMAGE_FORMATS = Arrays.asList(
|
||||
"jpg", "jpeg", "png", "gif", "bmp", "webp"
|
||||
);
|
||||
|
||||
/**
|
||||
* 最大文件大小(10MB)
|
||||
*/
|
||||
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* 上传根目录
|
||||
*/
|
||||
private static final String UPLOAD_ROOT_DIR = "uploads";
|
||||
|
||||
/**
|
||||
* 验证文件是否为图片
|
||||
*/
|
||||
public static boolean isImageFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = getFileExtension(originalFilename).toLowerCase();
|
||||
return SUPPORTED_IMAGE_FORMATS.contains(extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件大小
|
||||
*/
|
||||
public static boolean isValidFileSize(MultipartFile file) {
|
||||
return file != null && file.getSize() <= MAX_FILE_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
public static String getFileExtension(String filename) {
|
||||
if (filename == null || filename.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return filename.substring(lastDotIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一文件名
|
||||
*/
|
||||
public static String generateUniqueFileName(String originalFilename) {
|
||||
String extension = getFileExtension(originalFilename);
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
return timestamp + "_" + uuid + "." + extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录结构
|
||||
*/
|
||||
public static String createDirectoryStructure(String category) {
|
||||
String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
String fullPath = UPLOAD_ROOT_DIR + File.separator + category + File.separator + dateDir;
|
||||
|
||||
try {
|
||||
Path path = Paths.get(fullPath);
|
||||
Files.createDirectories(path);
|
||||
return fullPath;
|
||||
} catch (IOException e) {
|
||||
logger.error("创建目录失败: {}", fullPath, e);
|
||||
throw BusinessException.fileUploadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
*/
|
||||
public static String saveFile(MultipartFile file, String category) {
|
||||
// 验证文件
|
||||
validateFile(file);
|
||||
|
||||
// 创建目录
|
||||
String directoryPath = createDirectoryStructure(category);
|
||||
|
||||
// 生成文件名
|
||||
String filename = generateUniqueFileName(file.getOriginalFilename());
|
||||
|
||||
// 完整文件路径
|
||||
String fullPath = directoryPath + File.separator + filename;
|
||||
|
||||
try {
|
||||
// 保存文件
|
||||
Path filePath = Paths.get(fullPath);
|
||||
Files.write(filePath, file.getBytes());
|
||||
|
||||
logger.info("文件保存成功: {}", fullPath);
|
||||
|
||||
// 返回相对路径(用于数据库存储和访问)
|
||||
return fullPath.replace(File.separator, "/");
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("文件保存失败: {}", fullPath, e);
|
||||
throw BusinessException.fileUploadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
public static boolean deleteFile(String filePath) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||
boolean deleted = Files.deleteIfExists(path);
|
||||
|
||||
if (deleted) {
|
||||
logger.info("文件删除成功: {}", filePath);
|
||||
} else {
|
||||
logger.warn("文件不存在或删除失败: {}", filePath);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
} catch (IOException e) {
|
||||
logger.error("文件删除失败: {}", filePath, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
public static boolean fileExists(String filePath) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||
return Files.exists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小(字节)
|
||||
*/
|
||||
public static long getFileSize(String filePath) {
|
||||
if (!fileExists(filePath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||
return Files.size(path);
|
||||
} catch (IOException e) {
|
||||
logger.error("获取文件大小失败: {}", filePath, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
public static String formatFileSize(long size) {
|
||||
if (size < 1024) {
|
||||
return size + " B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return String.format("%.1f KB", size / 1024.0);
|
||||
} else if (size < 1024 * 1024 * 1024) {
|
||||
return String.format("%.1f MB", size / (1024.0 * 1024.0));
|
||||
} else {
|
||||
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件
|
||||
*/
|
||||
private static void validateFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw BusinessException.invalidParameter("file");
|
||||
}
|
||||
|
||||
if (!isImageFile(file)) {
|
||||
throw BusinessException.invalidFileFormat();
|
||||
}
|
||||
|
||||
if (!isValidFileSize(file)) {
|
||||
throw BusinessException.fileSizeExceeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的MIME类型
|
||||
*/
|
||||
public static String getContentType(String filename) {
|
||||
String extension = getFileExtension(filename).toLowerCase();
|
||||
|
||||
switch (extension) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "bmp":
|
||||
return "image/bmp";
|
||||
case "webp":
|
||||
return "image/webp";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期文件(可用于定时任务)
|
||||
*/
|
||||
public static void cleanupExpiredFiles(String directoryPath, int daysToKeep) {
|
||||
// TODO: 实现文件清理逻辑
|
||||
logger.info("清理过期文件: {} 天前的文件", daysToKeep);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.aida.lanecarford.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片合成工具类
|
||||
* 支持1-3张图片的智能合成
|
||||
*
|
||||
* @author Aida
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ImageCompositionUtil {
|
||||
|
||||
/**
|
||||
* 默认合成图片宽度
|
||||
*/
|
||||
private static final int DEFAULT_WIDTH = 800;
|
||||
|
||||
/**
|
||||
* 默认合成图片高度
|
||||
*/
|
||||
private static final int DEFAULT_HEIGHT = 600;
|
||||
|
||||
/**
|
||||
* 图片间距
|
||||
*/
|
||||
private static final int PADDING = 10;
|
||||
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
private static final Color BACKGROUND_COLOR = Color.WHITE;
|
||||
|
||||
/**
|
||||
* 合成多张图片
|
||||
*
|
||||
* @param imageUrls 图片URL列表(1-3张)
|
||||
* @return 合成后的图片字节数组
|
||||
* @throws IOException 图片处理异常
|
||||
*/
|
||||
public byte[] composeImages(List<String> imageUrls) throws IOException {
|
||||
if (imageUrls == null || imageUrls.isEmpty()) {
|
||||
throw new IllegalArgumentException("图片URL列表不能为空");
|
||||
}
|
||||
|
||||
if (imageUrls.size() > 3) {
|
||||
throw new IllegalArgumentException("最多支持3张图片合成");
|
||||
}
|
||||
|
||||
// 过滤空URL
|
||||
List<String> validUrls = imageUrls.stream()
|
||||
.filter(url -> url != null && !url.trim().isEmpty())
|
||||
.toList();
|
||||
|
||||
if (validUrls.isEmpty()) {
|
||||
throw new IllegalArgumentException("没有有效的图片URL");
|
||||
}
|
||||
|
||||
log.info("开始合成图片,图片数量: {}", validUrls.size());
|
||||
|
||||
// 下载图片
|
||||
List<BufferedImage> images = downloadImages(validUrls);
|
||||
|
||||
// 根据图片数量选择合成策略
|
||||
BufferedImage composedImage = switch (images.size()) {
|
||||
case 1 -> composeSingleImage(images.get(0));
|
||||
case 2 -> composeTwoImages(images.get(0), images.get(1));
|
||||
case 3 -> composeThreeImages(images.get(0), images.get(1), images.get(2));
|
||||
default -> throw new IllegalArgumentException("不支持的图片数量: " + images.size());
|
||||
};
|
||||
|
||||
// 转换为字节数组
|
||||
return imageToBytes(composedImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片
|
||||
*
|
||||
* @param imageUrls 图片URL列表
|
||||
* @return 图片列表
|
||||
* @throws IOException 下载异常
|
||||
*/
|
||||
private List<BufferedImage> downloadImages(List<String> imageUrls) throws IOException {
|
||||
List<BufferedImage> images = new ArrayList<>();
|
||||
|
||||
for (String imageUrl : imageUrls) {
|
||||
try {
|
||||
log.debug("下载图片: {}", imageUrl);
|
||||
URL url = new URL(imageUrl);
|
||||
BufferedImage image = ImageIO.read(url);
|
||||
|
||||
if (image == null) {
|
||||
log.warn("无法读取图片: {}", imageUrl);
|
||||
continue;
|
||||
}
|
||||
|
||||
images.add(image);
|
||||
log.debug("成功下载图片: {}, 尺寸: {}x{}", imageUrl, image.getWidth(), image.getHeight());
|
||||
} catch (Exception e) {
|
||||
log.error("下载图片失败: {}, 错误: {}", imageUrl, e.getMessage());
|
||||
// 继续处理其他图片,不抛出异常
|
||||
}
|
||||
}
|
||||
|
||||
if (images.isEmpty()) {
|
||||
throw new IOException("所有图片下载失败");
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单张图片处理(调整尺寸)
|
||||
*
|
||||
* @param image 原图片
|
||||
* @return 处理后的图片
|
||||
*/
|
||||
private BufferedImage composeSingleImage(BufferedImage image) {
|
||||
log.debug("处理单张图片");
|
||||
return resizeImage(image, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 两张图片合成(左右并排)
|
||||
*
|
||||
* @param image1 第一张图片
|
||||
* @param image2 第二张图片
|
||||
* @return 合成后的图片
|
||||
*/
|
||||
private BufferedImage composeTwoImages(BufferedImage image1, BufferedImage image2) {
|
||||
log.debug("合成两张图片(左右并排)");
|
||||
|
||||
int singleWidth = (DEFAULT_WIDTH - PADDING * 3) / 2;
|
||||
int singleHeight = DEFAULT_HEIGHT - PADDING * 2;
|
||||
|
||||
// 调整图片尺寸
|
||||
BufferedImage resized1 = resizeImage(image1, singleWidth, singleHeight);
|
||||
BufferedImage resized2 = resizeImage(image2, singleWidth, singleHeight);
|
||||
|
||||
// 创建画布
|
||||
BufferedImage canvas = new BufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = canvas.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
// 填充背景
|
||||
g2d.setColor(BACKGROUND_COLOR);
|
||||
g2d.fillRect(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
|
||||
// 绘制图片
|
||||
g2d.drawImage(resized1, PADDING, PADDING, null);
|
||||
g2d.drawImage(resized2, PADDING * 2 + singleWidth, PADDING, null);
|
||||
|
||||
g2d.dispose();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 三张图片合成(三宫格布局)
|
||||
*
|
||||
* @param image1 第一张图片
|
||||
* @param image2 第二张图片
|
||||
* @param image3 第三张图片
|
||||
* @return 合成后的图片
|
||||
*/
|
||||
private BufferedImage composeThreeImages(BufferedImage image1, BufferedImage image2, BufferedImage image3) {
|
||||
log.debug("合成三张图片(三宫格布局)");
|
||||
|
||||
int singleWidth = (DEFAULT_WIDTH - PADDING * 3) / 2;
|
||||
int singleHeight = (DEFAULT_HEIGHT - PADDING * 3) / 2;
|
||||
|
||||
// 调整图片尺寸
|
||||
BufferedImage resized1 = resizeImage(image1, singleWidth, singleHeight);
|
||||
BufferedImage resized2 = resizeImage(image2, singleWidth, singleHeight);
|
||||
BufferedImage resized3 = resizeImage(image3, singleWidth, singleHeight);
|
||||
|
||||
// 创建画布
|
||||
BufferedImage canvas = new BufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = canvas.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
// 填充背景
|
||||
g2d.setColor(BACKGROUND_COLOR);
|
||||
g2d.fillRect(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
|
||||
// 绘制图片(左上、右上、左下)
|
||||
g2d.drawImage(resized1, PADDING, PADDING, null);
|
||||
g2d.drawImage(resized2, PADDING * 2 + singleWidth, PADDING, null);
|
||||
g2d.drawImage(resized3, PADDING, PADDING * 2 + singleHeight, null);
|
||||
|
||||
g2d.dispose();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整图片尺寸
|
||||
*
|
||||
* @param originalImage 原图片
|
||||
* @param targetWidth 目标宽度
|
||||
* @param targetHeight 目标高度
|
||||
* @return 调整后的图片
|
||||
*/
|
||||
private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) {
|
||||
BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = resizedImage.createGraphics();
|
||||
|
||||
// 设置高质量渲染
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
|
||||
g2d.dispose();
|
||||
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片转换为字节数组
|
||||
*
|
||||
* @param image 图片
|
||||
* @return 字节数组
|
||||
* @throws IOException 转换异常
|
||||
*/
|
||||
private byte[] imageToBytes(BufferedImage image) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "jpg", baos);
|
||||
byte[] bytes = baos.toByteArray();
|
||||
log.debug("图片转换为字节数组,大小: {} bytes", bytes.length);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从字节数组创建输入流
|
||||
*
|
||||
* @param imageBytes 图片字节数组
|
||||
* @return 输入流
|
||||
*/
|
||||
public InputStream createInputStream(byte[] imageBytes) {
|
||||
return new ByteArrayInputStream(imageBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成合成图片的文件名
|
||||
*
|
||||
* @param originalUrls 原始图片URL列表
|
||||
* @return 文件名
|
||||
*/
|
||||
public String generateComposedFileName(List<String> originalUrls) {
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String suffix = originalUrls.size() + "_images_composed";
|
||||
return String.format("composed_%s_%s.jpg", suffix, timestamp);
|
||||
}
|
||||
}
|
||||
401
src/main/java/com/aida/lanecarford/util/MinioUtil.java
Normal file
401
src/main/java/com/aida/lanecarford/util/MinioUtil.java
Normal file
@@ -0,0 +1,401 @@
|
||||
package com.aida.lanecarford.util;
|
||||
|
||||
import com.aida.lanecarford.config.MinioConfig;
|
||||
import com.aida.lanecarford.exception.MinioException;
|
||||
import io.minio.*;
|
||||
import io.minio.http.Method;
|
||||
import io.minio.messages.Bucket;
|
||||
import io.minio.messages.Item;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* MinIO 工具类
|
||||
* 提供文件上传、下载、删除等操作
|
||||
*
|
||||
* @author Aida
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MinioUtil {
|
||||
|
||||
private MinioClient minioClient;
|
||||
|
||||
private MinioConfig minioConfig;
|
||||
|
||||
/**
|
||||
* 检查存储桶是否存在
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @return 是否存在
|
||||
*/
|
||||
public boolean bucketExists(String bucketName) {
|
||||
try {
|
||||
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
|
||||
} catch (Exception e) {
|
||||
log.error("检查存储桶是否存在失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("检查存储桶失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建存储桶
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
*/
|
||||
public void createBucket(String bucketName) {
|
||||
try {
|
||||
if (!bucketExists(bucketName)) {
|
||||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
||||
log.info("创建存储桶成功: {}", bucketName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("创建存储桶失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("创建存储桶失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有存储桶
|
||||
*
|
||||
* @return 存储桶列表
|
||||
*/
|
||||
public List<Bucket> getAllBuckets() {
|
||||
try {
|
||||
return minioClient.listBuckets();
|
||||
} catch (Exception e) {
|
||||
log.error("获取存储桶列表失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("获取存储桶列表失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除存储桶
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
*/
|
||||
public void removeBucket(String bucketName) {
|
||||
try {
|
||||
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
|
||||
log.info("删除存储桶成功: {}", bucketName);
|
||||
} catch (Exception e) {
|
||||
log.error("删除存储桶失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("删除存储桶失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param file 文件
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
public String uploadFile(MultipartFile file) {
|
||||
return uploadFile(file, minioConfig.getBucketName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到指定存储桶
|
||||
*
|
||||
* @param file 文件
|
||||
* @param bucketName 存储桶名称
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
public String uploadFile(MultipartFile file, String bucketName) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new MinioException("文件不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保存储桶存在
|
||||
createBucket(bucketName);
|
||||
|
||||
// 生成文件名
|
||||
String fileName = generateFileName(file.getOriginalFilename());
|
||||
|
||||
// 上传文件
|
||||
minioClient.putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(fileName)
|
||||
.stream(file.getInputStream(), file.getSize(), -1)
|
||||
.contentType(file.getContentType())
|
||||
.build()
|
||||
);
|
||||
|
||||
log.info("文件上传成功: {}", fileName);
|
||||
return getFileUrl(bucketName, fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("文件上传失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("文件上传失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节数组
|
||||
*
|
||||
* @param bytes 字节数组
|
||||
* @param fileName 文件名
|
||||
* @param contentType 内容类型
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
public String uploadBytes(byte[] bytes, String fileName, String contentType) {
|
||||
return uploadBytes(bytes, fileName, contentType, minioConfig.getBucketName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节数组到指定存储桶
|
||||
*
|
||||
* @param bytes 字节数组
|
||||
* @param fileName 文件名
|
||||
* @param contentType 内容类型
|
||||
* @param bucketName 存储桶名称
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
public String uploadBytes(byte[] bytes, String fileName, String contentType, String bucketName) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
throw new MinioException("文件内容不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保存储桶存在
|
||||
createBucket(bucketName);
|
||||
|
||||
// 生成文件名
|
||||
String objectName = generateFileName(fileName);
|
||||
|
||||
// 上传文件
|
||||
minioClient.putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(objectName)
|
||||
.stream(new ByteArrayInputStream(bytes), bytes.length, -1)
|
||||
.contentType(contentType)
|
||||
.build()
|
||||
);
|
||||
|
||||
log.info("字节数组上传成功: {}", objectName);
|
||||
return getFileUrl(bucketName, objectName);
|
||||
} catch (Exception e) {
|
||||
log.error("字节数组上传失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("字节数组上传失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return 文件输入流
|
||||
*/
|
||||
public InputStream downloadFile(String fileName) {
|
||||
return downloadFile(minioConfig.getBucketName(), fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定存储桶下载文件
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @param fileName 文件名
|
||||
* @return 文件输入流
|
||||
*/
|
||||
public InputStream downloadFile(String bucketName, String fileName) {
|
||||
try {
|
||||
return minioClient.getObject(
|
||||
GetObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(fileName)
|
||||
.build()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("文件下载失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("文件下载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
public void deleteFile(String fileName) {
|
||||
deleteFile(minioConfig.getBucketName(), fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定存储桶删除文件
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
public void deleteFile(String bucketName, String fileName) {
|
||||
try {
|
||||
minioClient.removeObject(
|
||||
RemoveObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(fileName)
|
||||
.build()
|
||||
);
|
||||
log.info("文件删除成功: {}", fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("文件删除失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("文件删除失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @return 文件列表
|
||||
*/
|
||||
public List<String> listFiles(String bucketName) {
|
||||
return listFiles(bucketName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @param prefix 文件前缀
|
||||
* @return 文件列表
|
||||
*/
|
||||
public List<String> listFiles(String bucketName, String prefix) {
|
||||
List<String> files = new ArrayList<>();
|
||||
try {
|
||||
Iterable<Result<Item>> results = minioClient.listObjects(
|
||||
ListObjectsArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.prefix(prefix)
|
||||
.build()
|
||||
);
|
||||
|
||||
for (Result<Item> result : results) {
|
||||
Item item = result.get();
|
||||
files.add(item.objectName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取文件列表失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("获取文件列表失败", e);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return 是否存在
|
||||
*/
|
||||
public boolean fileExists(String fileName) {
|
||||
return fileExists(minioConfig.getBucketName(), fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @param fileName 文件名
|
||||
* @return 是否存在
|
||||
*/
|
||||
public boolean fileExists(String bucketName, String fileName) {
|
||||
try {
|
||||
minioClient.statObject(
|
||||
StatObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(fileName)
|
||||
.build()
|
||||
);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件预签名URL(用于临时访问)
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @param expires 过期时间(秒)
|
||||
* @return 预签名URL
|
||||
*/
|
||||
public String getPresignedUrl(String fileName, int expires) {
|
||||
return getPresignedUrl(minioConfig.getBucketName(), fileName, expires);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件预签名URL(用于临时访问)
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @param fileName 文件名
|
||||
* @param expires 过期时间(秒)
|
||||
* @return 预签名URL
|
||||
*/
|
||||
public String getPresignedUrl(String bucketName, String fileName, int expires) {
|
||||
try {
|
||||
return minioClient.getPresignedObjectUrl(
|
||||
GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET)
|
||||
.bucket(bucketName)
|
||||
.object(fileName)
|
||||
.expiry(expires, TimeUnit.SECONDS)
|
||||
.build()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("获取预签名URL失败: {}", e.getMessage(), e);
|
||||
throw new MinioException("获取预签名URL失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @param fileName 文件名
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
public String getFileUrl(String bucketName, String fileName) {
|
||||
return minioConfig.getEndpoint() + "/" + bucketName + "/" + fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一文件名
|
||||
*
|
||||
* @param originalFilename 原始文件名
|
||||
* @return 生成的文件名
|
||||
*/
|
||||
private String generateFileName(String originalFilename) {
|
||||
if (originalFilename == null || originalFilename.trim().isEmpty()) {
|
||||
throw new MinioException("文件名不能为空");
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
String extension = "";
|
||||
int lastDotIndex = originalFilename.lastIndexOf(".");
|
||||
if (lastDotIndex > 0) {
|
||||
extension = originalFilename.substring(lastDotIndex);
|
||||
}
|
||||
|
||||
// 生成时间戳路径
|
||||
String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
|
||||
// 生成唯一文件名
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
return datePath + "/" + uuid + extension;
|
||||
}
|
||||
}
|
||||
13
src/main/resources/aida-461108-b4afaabebb84.json
Normal file
13
src/main/resources/aida-461108-b4afaabebb84.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "aida-461108",
|
||||
"private_key_id": "b4afaabebb84da24502b318a5fa175f1dc5c096a",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmk7LKrp8g9yD1\nWmF+mY2qHCEZ/5aIx6QRh0QoVPBL7Yi7ce009QxaE8fu8+QMgg8l3xMreXvgpt56\noFnVwpFusLjSdjgoFluElM2hYxXlO9q8cbBoU2nehOBLLJzGzkodT7xu/BOjNvKC\n//aTbjtJyk8Kj+ENa0/dPaUZs/PCtQqpAu8ag5nXrordVWfO0K25EjeYyoba35zk\nPp2fBi8KALZZI5Xfd2z9++K0K2mWWIMJic30idHvquj0WxlTRK2Pq8BmJXCQpJIi\nQ5E4egue16BfKjrF0Kxkpqd1RmdlEmaSKbbkZXe2z4jg0qknESRFOmRy8C3LnaB2\nHHJWLYM3AgMBAAECggEACUdroOQJSTTQSS/iWRhZ+S0yoC10nTnsZxg527qfiBs7\nOqB7WNqC+Ew8dDsca6CdvLuoaGDkCFJDTQwRn66u8JOM4sG4bxiPuzBEJBv45EQT\n8zCsuvhVNWgBdoPjAnq19jFdixvPnDqQrRYaY4FdxsaA5f24c57pW/xLGMYawLBt\n9RJZSuWmJdzKG1i5W8a8+4f/seNtuo2MtXU3mPJZPqRWPXTAZeaQPM/57ZQ+kzig\nOkAbQZNRmt1yPCjPCQD8vc8yCBMmjus/rlHXD/L7okYUlVZkob5I3FBrLl+ZyIXS\nqxEsBLBwRW3w8WbX+ZSVciQ72JK68W7LnOHSAENmAQKBgQDgBTCqp87KGLWVPb8w\nK+s1Sfh+nM3M4AlbLdcGBs1JCoddF6pAeY4wpf/ow1Tm4rqEuCYzMClPwxvkue+D\nY7lCQgy2FK3ahUzn8oVmvEPD/YPAojDSY3bH0lquHuS6oVKk834JUykButaAU3XY\nvUGNQuKdLKAeQRT8Q6um4m+EYQKBgQC+Wz6nYESKH6GiNnuFTH8hIkThPlbi4wua\nU1kGnPKe3ouE4zRLfPwQ6RRf1slQ/2hFLOatiTLYUgZWZQeBPSWp2EjYcOSzob+7\n11+KqeIRCD5DKxgf0cjJdihK9AM639OKlH2NvZ2507TksdeTPDzdaOMLwLWKexP5\nlYrdob0ulwKBgD81t7Gvf83Ogw4FSjkRa2Cx6ofvPrKcVIeBu7ZbnPkLG37M+qEO\nq2xWqorG8uHi/7YLL9wprr5u0yQKwuZT8SYc9PE7jIKoMjcQW0vNu2FF2zMzkIsM\nvatMU4Hl/awbcPJSMjH3YQ635WZ4Jjxtyl1NjhvDR7rBqmYzwe9o3QaBAoGANhPB\n1tbYYczepDCKIrI6o3US0FJfaJFLqInpDqHjoxJh3FyXbKKTEVLFwPxJsML+IjjB\nR6dkVGPo/P4yhZqTao7REvvvXMCksX5b3A6q9F+9IGPLtK5qNiFlDPYJPN59QC8z\nA+NMPZBRIW8MaP2B5Px5E8upRy/z2sGK86+RCP0CgYATGs75F97q+Zf8q+Pe3Nsb\ngqmhLoI3PZUSWgBcQgNF4nyCZceUrEl72wKO/NWLgxqQPtlra187ce69g7qARHLb\ntHq80nb0f7lil74B6+OlyNNO1htWA90fmGR2s16Mt0BwJRT+/EFuNqbJIUSLxKiW\nqlXBUbmHHzamo5DPYL8S/w==\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "aida-239@aida-461108.iam.gserviceaccount.com",
|
||||
"client_id": "103102077955178349079",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/aida-239%40aida-461108.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
@@ -44,6 +44,15 @@ springdoc:
|
||||
path: /swagger-ui.html
|
||||
enabled: true
|
||||
|
||||
# MinIO 对象存储配置
|
||||
minio:
|
||||
endpoint: https://www.minio-api.aida.com.hk
|
||||
access-key: admin
|
||||
secret-key: Aidlab123123
|
||||
bucket-name: lanecarford
|
||||
# 文件访问URL前缀
|
||||
url-prefix: ${minio.endpoint}/${minio.bucket-name}/
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
|
||||
@@ -116,10 +116,10 @@ CREATE TABLE try_on_effects (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '试穿效果ID',
|
||||
customer_id BIGINT NOT NULL COMMENT '顾客ID',
|
||||
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
|
||||
styles_id BIGINT NOT NULL COMMENT '风格ID',
|
||||
style_id BIGINT NOT NULL COMMENT '风格ID',
|
||||
model_photo_id BIGINT COMMENT '模特照片ID',
|
||||
customer_photo_id BIGINT COMMENT '顾客照片ID',
|
||||
prompt VARCHAR(500) COMMENT '提示词',
|
||||
prompt VARCHAR(500) COMMENT '提示词,当is_regenerated为1时才会有值',
|
||||
original_try_on_id BIGINT COMMENT '原试穿效果ID,当is_regenerated为1时才会有值',
|
||||
is_regenerated TINYINT DEFAULT 0 COMMENT '是否由生成结果重新生成(0-否,1-是)',
|
||||
result_image_url VARCHAR(500) COMMENT '试穿结果图片URL',
|
||||
@@ -131,12 +131,12 @@ CREATE TABLE try_on_effects (
|
||||
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (visit_record_id) REFERENCES visit_records(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (styles_id) REFERENCES styles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (style_id) REFERENCES styles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (model_photo_id) REFERENCES model_photos(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (customer_photo_id) REFERENCES customer_photos(id) ON DELETE SET NULL,
|
||||
INDEX idx_customer_id (customer_id),
|
||||
INDEX idx_visit_record_id (visit_record_id),
|
||||
INDEX idx_styles_id (styles_id),
|
||||
INDEX idx_style_id (style_id),
|
||||
INDEX idx_request_id (request_id),
|
||||
INDEX idx_generation_status (generation_status),
|
||||
INDEX idx_is_favorite (is_favorite)
|
||||
|
||||
Reference in New Issue
Block a user