tryon first commit

This commit is contained in:
litianxiang
2025-10-21 13:44:51 +08:00
parent af8fbd90e3
commit 917e0846c3
19 changed files with 1250 additions and 573 deletions

View File

@@ -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 -->

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
/**
* 错误信息

View File

@@ -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;
/**
* 创建时间
*/

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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) {
//调用模型生成试穿效果
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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;
}
}

View 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"
}

View File

@@ -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:

View File

@@ -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)