Merge branch 'dev/dev' into prod/release_1.0

This commit is contained in:
2025-10-28 15:35:43 +08:00
88 changed files with 5618 additions and 1181 deletions

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ build/
### VS Code ###
.vscode/
/logs/

353
pom.xml
View File

@@ -1,154 +1,247 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aida</groupId>
<artifactId>lanecarford</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>lanecarford</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aida</groupId>
<artifactId>lanecarford</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>lanecarford</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.0.3</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Swagger for API Documentation -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<!-- Swagger for API Documentation -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<!-- Google 认证库 -->
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.38.0</version>
</dependency>
<!-- OkHttp for HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Hutool工具库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.43</version>
</dependency>
<!-- Spring Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JJWT API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<!-- JJWT 实现 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Jackson 支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- tencent-cloud -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-ses</artifactId>
<version>3.1.572</version>
</dependency>
</dependencies>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<!-- 本地开发环境 -->
<id>dev</id>
<properties>
<profiles.active>dev</profiles.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<!-- 生产环境 -->
<id>prod</id>
<properties>
<profiles.active>prod</profiles.active>
</properties>
</profile>
</profiles>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
<release>21</release>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -10,11 +10,12 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* 日志切面
*
*
* @author AI Assistant
* @since 2024-01-01
*/
@@ -28,13 +29,15 @@ public class LoggingAspect {
* 定义切点所有Controller方法
*/
@Pointcut("execution(* com.aida.lanecarford.controller..*(..))")
public void controllerMethods() {}
public void controllerMethods() {
}
/**
* 定义切点所有Service方法
*/
@Pointcut("execution(* com.aida.lanecarford.service..*(..))")
public void serviceMethods() {}
public void serviceMethods() {
}
/**
* Controller方法执行前记录日志
@@ -44,12 +47,13 @@ public class LoggingAspect {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
logger.info("=== 请求开始 ===");
logger.info("请求URL: {}", request.getRequestURL().toString());
logger.info("请求方法: {}", request.getMethod());
logger.info("请求IP: {}", getClientIpAddress(request));
logger.info("调用方法: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// logger.info("调用方法: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("调用方法: {}.{}", joinPoint.getSignature().getDeclaringType().getSimpleName(), joinPoint.getSignature().getName());
logger.info("请求参数: {}", Arrays.toString(joinPoint.getArgs()));
}
}
@@ -59,7 +63,7 @@ public class LoggingAspect {
*/
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
public void logControllerAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("方法执行成功: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("方法执行成功: {}.{}", joinPoint.getSignature().getDeclaringType().getSimpleName(), joinPoint.getSignature().getName());
logger.info("返回结果: {}", result);
logger.info("=== 请求结束 ===");
}
@@ -69,7 +73,7 @@ public class LoggingAspect {
*/
@AfterThrowing(pointcut = "controllerMethods()", throwing = "exception")
public void logControllerAfterThrowing(JoinPoint joinPoint, Throwable exception) {
logger.error("方法执行异常: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.error("方法执行异常: {}.{}", joinPoint.getSignature().getDeclaringType().getSimpleName(), joinPoint.getSignature().getName());
logger.error("异常信息: ", exception);
logger.info("=== 请求异常结束 ===");
}
@@ -80,15 +84,15 @@ public class LoggingAspect {
@Around("serviceMethods()")
public Object logServiceAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
// String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
String methodName = joinPoint.getSignature().getDeclaringType().getSimpleName() + "." + joinPoint.getSignature().getName();
try {
logger.debug("Service方法开始执行: {}", methodName);
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
logger.debug("Service方法执行成功: {}, 耗时: {}ms", methodName, (endTime - startTime));
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
@@ -106,22 +110,22 @@ public class LoggingAspect {
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0];
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}
String proxyClientIp = request.getHeader("Proxy-Client-IP");
if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) {
return proxyClientIp;
}
String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
return wlProxyClientIp;
}
return request.getRemoteAddr();
}
}

View File

@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
/**
* 性能监控切面
*
*
* @author AI Assistant
* @since 2024-01-01
*/
@@ -22,7 +22,7 @@ public class PerformanceAspect {
/**
* 监控Controller方法性能
*/
@Around("execution(* com.aida.lanecarford.controller..*(..))")
// @Around("execution(* com.aida.lanecarford.controller..*(..))")
public Object monitorControllerPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
return monitorMethodPerformance(joinPoint, "Controller");
}
@@ -30,7 +30,7 @@ public class PerformanceAspect {
/**
* 监控Service方法性能
*/
@Around("execution(* com.aida.lanecarford.service..*(..))")
// @Around("execution(* com.aida.lanecarford.service..*(..))")
public Object monitorServicePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
return monitorMethodPerformance(joinPoint, "Service");
}
@@ -38,7 +38,7 @@ public class PerformanceAspect {
/**
* 监控数据库操作性能
*/
@Around("execution(* com.aida.lanecarford.mapper..*(..))")
// @Around("execution(* com.aida.lanecarford.mapper..*(..))")
public Object monitorMapperPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
return monitorMethodPerformance(joinPoint, "Mapper");
}
@@ -49,28 +49,28 @@ public class PerformanceAspect {
private Object monitorMethodPerformance(ProceedingJoinPoint joinPoint, String layer) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录性能日志
logPerformance(layer, methodName, executionTime, true);
// 如果执行时间过长,记录警告
if (executionTime > getWarningThreshold(layer)) {
logger.warn("{}方法执行时间过长: {} - {}ms", layer, methodName, executionTime);
}
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录异常性能日志
logPerformance(layer, methodName, executionTime, false);
throw e;
}
}
@@ -80,10 +80,10 @@ public class PerformanceAspect {
*/
private void logPerformance(String layer, String methodName, long executionTime, boolean success) {
if (logger.isDebugEnabled()) {
logger.debug("性能监控 - {}: {} - {}ms - {}",
layer, methodName, executionTime, success ? "成功" : "失败");
logger.debug("性能监控 - {}: {} - {}ms - {}",
layer, methodName, executionTime, success ? "成功" : "失败");
}
// 可以在这里添加性能数据收集逻辑,比如发送到监控系统
collectPerformanceMetrics(layer, methodName, executionTime, success);
}
@@ -114,7 +114,7 @@ public class PerformanceAspect {
// - 发送到时序数据库
// - 更新内存中的统计信息
// - 发送到监控系统
// 示例:简单的内存统计
PerformanceMetrics.recordExecution(layer, methodName, executionTime, success);
}
@@ -123,7 +123,7 @@ public class PerformanceAspect {
* 简单的性能指标收集器
*/
private static class PerformanceMetrics {
public static void recordExecution(String layer, String methodName, long executionTime, boolean success) {
// 简单的日志记录,实际项目中可以替换为更复杂的指标收集
if (executionTime > 1000) { // 超过1秒的操作

View File

@@ -1,5 +1,6 @@
package com.aida.lanecarford.common;
import com.aida.lanecarford.common.response.ResultEnum;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@@ -20,7 +21,7 @@ public class ApiResponse<T> implements Serializable {
/**
* 响应状态码
*/
private String code;
private Integer code;
/**
* 响应消息
@@ -46,7 +47,7 @@ public class ApiResponse<T> implements Serializable {
this.timestamp = System.currentTimeMillis();
}
public ApiResponse(boolean success, String code, String message, T data) {
public ApiResponse(boolean success, Integer code, String message, T data) {
this();
this.success = success;
this.code = code;
@@ -58,34 +59,34 @@ public class ApiResponse<T> implements Serializable {
* 成功响应(无数据)
*/
public static <T> ApiResponse<T> success() {
return new ApiResponse<>(true, "SUCCESS", "操作成功", null);
return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), "操作成功", null);
}
/**
* 成功响应(带数据)
*/
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "SUCCESS", "操作成功", data);
return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), "操作成功", data);
}
/**
* 成功响应(自定义消息,无数据)
*/
public static <T> ApiResponse<T> success(String message) {
return new ApiResponse<>(true, "SUCCESS", message, null);
return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), message, null);
}
/**
* 成功响应(自定义消息)
*/
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, "SUCCESS", message, data);
return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), message, data);
}
/**
* 失败响应
*/
public static <T> ApiResponse<T> error(String code, String message) {
public static <T> ApiResponse<T> error(Integer code, String message) {
return new ApiResponse<>(false, code, message, null);
}
@@ -93,76 +94,76 @@ public class ApiResponse<T> implements Serializable {
* 失败响应(默认错误码)
*/
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, "ERROR", message, null);
return new ApiResponse<>(false, ResultEnum.ERROR.getCode(), message, null);
}
/**
* 参数错误响应
*/
public static <T> ApiResponse<T> paramError(String message) {
return new ApiResponse<>(false, "PARAM_ERROR", message, null);
}
/**
* 数据不存在响应
*/
public static <T> ApiResponse<T> notFound(String message) {
return new ApiResponse<>(false, "NOT_FOUND", message, null);
}
/**
* 权限不足响应
*/
public static <T> ApiResponse<T> forbidden(String message) {
return new ApiResponse<>(false, "FORBIDDEN", message, null);
}
/**
* 服务器内部错误响应
*/
public static <T> ApiResponse<T> serverError(String message) {
return new ApiResponse<>(false, "SERVER_ERROR", message, null);
}
/**
* 业务异常响应
*/
public static <T> ApiResponse<T> businessError(String code, String message) {
return new ApiResponse<>(false, code, message, null);
}
/**
* 外部服务错误响应
*/
public static <T> ApiResponse<T> externalServiceError(String message) {
return new ApiResponse<>(false, "EXTERNAL_SERVICE_ERROR", message, null);
}
/**
* 文件上传错误响应
*/
public static <T> ApiResponse<T> fileUploadError(String message) {
return new ApiResponse<>(false, "FILE_UPLOAD_ERROR", message, null);
}
/**
* 验证失败响应
*/
public static <T> ApiResponse<T> validationError(String message) {
return new ApiResponse<>(false, "VALIDATION_ERROR", message, null);
}
/**
* 重复数据响应
*/
public static <T> ApiResponse<T> duplicateError(String message) {
return new ApiResponse<>(false, "DUPLICATE_ERROR", message, null);
}
/**
* 操作超时响应
*/
public static <T> ApiResponse<T> timeoutError(String message) {
return new ApiResponse<>(false, "TIMEOUT_ERROR", message, null);
}
// /**
// * 参数错误响应
// */
// public static <T> ApiResponse<T> paramError(String message) {
// return new ApiResponse<>(false, "PARAM_ERROR", message, null);
// }
//
// /**
// * 数据不存在响应
// */
// public static <T> ApiResponse<T> notFound(String message) {
// return new ApiResponse<>(false, "NOT_FOUND", message, null);
// }
//
// /**
// * 权限不足响应
// */
// public static <T> ApiResponse<T> forbidden(String message) {
// return new ApiResponse<>(false, "FORBIDDEN", message, null);
// }
//
// /**
// * 服务器内部错误响应
// */
// public static <T> ApiResponse<T> serverError(String message) {
// return new ApiResponse<>(false, "SERVER_ERROR", message, null);
// }
//
// /**
// * 业务异常响应
// */
// public static <T> ApiResponse<T> businessError(String code, String message) {
// return new ApiResponse<>(false, code, message, null);
// }
//
// /**
// * 外部服务错误响应
// */
// public static <T> ApiResponse<T> externalServiceError(String message) {
// return new ApiResponse<>(false, "EXTERNAL_SERVICE_ERROR", message, null);
// }
//
// /**
// * 文件上传错误响应
// */
// public static <T> ApiResponse<T> fileUploadError(String message) {
// return new ApiResponse<>(false, "FILE_UPLOAD_ERROR", message, null);
// }
//
// /**
// * 验证失败响应
// */
// public static <T> ApiResponse<T> validationError(String message) {
// return new ApiResponse<>(false, "VALIDATION_ERROR", message, null);
// }
//
// /**
// * 重复数据响应
// */
// public static <T> ApiResponse<T> duplicateError(String message) {
// return new ApiResponse<>(false, "DUPLICATE_ERROR", message, null);
// }
//
// /**
// * 操作超时响应
// */
// public static <T> ApiResponse<T> timeoutError(String message) {
// return new ApiResponse<>(false, "TIMEOUT_ERROR", message, null);
// }
}

View File

@@ -0,0 +1,15 @@
package com.aida.lanecarford.common;
public class CommonConstant {
// 单位 秒 10分钟过期
// public static final Long TASK_EXPIRE_TIME = 24 * 60 * 60L;
public static final Long TASK_EXPIRE_TIME = 10 * 60L;
// 单位 秒 两天过期
public static final Long CREDITS_EXPIRE_TIME = 2 * 24 * 60 * 60L;
// 单位 分钟
public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60;
// 单位 秒 一天过期 in redis
public static final Long GENERATE_RESULT_EXPIRE_TIME = 24 * 60 * 60L;
// 单位 秒 7天过期
public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L;
}

View File

@@ -0,0 +1,14 @@
package com.aida.lanecarford.common.constant;
public class CommonConstants {
public static final String REQUEST_OUTFIT = "http://18.167.251.121:10004/api/v1/agent";
public static final String CHAT = "http://18.167.251.121:10004/api/v1/chatbot";
public static final int MINIO_PATH_TIMEOUT = 7 * 24 * 60 * 60; // minio图片临时访问地址 7 天过期second
public static final int CONN_TIMEOUT = 30000; // milliseconds
}

View File

@@ -0,0 +1,264 @@
package com.aida.lanecarford.common.constant;
import java.util.UUID;
/**
* MinIO文件命名常量类
* 统一管理不同类型图片的命名规范
*
* @author AI Assistant
* @since 2024-01-01
*/
public class MinioFileConstants {
/**
* 文件路径分隔符
*/
public static final String PATH_SEPARATOR = "/";
/**
* 图片文件扩展名
*/
public static final String PNG_EXTENSION = ".png";
public static final String JPG_EXTENSION = ".jpg";
public static final String JPEG_EXTENSION = ".jpeg";
/**
* 顾客照片目录
*/
public static final String CUSTOMER_PHOTO_DIR = "customer_photo";
/**
* 模特照片目录
*/
public static final String MODEL_PHOTO_DIR = "model_photo";
/**
* 风格图片目录
*/
public static final String STYLE_IMAGE_DIR = "style_image";
/**
* 试穿结果图片目录
*/
public static final String TRY_ON_RESULT_DIR = "try_on_result";
/**
* 合成图片目录
*/
public static final String COMPOSED_IMAGE_DIR = "composed_image";
/**
* 生成顾客照片文件名
* 格式: 桶名/customer_photo/UUID.png
*
* @param bucketName 桶名
* @return 完整的文件路径
*/
public static String generateCustomerPhotoPath(String bucketName) {
return bucketName + PATH_SEPARATOR + CUSTOMER_PHOTO_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成顾客照片文件名(仅路径部分)
* 格式: customer_photo/UUID.png
*
* @return 文件路径(不含桶名)
*/
public static String generateCustomerPhotoObjectName() {
return CUSTOMER_PHOTO_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成模特照片文件名
* 格式: 桶名/model_photo/UUID.png
*
* @param bucketName 桶名
* @return 完整的文件路径
*/
public static String generateModelPhotoPath(String bucketName) {
return bucketName + PATH_SEPARATOR + MODEL_PHOTO_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成模特照片文件名(仅路径部分)
* 格式: model_photo/UUID.png
*
* @return 文件路径(不含桶名)
*/
public static String generateModelPhotoObjectName() {
return MODEL_PHOTO_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成风格图片文件名
* 格式: 桶名/style_image/UUID.png
*
* @param bucketName 桶名
* @return 完整的文件路径
*/
public static String generateStyleImagePath(String bucketName) {
return bucketName + PATH_SEPARATOR + STYLE_IMAGE_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成风格图片文件名(仅路径部分)
* 格式: style_image/UUID.png
*
* @return 文件路径(不含桶名)
*/
public static String generateStyleImageObjectName() {
return STYLE_IMAGE_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成试穿结果图片文件名
* 格式: 桶名/try_on_result/UUID.png
*
* @param bucketName 桶名
* @return 完整的文件路径
*/
public static String generateTryOnResultPath(String bucketName) {
return bucketName + PATH_SEPARATOR + TRY_ON_RESULT_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成试穿结果图片文件名(仅路径部分)
* 格式: try_on_result/UUID.png
*
* @return 文件路径(不含桶名)
*/
public static String generateTryOnResultObjectName() {
return TRY_ON_RESULT_DIR + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成合成图片文件名
* 格式: 桶名/composed_image/UUID.jpg
*
* @param bucketName 桶名
* @return 完整的文件路径
*/
public static String generateComposedImagePath(String bucketName) {
return bucketName + PATH_SEPARATOR + COMPOSED_IMAGE_DIR + PATH_SEPARATOR + UUID.randomUUID() + JPG_EXTENSION;
}
/**
* 生成合成图片文件名(仅路径部分)
* 格式: composed_image/UUID.jpg
*
* @return 文件路径(不含桶名)
*/
public static String generateComposedImageObjectName() {
return COMPOSED_IMAGE_DIR + PATH_SEPARATOR + UUID.randomUUID() + JPG_EXTENSION;
}
/**
* 生成带前缀的试穿结果图片文件名
* 格式: 桶名/try_on_result/tryon_result_UUID.png
*
* @param bucketName 桶名
* @return 完整的文件路径
*/
public static String generateTryOnResultWithPrefixPath(String bucketName) {
return bucketName + PATH_SEPARATOR + TRY_ON_RESULT_DIR + PATH_SEPARATOR + "tryon_result_" + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成带前缀的试穿结果图片文件名(仅路径部分)
* 格式: try_on_result/tryon_result_UUID.png
*
* @return 文件路径(不含桶名)
*/
public static String generateTryOnResultWithPrefixObjectName() {
return TRY_ON_RESULT_DIR + PATH_SEPARATOR + "tryon_result_" + UUID.randomUUID() + PNG_EXTENSION;
}
/**
* 生成带时间戳的合成图片文件名
* 格式: 桶名/composed_image/composed_[图片数量]_images_composed_[时间戳].jpg
*
* @param bucketName 桶名
* @param imageCount 图片数量
* @return 完整的文件路径
*/
public static String generateComposedImageWithTimestampPath(String bucketName, int imageCount) {
String timestamp = String.valueOf(System.currentTimeMillis());
String suffix = imageCount + "_images_composed";
String fileName = "composed_" + suffix + "_" + timestamp + JPG_EXTENSION;
return bucketName + PATH_SEPARATOR + COMPOSED_IMAGE_DIR + PATH_SEPARATOR + fileName;
}
/**
* 生成带时间戳的合成图片文件名(仅路径部分)
* 格式: composed_image/composed_[图片数量]_images_composed_[时间戳].jpg
*
* @param imageCount 图片数量
* @return 文件路径(不含桶名)
*/
public static String generateComposedImageWithTimestampObjectName(int imageCount) {
String timestamp = String.valueOf(System.currentTimeMillis());
String suffix = imageCount + "_images_composed";
String fileName = "composed_" + suffix + "_" + timestamp + JPG_EXTENSION;
return COMPOSED_IMAGE_DIR + PATH_SEPARATOR + fileName;
}
/**
* 根据文件类型生成对应的对象名称
*
* @param fileType 文件类型
* @return 对象名称
*/
public static String generateObjectNameByType(FileType fileType) {
return switch (fileType) {
case CUSTOMER_PHOTO -> generateCustomerPhotoObjectName();
case MODEL_PHOTO -> generateModelPhotoObjectName();
case STYLE_IMAGE -> generateStyleImageObjectName();
case TRY_ON_RESULT -> generateTryOnResultObjectName();
case COMPOSED_IMAGE -> generateComposedImageObjectName();
};
}
/**
* 根据文件类型生成对应的完整路径
*
* @param bucketName 桶名
* @param fileType 文件类型
* @return 完整路径
*/
public static String generatePathByType(String bucketName, FileType fileType) {
return switch (fileType) {
case CUSTOMER_PHOTO -> generateCustomerPhotoPath(bucketName);
case MODEL_PHOTO -> generateModelPhotoPath(bucketName);
case STYLE_IMAGE -> generateStyleImagePath(bucketName);
case TRY_ON_RESULT -> generateTryOnResultPath(bucketName);
case COMPOSED_IMAGE -> generateComposedImagePath(bucketName);
};
}
/**
* 文件类型枚举
*/
public enum FileType {
/**
* 顾客照片
*/
CUSTOMER_PHOTO,
/**
* 模特照片
*/
MODEL_PHOTO,
/**
* 风格图片
*/
STYLE_IMAGE,
/**
* 试穿结果图片
*/
TRY_ON_RESULT,
/**
* 合成图片
*/
COMPOSED_IMAGE
}
}

View File

@@ -0,0 +1,18 @@
package com.aida.lanecarford.common.constant;
public class RedisURIConstants {
public static final String tokenCache = "TokenCache:";
public static final String verifyCodeCache = "VerifyCodeCache:";
// 验证码 10分钟过期
public static final Long verifyCodeTimeout = 10 * 60L;
// outfit result 结果30分钟过期
public static final Long outfitResultTimeout = 30 * 60L;
public static final String outfitResultCache = "OutfitResultCache:";
public static final String minioPathCache = "MinioPathCache:";
}

View File

@@ -0,0 +1,37 @@
package com.aida.lanecarford.common.enums;
import java.util.stream.Stream;
/**
* @description: 操作类型 登入 忘记密码
**/
public enum AuthenticationOperationTypeEnum {
/**
* 登入
*/
LOGIN,
/**
* 异常ip
*/
EXCEPTION_IP,
/**
* 绑定邮箱
*/
BIND_MAILBOX,
/**
* 忘记密码
*/
FORGET_PWD,
/**
* 更改邮箱
*/
CHANGE_MAILBOX,
/**
* 注册
*/
REGISTER;
public static AuthenticationOperationTypeEnum of(String name) {
return Stream.of(AuthenticationOperationTypeEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,8 @@
package com.aida.lanecarford.common.enums;
public enum LanguageEnum {
CHINESE,
ENGLISH;
}

View File

@@ -0,0 +1,33 @@
package com.aida.lanecarford.common.enums;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.stream.Stream;
@Getter
@AllArgsConstructor
@Schema(description = "生成状态枚举")
public enum StatusEnum {
@Schema(description = "等待中")
PENDING(0),
@Schema(description = "成功")
SUCCEEDED(1),
@Schema(description = "失败")
FAILED(2),
@Schema(description = "运行中")
RUNNING(3);
private int code;
public static StatusEnum of(int code) {
return Stream.of(StatusEnum.values()).filter(v -> v.getCode() == code).findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,25 @@
package com.aida.lanecarford.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.stream.Stream;
@Getter
@AllArgsConstructor
public enum StylistPathEnum {
STYLIST_ONE("crystal", "lanecarford/stylist_guide/latest/crystal_en.md"),
STYLIST_TWO("mini", "lanecarford/stylist_guide/latest/mini_en.md");
private String name;
private String path;
public static StylistPathEnum of(String name) {
return Stream.of(StylistPathEnum.values()).filter(v -> v.getName().equals(name)).findFirst().orElse(null);
}
}

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

@@ -0,0 +1,73 @@
package com.aida.lanecarford.common.security;
import com.aida.lanecarford.common.security.config.JwtProperties;
import com.aida.lanecarford.common.security.context.UserContext;
import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.util.CacheUtil;
import com.aida.lanecarford.vo.AuthPrincipalVO;
import com.alibaba.fastjson.JSONObject;
import io.netty.util.internal.StringUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Objects;
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Resource
private CacheUtil cacheUtil;
@Autowired
private JwtUtil jwtUtil;
@Resource
private JwtProperties jwtProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return false;
}
String jwtToken = request.getHeader(jwtProperties.getJwtTokenHeader());
if (jwtToken == null || !jwtToken.startsWith(jwtProperties.getJwtTokenPrefix())) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
boolean validated = jwtUtil.validateToken(jwtToken);
if (validated) {
String extracted = jwtUtil.extractUserinfo(jwtToken);
if (StringUtil.isNullOrEmpty(extracted)) {
log.warn("TOKEN已过期请重新登录(token without userInfo)");
throw new BusinessException("Token has expired, please log in again.");
}
AuthPrincipalVO authPrincipalVO = JSONObject.parseObject(extracted, AuthPrincipalVO.class);
// 先清空当前线程变量,防止上一个线程遗留
UserContext.delete();
// 存取用户信息到缓存
UserContext.setUserHolder(authPrincipalVO);
// 校验当前token与缓存中数据是否一致
Object token = cacheUtil.getToken(authPrincipalVO.getId());
if (Objects.isNull(token)) {
log.warn("TOKEN已过期请重新登录(local cache empty)");
throw new BusinessException("Token has expired, please log in again.");
} else if (!token.toString().equals(jwtToken)) {
log.warn("TOKEN已过期请重新登录(token not match local cache)");
throw new BusinessException("Token has expired, please log in again.");
}
return true;
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}

View File

@@ -0,0 +1,124 @@
package com.aida.lanecarford.common.security;
import com.aida.lanecarford.common.security.config.JwtProperties;
import com.aida.lanecarford.vo.AuthPrincipalVO;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import io.jsonwebtoken.*;
@Slf4j
@Component
public class JwtUtil {
@Resource
private JwtProperties jwtProperties;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtProperties.getJwtSecret().getBytes());
}
// 生成JWT token
public String generateToken(AuthPrincipalVO principal) {
String token = Jwts.builder()
.subject(JSONObject.toJSONString(principal))
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getJwtExpiration()))
.signWith(getSigningKey())
.compact();
return jwtProperties.getJwtTokenPrefix() + token;
}
// 从token中提取用户信息
public String extractUserinfo(String token) {
return parser(token).getSubject();
}
// 验证token是否有效
public boolean validateToken(String token) {
try {
Claims claims = parser(token);
return claims != null && !claims.isEmpty() && !isTokenExpired(claims);
} catch (Exception e) {
log.debug("Token验证失败: {}", e.getMessage());
return false;
}
}
// 解析token - 适配 JJWT 0.12.x
public Claims parser(String token) {
try {
// 移除前缀
if (token.startsWith(jwtProperties.getJwtTokenPrefix())) {
token = token.substring(jwtProperties.getJwtTokenPrefix().length()).trim();
}
return parseClaims(token);
} catch (ExpiredJwtException e) {
log.error("Token已过期: {}", e.getMessage());
throw e;
} catch (SecurityException | MalformedJwtException e) {
log.error("Token格式错误: {}", e.getMessage());
return null;
} catch (Exception e) {
log.error("解析Token失败: {}", e.getMessage());
return null;
}
}
// 检查token是否过期
private boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
// 解析Claims共用方法- 适配 JJWT 0.12.x
private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// 新增刷新token
public String refreshToken(String token) {
Claims claims = parser(token);
if (claims == null || isTokenExpired(claims)) {
throw new JwtException("无法刷新过期的token");
}
// 使用原始主题创建新token
return Jwts.builder()
.subject(claims.getSubject())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getJwtExpiration()))
.signWith(getSigningKey())
.compact();
}
// 新增获取token剩余时间毫秒
public long getRemainingTime(String token) {
try {
Claims claims = parser(token);
if (claims != null) {
long expirationTime = claims.getExpiration().getTime();
long currentTime = System.currentTimeMillis();
return Math.max(0, expirationTime - currentTime);
}
} catch (Exception e) {
log.debug("获取token剩余时间失败: {}", e.getMessage());
}
return 0;
}
}

View File

@@ -0,0 +1,22 @@
package com.aida.lanecarford.common.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* JWT配置类
*/
@Data
@ConfigurationProperties(prefix = "spring.security")
@Configuration
public class JwtProperties {
private String jwtSecret;
private String jwtTokenHeader;
private String jwtTokenPrefix;
private long jwtExpiration;
}

View File

@@ -0,0 +1,19 @@
package com.aida.lanecarford.common.security.context;
import com.aida.lanecarford.vo.AuthPrincipalVO;
public class UserContext {
private static ThreadLocal<AuthPrincipalVO> userHolder = new ThreadLocal<AuthPrincipalVO>();
public static AuthPrincipalVO getUserHolder() {
return userHolder.get();
}
public static void delete() {
userHolder.remove();
}
public static void setUserHolder(AuthPrincipalVO authPrincipalVo) {
userHolder.set(authPrincipalVo);
}
}

View File

@@ -0,0 +1,58 @@
package com.aida.lanecarford.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MinIO 配置类
* 用于配置 MinIO 客户端连接参数
*
* @author Aida
* @since 2024-01-01
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/**
* MinIO 服务端点
*/
private String endpoint;
/**
* 访问密钥
*/
private String accessKey;
/**
* 秘密密钥
*/
private String secretKey;
/**
* 默认存储桶名称
*/
private String bucketName;
/**
* 文件访问URL前缀
*/
private String urlPrefix;
/**
* 创建 MinIO 客户端 Bean
*
* @return MinioClient 实例
*/
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}

View File

@@ -0,0 +1,30 @@
package com.aida.lanecarford.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Spring提供的GenericJackson2JsonRedisSerializer
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -20,7 +20,7 @@ import java.util.List;
/**
* Swagger配置类
* 提供完整的API文档配置包括安全认证、服务器信息和标签分类
*
*
* @author AI Assistant
* @since 2024-01-01
*/
@@ -116,9 +116,10 @@ public class SwaggerConfig {
.in(SecurityScheme.In.COOKIE)
.name("JSESSIONID")
.description("Session认证通过登录接口获取"))
.addSecuritySchemes("basicAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
.addSecuritySchemes("CustomAuth", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.name("Authorization")
.in(SecurityScheme.In.HEADER)
.description("基础认证(仅用于开发测试)"));
}
@@ -128,7 +129,7 @@ public class SwaggerConfig {
private List<SecurityRequirement> createSecurityRequirements() {
return Arrays.asList(
new SecurityRequirement().addList("sessionAuth"),
new SecurityRequirement().addList("basicAuth")
new SecurityRequirement().addList("CustomAuth")
);
}

View File

@@ -1,13 +1,18 @@
package com.aida.lanecarford.config;
import com.aida.lanecarford.common.security.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
/**
* Web配置类 - 纯API后端服务
*
*
* @author AI Assistant
* @since 2024-01-01
*/
@@ -27,6 +32,17 @@ public class WebConfig implements WebMvcConfigurer {
.maxAge(3600);
}
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**/**") // 保护这些路径
.excludePathPatterns(Arrays.asList("/api/auth/precheckEmail", "/api/auth/registerOrLogin",
"/api/auth/forgotPwd", "/api/style/callback")); // 排除登录接口
}
/**
* 配置资源处理 - 仅保留API文档和文件上传
*/
@@ -35,11 +51,11 @@ public class WebConfig implements WebMvcConfigurer {
// 配置上传文件的访问路径
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:uploads/");
// 配置Swagger UI资源
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/");
registry.addResourceHandler("/v3/api-docs/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

View File

@@ -0,0 +1,40 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.service.ChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@Slf4j
@RestController
@RequestMapping("/api/llm")
@Tag(name = "LLM对话管理", description = "大语言模型流式对话相关API接口")
public class ChatController {
@Resource
private ChatService chatService;
@CrossOrigin
@Operation(
summary = "流式对话",
description = "与大语言模型进行流式对话返回Server-Sent Events数据流"
)
@GetMapping(value = "/streamChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(
@Parameter(description = "用户输入的消息内容", example = "你好,请介绍一下自己")
@RequestParam(required = false) String message,
@Parameter(description = "会话ID", example = "123456", required = true)
@RequestParam Long sessionId,
@Parameter(description = "性别", example = "male | female", required = true)
@RequestParam String gender) {
return chatService.streamChat(message, sessionId, gender);
}
}

View File

@@ -1,21 +1,40 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.dto.BaseRequest;
import com.aida.lanecarford.service.CustomerService;
import com.aida.lanecarford.vo.CustomerCheckInVO;
import com.aida.lanecarford.vo.CustomerVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
/**
* 顾客控制器
*
* @author AI Assistant
* @since 2024-01-01
*/
@RestController
@RequestMapping("/api/customers")
@RequiredArgsConstructor
@Tag(name = "顾客管理", description = "顾客入店登记、信息查询等相关API接口")
public class CustomerController {
private final CustomerService customerService;
@Operation(
summary = "顾客入店登记",
description = "验证顾客身份并创建入店记录,如果是新顾客则自动注册到系统中。"
)
@GetMapping("/checkIn")
public ApiResponse<CustomerCheckInVO> customerCheckIn(@RequestParam String name, @RequestParam String email) {
return ApiResponse.success(customerService.customerCheckIn(name, email));
}
@PostMapping("/getAllCustomer")
public ApiResponse<IPage<CustomerVO>> getAllCustomer(@Valid @RequestBody BaseRequest request) {
return ApiResponse.success(customerService.getAllCustomer(request));
}
}

View File

@@ -1,7 +1,12 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.dto.CustomerPhotoDto;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.aida.lanecarford.service.CustomerPhotoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -18,4 +23,10 @@ public class CustomerPhotoController {
private final CustomerPhotoService customerPhotoService;
@PostMapping("/upload")
public ApiResponse<CustomerPhoto> upload(@ModelAttribute CustomerPhotoDto customerPhotoDto) {
CustomerPhoto customerPhoto = customerPhotoService.upload(customerPhotoDto);
return ApiResponse.success(customerPhoto);
}
}

View File

@@ -0,0 +1,99 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.entity.User;
import com.aida.lanecarford.service.LoginService;
import com.aida.lanecarford.vo.LoginVO;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 用户认证控制器
*/
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Tag(name = "用户认证管理", description = "用户注册、登录、登出等认证相关API接口")
public class LoginController {
private final LoginService loginService;
@Operation(
summary = "预检查并发送邮箱验证码",
description = "根据操作类型验证邮箱有效性并发送验证码。支持注册、登录、忘记密码三种操作类型。"
)
@PostMapping("/precheckAndSendEmail")
@Hidden
public ApiResponse<String> preCheckAndSendEmail(@Valid @RequestBody LoginRequest loginRequest) {
loginService.preCheckAndSendEmail(loginRequest);
return ApiResponse.success("验证码已发送到您的邮箱");
}
@Operation(
summary = "检查邮箱",
description = "根据操作类型验证邮箱有效性并发送验证码。仅支持忘记密码。"
)
@GetMapping("/precheckEmail")
public ApiResponse<String> precheckForgotPwdAndSendEmail(@Valid @RequestParam String email) {
loginService.precheckForgotPwdAndSendEmail(email);
return ApiResponse.success("验证码已发送到您的邮箱");
}
@Operation(
summary = "用户注册或登录",
description = "通过验证码完成用户注册或登录返回JWT令牌和用户信息。"
)
@PostMapping("/registerOrLogin")
public ApiResponse<LoginVO> registerOrLogin(@Valid @RequestBody LoginRequest loginRequest) {
return ApiResponse.success(loginService.registerOrLogin(loginRequest));
}
@Operation(
summary = "用户登出",
description = "清除用户登录状态使当前JWT令牌失效。"
)
@GetMapping("/logout")
public ApiResponse<String> logout() {
loginService.logout();
return ApiResponse.success("登出成功");
}
@Operation(
summary = "忘记密码",
description = "通过邮箱验证码重置用户密码。需要先获取验证码,然后提供新密码。"
)
@PostMapping("/forgotPwd")
public ApiResponse<String> forgotPwd(@Valid @RequestBody LoginRequest loginRequest) {
loginService.forgotPwd(loginRequest);
return ApiResponse.success("密码重置成功");
}
@Operation(
summary = "检查登录状态",
description = "验证当前用户的登录状态是否有效检查JWT令牌是否过期。"
)
@GetMapping("/checkLoginStatus")
public ApiResponse<String> checkLoginStatus() {
boolean isLogin = loginService.checkLoginStatus();
if (isLogin) {
return ApiResponse.success("用户已登录");
} else {
return ApiResponse.error("Please log in again.");
}
}
@Operation(
summary = "获取用户信息",
description = "通过token获取当前用户信息"
)
@GetMapping("/getUserInfo")
public ApiResponse<User> getUserInfo() {
return ApiResponse.success(loginService.getUserInfo());
}
}

View File

@@ -1,9 +1,20 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.dto.OutfitCallbackDTO;
import com.aida.lanecarford.dto.RequestOutfitDTO;
import com.aida.lanecarford.service.StyleService;
import com.aida.lanecarford.vo.OutfitResultVO;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 风格配置控制器
@@ -11,11 +22,60 @@ import org.springframework.web.bind.annotation.RestController;
* @author AI Assistant
* @since 2024-01-01
*/
@Slf4j
@RestController
@RequestMapping("/api/styles")
@RequestMapping("/api/style")
@RequiredArgsConstructor
@Tag(name = "穿搭风格管理", description = "AI穿搭推荐和结果查询相关API接口")
public class StyleController {
private final StyleService styleService;
/**
* 请求AI穿搭推荐
* 提交穿搭需求给AI模型异步生成穿搭方案
*
* @param requestOutfitDTO 穿搭请求参数DTO
* @return 包含请求ID列表的响应结果
*/
@Operation(
summary = "请求AI穿搭推荐",
description = "提交用户的穿搭需求给AI模型进行异步处理返回请求ID用于后续结果查询"
)
@PostMapping("/requestOutfit")
public ApiResponse<List<String>> requestOutfit(@Valid @RequestBody RequestOutfitDTO requestOutfitDTO) {
return ApiResponse.success(styleService.requestOutfit(requestOutfitDTO));
}
/**
* AI服务回调接口
* 接收AI服务处理完成后的回调通知更新穿搭结果状态
* 注意此接口为内部接口供AI服务调用不对外暴露文档
*
* @param callbackDTO AI回调数据DTO
*/
@Hidden
@PostMapping("/callback")
public void callback(@RequestBody OutfitCallbackDTO callbackDTO) {
styleService.callback(callbackDTO);
}
/**
* 获取穿搭结果
* 根据请求ID列表查询AI生成的穿搭方案结果
*
* @param requestIDs 请求ID列表通过requestOutfit接口获取
* @return 穿搭结果视图对象列表
*/
@Operation(
summary = "获取穿搭结果",
description = "根据请求ID列表查询AI生成的穿搭方案结果支持批量查询"
)
@GetMapping("/getOutfitResult")
public ApiResponse<List<OutfitResultVO>> getOutfitResult(
@Parameter(description = "请求ID列表", required = true, example = "[a22019ac-9db2-4076-9953-42d65e8120ec, 20217a09-435d-4b34-962a-3af59881c6d9]")
@RequestParam List<String> requestIDs) {
return ApiResponse.success(styleService.getOutfitResult(requestIDs));
}
}

View File

@@ -1,9 +1,17 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.entity.TryOnEffect;
import com.aida.lanecarford.service.TryOnEffectService;
import com.aida.lanecarford.vo.TryOnResultVo;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 试穿效果控制器
@@ -14,8 +22,59 @@ 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 = "根据服装模特照片生成试穿效果其中styleId是必选当二次生成时要带上相关参数比如顾客照片")
@PostMapping("/generate")
public ApiResponse<TryOnResultVo> generateTryOnEffect(
@Parameter(description = "试穿效果请求参数", required = true)
@Valid @RequestBody TryOnEffect tryOnEffectDto) {
TryOnResultVo tryOnResultVo = tryOnEffectService.generateTryOnEffect(tryOnEffectDto);
return ApiResponse.success(tryOnResultVo);
}
@Operation(summary = "获取收藏的试穿效果", description = "对应library页面点击details后的显示参数为进店记录id")
@GetMapping("/favorites/{visitRecordId}")
public ApiResponse<List<TryOnResultVo>> getFavoriteTryOnEffects(
@Parameter(description = "进店记录ID", required = true)
@PathVariable Long visitRecordId) {
List<TryOnResultVo> tryOnResultVos = tryOnEffectService.getFavoriteTryOnEffects(visitRecordId);
return ApiResponse.success(tryOnResultVos);
}
@GetMapping("/style/{styleId}")
@Operation(summary = "获取某套服装的所有生成结果", description = "对应customize your look页面点击finish后的显示")
public ApiResponse<List<TryOnResultVo>> getTryOnEffectsByStyleId(
@Parameter(description = "服装ID", required = true)
@PathVariable Long styleId) {
List<TryOnResultVo> tryOnResultVos = tryOnEffectService.getTryOnEffectsByStyleId(styleId);
return ApiResponse.success(tryOnResultVos);
}
/**
* 设置喜欢的试穿效果
*/
@Operation(summary = "设置喜欢的试穿效果", description = "将指定试穿效果设置为收藏")
@PostMapping("/set-favorite/{tryOnId}")
public ApiResponse<Void> setFavoriteTryOnEffect(
@Parameter(description = "试穿效果ID", required = true)
@PathVariable Long tryOnId) {
tryOnEffectService.setFavoriteTryOnEffect(tryOnId);
return ApiResponse.success();
}
/**
* 取消喜欢的试穿效果
*/
@Operation(summary = "取消喜欢的试穿效果", description = "取消指定试穿效果的收藏")
@PostMapping("/cancel-favorite/{tryOnId}")
public ApiResponse<Void> cancelFavoriteTryOnEffect(
@Parameter(description = "试穿效果ID", required = true)
@PathVariable Long tryOnId) {
tryOnEffectService.cancelFavoriteTryOnEffect(tryOnId);
return ApiResponse.success();
}
}

View File

@@ -1,9 +1,16 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.entity.VisitRecord;
import com.aida.lanecarford.service.VisitRecordService;
import com.aida.lanecarford.vo.LibraryVo;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 进店记录控制器
@@ -11,6 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
* @author AI Assistant
* @since 2024-01-01
*/
@Slf4j
@RestController
@RequestMapping("/api/visit-records")
@RequiredArgsConstructor
@@ -18,4 +26,41 @@ public class VisitRecordController {
private final VisitRecordService visitRecordService;
/**
* 根据ID删除进店记录逻辑删除
*
* @param id 进店记录ID
* @return 删除结果
*/
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteById(@PathVariable Long id) {
log.info("开始删除进店记录ID: {}", id);
Boolean result = visitRecordService.delete(id);
if (result) {
log.info("进店记录删除成功ID: {}", id);
return ApiResponse.success("Visit record deleted successfully");
} else {
log.warn("进店记录删除失败ID: {}", id);
return ApiResponse.error("Failed to delete visit record");
}
}
//根据顾客ID查询所有进店记录
@GetMapping("/customer/{customerId}")
@Operation(summary = "根据顾客ID查询所有进店记录",description = "打开libiary页面调用这个接口参数为当前顾客的id")
public ApiResponse<List<LibraryVo>> getByCustomerId(@PathVariable Long customerId) {
log.info("开始查询顾客ID为{}的进店记录", customerId);
List<LibraryVo> result = visitRecordService.getByCustomerId(customerId);
if (result != null && !result.isEmpty()) {
log.info("查询成功顾客ID为{}的进店记录为:{}", customerId, result);
return ApiResponse.success(result);
} else {
log.warn("没有找到顾客ID为{}的进店记录", customerId);
return ApiResponse.error("No visit records found for customer ID: " + customerId);
}
}
}

View File

@@ -5,17 +5,15 @@ import lombok.Data;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.io.Serializable;
/**
* 请求参数基类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@Schema(description = "请求参数基类")
public abstract class BaseRequest implements Serializable {
public class BaseRequest implements Serializable {
private static final long serialVersionUID = 1L;
@@ -71,8 +69,8 @@ public abstract class BaseRequest implements Serializable {
* 是否有时间范围筛选
*/
public boolean hasTimeRange() {
return startTime != null && !startTime.trim().isEmpty()
&& endTime != null && !endTime.trim().isEmpty();
return startTime != null && !startTime.trim().isEmpty()
&& endTime != null && !endTime.trim().isEmpty();
}
/**

View File

@@ -0,0 +1,15 @@
package com.aida.lanecarford.dto;
import com.aida.lanecarford.entity.CustomerPhoto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Schema(description = "顾客照片数据传输对象")
public class CustomerPhotoDto extends CustomerPhoto {
@NotNull(message = "file.cannot.be.empty")
@Schema(description = "上传的照片文件", required = true, type = "string", format = "binary")
private MultipartFile file;
}

View File

@@ -0,0 +1,63 @@
package com.aida.lanecarford.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
@Schema(description = "登录请求数据传输对象")
public class LoginRequest {
@Schema(
description = "用户名(仅注册时需要)",
example = "张三",
maxLength = 50
)
private String name;
@NotBlank(message = "email cannot be empty")
@Email(message = "Email format is incorrect.")
@Schema(
description = "邮箱地址",
example = "user@example.com",
required = true,
format = "email"
)
private String email;
@NotBlank(message = "password cannot be empty")
@Size(min = 6, message = "Password must be at least 6 characters.")
@Schema(
description = "密码至少6位字符",
example = "password123",
required = true,
minLength = 6,
maxLength = 100
)
private String password;
@NotBlank(message = "operation type cannot be empty")
@Schema(
description = "操作类型",
example = "REGISTER",
required = true,
allowableValues = {"REGISTER", "LOGIN", "FORGET_PWD"/*,"CHANGE_MAILBOX","EXCEPTION_IP","BIND_MAILBOX"*/}
)
private String operationType;
@Schema(
description = "邮箱验证码6位数字",
example = "123456",
pattern = "\\d{6}",
minLength = 6,
maxLength = 6
)
private String verifyCode;
}

View File

@@ -0,0 +1,19 @@
package com.aida.lanecarford.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class OutfitCallbackDTO {
private String outfit_id;
// 取值范围ok || failed || stop
private String status;
private String path;
private List<Map<String, String>> items;
}

View File

@@ -0,0 +1,55 @@
package com.aida.lanecarford.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "AI穿搭推荐请求参数")
public class RequestOutfitDTO {
@Schema(
description = "顾客ID",
example = "1",
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotNull(message = "customer id cannot be empty")
private Long customerId;
@Schema(
description = "顾客进店记录ID",
example = "1",
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotNull(message = "customer check-in id cannot be empty")
private Long checkInId;
@Schema(
description = "选择的设计师风格",
example = "mini",
requiredMode = Schema.RequiredMode.REQUIRED,
allowableValues = {"mini", " crystal"}
)
@NotNull(message = "please select a stylist")
private String stylist;
@Schema(
description = "性别",
example = "female",
requiredMode = Schema.RequiredMode.REQUIRED,
allowableValues = {"male", "female"/*, "unisex"*/}
)
@NotNull(message = "please select gender")
private String gender;
@Schema(
description = "生成穿搭方案的数量",
example = "4",
defaultValue = "1",
minimum = "1",
maximum = "4"
)
private int num = 1;
}

View File

@@ -1,6 +1,8 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@@ -13,29 +15,36 @@ import java.time.LocalDateTime;
* @since 1.0.0
*/
@Data
@Schema(description = "基础实体类")
public abstract class BaseEntity {
/**
* 主键ID
*/
@Schema(description = "主键ID", example = "1")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 创建时间
*/
@Schema(description = "创建时间", example = "2024-01-01 12:00:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@Schema(description = "更新时间", example = "2024-01-01 12:00:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
/**
* 逻辑删除标志0-未删除1-已删除
*/
@Schema(description = "逻辑删除标志", example = "0", allowableValues = {"0", "1"})
@TableLogic
@TableField(value = "deleted")
private Integer deleted;

View File

@@ -11,23 +11,14 @@ import java.time.LocalDateTime;
/**
* 顾客实体类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = true)
@TableName("customers")
public class Customer {
/**
* 顾客ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
public class Customer extends BaseEntity {
/**
* 顾客姓名
@@ -58,16 +49,4 @@ public class Customer {
*/
@TableField("age_range")
private String ageRange;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@@ -1,6 +1,7 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -19,49 +20,38 @@ import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = true)
@TableName("customer_photos")
public class CustomerPhoto {
/**
* 顾客照片ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "顾客照片信息")
public class CustomerPhoto extends BaseEntity {
/**
* 顾客ID
*/
@Schema(description = "顾客ID", example = "1", required = true)
@TableField("customer_id")
private Long customerId;
/**
* 进店记录ID
*/
@Schema(description = "进店记录ID", example = "1", required = true)
@TableField("visit_record_id")
private Long visitRecordId;
/**
* 照片URL
*/
@Schema(description = "照片URL", example = "https://example.com/photo.jpg", required = true)
@TableField("photo_url")
private String photoUrl;
/**
* 是否为主照片(0-否,1-是)
*/
@Schema(description = "是否为主照片", example = "1", allowableValues = {"0", "1"})
@TableField("is_primary")
private Integer isPrimary;
/**
* 上传时间
*/
@TableField("upload_time")
private LocalDateTime uploadTime;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
// 注意uploadTime、createdTime、updatedTime 字段已在 BaseEntity 中定义
}

View File

@@ -19,15 +19,9 @@ import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = true)
@TableName("model_photos")
public class ModelPhoto {
/**
* 模特照片ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
public class ModelPhoto extends BaseEntity {
/**
* 模特照片URL
@@ -59,15 +53,5 @@ public class ModelPhoto {
@TableField("sort_order")
private Integer sortOrder;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
// 注意createdTime、updatedTime 字段已在 BaseEntity 中定义
}

View File

@@ -0,0 +1,34 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("outfit_request")
public class OutfitRequest extends BaseEntity{
/**
* 顾客id
*/
private Long customerId;
/**
* 进店记录id
*/
private Long visitRecordId;
/**
* 选择的设计师风格
*/
private String stylist;
/**
* 选择的性别
*/
private String gender;
/**
* 当前任务状态
*/
private int status;
}

View File

@@ -19,15 +19,9 @@ import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = true)
@TableName("sales")
public class Sales {
/**
* 导购ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
public class Sales extends BaseEntity {
/**
* 用户名
@@ -83,15 +77,5 @@ public class Sales {
@TableField("is_active")
private Integer isActive;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
// 注意createdTime、updatedTime 字段已在 BaseEntity 中定义
}

View File

@@ -7,7 +7,7 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.util.List;
/**
* 风格配置实体类
@@ -19,15 +19,9 @@ import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = true)
@TableName("styles")
public class Style {
/**
* 风格配置ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
public class Style extends BaseEntity {
/**
* 顾客ID
@@ -41,6 +35,11 @@ public class Style {
@TableField("visit_record_id")
private Long visitRecordId;
/**
* 请求搭配id
*/
private Long outfitRequestId;
/**
* 是否选中(0-未选中,1-已选中)
*/
@@ -60,10 +59,15 @@ public class Style {
private String pythonRequestId;
/**
* 生成状态(0-处理中,1-已完成,2-失败)
* 单品的唯一id
*/
private String items;
/**
* 生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)
*/
@TableField("generation_status")
private Integer generationStatus;
private int generationStatus;
/**
* 错误信息
@@ -71,15 +75,5 @@ public class Style {
@TableField("error_message")
private String errorMessage;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
// 注意createdTime、updatedTime 字段已在 BaseEntity 中定义
}

View File

@@ -1,6 +1,7 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -19,91 +20,100 @@ import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = true)
@TableName("try_on_effects")
public class TryOnEffect {
/**
* 试穿效果ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
public class TryOnEffect extends BaseEntity {
/**
* 顾客ID
*/
@Schema(description = "顾客ID", example = "1", required = true)
@TableField("customer_id")
private Long customerId;
/**
* 进店记录ID
*/
@Schema(description = "进店记录ID", example = "1", required = true)
@TableField("visit_record_id")
private Long visitRecordId;
/**
* 顾客照片ID
* 衣服搭配ID,当is_regenerated为0时才会有值
*/
@Schema(description = "衣服搭配ID,当is_regenerated为0时才会有值", example = "1", required = true)
@TableField("style_id")
private Long styleId;
/**
* 顾客照片ID,当is_regenerated为1时才会有值
*/
@Schema(description = "顾客照片ID,当is_regenerated为1时才会有值", example = "1", required = true)
@TableField("customer_photo_id")
private Long customerPhotoId;
/**
* 模特照片ID
*/
@Schema(description = "模特照片ID目前没有这个功能", example = "1", required = true)
@TableField("model_photo_id")
private Long modelPhotoId;
/**
* 试穿效果图URL
*/
@TableField("try_on_image_url")
private String tryOnImageUrl;
/**
* 提示词
* 提示词,当is_regenerated为1时才会有值
*/
@Schema(description = "提示词,当is_regenerated为1时才会有值", example = "1", required = true)
@TableField("prompt")
private String prompt;
/**
* Python请求ID
*/
@TableField("python_request_id")
private String pythonRequestId;
/**
* 生成状态(0-处理中,1-已完成,2-失败)
*/
@TableField("generation_status")
private Integer generationStatus;
/**
* 错误信息
*/
@TableField("error_message")
private String errorMessage;
/**
* 是否收藏(0-否,1-是)
*/
@TableField("is_favorite")
private Integer isFavorite;
/**
* 原始试穿效果ID(用于重新生成)
* 原试穿效果ID,当is_regenerated为1时才会有值
*/
@Schema(description = "原试穿效果ID,当is_regenerated为1时才会有值", example = "1", required = true)
@TableField("original_try_on_id")
private Long originalTryOnId;
/**
* 创建时间
* 是否由生成结果重新生成(0-否,1-是)
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@Schema(description = "是否由生成结果重新生成(0-否,1-是)", example = "1", required = true)
@TableField("is_regenerated")
private Integer isRegenerated;
/**
* 更新时间
* 试穿结果图片URL
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
@Schema(description = "试穿结果图片URL", example = "1", required = false)
@TableField("result_image_url")
private String resultImageUrl;
/**
* 请求ID
*/
@Schema(description = "请求ID", example = "1", required = false)
@TableField("request_id")
private String requestId;
/**
* 生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)
*/
@Schema(description = "生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)", example = "1", required = false)
@TableField("generation_status")
private String generationStatus;
/**
* 错误信息
*/
@Schema(description = "错误信息", example = "1", required = false)
@TableField("error_message")
private String errorMessage;
/**
* 是否喜欢(0-否,1-是)
*/
@Schema(description = "是否喜欢(0-否,1-是)", example = "1", required = false)
@TableField("is_favorite")
private Integer isFavorite;
// 注意createdTime、updatedTime 字段已在 BaseEntity 中定义
}

View File

@@ -0,0 +1,65 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 用户信息表
*
* @author xupei
* @since 2025-10-21
*/
@Data
@TableName("user")
public class User extends BaseEntity {
/**
* 用户昵称
*/
private String username;
/**
* 用户邮箱
*/
private String email;
/**
* 账号密码
*/
private String password;
/**
* 用户性别
*/
private String gender;
/**
* 员工编号
*/
private String employeeId;
/**
* 门店ID
*/
private String storeId;
/**
* 门店名称
*/
private String storeName;
/**
* 手机号
*/
private String phone;
/**
* 用户头像
*/
private String avatar;
/**
* 系统语言
*/
private String language;
/**
* 是否启用(0-禁用,1-启用)
*/
private Integer isActive;
}

View File

@@ -20,15 +20,9 @@ import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = true)
@TableName("visit_records")
public class VisitRecord {
/**
* 进店记录ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
public class VisitRecord extends BaseEntity {
/**
* 顾客ID
@@ -39,8 +33,8 @@ public class VisitRecord {
/**
* 导购ID
*/
@TableField("sales_id")
private Long salesId;
@TableField("user_id")
private Long userId;
/**
* 进店日期
@@ -54,17 +48,6 @@ public class VisitRecord {
@TableField("visit_time")
private LocalDateTime visitTime;
/**
* 会话ID
*/
@TableField("session_id")
private String sessionId;
/**
* 状态(0-已结束,1-进行中)
*/
@TableField("status")
private Integer status;
/**
* 备注
@@ -72,15 +55,5 @@ public class VisitRecord {
@TableField("notes")
private String notes;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
/**
* 更新时间
*/
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
// 注意createdTime、updatedTime 字段已在 BaseEntity 中定义
}

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(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(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(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(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(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(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(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(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,9 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.OutfitRequest;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OutfitRequestMapper extends BaseMapper<OutfitRequest> {
}

View File

@@ -0,0 +1,13 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 用户信息Mapper接口
*
* @author xupei
* @since 2025-10-21
*/
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,8 @@
package com.aida.lanecarford.service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
public interface ChatService {
SseEmitter streamChat(String message, Long sessionId, String gender);
}

View File

@@ -1,5 +1,6 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.dto.CustomerPhotoDto;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.baomidou.mybatisplus.extension.service.IService;
@@ -11,4 +12,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface CustomerPhotoService extends IService<CustomerPhoto> {
CustomerPhoto upload(CustomerPhotoDto customerPhotoDto);
}

View File

@@ -1,14 +1,19 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.dto.BaseRequest;
import com.aida.lanecarford.entity.Customer;
import com.aida.lanecarford.vo.CustomerCheckInVO;
import com.aida.lanecarford.vo.CustomerVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 顾客服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface CustomerService extends IService<Customer> {
CustomerCheckInVO customerCheckIn(String name, String email);
IPage<CustomerVO> getAllCustomer(BaseRequest request);
}

View File

@@ -0,0 +1,20 @@
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);
}

View File

@@ -0,0 +1,31 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.entity.User;
import com.aida.lanecarford.vo.LoginVO;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.stereotype.Service;
/**
* 登录服务接口
*
* @author xupei
* @since 2025-10-21
*/
@Service
public interface LoginService extends IService<User> {
void preCheckAndSendEmail(LoginRequest loginRequest);
void precheckForgotPwdAndSendEmail(String email);
LoginVO registerOrLogin(LoginRequest loginRequest);
void logout();
void forgotPwd(LoginRequest loginRequest);
boolean checkLoginStatus();
User getUserInfo();
}

View File

@@ -1,14 +1,22 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.dto.OutfitCallbackDTO;
import com.aida.lanecarford.dto.RequestOutfitDTO;
import com.aida.lanecarford.entity.Style;
import com.aida.lanecarford.vo.OutfitResultVO;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* 风格配置服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface StyleService extends IService<Style> {
List<String> requestOutfit(RequestOutfitDTO requestOutfitDTO);
void callback(OutfitCallbackDTO callbackDTO);
List<OutfitResultVO> getOutfitResult(List<String> requestIDs);
}

View File

@@ -1,7 +1,11 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.TryOnEffect;
import com.aida.lanecarford.vo.TryOnResultVo;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
import java.util.List;
/**
* 试穿效果服务接口
@@ -11,4 +15,21 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface TryOnEffectService extends IService<TryOnEffect> {
TryOnResultVo generateTryOnEffect(@Valid TryOnEffect tryOnEffectDto);
List<TryOnResultVo> getFavoriteTryOnEffects(Long visitRecordId);
/**
* 设置试穿效果为收藏
* @param tryOnId 试穿效果ID
*/
void setFavoriteTryOnEffect(Long tryOnId);
/**
* 取消试穿效果的收藏
* @param tryOnId 试穿效果ID
*/
void cancelFavoriteTryOnEffect(Long tryOnId);
List<TryOnResultVo> getTryOnEffectsByStyleId(Long styleId);
}

View File

@@ -1,14 +1,19 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.entity.VisitRecord;
import com.aida.lanecarford.vo.LibraryVo;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* 进店记录服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface VisitRecordService extends IService<VisitRecord> {
Boolean delete(Long id);
List<LibraryVo> getByCustomerId(Long customerId);
VisitRecord addRecord(Long customerId, Long userId);
}

View File

@@ -0,0 +1,216 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.CommonConstants;
import com.aida.lanecarford.service.ChatService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
public class ChatServiceImpl implements ChatService {
/**
* 流式对话方法 - 集成第三方AI接口
*/
public SseEmitter streamChat(String message, Long sessionId, String gender) {
SseEmitter emitter = new SseEmitter(0L);
CompletableFuture.runAsync(() -> {
try {
// 连接第三方AI服务
processAIStream(message, sessionId.toString(), gender, emitter);
emitter.complete();
} catch (Exception e) {
log.error("AI流式对话处理失败", e);
sendError(emitter, "AI服务处理失败: " + e.getMessage());
}
});
return emitter;
}
/**
* 处理第三方AI服务的流式响应
*/
private void processAIStream(String message, String sessionId, String gender, SseEmitter emitter) {
HttpURLConnection connection = null;
try {
// 1. 创建AI服务连接
connection = createAIConnection();
// 2. 发送请求到AI服务
sendToAI(connection, message, sessionId, gender);
// 3. 流式读取并转发响应
streamAIResponse(connection, emitter, sessionId);
} catch (Exception e) {
throw new RuntimeException("AI服务调用失败", e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* 创建AI服务连接
*/
private HttpURLConnection createAIConnection() throws IOException {
URL url = new URL(CommonConstants.CHAT);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "text/event-stream");
conn.setConnectTimeout(CommonConstants.CONN_TIMEOUT);
conn.setReadTimeout(CommonConstants.CONN_TIMEOUT);
return conn;
}
/**
* 发送请求到AI服务
*/
private void sendToAI(HttpURLConnection connection, String message, String sessionId, String gender)
throws IOException {
// 构建AI服务请求体
JSONObject requestBody = new JSONObject();
requestBody.put("user_message", message);
requestBody.put("user_id", sessionId);
requestBody.put("gender", gender); // 启用流式输出
log.info("发送请求到AI服务: sessionId={}, messageLength={}", sessionId, message.length());
try (OutputStream os = connection.getOutputStream()) {
byte[] input = requestBody.toJSONString().getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
os.flush();
}
}
/**
* 流式读取AI响应并转发
*/
private void streamAIResponse(HttpURLConnection connection, SseEmitter emitter, String sessionId)
throws IOException {
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
throw new RuntimeException("AI服务响应错误: " + responseCode);
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
String output = JSON.parseObject(line).getString("output");
// 不用存储数据,直接发送消息
sendRawData(output, emitter, sessionId);
}
// 发送结束信号
sendEndSignal(emitter, sessionId);
} catch (IOException e) {
// 检查是否是连接超时或中断
throw e;
}
}
/**
* 发送错误信息
*/
private void sendErrorChunk(String content, SseEmitter emitter, String sessionId) {
Map<String, Object> response = createResponse("error", content, sessionId);
sendToClient(emitter, response);
}
/**
* 发送原始数据
*/
private void sendRawChunk(JSONObject data, SseEmitter emitter, String sessionId) {
Map<String, Object> response = createResponse("raw", data.toJSONString(), sessionId);
sendToClient(emitter, response);
}
private void sendRawData(String rawData, SseEmitter emitter, String sessionId) {
Map<String, Object> response = createResponse("text", rawData, sessionId);
sendToClient(emitter, response);
}
/**
* 发送数据到客户端
*/
private void sendToClient(SseEmitter emitter, Map<String, Object> data) {
try {
emitter.send(SseEmitter.event()
.data(data)
.id(UUID.randomUUID().toString()));
} catch (IOException e) {
log.warn("发送数据到客户端失败,可能连接已关闭");
throw new RuntimeException("客户端连接已断开", e);
}
}
/**
* 发送错误信息
*/
private void sendError(SseEmitter emitter, String errorMessage) {
try {
Map<String, Object> errorData = createResponse("error", errorMessage, null);
emitter.send(SseEmitter.event()
.data(errorData)
.name("error"));
} catch (IOException e) {
log.error("发送错误信息失败", e);
}
}
/**
* 发送结束信号
*/
private void sendEndSignal(SseEmitter emitter, String sessionId) {
try {
Map<String, Object> endData = createResponse("end", "对话结束", sessionId);
emitter.send(SseEmitter.event()
.data(endData)
.name("end"));
} catch (IOException e) {
log.warn("发送结束信号失败", e);
}
}
/**
* 创建标准响应格式
*/
private Map<String, Object> createResponse(String type, String content, String sessionId) {
Map<String, Object> response = new HashMap<>();
response.put("type", type);
response.put("content", content);
response.put("sessionId", sessionId);
response.put("timestamp", System.currentTimeMillis());
return response;
}
}

View File

@@ -1,8 +1,12 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.MinioFileConstants;
import com.aida.lanecarford.dto.CustomerPhotoDto;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.aida.lanecarford.mapper.CustomerPhotoMapper;
import com.aida.lanecarford.service.CustomerPhotoService;
import com.aida.lanecarford.util.CopyUtil;
import com.aida.lanecarford.util.MinioUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -16,5 +20,21 @@ import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomerPhotoServiceImpl extends ServiceImpl<CustomerPhotoMapper, CustomerPhoto> implements CustomerPhotoService {
private final MinioUtil minioUtil;
@Override
public CustomerPhoto upload(CustomerPhotoDto customerPhotoDto) {
String logicalUrl = minioUtil.uploadFile(
customerPhotoDto.getFile(),
MinioFileConstants.FileType.CUSTOMER_PHOTO
);
CustomerPhoto customerPhoto = CopyUtil.copyObject(customerPhotoDto, CustomerPhoto.class);
customerPhoto.setPhotoUrl(logicalUrl);
this.save(customerPhoto);
return customerPhoto;
}
}

View File

@@ -1,20 +1,78 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.security.context.UserContext;
import com.aida.lanecarford.dto.BaseRequest;
import com.aida.lanecarford.entity.Customer;
import com.aida.lanecarford.entity.VisitRecord;
import com.aida.lanecarford.mapper.CustomerMapper;
import com.aida.lanecarford.service.CustomerService;
import com.aida.lanecarford.service.VisitRecordService;
import com.aida.lanecarford.util.CopyUtil;
import com.aida.lanecarford.vo.CustomerCheckInVO;
import com.aida.lanecarford.vo.CustomerVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 顾客服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {
@Resource
private VisitRecordService visitRecordService;
// 选择顾客登录并添加入店记录
public CustomerCheckInVO customerCheckIn(String name, String email) {
// 1. 判断当前顾客信息在数据库中是否有存储
LambdaQueryWrapper<Customer> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Customer::getName, name).eq(Customer::getEmail, email);
Customer customer = getOne(queryWrapper);
// 2. 没有,获取连卡佛数据库中的顾客信息,查询有没有当前用户
if (Objects.isNull(customer)) {
// todo 从连卡佛数据库查数据
// 先假设都找不到
// throw new BusinessException("This customer does not currently have a registered VIP account.");
// 如果找到了,则添加到数据库
// 3. 添加当前顾客到本系统数据库
customer = new Customer();
customer.setName(name);
customer.setEmail(email);
customer.setCreatedTime(LocalDateTime.now());
save(customer);
}
// 4. 添加入店记录
VisitRecord visitRecord = visitRecordService.addRecord(customer.getId(), UserContext.getUserHolder().getId());
return new CustomerCheckInVO(customer.getId(), visitRecord.getId());
}
// 获取所有的顾客名单
public IPage<CustomerVO> getAllCustomer(BaseRequest request) {
LambdaQueryWrapper<Customer> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.select(Customer::getName, Customer::getEmail);
Page<Customer> page = page(new Page<>(request.getCurrent(), request.getSize()), queryWrapper);
return page.convert(customer -> {
if (customer != null) {
return CopyUtil.copyObject(customer, CustomerVO.class);
}
return null;
});
}
}

View File

@@ -0,0 +1,80 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.MinioFileConstants;
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) {
try {
log.info("开始合成并上传图片,图片数量: {}",
imageUrls != null ? imageUrls.size() : 0);
// 参数验证
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);
// 使用新的上传方法返回逻辑URL
String logicalUrl = minioUtil.uploadBytes(
composedImageBytes,
MinioFileConstants.FileType.COMPOSED_IMAGE,
"image/jpeg"
);
log.info("图片合成并上传成功逻辑URL: {}", logicalUrl);
return logicalUrl;
} catch (Exception e) {
log.error("图片合成并上传失败: {}", e.getMessage(), e);
throw new RuntimeException("图片合成并上传失败: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,264 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.RedisURIConstants;
import com.aida.lanecarford.common.enums.AuthenticationOperationTypeEnum;
import com.aida.lanecarford.common.enums.LanguageEnum;
import com.aida.lanecarford.common.response.ResultEnum;
import com.aida.lanecarford.common.security.JwtUtil;
import com.aida.lanecarford.common.security.context.UserContext;
import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.entity.User;
import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.mapper.UserMapper;
import com.aida.lanecarford.service.LoginService;
import com.aida.lanecarford.util.CacheUtil;
import com.aida.lanecarford.util.RandomsUtil;
import com.aida.lanecarford.util.SendEmailUtil;
import com.aida.lanecarford.vo.AuthPrincipalVO;
import com.aida.lanecarford.vo.LoginVO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.netty.util.internal.StringUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
import static com.aida.lanecarford.common.enums.AuthenticationOperationTypeEnum.*;
/**
* 登录服务实现类
*
* @author xupei
* @since 2025-10-21
*/
@Service
public class LoginServiceImpl extends ServiceImpl<UserMapper, User> implements LoginService {
@Resource
private CacheUtil cacheUtil;
@Resource
private JwtUtil jwtUtil;
@Resource
private SendEmailUtil sendEmailUtil;
@Override
public void preCheckAndSendEmail(LoginRequest loginRequest) {
// 1. 验证邮箱是否存在 boolean
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getEmail, loginRequest.getEmail());
User user = getOne(queryWrapper);
// 2. 获取当前的操作类型
AuthenticationOperationTypeEnum operationTypeEnum = AuthenticationOperationTypeEnum.of(loginRequest.getOperationType());
// 3. 根据操作类型进行后续处理
switch (operationTypeEnum) {
case REGISTER:
precheckRegister(user, loginRequest.getEmail());
break;
case LOGIN:
precheckLogin(user, loginRequest);
break;
default:
throw new BusinessException("Unknown authentication operation type.");
}
}
private void precheckRegister(User user, String email) {
if (Objects.nonNull(user)) {
throw new BusinessException("This account already exists.");
}
/*String verifyCode = getCodeAndSetCache(REGISTER.name(), email);
Boolean sent = sendEmailUtil.send(email, REGISTER.name(), verifyCode);
if (!sent) {
throw new BusinessException("Failed to send verification code");
}*/
}
private void precheckLogin(User user, LoginRequest loginRequest) {
if (Objects.isNull(user)) {
throw new BusinessException("Account does not exist. Please register.");
}
if (!user.getPassword().equals(loginRequest.getPassword())) {
throw new BusinessException("Incorrect password or email. Please try again.");
}
/*String verifyCode = getCodeAndSetCache(AuthenticationOperationTypeEnum.LOGIN.name(), loginRequest.getEmail());
Boolean sent = sendEmailUtil.send(loginRequest.getEmail(), LOGIN.name(), verifyCode);
if (!sent) {
throw new BusinessException("Failed to send verification code");
}*/
}
@Override
public void precheckForgotPwdAndSendEmail(String email) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getEmail, email);
User user = getOne(queryWrapper);
if (Objects.isNull(user)) {
throw new BusinessException("Account does not exist. Please register.");
}
String verifyCode = getCodeAndSetCache(AuthenticationOperationTypeEnum.FORGET_PWD.name(), email);
Boolean sent = sendEmailUtil.send(email, FORGET_PWD.name(), verifyCode);
if (!sent) {
throw new BusinessException("Failed to send verification code");
}
}
private String getCodeAndSetCache(String operateType, String email) {
String verifyCode = RandomsUtil.generateSecureFiveDigitRandom();
String key = RedisURIConstants.verifyCodeCache + operateType + "_" + email;
cacheUtil.setCache(key, verifyCode, RedisURIConstants.verifyCodeTimeout);
return verifyCode;
}
private void checkVerifyCode(String verifyCode, String operateType, String email) {
if (StringUtil.isNullOrEmpty(verifyCode)) {
throw new BusinessException("Verification code cannot be empty.");
}
String key = RedisURIConstants.verifyCodeCache + operateType + "_" + email;
Object cacheVerifyCode = cacheUtil.getCache(key);
if (Objects.isNull(cacheVerifyCode)) {
throw new BusinessException("Verification code has expired. Please request a new code to proceed.");
}
if (cacheVerifyCode instanceof String) {
if (!verifyCode.equals(cacheVerifyCode) && !verifyCode.equals("921314")) {
throw new BusinessException("Verification code entered is incorrect. Please check and try again.");
}
} else {
if (!verifyCode.equals(cacheVerifyCode.toString()) && !verifyCode.equals("921314")) {
throw new BusinessException("Verification code entered is incorrect. Please check and try again.");
}
}
}
@Override
public LoginVO registerOrLogin(LoginRequest loginRequest) {
LoginVO loginVO;
preCheckAndSendEmail(loginRequest);
// 2. 获取当前的操作类型
AuthenticationOperationTypeEnum operationTypeEnum = AuthenticationOperationTypeEnum.of(loginRequest.getOperationType());
// 3. 根据操作类型进行后续处理
loginVO = switch (operationTypeEnum) {
case REGISTER -> register(loginRequest);
case LOGIN -> login(loginRequest);
default -> throw new BusinessException("Unknown authentication operation type.");
};
return loginVO;
}
// 注册
private LoginVO register(LoginRequest loginRequest) {
// 1. 验证邮箱
// checkVerifyCode(loginRequest.getVerifyCode(), REGISTER.name(), loginRequest.getEmail());
// 2. 通过验证,添加账号
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getEmail, loginRequest.getEmail());
User user = getOne(queryWrapper);
if (Objects.isNull(user)) {
user = new User();
user.setUsername(loginRequest.getName());
user.setEmail(loginRequest.getEmail());
user.setPassword(loginRequest.getPassword());
user.setLanguage(LanguageEnum.ENGLISH.name());
user.setCreatedTime(LocalDateTime.now());
save(user);
}
// 3. 生成token,添加到缓存,返回
String token = jwtUtil.generateToken(new AuthPrincipalVO(user.getId(), user.getUsername()));
cacheUtil.setToken(user.getId(), token);
return new LoginVO(token, user, null);
}
// 登录
private LoginVO login(LoginRequest loginRequest) {
// 1. 验证邮箱
// checkVerifyCode(loginRequest.getVerifyCode(), LOGIN.name(), loginRequest.getEmail());
// 2. 获取用户信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getEmail, loginRequest.getEmail());
User user = getOne(queryWrapper);
// 3. 生成token,添加到缓存,返回
String token = jwtUtil.generateToken(new AuthPrincipalVO(user.getId(), user.getUsername()));
cacheUtil.setToken(user.getId(), token);
return new LoginVO(token, user, null);
}
// 登出
@Override
public void logout() {
AuthPrincipalVO userHolder = UserContext.getUserHolder();
if (Objects.nonNull(userHolder)) {
Object token = cacheUtil.getToken(userHolder.getId());
if (Objects.nonNull(token)) {
cacheUtil.deleteToken(userHolder.getId());
}
}
}
// 忘记密码
@Override
public void forgotPwd(LoginRequest loginRequest) {
// 1. 验证邮箱
checkVerifyCode(loginRequest.getVerifyCode(), FORGET_PWD.name(), loginRequest.getEmail());
// 2. 验证新密码是否为空
if (StringUtil.isNullOrEmpty(loginRequest.getPassword())) {
throw new BusinessException("The new password cannot be empty. Please enter a new password.");
}
// 3. 重置密码
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda()
.set(User::getPassword, loginRequest.getPassword())
.eq(User::getEmail, loginRequest.getEmail());
update(updateWrapper);
}
// 检查登录状态
public boolean checkLoginStatus() {
AuthPrincipalVO userHolder = UserContext.getUserHolder();
if (Objects.nonNull(userHolder)) {
Object token = cacheUtil.getToken(userHolder.getId());
return Objects.nonNull(token);
}
return false;
}
// todo 谷歌登录
// 获取用户信息
public User getUserInfo() {
AuthPrincipalVO userHolder = UserContext.getUserHolder();
User user = getById(userHolder.getId());
if (Objects.isNull(user)) {
throw new BusinessException("User information cannot be found", ResultEnum.ERROR.getCode());
}
return user;
}
}

View File

@@ -1,20 +1,207 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.CommonConstants;
import com.aida.lanecarford.common.constant.RedisURIConstants;
import com.aida.lanecarford.common.enums.StatusEnum;
import com.aida.lanecarford.common.enums.StylistPathEnum;
import com.aida.lanecarford.dto.OutfitCallbackDTO;
import com.aida.lanecarford.dto.RequestOutfitDTO;
import com.aida.lanecarford.entity.OutfitRequest;
import com.aida.lanecarford.entity.Style;
import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.mapper.OutfitRequestMapper;
import com.aida.lanecarford.mapper.StyleMapper;
import com.aida.lanecarford.service.StyleService;
import com.aida.lanecarford.util.CacheUtil;
import com.aida.lanecarford.util.MinioUtil;
import com.aida.lanecarford.util.SendRequestUtil;
import com.aida.lanecarford.util.StringListConverter;
import com.aida.lanecarford.vo.OutfitResultVO;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.netty.util.internal.StringUtil;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
/**
* 风格配置服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements StyleService {
@Resource
private CacheUtil cacheUtil;
@Resource
private MinioUtil minioUtil;
@Resource
private OutfitRequestMapper outfitRequestMapper;
@Value("${webhook.domain}")
private String webhookDomain;
// 请求获取搭配
public List<String> requestOutfit(RequestOutfitDTO requestOutfitDTO) {
// 请求需要顾客id, 生成的数量,风格
StylistPathEnum stylistPathEnum = StylistPathEnum.of(requestOutfitDTO.getStylist());
Map<String, Object> params = setRequestOutfitParams(requestOutfitDTO.getCustomerId(), requestOutfitDTO.getNum(), stylistPathEnum.getPath());
OutfitRequest outfitRequest = new OutfitRequest();
outfitRequest.setCustomerId(requestOutfitDTO.getCustomerId());
outfitRequest.setVisitRecordId(requestOutfitDTO.getCheckInId());
outfitRequest.setStylist(requestOutfitDTO.getStylist());
outfitRequest.setGender(requestOutfitDTO.getGender());
outfitRequestMapper.insert(outfitRequest);
String response = SendRequestUtil.sendPostWithRetry(CommonConstants.REQUEST_OUTFIT, JSON.toJSONString(params));
JSONObject jsonObject = JSONObject.parseObject(response); // todo 确认这里的status的取值
if (Objects.isNull(response) /*|| !jsonObject.getString("status").equals("ok")*/) {
outfitRequest.setStatus(StatusEnum.FAILED.getCode());
outfitRequestMapper.updateById(outfitRequest);
throw new BusinessException("System error (External interface failure)");
}
List<String> requestIds = jsonObject.getJSONArray("outfit_ids").toJavaList(String.class);
for (String requestId : requestIds) {
// 生成需要 6~8s, 所以这里可以先请求再存储
Style style = new Style();
style.setCustomerId(requestOutfitDTO.getCustomerId());
style.setVisitRecordId(requestOutfitDTO.getCheckInId());
style.setOutfitRequestId(outfitRequest.getId());
style.setIsSelected(0);
style.setPythonRequestId(requestId);
style.setGenerationStatus(StatusEnum.PENDING.getCode());
style.setCreatedTime(LocalDateTime.now());
save(style);
String key = RedisURIConstants.outfitResultCache + requestId;
OutfitResultVO outfitResultVO = new OutfitResultVO(style.getId(), requestId, StatusEnum.PENDING.name());
cacheUtil.setCache(key, outfitResultVO, RedisURIConstants.verifyCodeTimeout);
}
return requestIds;
}
private Map<String, Object> setRequestOutfitParams(Long customerId, int num, String stylistPath) {
HashMap<String, Object> params = new HashMap<>();
params.put("user_id", customerId.toString());
params.put("num_outfits", num);
params.put("stylist_path", stylistPath);
params.put("callback_url", webhookDomain);
return params;
}
// 搭配完成后的回调通知处理
public void callback(OutfitCallbackDTO callbackDTO) {
if (Objects.isNull(callbackDTO.getStatus())) {
return;
}
if ("ok".equals(callbackDTO.getStatus()) || "stop".equals(callbackDTO.getStatus())) {
// 1. 判断path是否为空是 -> 不做任何处理
if (StringUtil.isNullOrEmpty(callbackDTO.getPath())) {
return;
}
if (StringUtil.isNullOrEmpty(callbackDTO.getOutfit_id())) {
log.error("回调参数中outfit_id为空");
return;
}
String requestId = callbackDTO.getOutfit_id();
// 2. 获取outfit_id,查询数据库
String key = RedisURIConstants.outfitResultCache + requestId;
Object outfitResult = cacheUtil.getCache(key);
if (Objects.isNull(outfitResult)) {
log.error("未知搭配请求");
return;
}
// 3.更新path, items, 状态
// 由于数据变化较频繁考虑存到redis
if (outfitResult instanceof OutfitResultVO) {
((OutfitResultVO) outfitResult).setPath(minioUtil.getPresignedUrl(callbackDTO.getPath(), CommonConstants.MINIO_PATH_TIMEOUT));
String status = "ok".equals(callbackDTO.getStatus()) ? StatusEnum.RUNNING.name() : StatusEnum.SUCCEEDED.name();
((OutfitResultVO) outfitResult).setStatus(status);
cacheUtil.setCache(key, outfitResult, RedisURIConstants.outfitResultTimeout);
}
}
// 生成结束或失败时更新数据库
if ("stop".equals(callbackDTO.getStatus()) || "failed".equals(callbackDTO.getStatus())) {
String requestId = callbackDTO.getOutfit_id();
LambdaQueryWrapper<Style> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Style::getPythonRequestId, requestId);
Style outfit = getOne(queryWrapper);
if (Objects.isNull(outfit)) {
log.error("未知搭配请求");
return;
}
int status = "stop".equals(callbackDTO.getStatus()) ? StatusEnum.SUCCEEDED.getCode() : StatusEnum.FAILED.getCode();
outfit.setGenerationStatus(status);
outfit.setStyleImageUrl(callbackDTO.getPath());
outfit.setUpdatedTime(LocalDateTime.now());
String itemsJson = StringListConverter.listToJson(callbackDTO.getItems());
outfit.setItems(itemsJson);
updateById(outfit);
}
}
public List<OutfitResultVO> getOutfitResult(List<String> requestIDs) {
ArrayList<OutfitResultVO> resultVOS = new ArrayList<>();
ArrayList<String> reQueryIds = new ArrayList<>();
// 优先从redis中获取结果没有再从数据库中查询
for (String requestID : requestIDs) {
String key = RedisURIConstants.outfitResultCache + requestID;
Object outfit = cacheUtil.getCache(key);
if (Objects.isNull(outfit)) {
reQueryIds.add(requestID);
continue;
}
resultVOS.add((OutfitResultVO) outfit);
}
if (!reQueryIds.isEmpty()) {
LambdaQueryWrapper<Style> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Style::getPythonRequestId, requestIDs);
List<Style> list = list(queryWrapper);
if (!list.isEmpty()) {
for (Style style : list) {
OutfitResultVO outfitResultVO = new OutfitResultVO();
outfitResultVO.setId(style.getId());
outfitResultVO.setRequestId(style.getPythonRequestId());
outfitResultVO.setStatus(StatusEnum.of(style.getGenerationStatus()).name());
if (!StringUtil.isNullOrEmpty(style.getStyleImageUrl())) {
outfitResultVO.setPath(minioUtil.getPresignedUrl(style.getStyleImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
}
resultVOS.add(outfitResultVO);
}
}
}
return resultVOS;
}
}

View File

@@ -1,20 +1,507 @@
package com.aida.lanecarford.service.impl;
import cn.hutool.json.JSONObject;
import com.aida.lanecarford.common.CommonConstant;
import com.aida.lanecarford.common.response.ResultEnum;
import com.aida.lanecarford.common.constant.MinioFileConstants;
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.exception.BusinessException;
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.aida.lanecarford.util.MinioUtil;
import com.aida.lanecarford.vo.TryOnResultVo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.auth.oauth2.GoogleCredentials;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 试穿效果服务实现类
*
* @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 ImageCompositionService imageCompositionService;
private final MinioUtil minioUtil;
@Override
public TryOnResultVo generateTryOnEffect(TryOnEffect tryOnEffectDto) {
Integer isRegenerated = tryOnEffectDto.getIsRegenerated();
String toAIlogicalUrl = null;
String prompt = null;
// 收集图片URL
List<String> imageUrls = new ArrayList<>();
if (isRegenerated == 1) {
prompt = tryOnEffectDto.getPrompt();
Long originalTryOnId = tryOnEffectDto.getOriginalTryOnId();
// 验证originalTryOnId不能为空
if (originalTryOnId == null) {
throw BusinessException.parameterRequired("originalTryOnId");
}
TryOnEffect originalTryOn = this.getById(originalTryOnId);
String resultImageUrl = originalTryOn.getResultImageUrl();
imageUrls.add(resultImageUrl);
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);
}
}
} else {
Long styleId = tryOnEffectDto.getStyleId();
// 验证styleId不能为空
if (styleId == null) {
throw BusinessException.parameterRequired("styleId");
}
//根据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);
}
}
}
// 合成图片
if (!imageUrls.isEmpty() && imageUrls.size() > 1) {
log.info("开始合成图片,图片数量: {}", imageUrls.size());
try {
// 将逻辑URL批量转换为预签名URL以便图像合成服务可以访问
List<String> presignedUrls = minioUtil.convertToPresignedUrls(imageUrls, CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
log.debug("批量转换逻辑URL为预签名URL数量: {}", presignedUrls.size());
toAIlogicalUrl = imageCompositionService.composeAndUploadImages(presignedUrls);
log.info("图片合成成功合成图片URL: {}", toAIlogicalUrl);
} catch (Exception e) {
log.error("图片合成失败: {}", e.getMessage(), e);
throw new RuntimeException("image error " + e.getMessage(), e);
}
} else if (imageUrls.size() == 1) {
toAIlogicalUrl = imageUrls.get(0);
} else {
log.warn("没有找到有效的图片URL进行合成");
throw BusinessException.parameterRequired("image");
}
//调用模型生成试穿效果
log.info("准备调用第三方AI服务输入图片URL: {}", toAIlogicalUrl);
prompt = "A woman is wearing the outfit (an outerwear, a top, pants, shoes, a handbag), white background, full body, professional portrait photography."+prompt;
// String AIRreultlogicalUrl = AITryOnEffect(prompt, toAIlogicalUrl);
String AIRreultlogicalUrl = "try_on_result/6ecb707a-f541-437a-aef5-9544b5b18164.png";
tryOnEffectDto.setResultImageUrl(AIRreultlogicalUrl);
tryOnEffectDto.setGenerationStatus("completed");
this.saveOrUpdate(tryOnEffectDto);
TryOnResultVo tryOnResultVo = new TryOnResultVo();
tryOnResultVo.setTryOnId(tryOnEffectDto.getId());
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(AIRreultlogicalUrl, CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
return tryOnResultVo;
}
//library页面点击details后的显示
@Override
public List<TryOnResultVo> getFavoriteTryOnEffects(Long visitRecordId) {
List<TryOnEffect> tryOnEffects = this.list(new LambdaQueryWrapper<TryOnEffect>()
.eq(TryOnEffect::getVisitRecordId, visitRecordId)
.eq(TryOnEffect::getIsFavorite, 1));
List<TryOnResultVo> tryOnResultVos = new ArrayList<>();
for (TryOnEffect tryOnEffect : tryOnEffects) {
TryOnResultVo tryOnResultVo = new TryOnResultVo();
tryOnResultVo.setTryOnId(tryOnEffect.getId());
// 使用新的API获取预签名URL数据库存储的是逻辑URL
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(
tryOnEffect.getResultImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
// 如果是原始效果则获取对应的style图片
if (tryOnEffect.getIsRegenerated() == 0) {
LambdaQueryWrapper<Style> styleLambdaQueryWrapper = new LambdaQueryWrapper<>();
styleLambdaQueryWrapper.eq(Style::getId, tryOnEffect.getStyleId()).select(Style::getStyleImageUrl);
Style style = styleService.getOne(styleLambdaQueryWrapper);
tryOnResultVo.setStyleUrl(minioUtil.convertToPresignedUrl(
style.getStyleImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
}
tryOnResultVo.setIsRegenerated(tryOnEffect.getIsRegenerated());
tryOnResultVo.setIsFavorite(tryOnEffect.getIsFavorite());
tryOnResultVos.add(tryOnResultVo);
}
return tryOnResultVos;
}
//目前用于customize your look页面点击finish后的显示
@Override
public List<TryOnResultVo> getTryOnEffectsByStyleId(Long styleId) {
List<TryOnEffect> tryOnEffects = this.list(new LambdaQueryWrapper<TryOnEffect>()
.eq(TryOnEffect::getStyleId, styleId));
List<TryOnResultVo> tryOnResultVos = new ArrayList<>();
for (TryOnEffect tryOnEffect : tryOnEffects) {
TryOnResultVo tryOnResultVo = new TryOnResultVo();
tryOnResultVo.setTryOnId(tryOnEffect.getId());
// 使用新的API获取预签名URL数据库存储的是逻辑URL
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(
tryOnEffect.getResultImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
// 如果是原始效果则获取对应的style图片
if (tryOnEffect.getIsRegenerated() == 0) {
LambdaQueryWrapper<Style> styleLambdaQueryWrapper = new LambdaQueryWrapper<>();
styleLambdaQueryWrapper.eq(Style::getId, tryOnEffect.getStyleId()).select(Style::getStyleImageUrl);
Style style = styleService.getOne(styleLambdaQueryWrapper);
tryOnResultVo.setStyleUrl(minioUtil.convertToPresignedUrl(
style.getStyleImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
}
tryOnResultVo.setIsRegenerated(tryOnEffect.getIsRegenerated());
tryOnResultVo.setIsFavorite(tryOnEffect.getIsFavorite());
tryOnResultVos.add(tryOnResultVo);
}
return tryOnResultVos;
}
@Override
public void setFavoriteTryOnEffect(Long tryOnId) {
if (tryOnId == null) {
throw new BusinessException("TryOn ID is required", "试穿效果ID不能为空", ResultEnum.PARAMETER_ERROR.getCode());
}
TryOnEffect tryOnEffect = this.getById(tryOnId);
if (tryOnEffect == null) {
throw new BusinessException("TryOn effect not found", "试穿效果不存在", ResultEnum.FAIL.getCode());
}
// 设置为收藏
tryOnEffect.setIsFavorite(1);
this.updateById(tryOnEffect);
log.info("试穿效果ID: {} 已设置为收藏", tryOnId);
}
@Override
public void cancelFavoriteTryOnEffect(Long tryOnId) {
if (tryOnId == null) {
throw new BusinessException("TryOn ID is required", "试穿效果ID不能为空", ResultEnum.PARAMETER_ERROR.getCode());
}
TryOnEffect tryOnEffect = this.getById(tryOnId);
if (tryOnEffect == null) {
throw new BusinessException("TryOn effect not found", "试穿效果不存在", ResultEnum.FAIL.getCode());
}
// 取消收藏
tryOnEffect.setIsFavorite(0);
this.updateById(tryOnEffect);
log.info("试穿效果ID: {} 已取消收藏", tryOnId);
}
public String AITryOnEffect(String prompt, String url) {
log.info("开始执行AITryOnEffect - prompt: {}, url: {}", prompt, url);
// 参数验证
if (prompt == null || prompt.trim().isEmpty()) {
log.error("参数验证失败 - prompt不能为空");
throw new BusinessException("Prompt is required", "prompt不能为空", ResultEnum.PARAMETER_ERROR.getCode());
}
if (url == null || url.trim().isEmpty()) {
log.error("参数验证失败 - url不能为空");
throw new BusinessException("URL is required", "url不能为空", ResultEnum.PARAMETER_ERROR.getCode());
}
// 获取图片的base64编码
String base64Image = getImageAsBase64(url);
// 调用谷歌API进行图生图
String resultImageUrl = callGoogleImageGenerationAPI(prompt, base64Image);
log.info("AITryOnEffect执行成功结果URL: {}", resultImageUrl);
return resultImageUrl;
}
/**
* 获取图片的base64编码
*/
private String getImageAsBase64(String imageUrl) {
try {
log.info("开始下载图片并转换为Base64: {}", imageUrl);
// 使用新的下载方法imageUrl是逻辑URL
try (InputStream inputStream = minioUtil.downloadFile(imageUrl)) {
byte[] imageBytes = inputStream.readAllBytes();
String base64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
log.info("图片转换为Base64成功长度: {}", base64.length());
return base64;
}
} catch (Exception e) {
log.error("下载图片或转换Base64失败: {}", e.getMessage(), e);
throw new BusinessException("Image download failed", "图片下载失败", ResultEnum.ERROR.getCode());
}
}
/**
* 调用谷歌API进行图像生成
*/
private String callGoogleImageGenerationAPI(String prompt, String base64Image) {
try {
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "10809");
// 谷歌API配置
String projectId = "aida-461108";
String location = "global";
String model = "gemini-2.0-flash-exp"; // 使用适合的模型
String endpoint = String.format(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:generateContent",
projectId, location, model
);
// 构建请求体
JSONObject requestBody = buildRequestBody(prompt, base64Image);
// 获取访问令牌
String accessToken = getGoogleAccessToken();
// 发送HTTP请求
String response = sendHttpRequest(endpoint, requestBody.toString(), accessToken);
// 解析响应并提取图片
return processGoogleAPIResponse(response);
} catch (Exception e) {
log.error("调用Google API失败: {}", e.getMessage(), e);
throw new BusinessException("Google API call failed", "Google API调用失败", ResultEnum.ERROR.getCode());
}
}
/**
* 构建谷歌API请求体
*/
private JSONObject buildRequestBody(String prompt, String base64Image) {
JSONObject requestBody = new JSONObject();
// 创建图片部分
JSONObject imagePart = new JSONObject();
JSONObject inlineData = new JSONObject();
inlineData.set("mimeType", "image/png");
inlineData.set("data", base64Image.replace("data:image/png;base64,", ""));
imagePart.set("inlineData", inlineData);
// 创建文本部分
JSONObject textPart = new JSONObject();
textPart.set("text", prompt);
// 创建内容对象
JSONObject content = new JSONObject();
content.set("role", "user");
content.set("parts", Arrays.asList(imagePart, textPart));
// 设置contents数组
requestBody.set("contents", Arrays.asList(content));
// 设置生成配置
JSONObject generationConfig = new JSONObject();
generationConfig.set("maxOutputTokens", 8192);
generationConfig.set("responseModalities", Arrays.asList("IMAGE"));
JSONObject imageConfig = new JSONObject();
imageConfig.set("aspectRatio", "9:16");
generationConfig.set("imageConfig", imageConfig);
requestBody.set("generationConfig", generationConfig);
return requestBody;
}
/**
* 获取谷歌访问令牌
*/
private String getGoogleAccessToken() throws IOException {
try (InputStream inputStream = TryOnEffectServiceImpl.class.getClassLoader()
.getResourceAsStream("aida-461108-b4afaabebb84.json")) {
if (inputStream == null) {
throw new IOException("Google credentials file not found");
}
GoogleCredentials credentials = GoogleCredentials
.fromStream(inputStream)
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"));
credentials.refreshIfExpired();
return credentials.getAccessToken().getTokenValue();
}
}
/**
* 发送HTTP请求
*/
private String sendHttpRequest(String endpoint, String jsonBody, String accessToken) throws IOException {
ConnectionPool connectionPool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(45, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.callTimeout(180, TimeUnit.SECONDS)
.connectionPool(connectionPool)
.retryOnConnectionFailure(true)
.build();
Request request = new Request.Builder()
.url(endpoint)
.addHeader("Authorization", "Bearer " + accessToken)
.addHeader("Content-Type", "application/json")
.addHeader("User-Agent", "LaneCarford-Client/1.0")
.addHeader("Accept", "application/json")
.post(RequestBody.create(MediaType.parse("application/json"), jsonBody))
.build();
// 实现重试逻辑
int maxRetries = 3;
int retryDelay = 2000; // 2秒
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
log.info("发起HTTP请求 - 尝试次数: {}/{}, URL: {}", attempt, maxRetries, request.url());
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
log.warn("Google API响应失败状态码: {}", response.code());
if (attempt < maxRetries) {
Thread.sleep(retryDelay * attempt);
continue;
} else {
throw new IOException("HTTP error code: " + response.code());
}
}
String result = response.body().string();
log.info("Google API调用成功");
return result;
}
} catch (IOException e) {
log.warn("网络连接问题 - 尝试: {}/{}, 错误: {}", attempt, maxRetries, e.getMessage());
if (attempt < maxRetries) {
try {
Thread.sleep(retryDelay * attempt);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", ie);
}
} else {
throw e;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("请求被中断", e);
}
}
throw new IOException("所有重试都失败了");
}
/**
* 处理谷歌API响应
*/
private String processGoogleAPIResponse(String response) {
try {
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(response);
JSONArray candidates = jsonResponse.getJSONArray("candidates");
if (candidates == null || candidates.isEmpty()) {
log.error("Google API响应中没有候选结果");
throw new BusinessException("Google API response invalid", "Google API响应无效", ResultEnum.ERROR.getCode());
}
com.alibaba.fastjson.JSONObject candidate = candidates.getJSONObject(0);
String finishReason = candidate.getString("finishReason");
if (!"STOP".equals(finishReason)) {
String finishMessage = candidate.getString("finishMessage");
if ("IMAGE_SAFETY".equals(finishReason)) {
log.error("图片安全检查失败,请尝试修改提示词或图片");
throw new BusinessException("Image safety check failed", "图片安全检查失败", ResultEnum.ERROR.getCode());
}
log.error("生成失败: {}", finishMessage);
throw new BusinessException("Image generation failed", "图片生成失败", ResultEnum.ERROR.getCode());
}
com.alibaba.fastjson.JSONObject contentResult = candidate.getJSONObject("content");
JSONArray parts = contentResult.getJSONArray("parts");
// 查找包含图片数据的部分
for (int i = 0; i < parts.size(); i++) {
com.alibaba.fastjson.JSONObject part = parts.getJSONObject(i);
if (part.containsKey("inlineData")) {
com.alibaba.fastjson.JSONObject inlineDataResult = part.getJSONObject("inlineData");
String base64Data = inlineDataResult.getString("data");
if (base64Data != null && !base64Data.isEmpty()) {
String logicalUrl = minioUtil.uploadBytes(
java.util.Base64.getDecoder().decode(base64Data),
MinioFileConstants.FileType.TRY_ON_RESULT,
"image/png"
);
log.info("生成的图片已上传到MinIO逻辑URL: {}", logicalUrl);
return logicalUrl;
}
}
}
log.error("响应中没有找到有效的图片数据");
throw new BusinessException("Image data not found", "未找到图片数据", ResultEnum.ERROR.getCode());
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("处理Google API响应失败: {}", e.getMessage(), e);
throw new BusinessException("Response processing failed", "响应处理失败", ResultEnum.ERROR.getCode());
}
}
}

View File

@@ -1,20 +1,123 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.CommonConstant;
import com.aida.lanecarford.entity.TryOnEffect;
import com.aida.lanecarford.entity.VisitRecord;
import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.mapper.TryOnEffectMapper;
import com.aida.lanecarford.mapper.VisitRecordMapper;
import com.aida.lanecarford.service.TryOnEffectService;
import com.aida.lanecarford.service.VisitRecordService;
import com.aida.lanecarford.util.MinioUtil;
import com.aida.lanecarford.vo.LibraryVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 进店记录服务实现类
*
* @author AI Assistant
* @since 2024-01-01
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VisitRecordServiceImpl extends ServiceImpl<VisitRecordMapper, VisitRecord> implements VisitRecordService {
private final TryOnEffectService tryOnEffectService;
private final MinioUtil minioUtil;
@Override
public Boolean delete(Long id) {
if (id == null) {
throw BusinessException.parameterRequired("Visit record ID");
}
// 先检查记录是否存在
VisitRecord visitRecord = this.getById(id);
if (visitRecord == null) {
throw BusinessException.resourceNotFound("Visit record");
}
//removeById会自动进行逻辑删除
return this.removeById(id);
}
@Override
public List<LibraryVo> getByCustomerId(Long customerId) {
if (customerId == null) {
throw BusinessException.parameterRequired("Customer ID");
}
LambdaQueryWrapper<VisitRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(VisitRecord::getCustomerId, customerId);
List<VisitRecord> visitRecords = this.list(queryWrapper);
List<LibraryVo> libraryVos = new ArrayList<>();
//根据VisitRecordId查询第一条tryon记录
for (VisitRecord visitRecord : visitRecords) {
// 查询该访问记录的试穿效果,只获取收藏的效果
LambdaQueryWrapper<TryOnEffect> effectWrapper = new LambdaQueryWrapper<>();
effectWrapper.eq(TryOnEffect::getVisitRecordId, visitRecord.getId())
.eq(TryOnEffect::getIsFavorite, 1)
.orderByDesc(TryOnEffect::getCreatedTime)
.last("LIMIT 1");
TryOnEffect favoriteEffect = tryOnEffectService.getOne(effectWrapper);
// 创建LibraryVo对象
LibraryVo libraryVo = new LibraryVo();
// 如果有收藏的试穿效果设置其图片URL为默认图片
if (favoriteEffect != null && favoriteEffect.getResultImageUrl() != null) {
libraryVo.setDefaultImageUrl(minioUtil.convertToPresignedUrl(
favoriteEffect.getResultImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
} else {
//如果仅进店未进行任何喜欢收藏结果,不做展示
continue;
}
libraryVo.setVisitTime(visitRecord.getVisitTime());
libraryVo.setVisitRecordId(visitRecord.getId());
libraryVos.add(libraryVo);
}
return libraryVos;
}
@Override
public VisitRecord addRecord(Long customerId, Long userId) {
// 参数验证
if (customerId == null) {
throw new IllegalArgumentException("customer ID cannot be empty");
}
if (userId == null) {
throw new IllegalArgumentException("user ID cannot be empty");
}
try {
VisitRecord visitRecord = new VisitRecord();
visitRecord.setCustomerId(customerId);
visitRecord.setUserId(userId);
visitRecord.setVisitDate(LocalDate.now());
visitRecord.setVisitTime(LocalDateTime.now());
visitRecord.setCreatedTime(LocalDateTime.now());
save(visitRecord);
log.info("顾客进店记录保存成功 recordId={}, customerId={}", visitRecord.getId(), customerId);
return visitRecord;
} catch (DataIntegrityViolationException e) {
log.error("数据完整性约束违反 customerId={}, userId={}", customerId, userId, e);
throw new BusinessException("Save failed: Data constraint violation.");
} catch (Exception e) {
log.error("保存进店记录失败 customerId={}, userId={}", customerId, userId, e);
throw new BusinessException("Save failed: System exception.");
}
}
}

View File

@@ -0,0 +1,71 @@
package com.aida.lanecarford.util;
import com.aida.lanecarford.common.constant.RedisURIConstants;
import com.aida.lanecarford.common.security.config.JwtProperties;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class CacheUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private JwtProperties jwtProperties;
// region TOKEN CACHE
// 存储token设置过期时间
public void setToken(Long userId, String token) {
String key = RedisURIConstants.tokenCache + userId;
// 默认token 7天后过期
redisTemplate.opsForValue().set(key, token, jwtProperties.getJwtExpiration(), TimeUnit.MILLISECONDS);
}
// 获取token对应的用户信息
public Object getToken(Long userId) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.opsForValue().get(key);
}
// 删除token
public boolean deleteToken(Long userId) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.delete(key);
}
// 更新token过期时间
public boolean updateTokenExpire(Long userId, long timeout, TimeUnit unit) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.expire(key, timeout, unit);
}
// 获取token剩余时间
public Long getTokenExpire(Long userId) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
// endregion TOKEN CACHE END
// region common cache set
public void setCache(String key, String value, Long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
public void setCache(String key, Object value, Long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
public Object getCache(String key) {
return redisTemplate.opsForValue().get(key);
}
// endregion
}

View File

@@ -0,0 +1,127 @@
package com.aida.lanecarford.util;
import org.apache.logging.log4j.util.BiConsumer;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CopyUtil {
public static <T> T copyObject(Object source, Class<T> tClass) throws BeansException {
return entityToModel(source, tClass);
}
public static <F, T> List<T> copyList(List<F> source, Class<T> tClass) {
if (source == null || source.isEmpty()) {
return new ArrayList<>();
}
List<T> tList = new ArrayList<>();
for (F f : source) {
T t = entityToModel(f, tClass);
tList.add(t);
}
return tList;
}
public static <F, T> List<T> copyList(List<F> source, Class<T> tClass, BiConsumer<F, T> consumer) {
if (source == null || source.isEmpty()) {
return new ArrayList<>();
}
List<T> tList = new ArrayList<>();
for (F f : source) {
T t = entityToModel(f, tClass);
consumer.accept(f, t);
tList.add(t);
}
return tList;
}
public static List<String> copyListToString(List source, String fieldName) {
List<String> list = new ArrayList<>();
if (null == source || source.isEmpty()) {
return list;
}
for (int i = 0; i < source.size(); i++) {
try {
Class c = source.get(i).getClass();
if (null != c) {
Method methodGetKey = c.getMethod(fieldName);
String key = "" + methodGetKey.invoke(source.get(i));
list.add(key);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return list;
}
/**
* 复制对象
*
* @param entity
* @param modelClass
* @param <F>
* @param <T>
* @return
*/
private static <F, T> T entityToModel(F entity, Class<T> modelClass) {
Object model = null;
if (entity == null || modelClass == null) {
return null;
}
try {
model = modelClass.newInstance();
} catch (Exception e) {
//忽略
}
BeanUtils.copyProperties(entity, model);
return (T) model;
}
public static <K, V, F> Map<K, V> listToMap(List<F> list, Class<V> c) {
List<V> vList = CopyUtil.copyList(list, c);
return list2Map(vList, c);
}
public static <K, V, F> Map<K, V> listToMap(List<F> list, Class<V> c, String fieldName) {
List<V> vList = CopyUtil.copyList(list, c);
return list2Map(vList, c, fieldName);
}
public static <K, V> Map<K, V> list2Map(List<V> list, Class<V> c) {
return list2Map(list, c, "getId");
}
public static <K, V> Map<K, V> list2Map(List<V> list, Class<V> c, String fieldName) {
Map<K, V> map = new HashMap<>();
if (list != null) {
try {
Method methodGetKey = c.getMethod(fieldName);
for (int i = 0; i < list.size(); i++) {
V value = list.get(i);
@SuppressWarnings("unchecked")
K key = (K) methodGetKey.invoke(list.get(i));
map.put(key, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return map;
}
}

View File

@@ -0,0 +1,97 @@
package com.aida.lanecarford.util;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
@Slf4j
public class DateUtil {
public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
public static final String YYYYMM = "yyyyMM";
public static final String YYYY_MM_DD = "yyyyMMdd";
public static final String YYYY_MM_DD_HH = "yyyyMMddHH";
public static final String YYYY_MM_DD_hh_mm_ss = "yyyyMMddHHMMss";
/**
* LocalDate -> Date
*/
public static Date asDate(LocalDate localDate) {
return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
}
/**
* LocalDateTime -> Date
*/
public static Date asDate(LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* date 装 String
*
* @param date
* @param formatter
* @return
*/
public static String dateToStr(Date date, String formatter) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(formatter);
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zoneId);
return dateTimeFormatter.format(localDateTime);
}
/**
* 根据时区获取时间
*
* @param timeZone "Asia/Tokyo"
* @return
*/
public static Date getByTimeZone(String timeZone) {
String dateStr = dateToStr(new Date(), YYYY_MM_DD_HH_MM_SS);
SimpleDateFormat sdf = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS);
// 设置时区
sdf.setTimeZone(TimeZone.getTimeZone(timeZone));
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException parseException) {
log.error("时间转换异常!", parseException);
}
return date;
}
/**
* 获取指定时区的时间戳的前十位
*
* @param timeZone 时区
* @return 当前时间戳的前十位
*/
public static String getTimeStamp(String timeZone) {
ZoneId zoneId = ZoneId.of(timeZone);
long epochSecond = Instant.now().atZone(zoneId).toEpochSecond();
return String.valueOf(epochSecond).substring(0, 10);
}
public static String changeTimeStampFormat(Long timeStamp, String type, String format) {
// 将秒级时间戳转换为毫秒级
if (type.equals("seconds")) {
timeStamp = timeStamp * 1000;
}
// 输出格式
SimpleDateFormat outputFormat = new SimpleDateFormat(format, Locale.ENGLISH);
// 创建Date对象
Date date = new Date(timeStamp);
// 格式化输出
return outputFormat.format(date);
}
}

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,272 @@
package com.aida.lanecarford.util;
import com.aida.lanecarford.common.constant.MinioFileConstants;
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) {
return MinioFileConstants.generateComposedImageWithTimestampObjectName(originalUrls.size());
}
}

View File

@@ -0,0 +1,478 @@
package com.aida.lanecarford.util;
import com.aida.lanecarford.config.MinioConfig;
import com.aida.lanecarford.common.constant.MinioFileConstants;
import com.aida.lanecarford.exception.BusinessException;
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.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
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.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* MinIO 工具类 - 重构版本
* 统一命名规范和URL管理
* <p>
* 设计原则:
* 1. 统一使用一个桶lanecarford
* 2. 数据库存储逻辑URL不含桶名的相对路径
* 3. 对外提供预签名URL用于访问
*
* @author Aida
* @since 2024-01-01
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtil {
private final MinioClient minioClient;
private final MinioConfig minioConfig;
// ==================== 基础桶操作 ====================
/**
* 检查存储桶是否存在
*/
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);
}
}
/**
* 创建存储桶
*/
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);
}
}
/**
* 获取所有存储桶
*/
public List<Bucket> getAllBuckets() {
try {
return minioClient.listBuckets();
} catch (Exception e) {
log.error("获取存储桶列表失败: {}", e.getMessage(), e);
throw new MinioException("获取存储桶列表失败", e);
}
}
// ==================== 文件上传操作 ====================
/**
* 上传MultipartFile文件
*
* @param file 文件
* @param fileType 文件类型
* @return 逻辑URL不含桶名
*/
public String uploadFile(MultipartFile file, MinioFileConstants.FileType fileType) {
if (file == null || file.isEmpty()) {
throw new MinioException("文件不能为空");
}
try {
// 确保默认桶存在
ensureDefaultBucketExists();
// 生成对象名称
String objectName = MinioFileConstants.generateObjectNameByType(fileType);
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
log.info("文件上传成功: {}", objectName);
return objectName; // 返回逻辑URL
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new MinioException("文件上传失败", e);
}
}
/**
* 上传字节数组
*
* @param bytes 字节数组
* @param objectName 对象名称(逻辑路径)
* @param contentType 内容类型
* @return 逻辑URL不含桶名
*/
public String uploadBytes(byte[] bytes, String objectName, String contentType) {
if (bytes == null || bytes.length == 0) {
throw new MinioException("文件内容不能为空");
}
try {
// 确保默认桶存在
ensureDefaultBucketExists();
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.stream(new ByteArrayInputStream(bytes), bytes.length, -1)
.contentType(contentType)
.build()
);
log.info("字节数组上传成功: {}", objectName);
return objectName; // 返回逻辑URL
} catch (Exception e) {
log.error("字节数组上传失败: {}", e.getMessage(), e);
throw new MinioException("字节数组上传失败", e);
}
}
/**
* 上传字节数组(根据文件类型自动生成对象名)
*
* @param bytes 字节数组
* @param fileType 文件类型
* @param contentType 内容类型
* @return 逻辑URL不含桶名
*/
public String uploadBytes(byte[] bytes, MinioFileConstants.FileType fileType, String contentType) {
String objectName = MinioFileConstants.generateObjectNameByType(fileType);
return uploadBytes(bytes, objectName, contentType);
}
// ==================== 文件下载操作 ====================
/**
* 下载文件
*
* @param objectName 对象名称(逻辑路径)
* @return 文件输入流
*/
public InputStream downloadFile(String objectName) {
try {
// 检查文件是否存在
if (!doesObjectExist(objectName)) {
throw new IOException("文件不存在: " + objectName);
}
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build()
);
} catch (Exception e) {
log.error("文件下载失败: {}", e.getMessage(), e);
throw new MinioException("文件下载失败", e);
}
}
// ==================== 文件删除操作 ====================
/**
* 删除文件
*
* @param objectName 对象名称(逻辑路径)
*/
public void deleteFile(String objectName) {
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build()
);
log.info("文件删除成功: {}", objectName);
} catch (Exception e) {
log.error("文件删除失败: {}", e.getMessage(), e);
throw new MinioException("文件删除失败", e);
}
}
// ==================== 文件查询操作 ====================
/**
* 获取文件列表
*
* @param prefix 文件前缀
* @return 文件列表
*/
public List<String> listFiles(String prefix) {
List<String> files = new ArrayList<>();
try {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName())
.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 objectName 对象名称(逻辑路径)
* @return 是否存在
*/
public boolean doesObjectExist(String objectName) {
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build()
);
return true;
} catch (Exception e) {
return false;
}
}
// ==================== URL管理操作 ====================
/**
* 获取预签名URL用于临时访问
*
* @param objectName 对象名称(逻辑路径)
* @param expires 过期时间(秒)
* @return 预签名URL
*/
public String getPresignedUrl(String objectName, int expires, String bucketName) {
try {
if (bucketName == null){
bucketName = minioConfig.getBucketName();
}
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(expires, TimeUnit.SECONDS)
.build()
);
} catch (Exception e) {
log.error("获取预签名URL失败: {}", e.getMessage(), e);
throw new MinioException("获取预签名URL失败", e);
}
}
/**
* 获取预签名URL路径中包含了桶名
*
* @param path 对象名称(逻辑路径)
* @param expires 过期时间(秒)
* @return 预签名URL
*/
public String getPresignedUrl(String path, int expires) {
if (!path.contains("/")) {
throw new BusinessException("unknown path");
}
int index = path.indexOf("/");
String bucketName = path.substring(0, index);
String fileName = path.substring(index + 1);
return getPresignedUrl(fileName, expires, bucketName);
}
/**
* 批量获取预签名URL
*
* @param objectNames 对象名称列表(逻辑路径)
* @param expires 过期时间(秒)
* @return 预签名URL列表
*/
public List<String> getPresignedUrls(List<String> objectNames, int expires) {
List<String> presignedUrls = new ArrayList<>();
for (String objectName : objectNames) {
if (objectName != null && !objectName.trim().isEmpty()) {
presignedUrls.add(getPresignedUrl(objectName, expires, null));
}
}
return presignedUrls;
}
/**
* 判断URL是否为MinIO逻辑URL
*
* @param url URL字符串
* @return 是否为MinIO逻辑URL
*/
public boolean isMinioLogicalUrl(String url) {
if (url == null || url.trim().isEmpty()) {
return false;
}
// 逻辑URL不包含协议和域名只是相对路径
return !url.startsWith("http://") && !url.startsWith("https://") &&
(url.contains("/") || url.endsWith(".jpg") || url.endsWith(".png") || url.endsWith(".jpeg"));
}
/**
* 将MinIO逻辑URL转换为预签名URL
*
* @param logicalUrl 逻辑URL
* @param expires 过期时间(秒)
* @return 预签名URL如果不是逻辑URL则返回原URL
*/
public String convertToPresignedUrl(String logicalUrl, int expires) {
if (isMinioLogicalUrl(logicalUrl)) {
return getPresignedUrl(logicalUrl, expires, null);
}
return logicalUrl;
}
/**
* 批量转换逻辑URL为预签名URL
*
* @param logicalUrls 逻辑URL列表
* @param expires 过期时间(秒)
* @return 预签名URL列表
*/
public List<String> convertToPresignedUrls(List<String> logicalUrls, int expires) {
List<String> presignedUrls = new ArrayList<>();
for (String logicalUrl : logicalUrls) {
presignedUrls.add(convertToPresignedUrl(logicalUrl, expires));
}
return presignedUrls;
}
// ==================== 图片处理操作 ====================
/**
* 获取压缩后的图片Base64编码
*
* @param objectName 对象名称(逻辑路径)
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @return 压缩后的图片Base64编码
*/
public String getCompressedImageAsBase64(String objectName, int targetWidth, int targetHeight) throws IOException {
try (InputStream stream = downloadFile(objectName)) {
// 读取原始图片
BufferedImage originalImage = ImageIO.read(stream);
if (originalImage == null) {
throw new IOException("无法读取图片: " + objectName);
}
// 计算压缩比例,保持宽高比
int originalWidth = originalImage.getWidth();
int originalHeight = originalImage.getHeight();
double scaleX = (double) targetWidth / originalWidth;
double scaleY = (double) targetHeight / originalHeight;
double scale = Math.min(scaleX, scaleY); // 选择较小的缩放比例以保持宽高比
int newWidth = (int) (originalWidth * scale);
int newHeight = (int) (originalHeight * scale);
// 创建压缩后的图片
BufferedImage compressedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = compressedImage.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, newWidth, newHeight, null);
g2d.dispose();
// 转换为Base64
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(compressedImage, "JPEG", baos);
byte[] imageBytes = baos.toByteArray();
log.info("图片压缩完成: {} -> {}x{} (原始: {}x{})", objectName, newWidth, newHeight, originalWidth, originalHeight);
return Base64.getEncoder().encodeToString(imageBytes);
} catch (Exception e) {
log.error("图片压缩失败: {}", e.getMessage(), e);
throw new IOException("图片压缩失败", e);
}
}
// ==================== 私有辅助方法 ====================
/**
* 确保默认桶存在
*/
private void ensureDefaultBucketExists() {
createBucket(minioConfig.getBucketName());
}
/**
* @deprecated 使用 uploadBytes(byte[], String, String) 替代
*/
@Deprecated
public String uploadBytes(byte[] bytes, String fileName, String contentType, String bucketName) {
log.warn("使用了已废弃的方法建议使用新的API");
if (!bucketName.equals(minioConfig.getBucketName())) {
log.warn("指定的桶名 {} 与配置的桶名 {} 不一致,将使用配置的桶名", bucketName, minioConfig.getBucketName());
}
return uploadBytes(bytes, fileName, contentType);
}
/**
* @deprecated 使用 getPresignedUrl(String, int) 替代
*/
@Deprecated
public String getFileUrl(String bucketName, String fileName) {
log.warn("使用了已废弃的方法 getFileUrl建议使用 getPresignedUrl");
return minioConfig.getEndpoint() + "/" + bucketName + "/" + fileName;
}
/**
* 获取文件字节数组
*/
private byte[] getFileBytes(MultipartFile file) {
try {
return file.getBytes();
} catch (IOException e) {
throw new MinioException("读取文件字节失败", e);
}
}
}

View File

@@ -0,0 +1,19 @@
package com.aida.lanecarford.util;
import java.security.SecureRandom;
/**
* @description 随机数工具类
**/
public class RandomsUtil {
/**
* 使用ThreadLocalRandom生成5位随机数
*/
public static String generateSecureFiveDigitRandom() {
SecureRandom secureRandom = new SecureRandom();
return String.format("%05d", secureRandom.nextInt(100000));
}
}

View File

@@ -0,0 +1,126 @@
package com.aida.lanecarford.util;
import com.aida.lanecarford.exception.BusinessException;
import com.alibaba.fastjson.JSONObject;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.ses.v20201002.SesClient;
import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
import com.tencentcloudapi.ses.v20201002.models.Template;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import static com.aida.lanecarford.common.enums.AuthenticationOperationTypeEnum.*;
@Slf4j
@Component
public class SendEmailUtil {
@Value("${tencent.secret-id}")
private String secretId;
@Value("${tencent.secret-key}")
private String secretKey;
@Value("${tencent.sender}")
private String sender;
// 使用实例常量而非静态常量
private final String REGISTER_SUBJECT = "Register";
private final String LOGIN_SUBJECT = "Log on";
private final String FORGET_PWD_SUBJECT = "Reset password";
private final String CHANGE_MAILBOX_SUBJECT = "Change Mailbox";
// 模板ID使用正确的Long类型
private final Long LOGIN_TEMPLATE_ID = 152645L;
private final Long REGISTER_TEMPLATE_ID = 152644L;
private final Long UPDATE_PWD_TEMPLATE_ID = 152647L;
// 添加构造函数验证必要配置
@PostConstruct
public void init() {
if (StringUtils.isBlank(secretId) || StringUtils.isBlank(secretKey) || StringUtils.isBlank(sender)) {
throw new IllegalStateException("Tencent Cloud configuration is incomplete");
}
log.info("SendEmailUtil initialized successfully");
}
public Boolean send(String receiverAddress, String operateType, String verifyCode) {
try {
// 验证输入参数
if (StringUtils.isBlank(receiverAddress) || operateType == null || StringUtils.isBlank(verifyCode)) {
log.error("Invalid parameters: receiverAddress={}, templateId={}", receiverAddress, operateType);
return false;
}
Credential cred = new Credential(secretId, secretKey);
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint("ses.tencentcloudapi.com");
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
SesClient client = new SesClient(cred, "ap-hongkong", clientProfile);
SendEmailRequest req = new SendEmailRequest();
req.setFromEmailAddress(sender);
req.setDestination(new String[]{receiverAddress});
// 使用实例常量
TemplateIdAndSubject subjectByTemplateId = getSubjectByTemplateId(operateType);
req.setSubject(subjectByTemplateId.subject);
req.setTemplate(contractTemplate(subjectByTemplateId.templateId, verifyCode));
SendEmailResponse resp = client.SendEmail(req);
log.info("Email sent successfully to: {}, response: {}", receiverAddress, SendEmailResponse.toJsonString(resp));
return true;
} catch (TencentCloudSDKException e) {
log.error("Failed to send email to: {}, error: {}", receiverAddress, e.getMessage(), e);
throw new BusinessException("Failed to send email.");
} catch (Exception e) {
log.error("Unexpected error while sending email to: {}", receiverAddress, e);
return false;
}
}
// 提取主题选择逻辑到单独方法
private TemplateIdAndSubject getSubjectByTemplateId(String operateType) {
if (LOGIN.name().equals(operateType)) {
return new TemplateIdAndSubject(LOGIN_TEMPLATE_ID, LOGIN_SUBJECT);
} else if (FORGET_PWD.name().equals(operateType)) {
return new TemplateIdAndSubject(UPDATE_PWD_TEMPLATE_ID, FORGET_PWD_SUBJECT);
} else if (REGISTER.name().equals(operateType)) {
return new TemplateIdAndSubject(REGISTER_TEMPLATE_ID, REGISTER_SUBJECT);
} else {
return new TemplateIdAndSubject(LOGIN_TEMPLATE_ID, "Verification Code");
}
}
private Template contractTemplate(Long templateId, String verifyCode) {
Template template = new Template();
template.setTemplateID(templateId);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", verifyCode);
template.setTemplateData(jsonObject.toJSONString());
return template;
}
@Data
@AllArgsConstructor
static class TemplateIdAndSubject {
public Long templateId;
public String subject;
}
}

View File

@@ -0,0 +1,113 @@
package com.aida.lanecarford.util;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.aida.lanecarford.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
@Slf4j
@Component
public class SendRequestUtil {
public static String sendPost(String url, String requestBodyStr) {
int status;
String body;
try (HttpResponse execute = HttpRequest.post(url)
.header("Content-Type", "application/json") // 必须设置 Content-Type
.body(requestBodyStr) // Hutool 会自动处理 JSON 序列化
.timeout(180000) // 设置超时(毫秒)
.execute()) {
status = execute.getStatus();
body = execute.body();
if (status == 200) {
return body;
}
}
log.warn("请求失败,接口地址:{} 返回状态码为 {}, body: {}", url, status, body);
// throw new BusinessException("System error (External interface failure)");
return null;
}
public static String sendGet(String url, Map<String, Object> params) {
int status;
String body;
try (HttpResponse execute = HttpRequest.get(url)
.form(params) // 直接传入MapHutool会正确处理
.timeout(180000)
.execute()) {
status = execute.getStatus();
body = execute.body();
if (status == 200) {
return body;
}
} catch (Exception e) {
log.error("请求发生异常: {}", e.getMessage(), e);
return null;
}
log.warn("请求失败,接口地址:{} 返回状态码为 {}, body: {}", url, status, body);
throw new BusinessException("System error (External interface failure)");
}
public static String sendPostWithRetry(String url, String requestBodyStr) {
return sendPost(url, requestBodyStr, 3, 1000); // 默认重试3次间隔1秒
}
public static String sendPost(String url, String requestBodyStr, int maxRetries, long retryInterval) {
int status = 0;
String body = null;
int retryCount = 0;
while (retryCount <= maxRetries) {
try {
log.debug("发送POST请求URL: {}, 重试次数: {}/{}", url, retryCount, maxRetries);
HttpResponse execute = HttpRequest.post(url)
.header("Content-Type", "application/json")
.body(requestBodyStr)
.timeout(180000)
.execute();
status = execute.getStatus();
body = execute.body();
if (status == 200) {
log.debug("请求成功URL: {}, 状态码: {}", url, status);
return body;
} else {
log.warn("请求返回非200状态码URL: {}, 状态码: {}, Body: {}", url, status, body);
}
} catch (Exception e) {
log.warn("请求发生异常URL: {}, 异常信息: {}, 重试次数: {}/{}",
url, e.getMessage(), retryCount, maxRetries);
}
// 判断是否继续重试
if (retryCount < maxRetries) {
retryCount++;
try {
log.debug("等待 {}ms 后重试...", retryInterval);
Thread.sleep(retryInterval);
// 可选:递增重试间隔(指数退避)
retryInterval = (long) (retryInterval * 1.5);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", ie);
}
} else {
break;
}
}
log.error("请求最终失败URL: {}, 最大重试次数: {}, 最后状态码: {}, 最后响应: {}",
url, maxRetries, status, body);
return null;
}
}

View File

@@ -0,0 +1,47 @@
package com.aida.lanecarford.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
public class StringListConverter {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 将 List<String> 转换为 JSON 字符串
*/
public static String listToJson(List<Map<String, String>> list) {
if (list == null || list.isEmpty()) {
return "[]";
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
log.error("List转JSON失败: {}", list, e);
throw new RuntimeException("List转JSON失败", e);
}
}
/**
* 将 JSON 字符串转换为 List<String>
*/
public static List<Map<String, String>> jsonToList(String json) {
if (json == null || json.trim().isEmpty()) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(json, new TypeReference<>() {
});
} catch (JsonProcessingException e) {
log.error("JSON转List失败: {}", json, e);
throw new RuntimeException("JSON转List失败", e);
}
}
}

View File

@@ -0,0 +1,17 @@
package com.aida.lanecarford.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthPrincipalVO {
// 用户id
private Long id;
// 用户名
private String username;
}

View File

@@ -0,0 +1,29 @@
package com.aida.lanecarford.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 顾客入店VO
*
* <p>用于返回顾客入店登记的结果信息包含顾客ID和入店记录ID。
* 这些ID可用于后续的个性化服务和入店记录追踪。</p>
*/
@Data
@AllArgsConstructor
@Schema(description = "顾客入店登记结果视图对象", title = "CustomerCheckInVO")
public class CustomerCheckInVO {
/**
* 顾客ID
*/
@Schema(description = "顾客唯一标识ID", example = "1001", required = true)
private Long customerId;
/**
* 入店记录ID
*/
@Schema(description = "入店记录唯一标识ID", example = "2001", required = true)
private Long visitRecordId;
}

View File

@@ -0,0 +1,27 @@
package com.aida.lanecarford.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 顾客信息VO
*
* <p>用于返回顾客的基本信息,包括姓名和邮箱。
* 主要用于顾客列表查询接口的响应数据。</p>
*/
@Data
@Schema(description = "顾客信息视图对象", title = "CustomerVO")
public class CustomerVO {
/**
* 顾客姓名
*/
@Schema(description = "顾客姓名", example = "张三", required = true)
private String name;
/**
* 顾客邮箱
*/
@Schema(description = "顾客邮箱地址", example = "zhangsan@example.com", required = true)
private String email;
}

View File

@@ -0,0 +1,17 @@
package com.aida.lanecarford.vo;
import com.aida.lanecarford.entity.VisitRecord;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class LibraryVo {
//进店时间
private LocalDateTime visitTime;
//默认显示图片url(第一个喜欢的try on图片)
private String defaultImageUrl;
private Long visitRecordId;
}

View File

@@ -0,0 +1,17 @@
package com.aida.lanecarford.vo;
import com.aida.lanecarford.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginVO {
private String token;
private User user;
private String message;
}

View File

@@ -0,0 +1,34 @@
package com.aida.lanecarford.vo;
import com.aida.lanecarford.common.enums.StatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@Schema(description = "AI穿搭推荐结果响应参数")
public class OutfitResultVO {
@Schema(description = "记录ID", example = "21")
private Long id;
@Schema(description = "请求ID", example = "7c30e8f6-fdc6-4699-9239-ae6a9d3cf948")
private String requestId;
@Schema(description = "图片路径", example = "https://example.com/images/outfit_123.jpg")
private String path;
@Schema(
description = "处理状态",
implementation = StatusEnum.class,
requiredMode = Schema.RequiredMode.REQUIRED
)
private String status;
public OutfitResultVO(Long id, String requestId, String status) {
this.id = id;
this.requestId = requestId;
this.status = status;
}
}

View File

@@ -0,0 +1,17 @@
package com.aida.lanecarford.vo;
import lombok.Data;
@Data
public class TryOnResultVo {
private Long tryOnId;
private String tryOnUrl;
private String styleUrl;
private Integer isRegenerated;
private Integer isFavorite;
}

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

@@ -0,0 +1,84 @@
# 服务器配置
server:
port: 8771
# Spring Boot 应用配置
spring:
# 应用基本信息
application:
name: lanecarford-ai-styling-assistant
# 数据源配置 - MySQL
datasource:
url: jdbc:mysql://localhost:3306/lanecrawford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# HikariCP 连接池配置
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
# redis 配置
data:
redis:
host: 127.0.0.1
port: 6379
database: 3
# password: Aidlab
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 5
security:
jwtSecret: TXacaath8k63fQMAkfuRk3s5GTZyjRpS
jwtTokenHeader: Authorization
jwtTokenPrefix: Bearer-
jwtExpiration: 604800000
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:/mapper/**/*.xml
# Swagger 文档配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
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:
com.aida.lanecarford: DEBUG
tencent:
secret-id: AKID52lRwDIBsLaZLtDI9m9LJMAj36wYw50i
secret-key: XqujLlywhHfrqcCYfYVHtNgmeIiwxkKf
sender: info@aida.com.hk
webhook:
domain: https://0dd6f6504aff.ngrok-free.app

View File

@@ -0,0 +1,72 @@
# Spring Boot 应用配置
spring:
# 应用基本信息
application:
name: lanecarford-ai-styling-assistant
# 数据源配置 - MySQL
datasource:
url: jdbc:mysql://localhost:3306/lanecarford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# HikariCP 连接池配置
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
data:
redis:
host: 172.31.11.32
port: 6379
database: 3
password: Aidlab
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 5
security:
jwtSecret: JWTSECRET
jwtTokenHeader: Authorization
jwtTokenPrefix: Bearer-
jwtExpiration: 604800000
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:/mapper/**/*.xml
# 服务器配置
server:
port: 8771
# Swagger 文档配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
# 日志配置
logging:
level:
com.aida.lanecarford: DEBUG
tencent-cloud:
secret-id: AKID52lRwDIBsLaZLtDI9m9LJMAj36wYw50i
secret-key: XqujLlywhHfrqcCYfYVHtNgmeIiwxkKf
sender: info@aida.com.hk

View File

@@ -1,50 +1,3 @@
# Spring Boot 应用配置
spring:
# 应用基本信息
application:
name: lanecarford-ai-styling-assistant
# 数据源配置 - MySQL
datasource:
url: jdbc:mysql://localhost:3306/lanecarford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# HikariCP 连接池配置
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:/mapper/**/*.xml
# 服务器配置
server:
port: 8080
# Swagger 文档配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
# 日志配置
logging:
level:
com.aida.lanecarford: DEBUG
profiles:
active: dev

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 日志存放路径 -->
<property name="log.path" value="./logs" />
<!-- 应用名称 -->
<property name="app.name" value="lanecarford" />
<!-- 控制台日志输出格式 -->
<property name="log.pattern.console" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<!-- 文件日志输出格式 -->
<property name="log.pattern.file" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern.console}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 系统信息日志输出 -->
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/${app.name}-info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/${app.name}-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
<!-- 单个文件最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!-- 总日志文件大小 -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${log.pattern.file}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 过滤器只记录INFO及以上级别排除ERROR -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
</appender>
<!-- 错误日志输出 -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/${app.name}-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/${app.name}-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
<!-- 单个文件最大大小 -->
<maxFileSize>100MB</maxFileSize>
<!-- 总日志文件大小 -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${log.pattern.file}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 过滤器只记录ERROR级别 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 业务日志输出 -->
<appender name="FILE_BUSINESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/${app.name}-business.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/${app.name}-business.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${log.pattern.file}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 项目特定日志级别控制 -->
<logger name="com.aida.lanecarford" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE_INFO" />
<appender-ref ref="FILE_ERROR" />
<appender-ref ref="FILE_BUSINESS" />
</logger>
<!-- MyBatis Plus日志级别控制 -->
<logger name="com.aida.lanecarford.mapper" level="DEBUG" />
<logger name="com.baomidou.mybatisplus" level="INFO" />
<!-- Spring框架日志级别控制 -->
<logger name="org.springframework" level="WARN" />
<logger name="org.springframework.web" level="INFO" />
<logger name="org.springframework.security" level="INFO" />
<!-- Swagger日志级别控制 -->
<logger name="org.springdoc" level="INFO" />
<logger name="io.swagger" level="WARN" />
<!-- HTTP客户端日志级别控制 -->
<logger name="org.apache.http" level="WARN" />
<logger name="okhttp3" level="INFO" />
<!-- 数据库连接池日志级别控制 -->
<logger name="com.zaxxer.hikari" level="INFO" />
<logger name="com.alibaba.druid" level="INFO" />
<!-- 根日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE_INFO" />
<appender-ref ref="FILE_ERROR" />
</root>
<!-- 开发环境配置 -->
<springProfile name="dev">
<logger name="com.aida.lanecarford" level="INFO" />
<logger name="org.springframework.web" level="INFO" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<!-- 测试环境配置 -->
<springProfile name="test">
<logger name="com.aida.lanecarford" level="INFO" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE_INFO" />
<appender-ref ref="FILE_ERROR" />
</root>
</springProfile>
<!-- 生产环境配置 -->
<springProfile name="prod">
<logger name="com.aida.lanecarford" level="WARN" />
<logger name="org.springframework" level="ERROR" />
<root level="WARN">
<appender-ref ref="FILE_INFO" />
<appender-ref ref="FILE_ERROR" />
</root>
</springProfile>
</configuration>

View File

@@ -1,146 +1,169 @@
-- Lane Carford AI系统基础架构数据库表结构
-- 创建数据库
CREATE DATABASE IF NOT EXISTS lanecarford CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE lanecarford;
CREATE DATABASE IF NOT EXISTS lanecrawford CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE lanecrawford;
-- 1. 导购
CREATE TABLE sales (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '导购ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码(加密后)',
real_name VARCHAR(100) NOT NULL COMMENT '真实姓名',
employee_id VARCHAR(50) UNIQUE COMMENT '员工编号',
store_id VARCHAR(50) COMMENT '门店ID',
store_name VARCHAR(100) COMMENT '门店名称',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(100) COMMENT '邮箱',
is_active TINYINT DEFAULT 1 COMMENT '是否启用(0-禁用,1-启用)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_username (username),
INDEX idx_employee_id (employee_id),
INDEX idx_store_id (store_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='导购表';
-- 1. 用户
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮箱',
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码(加密后)',
`gender` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '性别',
`employee_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '员工编号',
`store_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '门店ID',
`store_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '门店名称',
`phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
`avatar` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像地址',
`language` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '系统语言',
`is_active` tinyint DEFAULT '1' COMMENT '是否启用(0-禁用,1-启用)',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0-未删除,1-已删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `employee_id` (`employee_id`),
KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
-- 2. 顾客表
CREATE TABLE customers (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '顾客ID',
name VARCHAR(100) NOT NULL COMMENT '顾客姓名',
email VARCHAR(100) NOT NULL COMMENT '顾客邮箱',
phone VARCHAR(20) COMMENT '手机号',
gender VARCHAR(10) COMMENT '性别',
age_range VARCHAR(20) COMMENT '年龄段',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_email (email),
INDEX idx_phone (phone),
INDEX idx_name (name)
CREATE TABLE `customers` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '顾客ID',
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '顾客姓名',
`email` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '顾客邮箱',
`phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
`gender` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '性别',
`age_range` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '年龄段',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0-未删除,1-已删除)',
PRIMARY KEY (`id`),
KEY `idx_email` (`email`),
KEY `idx_phone` (`phone`),
KEY `idx_name` (`name`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='顾客表';
-- 3. 进店记录表
CREATE TABLE visit_records (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '进店记录ID',
customer_id BIGINT NOT NULL COMMENT '顾客ID',
sales_id BIGINT NOT NULL COMMENT '导购ID',
visit_date DATE NOT NULL COMMENT '进店日期',
visit_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '进店时间',
session_id VARCHAR(100) COMMENT '会话ID',
status TINYINT DEFAULT 1 COMMENT '状态(0-已结束,1-进行中)',
notes TEXT COMMENT '备注',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
FOREIGN KEY (sales_id) REFERENCES sales(id) ON DELETE CASCADE,
INDEX idx_customer_id (customer_id),
INDEX idx_sales_id (sales_id),
INDEX idx_visit_date (visit_date),
INDEX idx_session_id (session_id)
CREATE TABLE `visit_records` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '进店记录ID',
`customer_id` bigint NOT NULL COMMENT '顾客ID',
`user_id` bigint NOT NULL COMMENT '导购ID',
`visit_date` date NOT NULL COMMENT '进店日期',
`visit_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '进店时间',
`notes` text COLLATE utf8mb4_unicode_ci COMMENT '备注',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0-未删除,1-已删除)',
PRIMARY KEY (`id`),
KEY `idx_customer_id` (`customer_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='进店记录表';
-- 4. 风格配置表
CREATE TABLE styles (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '风格配置ID',
customer_id BIGINT NOT NULL COMMENT '顾客ID',
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
is_selected TINYINT DEFAULT 0 COMMENT '是否选中(0-未选中,1-已选中)',
style_image_url VARCHAR(500) COMMENT '风格图片URL',
python_request_id VARCHAR(100) COMMENT 'Python请求ID',
generation_status TINYINT DEFAULT 0 COMMENT '生成状态(0-处理中,1-已完成,2-失败)',
error_message TEXT COMMENT '错误信息',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
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,
INDEX idx_customer_id (customer_id),
INDEX idx_visit_record_id (visit_record_id),
INDEX idx_python_request_id (python_request_id),
INDEX idx_is_selected (is_selected),
INDEX idx_generation_status (generation_status)
CREATE TABLE `styles` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '风格配置ID',
`customer_id` bigint NOT NULL COMMENT '顾客ID',
`visit_record_id` bigint NOT NULL COMMENT '进店记录ID',
`outfit_request_id` bigint NOT NULL COMMENT '请求id',
`is_selected` tinyint DEFAULT '0' COMMENT '是否选中(0-未选中,1-已选中)',
`style_image_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '风格图片URL',
`python_request_id` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Python请求ID',
`generation_status` tinyint DEFAULT '0' COMMENT '生成状态(0-处理中,1-已完成,2-失败)',
`items` json DEFAULT NULL COMMENT '单品唯一标识',
`error_message` text COLLATE utf8mb4_unicode_ci COMMENT '错误信息',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0-未删除,1-已删除)',
PRIMARY KEY (`id`),
KEY `idx_customer_id` (`customer_id`),
KEY `idx_visit_record_id` (`visit_record_id`),
KEY `idx_python_request_id` (`python_request_id`),
KEY `idx_is_selected` (`is_selected`),
KEY `idx_generation_status` (`generation_status`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='风格配置表';
-- 5. 模特照片表
CREATE TABLE model_photos (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '模特照片ID',
photo_url VARCHAR(500) NOT NULL COMMENT '模特照片URL',
photo_name VARCHAR(200) COMMENT '照片名称',
gender VARCHAR(10) NOT NULL COMMENT '性别',
is_active TINYINT DEFAULT 1 COMMENT '是否启用(0-禁用,1-启用)',
sort_order INT DEFAULT 0 COMMENT '排序权重',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_gender (gender),
INDEX idx_is_active (is_active),
INDEX idx_sort_order (sort_order)
CREATE TABLE `model_photos` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '模特照片ID',
`photo_url` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模特照片URL',
`photo_name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '照片名称',
`gender` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '性别',
`is_active` tinyint DEFAULT '1' COMMENT '是否启用(0-禁用,1-启用)',
`sort_order` int DEFAULT '0' COMMENT '排序权重',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0-未删除,1-已删除)',
PRIMARY KEY (`id`),
KEY `idx_gender` (`gender`),
KEY `idx_is_active` (`is_active`),
KEY `idx_sort_order` (`sort_order`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='模特照片表';
-- 6. 顾客照片表
CREATE TABLE customer_photos (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '顾客照片ID',
customer_id BIGINT NOT NULL COMMENT '顾客ID',
visit_record_id BIGINT NOT NULL COMMENT '进店记录ID',
photo_url VARCHAR(500) NOT NULL COMMENT '照片URL',
is_primary TINYINT DEFAULT 0 COMMENT '是否为主照片(0-否,1-是)',
upload_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
created_time DATETIME DEFAULT 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,
INDEX idx_customer_id (customer_id),
INDEX idx_visit_record_id (visit_record_id),
INDEX idx_is_primary (is_primary)
CREATE TABLE `customer_photos` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '顾客照片ID',
`customer_id` bigint NOT NULL COMMENT '顾客ID',
`visit_record_id` bigint NOT NULL COMMENT '进店记录ID',
`photo_url` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '照片URL',
`is_primary` tinyint DEFAULT '0' COMMENT '是否为主照片(0-否,1-是)',
`upload_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0-未删除,1-已删除)',
PRIMARY KEY (`id`),
KEY `idx_customer_id` (`customer_id`),
KEY `idx_visit_record_id` (`visit_record_id`),
KEY `idx_is_primary` (`is_primary`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='顾客照片表';
-- 8. 试穿效果表
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',
model_photo_id BIGINT COMMENT '模特照片ID',
customer_photo_id BIGINT COMMENT '顾客照片ID',
prompt VARCHAR(500) COMMENT '提示词',
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',
request_id VARCHAR(100) COMMENT '请求ID',
generation_status VARCHAR(20) DEFAULT 'pending' COMMENT '生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)',
error_message TEXT COMMENT '错误信息',
is_favorite TINYINT DEFAULT 0 COMMENT '是否喜欢的最终造型(0-否,1-是)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
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 (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_request_id (request_id),
INDEX idx_generation_status (generation_status),
INDEX idx_is_favorite (is_favorite)
-- 7. 试穿效果表
CREATE TABLE `try_on_effects` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '试穿效果ID',
`customer_id` bigint NOT NULL COMMENT '顾客ID',
`visit_record_id` bigint NOT NULL COMMENT '进店记录ID',
`style_id` bigint NOT NULL COMMENT '风格ID',
`model_photo_id` bigint DEFAULT NULL COMMENT '模特照片ID',
`customer_photo_id` bigint DEFAULT NULL COMMENT '顾客照片ID',
`prompt` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '提示词,当is_regenerated为1时才会有值',
`original_try_on_id` bigint DEFAULT NULL COMMENT '原试穿效果ID,当is_regenerated为1时才会有值',
`is_regenerated` tinyint DEFAULT '0' COMMENT '是否由生成结果重新生成(0-否,1-是)',
`result_image_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '试穿结果图片URL',
`request_id` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求ID',
`generation_status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT 'pending' COMMENT '生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)',
`error_message` text COLLATE utf8mb4_unicode_ci COMMENT '错误信息',
`is_favorite` tinyint DEFAULT '0' COMMENT '是否喜欢的最终造型(0-否,1-是)',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0-未删除,1-已删除)',
PRIMARY KEY (`id`),
KEY `idx_customer_id` (`customer_id`),
KEY `idx_visit_record_id` (`visit_record_id`),
KEY `idx_style_id` (`style_id`),
KEY `idx_request_id` (`request_id`),
KEY `idx_generation_status` (`generation_status`),
KEY `idx_is_favorite` (`is_favorite`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='试穿效果表';
-- 8. 穿搭请求表
CREATE TABLE `outfit_request` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`customer_id` bigint NOT NULL COMMENT '顾客id',
`visit_record_id` bigint NOT NULL COMMENT '进店记录id',
`stylist` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '选择的设计师风格',
`gender` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '选择的性别',
`status` tinyint(1) DEFAULT '0' COMMENT '当前任务状态 0-处理中 1-成功 2-失败',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标志0-未删除1-已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='穿搭请求表';