This commit is contained in:
litianxiang
2025-10-24 17:42:54 +08:00
parent 1444f3adb1
commit e1deaf194a
12 changed files with 232 additions and 123 deletions

View File

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

View File

@@ -40,7 +40,8 @@ public class WebConfig implements WebMvcConfigurer {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**/**") // 保护这些路径
.excludePathPatterns(Arrays.asList("/api/auth/precheckAndSendEmail", "/api/auth/register",
"/api/auth/login", "/api/auth/forgotPwd", "/api/style/callback")); // 排除登录接口
"/api/auth/login", "/api/auth/forgotPwd", "/api/style/callback","/api/try-on-effects","/api/customer-photos","/api/visit-records")); // 排除登录接口
}
/**

View File

@@ -6,6 +6,8 @@ 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.*;
@@ -16,10 +18,15 @@ import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/customers")
@RequiredArgsConstructor
@Tag(name = "顾客管理", description = "顾客入店登记、信息查询等相关API接口")
public class CustomerController {
private final CustomerService customerService;
@Operation(
summary = "顾客入店登记",
description = "验证顾客身份并创建入店记录,如果是新顾客则自动注册到系统中。"
)
@GetMapping("/checkIn")
public ApiResponse<CustomerCheckInVO> customerCheckIn(@RequestParam String name, @RequestParam String email) {
return ApiResponse.success(customerService.customerCheckIn(name, email));

View File

@@ -5,8 +5,8 @@ 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -24,7 +24,7 @@ public class CustomerPhotoController {
private final CustomerPhotoService customerPhotoService;
@PostMapping("/upload")
public ApiResponse<CustomerPhoto> upload(@RequestBody CustomerPhotoDto customerPhotoDto) {
public ApiResponse<CustomerPhoto> upload(@ModelAttribute CustomerPhotoDto customerPhotoDto) {
CustomerPhoto customerPhoto = customerPhotoService.upload(customerPhotoDto);
return ApiResponse.success(customerPhoto);
}

View File

@@ -4,45 +4,72 @@ import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.service.LoginService;
import com.aida.lanecarford.vo.LoginVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
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 {
@Resource
private LoginService loginService;
private final LoginService loginService;
@Operation(
summary = "预检查并发送邮箱验证码",
description = "根据操作类型验证邮箱有效性并发送验证码。支持注册、登录、忘记密码三种操作类型。"
)
@PostMapping("/precheckAndSendEmail")
public ApiResponse<String> preCheckAndSendEmail(@Valid @RequestBody LoginRequest loginRequest) {
loginService.preCheckAndSendEmail(loginRequest);
return ApiResponse.success();
return ApiResponse.success("验证码已发送到您的邮箱");
}
@Operation(
summary = "用户注册或登录",
description = "通过验证码完成用户注册或登录返回JWT令牌和用户信息。"
)
@PostMapping("/registerOrLogin")
public ApiResponse<LoginVO> registerOrLogin(@Valid @RequestBody LoginRequest loginRequest) {
return ApiResponse.success(loginService.registerOrLogin(loginRequest));
}
@Operation(
summary = "用户登出",
description = "清除用户登录状态使当前JWT令牌失效。"
)
@GetMapping("/logout")
public ApiResponse<String> logout() {
loginService.logout();
return ApiResponse.success();
return ApiResponse.success("登出成功");
}
@Operation(
summary = "忘记密码",
description = "通过邮箱验证码重置用户密码。需要先获取验证码,然后提供新密码。"
)
@PostMapping("/forgotPwd")
public ApiResponse<String> forgotPwd(@Valid @RequestBody LoginRequest loginRequest) {
loginService.forgotPwd(loginRequest);
return ApiResponse.success();
return ApiResponse.success("密码重置成功");
}
@Operation(
summary = "检查登录状态",
description = "验证当前用户的登录状态是否有效检查JWT令牌是否过期。"
)
@GetMapping("/checkLoginStatus")
public ApiResponse<String> checkLoginStatus() {
boolean isLogin = loginService.checkLoginStatus();
if (isLogin){
return ApiResponse.success();
if (isLogin) {
return ApiResponse.success("用户已登录");
} else {
return ApiResponse.error("Please log in again.");
}

View File

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

View File

@@ -32,7 +32,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ApiResponse<Object> handleBusinessException(BusinessException e) {
logger.warn("业务异常: {}", e.getMsgCn() != null ? e.getMsgCn() : e.getMsg());
return ApiResponse.error(String.valueOf(e.getCode()), e.getMsg());
return ApiResponse.error(e.getCode(), e.getMsg());
}
/**
@@ -49,7 +49,7 @@ public class GlobalExceptionHandler {
.reduce((msg1, msg2) -> msg1 + "; " + msg2)
.orElse("Parameter validation failed");
return ApiResponse.error(String.valueOf(ResultEnum.PARAMETER_ERROR.getCode()), errorMessageEn);
return ApiResponse.error(ResultEnum.PARAMETER_ERROR.getCode(), errorMessageEn);
}
/**
@@ -66,7 +66,7 @@ public class GlobalExceptionHandler {
.reduce((msg1, msg2) -> msg1 + "; " + msg2)
.orElse("Data binding failed");
return ApiResponse.error(String.valueOf(ResultEnum.PARAMETER_ERROR.getCode()), errorMessageEn);
return ApiResponse.error(ResultEnum.PARAMETER_ERROR.getCode(), errorMessageEn);
}
/**
@@ -77,7 +77,7 @@ public class GlobalExceptionHandler {
logger.warn("文件上传大小超限: {}", e.getMessage());
return ApiResponse.error(String.valueOf(ResultEnum.PARAMETER_ERROR.getCode()), "File size exceeds limit");
return ApiResponse.error(ResultEnum.PARAMETER_ERROR.getCode(), "File size exceeds limit");
}
/**
@@ -88,7 +88,7 @@ public class GlobalExceptionHandler {
logger.error("MinIO异常: {}", e.getMessage(), e);
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "File storage service error");
return ApiResponse.error(ResultEnum.ERROR.getCode(), "File storage service error");
}
/**
@@ -99,7 +99,7 @@ public class GlobalExceptionHandler {
logger.error("数据库异常: {}", e.getMessage(), e);
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "Database operation failed");
return ApiResponse.error(ResultEnum.ERROR.getCode(), "Database operation failed");
}
/**
@@ -110,7 +110,7 @@ public class GlobalExceptionHandler {
logger.error("运行时异常: {}", e.getMessage(), e);
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "System runtime error");
return ApiResponse.error(ResultEnum.ERROR.getCode(), "System runtime error");
}
/**
@@ -121,6 +121,6 @@ public class GlobalExceptionHandler {
logger.error("未知异常: {}", e.getMessage(), e);
return ApiResponse.error(String.valueOf(ResultEnum.ERROR.getCode()), "System internal error");
return ApiResponse.error(ResultEnum.ERROR.getCode(), "System internal error");
}
}

View File

@@ -129,7 +129,7 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
// 3.更新path, items, 状态
// 由于数据变化较频繁考虑存到redis
if (outfitResult instanceof OutfitResultVO) {
((OutfitResultVO) outfitResult).setPath(minioUtil.getPresignedUrl(callbackDTO.getPath(), CommonConstants.MINIO_PATH_TIMEOUT));
((OutfitResultVO) outfitResult).setPath(minioUtil.getPresignedUrl(callbackDTO.getPath(), CommonConstants.MINIO_PATH_TIMEOUT, null));
((OutfitResultVO) outfitResult).setStatus(StatusEnum.SUCCEEDED.name());
cacheUtil.setCache(key, outfitResult, RedisURIConstants.verifyCodeTimeout);
}
@@ -181,7 +181,7 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
outfitResultVO.setRequestId(style.getPythonRequestId());
outfitResultVO.setStatus(style.getGenerationStatus());
if (!StringUtil.isNullOrEmpty(style.getStyleImageUrl())) {
outfitResultVO.setPath(minioUtil.getPresignedUrl(style.getStyleImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
outfitResultVO.setPath(minioUtil.getPresignedUrl(style.getStyleImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT, null));
}
resultVOS.add(outfitResultVO);
}

View File

@@ -103,34 +103,37 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
}
}
// 合成图片
if (!imageUrls.isEmpty() && imageUrls.size() > 1) {
log.info("开始合成图片,图片数量: {}", imageUrls.size());
try {
// 将逻辑URL批量转换为预签名URL以便图像合成服务可以访问
List<String> presignedUrls = minioUtil.convertToPresignedUrls(imageUrls, CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
log.debug("批量转换逻辑URL为预签名URL数量: {}", presignedUrls.size());
toAIlogicalUrl = imageCompositionService.composeAndUploadImages(presignedUrls);
log.info("图片合成成功合成图片URL: {}", toAIlogicalUrl);
} catch (Exception e) {
log.error("图片合成失败: {}", e.getMessage(), e);
throw new RuntimeException("image error " + e.getMessage(), e);
}
} else if (imageUrls.size() == 1) {
toAIlogicalUrl = imageUrls.get(0);
} else {
log.warn("没有找到有效的图片URL进行合成");
throw BusinessException.parameterRequired("image");
}
}
// 合成图片
if (!imageUrls.isEmpty() && imageUrls.size() > 1) {
log.info("开始合成图片,图片数量: {}", imageUrls.size());
try {
// 将逻辑URL批量转换为预签名URL以便图像合成服务可以访问
List<String> presignedUrls = minioUtil.convertToPresignedUrls(imageUrls, CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
log.debug("批量转换逻辑URL为预签名URL数量: {}", presignedUrls.size());
toAIlogicalUrl = imageCompositionService.composeAndUploadImages(presignedUrls);
log.info("图片合成成功合成图片URL: {}", toAIlogicalUrl);
} catch (Exception e) {
log.error("图片合成失败: {}", e.getMessage(), e);
throw new RuntimeException("image error " + e.getMessage(), e);
}
} else if (imageUrls.size() == 1) {
toAIlogicalUrl = imageUrls.get(0);
} else {
log.warn("没有找到有效的图片URL进行合成");
throw BusinessException.parameterRequired("image");
}
//调用模型生成试穿效果
log.info("准备调用第三方AI服务输入图片URL: {}", toAIlogicalUrl);
String AIRreultlogicalUrl = AITryOnEffect(prompt, toAIlogicalUrl);
prompt = "A woman is wearing the outfit (an outerwear, a top, pants, shoes, a handbag), white background, full body, professional portrait photography."+prompt;
// String AIRreultlogicalUrl = AITryOnEffect(prompt, toAIlogicalUrl);
String AIRreultlogicalUrl = "try_on_result/6ecb707a-f541-437a-aef5-9544b5b18164.png";
tryOnEffectDto.setResultImageUrl(AIRreultlogicalUrl);
tryOnEffectDto.setGenerationStatus("completed");
this.saveOrUpdate(tryOnEffectDto);
TryOnResultVo tryOnResultVo = new TryOnResultVo();

View File

@@ -65,7 +65,6 @@ public class VisitRecordServiceImpl extends ServiceImpl<VisitRecordMapper, Visit
LambdaQueryWrapper<TryOnEffect> effectWrapper = new LambdaQueryWrapper<>();
effectWrapper.eq(TryOnEffect::getVisitRecordId, visitRecord.getId())
.eq(TryOnEffect::getIsFavorite, 1)
.eq(TryOnEffect::getIsRegenerated, 0)
.orderByDesc(TryOnEffect::getCreatedTime)
.last("LIMIT 1");

View File

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

View File

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