Merge branch 'dev/dev' into prod/release_1.0
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/logs/
|
||||
|
||||
353
pom.xml
353
pom.xml
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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秒的操作
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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:";
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.aida.lanecarford.common.enums;
|
||||
|
||||
public enum LanguageEnum {
|
||||
|
||||
CHINESE,
|
||||
|
||||
ENGLISH;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
124
src/main/java/com/aida/lanecarford/common/security/JwtUtil.java
Normal file
124
src/main/java/com/aida/lanecarford/common/security/JwtUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
58
src/main/java/com/aida/lanecarford/config/MinioConfig.java
Normal file
58
src/main/java/com/aida/lanecarford/config/MinioConfig.java
Normal 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();
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/aida/lanecarford/config/RedisConfig.java
Normal file
30
src/main/java/com/aida/lanecarford/config/RedisConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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/");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
15
src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java
Normal file
15
src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java
Normal 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;
|
||||
}
|
||||
63
src/main/java/com/aida/lanecarford/dto/LoginRequest.java
Normal file
63
src/main/java/com/aida/lanecarford/dto/LoginRequest.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
55
src/main/java/com/aida/lanecarford/dto/RequestOutfitDTO.java
Normal file
55
src/main/java/com/aida/lanecarford/dto/RequestOutfitDTO.java
Normal 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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 中定义
|
||||
}
|
||||
@@ -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 中定义
|
||||
}
|
||||
34
src/main/java/com/aida/lanecarford/entity/OutfitRequest.java
Normal file
34
src/main/java/com/aida/lanecarford/entity/OutfitRequest.java
Normal 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;
|
||||
}
|
||||
@@ -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 中定义
|
||||
}
|
||||
@@ -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 中定义
|
||||
}
|
||||
@@ -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 中定义
|
||||
}
|
||||
65
src/main/java/com/aida/lanecarford/entity/User.java
Normal file
65
src/main/java/com/aida/lanecarford/entity/User.java
Normal 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;
|
||||
}
|
||||
@@ -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 中定义
|
||||
}
|
||||
@@ -1,121 +1,126 @@
|
||||
package com.aida.lanecarford.exception;
|
||||
|
||||
import com.aida.lanecarford.common.response.ResultEnum;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 业务异常类
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private String code;
|
||||
private String message;
|
||||
private Integer code;
|
||||
private String msg; // 英文消息,返回给前端
|
||||
private String msgCn; // 中文消息,用于日志
|
||||
|
||||
public BusinessException(String code, String message) {
|
||||
super(message);
|
||||
public BusinessException(String msg) {
|
||||
this.code = ResultEnum.FAIL.getCode();
|
||||
this.msg = msg;
|
||||
this.msgCn = msg;
|
||||
}
|
||||
|
||||
public BusinessException(String msg, Integer code) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.msg = msg;
|
||||
this.msgCn = msg;
|
||||
}
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = "BUSINESS_ERROR";
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BusinessException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
public BusinessException(String msg, String msgCn, Integer code) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.msg = msg;
|
||||
this.msgCn = msgCn;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
public BusinessException(ResultEnum resultEnum) {
|
||||
this.code = resultEnum.getCode();
|
||||
this.msg = resultEnum.getMsg();
|
||||
this.msgCn = resultEnum.getMsgCn();
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
public BusinessException(ResultEnum resultEnum, String customMessage, String customMessageCn) {
|
||||
this.code = resultEnum.getCode();
|
||||
this.msg = customMessage;
|
||||
this.msgCn = customMessageCn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
/**
|
||||
* 创建原始试穿ID必填异常
|
||||
*/
|
||||
public static BusinessException originalTryOnIdRequired() {
|
||||
return new BusinessException(ResultEnum.PARAMETER_ERROR, "Original try-on ID is required", "原始试穿ID不能为空");
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
/**
|
||||
* 创建风格ID必填异常
|
||||
*/
|
||||
public static BusinessException styleIdRequired() {
|
||||
return new BusinessException(ResultEnum.PARAMETER_ERROR, "Style ID is required", "风格ID不能为空");
|
||||
}
|
||||
|
||||
// 常用的业务异常静态方法
|
||||
public static BusinessException customerNotFound() {
|
||||
return new BusinessException("CUSTOMER_NOT_FOUND", "客户不存在");
|
||||
/**
|
||||
* 创建通用参数为空异常
|
||||
*/
|
||||
public static BusinessException parameterRequired(String parameterName) {
|
||||
return new BusinessException(ResultEnum.PARAMETER_ERROR,
|
||||
parameterName + " is required",
|
||||
parameterName + "不能为空");
|
||||
}
|
||||
|
||||
public static BusinessException styleOutfitNotFound() {
|
||||
return new BusinessException("STYLE_OUTFIT_NOT_FOUND", "风格搭配不存在");
|
||||
}
|
||||
|
||||
public static BusinessException modelPhotoNotFound() {
|
||||
return new BusinessException("MODEL_PHOTO_NOT_FOUND", "模特照片不存在");
|
||||
}
|
||||
|
||||
public static BusinessException virtualTryOnNotFound() {
|
||||
return new BusinessException("VIRTUAL_TRYON_NOT_FOUND", "虚拟试穿记录不存在");
|
||||
}
|
||||
|
||||
public static BusinessException favoriteNotFound() {
|
||||
return new BusinessException("FAVORITE_NOT_FOUND", "收藏记录不存在");
|
||||
}
|
||||
|
||||
public static BusinessException fileUploadFailed() {
|
||||
return new BusinessException("FILE_UPLOAD_FAILED", "文件上传失败");
|
||||
}
|
||||
|
||||
public static BusinessException invalidFileFormat() {
|
||||
return new BusinessException("INVALID_FILE_FORMAT", "文件格式不支持");
|
||||
}
|
||||
|
||||
public static BusinessException fileSizeExceeded() {
|
||||
return new BusinessException("FILE_SIZE_EXCEEDED", "文件大小超过限制");
|
||||
}
|
||||
|
||||
public static BusinessException emailAlreadyExists() {
|
||||
return new BusinessException("EMAIL_ALREADY_EXISTS", "邮箱已存在");
|
||||
}
|
||||
|
||||
public static BusinessException phoneAlreadyExists() {
|
||||
return new BusinessException("PHONE_ALREADY_EXISTS", "手机号已存在");
|
||||
}
|
||||
|
||||
public static BusinessException invalidCredentials() {
|
||||
return new BusinessException("INVALID_CREDENTIALS", "用户名或密码错误");
|
||||
/**
|
||||
* 创建资源不存在异常
|
||||
*/
|
||||
public static BusinessException resourceNotFound(String resourceName) {
|
||||
return new BusinessException(ResultEnum.FAIL,
|
||||
resourceName + " not found",
|
||||
resourceName + "不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建权限不足异常
|
||||
*/
|
||||
public static BusinessException accessDenied() {
|
||||
return new BusinessException("ACCESS_DENIED", "访问被拒绝");
|
||||
return new BusinessException(ResultEnum.NO_PERMISSION);
|
||||
}
|
||||
|
||||
public static BusinessException operationFailed() {
|
||||
return new BusinessException("OPERATION_FAILED", "操作失败");
|
||||
/**
|
||||
* 创建操作失败异常
|
||||
*/
|
||||
public static BusinessException operationFailed(String operation) {
|
||||
return new BusinessException(ResultEnum.ERROR,
|
||||
operation + " operation failed",
|
||||
operation + "操作失败");
|
||||
}
|
||||
|
||||
public static BusinessException dataNotFound() {
|
||||
return new BusinessException("DATA_NOT_FOUND", "数据不存在");
|
||||
/**
|
||||
* 创建用户未登录异常
|
||||
*/
|
||||
public static BusinessException notLogin() {
|
||||
return new BusinessException(ResultEnum.NO_LOGIN);
|
||||
}
|
||||
|
||||
public static BusinessException duplicateData() {
|
||||
return new BusinessException("DUPLICATE_DATA", "数据重复");
|
||||
/**
|
||||
* 创建账户锁定异常
|
||||
*/
|
||||
public static BusinessException accountLocked() {
|
||||
return new BusinessException(ResultEnum.ACCOUNT_LOCK);
|
||||
}
|
||||
|
||||
public static BusinessException invalidParameter(String paramName) {
|
||||
return new BusinessException("INVALID_PARAMETER", "参数 " + paramName + " 无效");
|
||||
/**
|
||||
* 创建提示异常
|
||||
*/
|
||||
public static BusinessException prompt(String message, String messageCn) {
|
||||
return new BusinessException(ResultEnum.PROMPT, message, messageCn);
|
||||
}
|
||||
|
||||
public static BusinessException serviceUnavailable() {
|
||||
return new BusinessException("SERVICE_UNAVAILABLE", "服务暂时不可用");
|
||||
}
|
||||
|
||||
public static BusinessException externalServiceError() {
|
||||
return new BusinessException("EXTERNAL_SERVICE_ERROR", "外部服务调用失败");
|
||||
/**
|
||||
* 创建警告异常
|
||||
*/
|
||||
public static BusinessException warning(String message, String messageCn) {
|
||||
return new BusinessException(ResultEnum.WARNING, message, messageCn);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,23 @@
|
||||
package com.aida.lanecarford.exception;
|
||||
|
||||
import com.aida.lanecarford.common.ApiResponse;
|
||||
import com.aida.lanecarford.common.response.ResultEnum;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
* 统一处理应用程序中的各种异常,提供一致的错误响应格式
|
||||
*
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
@@ -39,246 +25,102 @@ import java.util.stream.Collectors;
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
// 错误代码常量
|
||||
private static final String BUSINESS_ERROR = "BUSINESS_ERROR";
|
||||
private static final String VALIDATION_ERROR = "VALIDATION_ERROR";
|
||||
private static final String BIND_ERROR = "BIND_ERROR";
|
||||
private static final String FILE_SIZE_EXCEEDED = "FILE_SIZE_EXCEEDED";
|
||||
private static final String ILLEGAL_ARGUMENT = "ILLEGAL_ARGUMENT";
|
||||
private static final String NULL_POINTER = "NULL_POINTER";
|
||||
private static final String RUNTIME_ERROR = "RUNTIME_ERROR";
|
||||
private static final String UNKNOWN_ERROR = "UNKNOWN_ERROR";
|
||||
private static final String DATABASE_ERROR = "DATABASE_ERROR";
|
||||
private static final String METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
|
||||
private static final String NOT_FOUND = "NOT_FOUND";
|
||||
private static final String MEDIA_TYPE_NOT_SUPPORTED = "MEDIA_TYPE_NOT_SUPPORTED";
|
||||
private static final String MESSAGE_NOT_READABLE = "MESSAGE_NOT_READABLE";
|
||||
private static final String MISSING_PARAMETER = "MISSING_PARAMETER";
|
||||
private static final String TYPE_MISMATCH = "TYPE_MISMATCH";
|
||||
private static final String CONSTRAINT_VIOLATION = "CONSTRAINT_VIOLATION";
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||
logger.warn("业务异常 [{}]: {} - 请求路径: {}", e.getCode(), e.getMessage(), request.getRequestURI());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(createErrorResponse(BUSINESS_ERROR, e.getMessage(), request.getRequestURI(), null));
|
||||
public ApiResponse<Object> handleBusinessException(BusinessException e) {
|
||||
logger.warn("业务异常: {}", e.getMsgCn() != null ? e.getMsgCn() : e.getMsg());
|
||||
return ApiResponse.error(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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.aida.lanecarford.exception;
|
||||
|
||||
/**
|
||||
* MinIO 操作异常类
|
||||
* 用于处理 MinIO 相关的业务异常
|
||||
*
|
||||
* @author Aida
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public class MinioException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param message 异常信息
|
||||
*/
|
||||
public MinioException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param message 异常信息
|
||||
* @param cause 原因
|
||||
*/
|
||||
public MinioException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
* @param message 异常信息
|
||||
*/
|
||||
public MinioException(String errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
* @param message 异常信息
|
||||
* @param cause 原因
|
||||
*/
|
||||
public MinioException(String errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误码
|
||||
*
|
||||
* @return 错误码
|
||||
*/
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误码
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
*/
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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> {
|
||||
}
|
||||
13
src/main/java/com/aida/lanecarford/mapper/UserMapper.java
Normal file
13
src/main/java/com/aida/lanecarford/mapper/UserMapper.java
Normal 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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
31
src/main/java/com/aida/lanecarford/service/LoginService.java
Normal file
31
src/main/java/com/aida/lanecarford/service/LoginService.java
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/main/java/com/aida/lanecarford/util/CacheUtil.java
Normal file
71
src/main/java/com/aida/lanecarford/util/CacheUtil.java
Normal 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
|
||||
|
||||
}
|
||||
127
src/main/java/com/aida/lanecarford/util/CopyUtil.java
Normal file
127
src/main/java/com/aida/lanecarford/util/CopyUtil.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
97
src/main/java/com/aida/lanecarford/util/DateUtil.java
Normal file
97
src/main/java/com/aida/lanecarford/util/DateUtil.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package com.aida.lanecarford.util;
|
||||
|
||||
import com.aida.lanecarford.exception.BusinessException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 文件工具类
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public class FileUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
|
||||
|
||||
/**
|
||||
* 支持的图片格式
|
||||
*/
|
||||
private static final List<String> SUPPORTED_IMAGE_FORMATS = Arrays.asList(
|
||||
"jpg", "jpeg", "png", "gif", "bmp", "webp"
|
||||
);
|
||||
|
||||
/**
|
||||
* 最大文件大小(10MB)
|
||||
*/
|
||||
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* 上传根目录
|
||||
*/
|
||||
private static final String UPLOAD_ROOT_DIR = "uploads";
|
||||
|
||||
/**
|
||||
* 验证文件是否为图片
|
||||
*/
|
||||
public static boolean isImageFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = getFileExtension(originalFilename).toLowerCase();
|
||||
return SUPPORTED_IMAGE_FORMATS.contains(extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件大小
|
||||
*/
|
||||
public static boolean isValidFileSize(MultipartFile file) {
|
||||
return file != null && file.getSize() <= MAX_FILE_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
public static String getFileExtension(String filename) {
|
||||
if (filename == null || filename.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return filename.substring(lastDotIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一文件名
|
||||
*/
|
||||
public static String generateUniqueFileName(String originalFilename) {
|
||||
String extension = getFileExtension(originalFilename);
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
return timestamp + "_" + uuid + "." + extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录结构
|
||||
*/
|
||||
public static String createDirectoryStructure(String category) {
|
||||
String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
String fullPath = UPLOAD_ROOT_DIR + File.separator + category + File.separator + dateDir;
|
||||
|
||||
try {
|
||||
Path path = Paths.get(fullPath);
|
||||
Files.createDirectories(path);
|
||||
return fullPath;
|
||||
} catch (IOException e) {
|
||||
logger.error("创建目录失败: {}", fullPath, e);
|
||||
throw BusinessException.fileUploadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
*/
|
||||
public static String saveFile(MultipartFile file, String category) {
|
||||
// 验证文件
|
||||
validateFile(file);
|
||||
|
||||
// 创建目录
|
||||
String directoryPath = createDirectoryStructure(category);
|
||||
|
||||
// 生成文件名
|
||||
String filename = generateUniqueFileName(file.getOriginalFilename());
|
||||
|
||||
// 完整文件路径
|
||||
String fullPath = directoryPath + File.separator + filename;
|
||||
|
||||
try {
|
||||
// 保存文件
|
||||
Path filePath = Paths.get(fullPath);
|
||||
Files.write(filePath, file.getBytes());
|
||||
|
||||
logger.info("文件保存成功: {}", fullPath);
|
||||
|
||||
// 返回相对路径(用于数据库存储和访问)
|
||||
return fullPath.replace(File.separator, "/");
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("文件保存失败: {}", fullPath, e);
|
||||
throw BusinessException.fileUploadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
public static boolean deleteFile(String filePath) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||
boolean deleted = Files.deleteIfExists(path);
|
||||
|
||||
if (deleted) {
|
||||
logger.info("文件删除成功: {}", filePath);
|
||||
} else {
|
||||
logger.warn("文件不存在或删除失败: {}", filePath);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
} catch (IOException e) {
|
||||
logger.error("文件删除失败: {}", filePath, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
public static boolean fileExists(String filePath) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||
return Files.exists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小(字节)
|
||||
*/
|
||||
public static long getFileSize(String filePath) {
|
||||
if (!fileExists(filePath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = Paths.get(filePath.replace("/", File.separator));
|
||||
return Files.size(path);
|
||||
} catch (IOException e) {
|
||||
logger.error("获取文件大小失败: {}", filePath, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
public static String formatFileSize(long size) {
|
||||
if (size < 1024) {
|
||||
return size + " B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return String.format("%.1f KB", size / 1024.0);
|
||||
} else if (size < 1024 * 1024 * 1024) {
|
||||
return String.format("%.1f MB", size / (1024.0 * 1024.0));
|
||||
} else {
|
||||
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件
|
||||
*/
|
||||
private static void validateFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw BusinessException.invalidParameter("file");
|
||||
}
|
||||
|
||||
if (!isImageFile(file)) {
|
||||
throw BusinessException.invalidFileFormat();
|
||||
}
|
||||
|
||||
if (!isValidFileSize(file)) {
|
||||
throw BusinessException.fileSizeExceeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的MIME类型
|
||||
*/
|
||||
public static String getContentType(String filename) {
|
||||
String extension = getFileExtension(filename).toLowerCase();
|
||||
|
||||
switch (extension) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "bmp":
|
||||
return "image/bmp";
|
||||
case "webp":
|
||||
return "image/webp";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期文件(可用于定时任务)
|
||||
*/
|
||||
public static void cleanupExpiredFiles(String directoryPath, int daysToKeep) {
|
||||
// TODO: 实现文件清理逻辑
|
||||
logger.info("清理过期文件: {} 天前的文件", daysToKeep);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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());
|
||||
}
|
||||
}
|
||||
478
src/main/java/com/aida/lanecarford/util/MinioUtil.java
Normal file
478
src/main/java/com/aida/lanecarford/util/MinioUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/main/java/com/aida/lanecarford/util/RandomsUtil.java
Normal file
19
src/main/java/com/aida/lanecarford/util/RandomsUtil.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
126
src/main/java/com/aida/lanecarford/util/SendEmailUtil.java
Normal file
126
src/main/java/com/aida/lanecarford/util/SendEmailUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
113
src/main/java/com/aida/lanecarford/util/SendRequestUtil.java
Normal file
113
src/main/java/com/aida/lanecarford/util/SendRequestUtil.java
Normal 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) // 直接传入Map,Hutool会正确处理
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/aida/lanecarford/vo/AuthPrincipalVO.java
Normal file
17
src/main/java/com/aida/lanecarford/vo/AuthPrincipalVO.java
Normal 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;
|
||||
}
|
||||
29
src/main/java/com/aida/lanecarford/vo/CustomerCheckInVO.java
Normal file
29
src/main/java/com/aida/lanecarford/vo/CustomerCheckInVO.java
Normal 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;
|
||||
}
|
||||
27
src/main/java/com/aida/lanecarford/vo/CustomerVO.java
Normal file
27
src/main/java/com/aida/lanecarford/vo/CustomerVO.java
Normal 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;
|
||||
}
|
||||
17
src/main/java/com/aida/lanecarford/vo/LibraryVo.java
Normal file
17
src/main/java/com/aida/lanecarford/vo/LibraryVo.java
Normal 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;
|
||||
}
|
||||
17
src/main/java/com/aida/lanecarford/vo/LoginVO.java
Normal file
17
src/main/java/com/aida/lanecarford/vo/LoginVO.java
Normal 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;
|
||||
}
|
||||
34
src/main/java/com/aida/lanecarford/vo/OutfitResultVO.java
Normal file
34
src/main/java/com/aida/lanecarford/vo/OutfitResultVO.java
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/aida/lanecarford/vo/TryOnResultVo.java
Normal file
17
src/main/java/com/aida/lanecarford/vo/TryOnResultVo.java
Normal 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;
|
||||
}
|
||||
13
src/main/resources/aida-461108-b4afaabebb84.json
Normal file
13
src/main/resources/aida-461108-b4afaabebb84.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "aida-461108",
|
||||
"private_key_id": "b4afaabebb84da24502b318a5fa175f1dc5c096a",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmk7LKrp8g9yD1\nWmF+mY2qHCEZ/5aIx6QRh0QoVPBL7Yi7ce009QxaE8fu8+QMgg8l3xMreXvgpt56\noFnVwpFusLjSdjgoFluElM2hYxXlO9q8cbBoU2nehOBLLJzGzkodT7xu/BOjNvKC\n//aTbjtJyk8Kj+ENa0/dPaUZs/PCtQqpAu8ag5nXrordVWfO0K25EjeYyoba35zk\nPp2fBi8KALZZI5Xfd2z9++K0K2mWWIMJic30idHvquj0WxlTRK2Pq8BmJXCQpJIi\nQ5E4egue16BfKjrF0Kxkpqd1RmdlEmaSKbbkZXe2z4jg0qknESRFOmRy8C3LnaB2\nHHJWLYM3AgMBAAECggEACUdroOQJSTTQSS/iWRhZ+S0yoC10nTnsZxg527qfiBs7\nOqB7WNqC+Ew8dDsca6CdvLuoaGDkCFJDTQwRn66u8JOM4sG4bxiPuzBEJBv45EQT\n8zCsuvhVNWgBdoPjAnq19jFdixvPnDqQrRYaY4FdxsaA5f24c57pW/xLGMYawLBt\n9RJZSuWmJdzKG1i5W8a8+4f/seNtuo2MtXU3mPJZPqRWPXTAZeaQPM/57ZQ+kzig\nOkAbQZNRmt1yPCjPCQD8vc8yCBMmjus/rlHXD/L7okYUlVZkob5I3FBrLl+ZyIXS\nqxEsBLBwRW3w8WbX+ZSVciQ72JK68W7LnOHSAENmAQKBgQDgBTCqp87KGLWVPb8w\nK+s1Sfh+nM3M4AlbLdcGBs1JCoddF6pAeY4wpf/ow1Tm4rqEuCYzMClPwxvkue+D\nY7lCQgy2FK3ahUzn8oVmvEPD/YPAojDSY3bH0lquHuS6oVKk834JUykButaAU3XY\nvUGNQuKdLKAeQRT8Q6um4m+EYQKBgQC+Wz6nYESKH6GiNnuFTH8hIkThPlbi4wua\nU1kGnPKe3ouE4zRLfPwQ6RRf1slQ/2hFLOatiTLYUgZWZQeBPSWp2EjYcOSzob+7\n11+KqeIRCD5DKxgf0cjJdihK9AM639OKlH2NvZ2507TksdeTPDzdaOMLwLWKexP5\nlYrdob0ulwKBgD81t7Gvf83Ogw4FSjkRa2Cx6ofvPrKcVIeBu7ZbnPkLG37M+qEO\nq2xWqorG8uHi/7YLL9wprr5u0yQKwuZT8SYc9PE7jIKoMjcQW0vNu2FF2zMzkIsM\nvatMU4Hl/awbcPJSMjH3YQ635WZ4Jjxtyl1NjhvDR7rBqmYzwe9o3QaBAoGANhPB\n1tbYYczepDCKIrI6o3US0FJfaJFLqInpDqHjoxJh3FyXbKKTEVLFwPxJsML+IjjB\nR6dkVGPo/P4yhZqTao7REvvvXMCksX5b3A6q9F+9IGPLtK5qNiFlDPYJPN59QC8z\nA+NMPZBRIW8MaP2B5Px5E8upRy/z2sGK86+RCP0CgYATGs75F97q+Zf8q+Pe3Nsb\ngqmhLoI3PZUSWgBcQgNF4nyCZceUrEl72wKO/NWLgxqQPtlra187ce69g7qARHLb\ntHq80nb0f7lil74B6+OlyNNO1htWA90fmGR2s16Mt0BwJRT+/EFuNqbJIUSLxKiW\nqlXBUbmHHzamo5DPYL8S/w==\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "aida-239@aida-461108.iam.gserviceaccount.com",
|
||||
"client_id": "103102077955178349079",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/aida-239%40aida-461108.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
84
src/main/resources/application-dev.yml
Normal file
84
src/main/resources/application-dev.yml
Normal 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
|
||||
72
src/main/resources/application-prod.yml
Normal file
72
src/main/resources/application-prod.yml
Normal 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
|
||||
@@ -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
|
||||
156
src/main/resources/logback-spring.xml
Normal file
156
src/main/resources/logback-spring.xml
Normal 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>
|
||||
@@ -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='穿搭请求表';
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user