diff --git a/.gitignore b/.gitignore index 667aaef..cce3969 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ build/ ### VS Code ### .vscode/ +/logs/ diff --git a/pom.xml b/pom.xml index 98f4870..e8faec2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,154 +1,247 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.1.6 - - - com.aida - lanecarford - 0.0.1-SNAPSHOT - lanecarford - Demo project for Spring Boot - - - - - - - - - - - - - - - 21 - - - - - org.springframework.boot - spring-boot-starter-web - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.6 + + + com.aida + lanecarford + 0.0.1-SNAPSHOT + lanecarford + Demo project for Spring Boot + + + + + + + + + + + + + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + - - - org.springframework.boot - spring-boot-starter-security - + + + org.springframework.boot + spring-boot-starter-security + + + + + io.minio + minio + 8.0.3 + - - - - com.baomidou - mybatis-plus-boot-starter - 3.5.5 - + + + com.baomidou + mybatis-plus-boot-starter + 3.5.5 + + + + com.mysql + mysql-connector-j + 8.2.0 + - - - com.mysql - mysql-connector-j - 8.2.0 - + + + org.projectlombok + lombok + true + - - - org.projectlombok - lombok - true - + + + org.springframework.boot + spring-boot-starter-validation + - - - org.springframework.boot - spring-boot-starter-validation - + + + org.springframework.boot + spring-boot-starter-actuator + - - - org.springframework.boot - spring-boot-starter-actuator - + + + com.fasterxml.jackson.core + jackson-databind + - - - com.fasterxml.jackson.core - jackson-databind - + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.6.0 - + + + com.google.auth + google-auth-library-oauth2-http + 1.38.0 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + cn.hutool + hutool-all + 5.8.25 + + + + + com.alibaba + fastjson + 2.0.43 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.security + spring-security-test + test + + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + com.tencentcloudapi + tencentcloud-sdk-java-ses + 3.1.572 + + - - - org.springframework.boot - spring-boot-starter-aop - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + + dev + + dev + + + true + + + + + prod + + prod + + + - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.springframework.security - spring-security-test - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.12.1 - - 21 - 21 - 21 - UTF-8 - - --enable-preview - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - diff --git a/src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java b/src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java index 8c7e272..cc1f866 100644 --- a/src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java +++ b/src/main/java/com/aida/lanecarford/aspect/LoggingAspect.java @@ -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(); } } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java b/src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java index be3394d..a52a443 100644 --- a/src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java +++ b/src/main/java/com/aida/lanecarford/aspect/PerformanceAspect.java @@ -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秒的操作 diff --git a/src/main/java/com/aida/lanecarford/common/ApiResponse.java b/src/main/java/com/aida/lanecarford/common/ApiResponse.java index 491ce98..2d8135d 100644 --- a/src/main/java/com/aida/lanecarford/common/ApiResponse.java +++ b/src/main/java/com/aida/lanecarford/common/ApiResponse.java @@ -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 implements Serializable { /** * 响应状态码 */ - private String code; + private Integer code; /** * 响应消息 @@ -46,7 +47,7 @@ public class ApiResponse 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 implements Serializable { * 成功响应(无数据) */ public static ApiResponse success() { - return new ApiResponse<>(true, "SUCCESS", "操作成功", null); + return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), "操作成功", null); } /** * 成功响应(带数据) */ public static ApiResponse success(T data) { - return new ApiResponse<>(true, "SUCCESS", "操作成功", data); + return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), "操作成功", data); } /** * 成功响应(自定义消息,无数据) */ public static ApiResponse success(String message) { - return new ApiResponse<>(true, "SUCCESS", message, null); + return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), message, null); } /** * 成功响应(自定义消息) */ public static ApiResponse success(String message, T data) { - return new ApiResponse<>(true, "SUCCESS", message, data); + return new ApiResponse<>(true, ResultEnum.SUCCESS.getCode(), message, data); } /** * 失败响应 */ - public static ApiResponse error(String code, String message) { + public static ApiResponse error(Integer code, String message) { return new ApiResponse<>(false, code, message, null); } @@ -93,76 +94,76 @@ public class ApiResponse implements Serializable { * 失败响应(默认错误码) */ public static ApiResponse error(String message) { - return new ApiResponse<>(false, "ERROR", message, null); + return new ApiResponse<>(false, ResultEnum.ERROR.getCode(), message, null); } - /** - * 参数错误响应 - */ - public static ApiResponse paramError(String message) { - return new ApiResponse<>(false, "PARAM_ERROR", message, null); - } - - /** - * 数据不存在响应 - */ - public static ApiResponse notFound(String message) { - return new ApiResponse<>(false, "NOT_FOUND", message, null); - } - - /** - * 权限不足响应 - */ - public static ApiResponse forbidden(String message) { - return new ApiResponse<>(false, "FORBIDDEN", message, null); - } - - /** - * 服务器内部错误响应 - */ - public static ApiResponse serverError(String message) { - return new ApiResponse<>(false, "SERVER_ERROR", message, null); - } - - /** - * 业务异常响应 - */ - public static ApiResponse businessError(String code, String message) { - return new ApiResponse<>(false, code, message, null); - } - - /** - * 外部服务错误响应 - */ - public static ApiResponse externalServiceError(String message) { - return new ApiResponse<>(false, "EXTERNAL_SERVICE_ERROR", message, null); - } - - /** - * 文件上传错误响应 - */ - public static ApiResponse fileUploadError(String message) { - return new ApiResponse<>(false, "FILE_UPLOAD_ERROR", message, null); - } - - /** - * 验证失败响应 - */ - public static ApiResponse validationError(String message) { - return new ApiResponse<>(false, "VALIDATION_ERROR", message, null); - } - - /** - * 重复数据响应 - */ - public static ApiResponse duplicateError(String message) { - return new ApiResponse<>(false, "DUPLICATE_ERROR", message, null); - } - - /** - * 操作超时响应 - */ - public static ApiResponse timeoutError(String message) { - return new ApiResponse<>(false, "TIMEOUT_ERROR", message, null); - } +// /** +// * 参数错误响应 +// */ +// public static ApiResponse paramError(String message) { +// return new ApiResponse<>(false, "PARAM_ERROR", message, null); +// } +// +// /** +// * 数据不存在响应 +// */ +// public static ApiResponse notFound(String message) { +// return new ApiResponse<>(false, "NOT_FOUND", message, null); +// } +// +// /** +// * 权限不足响应 +// */ +// public static ApiResponse forbidden(String message) { +// return new ApiResponse<>(false, "FORBIDDEN", message, null); +// } +// +// /** +// * 服务器内部错误响应 +// */ +// public static ApiResponse serverError(String message) { +// return new ApiResponse<>(false, "SERVER_ERROR", message, null); +// } +// +// /** +// * 业务异常响应 +// */ +// public static ApiResponse businessError(String code, String message) { +// return new ApiResponse<>(false, code, message, null); +// } +// +// /** +// * 外部服务错误响应 +// */ +// public static ApiResponse externalServiceError(String message) { +// return new ApiResponse<>(false, "EXTERNAL_SERVICE_ERROR", message, null); +// } +// +// /** +// * 文件上传错误响应 +// */ +// public static ApiResponse fileUploadError(String message) { +// return new ApiResponse<>(false, "FILE_UPLOAD_ERROR", message, null); +// } +// +// /** +// * 验证失败响应 +// */ +// public static ApiResponse validationError(String message) { +// return new ApiResponse<>(false, "VALIDATION_ERROR", message, null); +// } +// +// /** +// * 重复数据响应 +// */ +// public static ApiResponse duplicateError(String message) { +// return new ApiResponse<>(false, "DUPLICATE_ERROR", message, null); +// } +// +// /** +// * 操作超时响应 +// */ +// public static ApiResponse timeoutError(String message) { +// return new ApiResponse<>(false, "TIMEOUT_ERROR", message, null); +// } } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/common/CommonConstant.java b/src/main/java/com/aida/lanecarford/common/CommonConstant.java new file mode 100644 index 0000000..34f2d79 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/CommonConstant.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/common/constant/CommonConstants.java b/src/main/java/com/aida/lanecarford/common/constant/CommonConstants.java new file mode 100644 index 0000000..ad084ad --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/constant/CommonConstants.java @@ -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) + + +} diff --git a/src/main/java/com/aida/lanecarford/common/constant/MinioFileConstants.java b/src/main/java/com/aida/lanecarford/common/constant/MinioFileConstants.java new file mode 100644 index 0000000..aaad702 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/constant/MinioFileConstants.java @@ -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 + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/common/constant/RedisURIConstants.java b/src/main/java/com/aida/lanecarford/common/constant/RedisURIConstants.java new file mode 100644 index 0000000..6a95f2c --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/constant/RedisURIConstants.java @@ -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:"; + + +} diff --git a/src/main/java/com/aida/lanecarford/common/enums/AuthenticationOperationTypeEnum.java b/src/main/java/com/aida/lanecarford/common/enums/AuthenticationOperationTypeEnum.java new file mode 100644 index 0000000..f34ba1d --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/enums/AuthenticationOperationTypeEnum.java @@ -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); + } +} diff --git a/src/main/java/com/aida/lanecarford/common/enums/LanguageEnum.java b/src/main/java/com/aida/lanecarford/common/enums/LanguageEnum.java new file mode 100644 index 0000000..2a11c96 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/enums/LanguageEnum.java @@ -0,0 +1,8 @@ +package com.aida.lanecarford.common.enums; + +public enum LanguageEnum { + + CHINESE, + + ENGLISH; +} diff --git a/src/main/java/com/aida/lanecarford/common/enums/StatusEnum.java b/src/main/java/com/aida/lanecarford/common/enums/StatusEnum.java new file mode 100644 index 0000000..2582866 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/enums/StatusEnum.java @@ -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); + } + + +} diff --git a/src/main/java/com/aida/lanecarford/common/enums/StylistPathEnum.java b/src/main/java/com/aida/lanecarford/common/enums/StylistPathEnum.java new file mode 100644 index 0000000..4038c25 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/enums/StylistPathEnum.java @@ -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); + } + + +} diff --git a/src/main/java/com/aida/lanecarford/common/response/ResultEnum.java b/src/main/java/com/aida/lanecarford/common/response/ResultEnum.java new file mode 100644 index 0000000..d1df838 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/response/ResultEnum.java @@ -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; + } +} diff --git a/src/main/java/com/aida/lanecarford/common/security/JwtInterceptor.java b/src/main/java/com/aida/lanecarford/common/security/JwtInterceptor.java new file mode 100644 index 0000000..fac1e28 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/security/JwtInterceptor.java @@ -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; + } +} diff --git a/src/main/java/com/aida/lanecarford/common/security/JwtUtil.java b/src/main/java/com/aida/lanecarford/common/security/JwtUtil.java new file mode 100644 index 0000000..b2e7eb0 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/security/JwtUtil.java @@ -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; + } +} diff --git a/src/main/java/com/aida/lanecarford/common/security/config/JwtProperties.java b/src/main/java/com/aida/lanecarford/common/security/config/JwtProperties.java new file mode 100644 index 0000000..8b6f40e --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/security/config/JwtProperties.java @@ -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; +} diff --git a/src/main/java/com/aida/lanecarford/common/security/context/UserContext.java b/src/main/java/com/aida/lanecarford/common/security/context/UserContext.java new file mode 100644 index 0000000..47458ae --- /dev/null +++ b/src/main/java/com/aida/lanecarford/common/security/context/UserContext.java @@ -0,0 +1,19 @@ +package com.aida.lanecarford.common.security.context; + +import com.aida.lanecarford.vo.AuthPrincipalVO; + +public class UserContext { + private static ThreadLocal userHolder = new ThreadLocal(); + + public static AuthPrincipalVO getUserHolder() { + return userHolder.get(); + } + + public static void delete() { + userHolder.remove(); + } + + public static void setUserHolder(AuthPrincipalVO authPrincipalVo) { + userHolder.set(authPrincipalVo); + } +} diff --git a/src/main/java/com/aida/lanecarford/config/MinioConfig.java b/src/main/java/com/aida/lanecarford/config/MinioConfig.java new file mode 100644 index 0000000..c451c3a --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/MinioConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/config/RedisConfig.java b/src/main/java/com/aida/lanecarford/config/RedisConfig.java new file mode 100644 index 0000000..9d80380 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory factory) { + RedisTemplate 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; + } +} diff --git a/src/main/java/com/aida/lanecarford/config/SwaggerConfig.java b/src/main/java/com/aida/lanecarford/config/SwaggerConfig.java index cc6acc0..3363158 100644 --- a/src/main/java/com/aida/lanecarford/config/SwaggerConfig.java +++ b/src/main/java/com/aida/lanecarford/config/SwaggerConfig.java @@ -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 createSecurityRequirements() { return Arrays.asList( new SecurityRequirement().addList("sessionAuth"), - new SecurityRequirement().addList("basicAuth") + new SecurityRequirement().addList("CustomAuth") ); } diff --git a/src/main/java/com/aida/lanecarford/config/WebConfig.java b/src/main/java/com/aida/lanecarford/config/WebConfig.java index 5fe4125..c034a95 100644 --- a/src/main/java/com/aida/lanecarford/config/WebConfig.java +++ b/src/main/java/com/aida/lanecarford/config/WebConfig.java @@ -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/"); } diff --git a/src/main/java/com/aida/lanecarford/controller/ChatController.java b/src/main/java/com/aida/lanecarford/controller/ChatController.java new file mode 100644 index 0000000..c5ff0c2 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/ChatController.java @@ -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); + } +} diff --git a/src/main/java/com/aida/lanecarford/controller/CustomerController.java b/src/main/java/com/aida/lanecarford/controller/CustomerController.java index e2d2435..b524432 100644 --- a/src/main/java/com/aida/lanecarford/controller/CustomerController.java +++ b/src/main/java/com/aida/lanecarford/controller/CustomerController.java @@ -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 customerCheckIn(@RequestParam String name, @RequestParam String email) { + return ApiResponse.success(customerService.customerCheckIn(name, email)); + } + + @PostMapping("/getAllCustomer") + public ApiResponse> getAllCustomer(@Valid @RequestBody BaseRequest request) { + return ApiResponse.success(customerService.getAllCustomer(request)); + } + } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/CustomerPhotoController.java b/src/main/java/com/aida/lanecarford/controller/CustomerPhotoController.java index a8bcf2e..c15cd48 100644 --- a/src/main/java/com/aida/lanecarford/controller/CustomerPhotoController.java +++ b/src/main/java/com/aida/lanecarford/controller/CustomerPhotoController.java @@ -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 upload(@ModelAttribute CustomerPhotoDto customerPhotoDto) { + CustomerPhoto customerPhoto = customerPhotoService.upload(customerPhotoDto); + return ApiResponse.success(customerPhoto); + } + } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/LoginController.java b/src/main/java/com/aida/lanecarford/controller/LoginController.java new file mode 100644 index 0000000..d856d41 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/controller/LoginController.java @@ -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 preCheckAndSendEmail(@Valid @RequestBody LoginRequest loginRequest) { + loginService.preCheckAndSendEmail(loginRequest); + return ApiResponse.success("验证码已发送到您的邮箱"); + } + + @Operation( + summary = "检查邮箱", + description = "根据操作类型验证邮箱有效性并发送验证码。仅支持忘记密码。" + ) + @GetMapping("/precheckEmail") + public ApiResponse precheckForgotPwdAndSendEmail(@Valid @RequestParam String email) { + loginService.precheckForgotPwdAndSendEmail(email); + return ApiResponse.success("验证码已发送到您的邮箱"); + } + + @Operation( + summary = "用户注册或登录", + description = "通过验证码完成用户注册或登录,返回JWT令牌和用户信息。" + ) + @PostMapping("/registerOrLogin") + public ApiResponse registerOrLogin(@Valid @RequestBody LoginRequest loginRequest) { + return ApiResponse.success(loginService.registerOrLogin(loginRequest)); + } + + @Operation( + summary = "用户登出", + description = "清除用户登录状态,使当前JWT令牌失效。" + ) + @GetMapping("/logout") + public ApiResponse logout() { + loginService.logout(); + return ApiResponse.success("登出成功"); + } + + @Operation( + summary = "忘记密码", + description = "通过邮箱验证码重置用户密码。需要先获取验证码,然后提供新密码。" + ) + @PostMapping("/forgotPwd") + public ApiResponse forgotPwd(@Valid @RequestBody LoginRequest loginRequest) { + loginService.forgotPwd(loginRequest); + return ApiResponse.success("密码重置成功"); + } + + @Operation( + summary = "检查登录状态", + description = "验证当前用户的登录状态是否有效,检查JWT令牌是否过期。" + ) + @GetMapping("/checkLoginStatus") + public ApiResponse 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 getUserInfo() { + return ApiResponse.success(loginService.getUserInfo()); + } + +} diff --git a/src/main/java/com/aida/lanecarford/controller/StyleController.java b/src/main/java/com/aida/lanecarford/controller/StyleController.java index ec08b9c..d11a982 100644 --- a/src/main/java/com/aida/lanecarford/controller/StyleController.java +++ b/src/main/java/com/aida/lanecarford/controller/StyleController.java @@ -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> 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> getOutfitResult( + @Parameter(description = "请求ID列表", required = true, example = "[a22019ac-9db2-4076-9953-42d65e8120ec, 20217a09-435d-4b34-962a-3af59881c6d9]") + @RequestParam List requestIDs) { + return ApiResponse.success(styleService.getOutfitResult(requestIDs)); + } + } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java b/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java index 128a9be..702650a 100644 --- a/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java +++ b/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java @@ -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 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> getFavoriteTryOnEffects( + @Parameter(description = "进店记录ID", required = true) + @PathVariable Long visitRecordId) { + List tryOnResultVos = tryOnEffectService.getFavoriteTryOnEffects(visitRecordId); + return ApiResponse.success(tryOnResultVos); + } + + @GetMapping("/style/{styleId}") + @Operation(summary = "获取某套服装的所有生成结果", description = "对应customize your look页面点击finish后的显示") + public ApiResponse> getTryOnEffectsByStyleId( + @Parameter(description = "服装ID", required = true) + @PathVariable Long styleId) { + List tryOnResultVos = tryOnEffectService.getTryOnEffectsByStyleId(styleId); + return ApiResponse.success(tryOnResultVos); + } + + /** + * 设置喜欢的试穿效果 + */ + @Operation(summary = "设置喜欢的试穿效果", description = "将指定试穿效果设置为收藏") + @PostMapping("/set-favorite/{tryOnId}") + public ApiResponse setFavoriteTryOnEffect( + @Parameter(description = "试穿效果ID", required = true) + @PathVariable Long tryOnId) { + tryOnEffectService.setFavoriteTryOnEffect(tryOnId); + return ApiResponse.success(); + } + + /** + * 取消喜欢的试穿效果 + */ + @Operation(summary = "取消喜欢的试穿效果", description = "取消指定试穿效果的收藏") + @PostMapping("/cancel-favorite/{tryOnId}") + public ApiResponse cancelFavoriteTryOnEffect( + @Parameter(description = "试穿效果ID", required = true) + @PathVariable Long tryOnId) { + tryOnEffectService.cancelFavoriteTryOnEffect(tryOnId); + return ApiResponse.success(); + } } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java b/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java index 14be7d4..e056662 100644 --- a/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java +++ b/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java @@ -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 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> getByCustomerId(@PathVariable Long customerId) { + log.info("开始查询顾客ID为{}的进店记录", customerId); + + List 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); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/dto/BaseRequest.java b/src/main/java/com/aida/lanecarford/dto/BaseRequest.java index 6d8e6df..dd527ba 100644 --- a/src/main/java/com/aida/lanecarford/dto/BaseRequest.java +++ b/src/main/java/com/aida/lanecarford/dto/BaseRequest.java @@ -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(); } /** diff --git a/src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java b/src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java new file mode 100644 index 0000000..f7d136b --- /dev/null +++ b/src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/dto/LoginRequest.java b/src/main/java/com/aida/lanecarford/dto/LoginRequest.java new file mode 100644 index 0000000..04e2fea --- /dev/null +++ b/src/main/java/com/aida/lanecarford/dto/LoginRequest.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/dto/OutfitCallbackDTO.java b/src/main/java/com/aida/lanecarford/dto/OutfitCallbackDTO.java new file mode 100644 index 0000000..275bc67 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/dto/OutfitCallbackDTO.java @@ -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> items; +} diff --git a/src/main/java/com/aida/lanecarford/dto/RequestOutfitDTO.java b/src/main/java/com/aida/lanecarford/dto/RequestOutfitDTO.java new file mode 100644 index 0000000..6c1d450 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/dto/RequestOutfitDTO.java @@ -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; + +} diff --git a/src/main/java/com/aida/lanecarford/entity/BaseEntity.java b/src/main/java/com/aida/lanecarford/entity/BaseEntity.java index 04ca019..c40c0c6 100644 --- a/src/main/java/com/aida/lanecarford/entity/BaseEntity.java +++ b/src/main/java/com/aida/lanecarford/entity/BaseEntity.java @@ -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; diff --git a/src/main/java/com/aida/lanecarford/entity/Customer.java b/src/main/java/com/aida/lanecarford/entity/Customer.java index 851f381..928f547 100644 --- a/src/main/java/com/aida/lanecarford/entity/Customer.java +++ b/src/main/java/com/aida/lanecarford/entity/Customer.java @@ -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; } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java b/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java index e2a0799..14a3c4b 100644 --- a/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java +++ b/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java @@ -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 中定义 } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/ModelPhoto.java b/src/main/java/com/aida/lanecarford/entity/ModelPhoto.java index 92d39cc..9ad1c36 100644 --- a/src/main/java/com/aida/lanecarford/entity/ModelPhoto.java +++ b/src/main/java/com/aida/lanecarford/entity/ModelPhoto.java @@ -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 中定义 } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/OutfitRequest.java b/src/main/java/com/aida/lanecarford/entity/OutfitRequest.java new file mode 100644 index 0000000..493573d --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/OutfitRequest.java @@ -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; +} diff --git a/src/main/java/com/aida/lanecarford/entity/Sales.java b/src/main/java/com/aida/lanecarford/entity/Sales.java index cf61be6..8c46ea8 100644 --- a/src/main/java/com/aida/lanecarford/entity/Sales.java +++ b/src/main/java/com/aida/lanecarford/entity/Sales.java @@ -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 中定义 } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/Style.java b/src/main/java/com/aida/lanecarford/entity/Style.java index 00d5f8b..3f170c8 100644 --- a/src/main/java/com/aida/lanecarford/entity/Style.java +++ b/src/main/java/com/aida/lanecarford/entity/Style.java @@ -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 中定义 } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java b/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java index 5909738..6c00a1f 100644 --- a/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java +++ b/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java @@ -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 中定义 } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/User.java b/src/main/java/com/aida/lanecarford/entity/User.java new file mode 100644 index 0000000..03ec098 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/entity/User.java @@ -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; +} diff --git a/src/main/java/com/aida/lanecarford/entity/VisitRecord.java b/src/main/java/com/aida/lanecarford/entity/VisitRecord.java index dd087e2..136338c 100644 --- a/src/main/java/com/aida/lanecarford/entity/VisitRecord.java +++ b/src/main/java/com/aida/lanecarford/entity/VisitRecord.java @@ -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 中定义 } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/exception/BusinessException.java b/src/main/java/com/aida/lanecarford/exception/BusinessException.java index e39b7f8..31f1508 100644 --- a/src/main/java/com/aida/lanecarford/exception/BusinessException.java +++ b/src/main/java/com/aida/lanecarford/exception/BusinessException.java @@ -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); } } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/exception/GlobalExceptionHandler.java b/src/main/java/com/aida/lanecarford/exception/GlobalExceptionHandler.java index f403b64..dabf66f 100644 --- a/src/main/java/com/aida/lanecarford/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/aida/lanecarford/exception/GlobalExceptionHandler.java @@ -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> 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 handleBusinessException(BusinessException e) { + logger.warn("业务异常: {}", e.getMsgCn() != null ? e.getMsgCn() : e.getMsg()); + return ApiResponse.error(e.getCode(), e.getMsg()); } /** * 处理参数验证异常 */ @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) { - logger.warn("参数验证异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); - - Map 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 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> handleBindException(BindException e, HttpServletRequest request) { - logger.warn("绑定异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); - - Map 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 handleBindException(BindException e) { - /** - * 处理约束违反异常 - */ - @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) { - logger.warn("约束违反异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); - - Set> violations = e.getConstraintViolations(); - Map 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> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) { - logger.warn("文件上传大小超限: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); + public ApiResponse 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> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) { - logger.warn("非法参数异常: {} - 请求路径: {}", e.getMessage(), request.getRequestURI()); + @ExceptionHandler(MinioException.class) + public ApiResponse 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> 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 handleSQLException(SQLException e) { - /** - * 处理参数类型不匹配异常 - */ - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity> 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> 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> 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> 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> 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> 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> 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> handleRuntimeException(RuntimeException e, HttpServletRequest request) { - logger.error("运行时异常 - 请求路径: {}", request.getRequestURI(), e); + public ApiResponse 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> handleException(Exception e, HttpServletRequest request) { - logger.error("未知异常 - 请求路径: {}", request.getRequestURI(), e); + public ApiResponse handleException(Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(createErrorResponse(UNKNOWN_ERROR, "系统内部错误", request.getRequestURI(), null)); - } - - /** - * 创建统一的错误响应格式 - */ - private Map createErrorResponse(String code, String message, String path, Map errors) { - Map 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"); } } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/exception/MinioException.java b/src/main/java/com/aida/lanecarford/exception/MinioException.java new file mode 100644 index 0000000..66c9746 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/exception/MinioException.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/mapper/OutfitRequestMapper.java b/src/main/java/com/aida/lanecarford/mapper/OutfitRequestMapper.java new file mode 100644 index 0000000..a891880 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/mapper/OutfitRequestMapper.java @@ -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 { +} diff --git a/src/main/java/com/aida/lanecarford/mapper/UserMapper.java b/src/main/java/com/aida/lanecarford/mapper/UserMapper.java new file mode 100644 index 0000000..2145d53 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/mapper/UserMapper.java @@ -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 { +} diff --git a/src/main/java/com/aida/lanecarford/service/ChatService.java b/src/main/java/com/aida/lanecarford/service/ChatService.java new file mode 100644 index 0000000..4dadc8c --- /dev/null +++ b/src/main/java/com/aida/lanecarford/service/ChatService.java @@ -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); +} diff --git a/src/main/java/com/aida/lanecarford/service/CustomerPhotoService.java b/src/main/java/com/aida/lanecarford/service/CustomerPhotoService.java index 5a992c8..b5afc77 100644 --- a/src/main/java/com/aida/lanecarford/service/CustomerPhotoService.java +++ b/src/main/java/com/aida/lanecarford/service/CustomerPhotoService.java @@ -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 upload(CustomerPhotoDto customerPhotoDto); } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/service/CustomerService.java b/src/main/java/com/aida/lanecarford/service/CustomerService.java index e79851d..c5abcf3 100644 --- a/src/main/java/com/aida/lanecarford/service/CustomerService.java +++ b/src/main/java/com/aida/lanecarford/service/CustomerService.java @@ -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 { + CustomerCheckInVO customerCheckIn(String name, String email); + + IPage getAllCustomer(BaseRequest request); + } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/service/ImageCompositionService.java b/src/main/java/com/aida/lanecarford/service/ImageCompositionService.java new file mode 100644 index 0000000..c74f69b --- /dev/null +++ b/src/main/java/com/aida/lanecarford/service/ImageCompositionService.java @@ -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 imageUrls); +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/service/LoginService.java b/src/main/java/com/aida/lanecarford/service/LoginService.java new file mode 100644 index 0000000..4ddefb5 --- /dev/null +++ b/src/main/java/com/aida/lanecarford/service/LoginService.java @@ -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 { + + void preCheckAndSendEmail(LoginRequest loginRequest); + + void precheckForgotPwdAndSendEmail(String email); + + LoginVO registerOrLogin(LoginRequest loginRequest); + + void logout(); + + void forgotPwd(LoginRequest loginRequest); + + boolean checkLoginStatus(); + + User getUserInfo(); +} diff --git a/src/main/java/com/aida/lanecarford/service/StyleService.java b/src/main/java/com/aida/lanecarford/service/StyleService.java index 89c456f..780e462 100644 --- a/src/main/java/com/aida/lanecarford/service/StyleService.java +++ b/src/main/java/com/aida/lanecarford/service/StyleService.java @@ -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