From ac7de2709960871bf983cd5f7094a8400d7778ec Mon Sep 17 00:00:00 2001 From: litianxiang Date: Fri, 15 May 2026 15:09:21 +0800 Subject: [PATCH] first --- .gitignore | 3 + pom.xml | 227 +++++++++++++++ .../com/aida/buyer/AidaBuyerApplication.java | 21 ++ .../common/constants/BuyerConstants.java | 13 + .../common/constants/CommonConstants.java | 8 + .../common/constants/MinioFileConstants.java | 19 ++ .../common/constants/StatusConstants.java | 12 + .../buyer/common/context/UserContext.java | 54 ++++ .../common/exception/BusinessException.java | 37 +++ .../exception/GlobalExceptionHandler.java | 53 ++++ .../exception/UnauthorizedException.java | 14 + .../interceptor/UserContextInterceptor.java | 79 +++++ .../buyer/common/result/PageResponse.java | 29 ++ .../aida/buyer/common/result/Response.java | 48 ++++ .../aida/buyer/common/result/ResultEnum.java | 19 ++ .../buyer/config/GatewayAuthProperties.java | 16 ++ .../java/com/aida/buyer/config/JwtConfig.java | 14 + .../com/aida/buyer/config/MinioConfig.java | 27 ++ .../aida/buyer/config/MyBatisPlusConfig.java | 18 ++ .../buyer/config/MyMetaObjectHandler.java | 22 ++ .../com/aida/buyer/config/RedisConfig.java | 29 ++ .../com/aida/buyer/config/SwaggerConfig.java | 23 ++ .../java/com/aida/buyer/config/WebConfig.java | 33 +++ .../aida/buyer/model/vo/AuthPrincipalVo.java | 24 ++ .../controller/BuyerAccountController.java | 69 +++++ .../module/account/dto/BindEmailDTO.java | 22 ++ .../buyer/module/account/dto/BindEmailVO.java | 19 ++ .../buyer/module/account/dto/LoginDTO.java | 22 ++ .../buyer/module/account/dto/PreLoginDTO.java | 18 ++ .../buyer/module/account/dto/PreLoginVO.java | 16 ++ .../buyer/module/account/dto/RegisterDTO.java | 30 ++ .../buyer/module/account/dto/RegisterVO.java | 30 ++ .../module/account/dto/ResetPasswordDTO.java | 22 ++ .../buyer/module/account/dto/SendCodeDTO.java | 14 + .../module/account/dto/SendEmailCodeDTO.java | 18 ++ .../module/account/dto/VerifyCodeDTO.java | 22 ++ .../module/account/dto/VerifyCodeVO.java | 16 ++ .../module/account/entity/BuyerAccount.java | 40 +++ .../account/mapper/BuyerAccountMapper.java | 11 + .../account/service/IBuyerAccountService.java | 23 ++ .../service/impl/BuyerAccountServiceImpl.java | 269 ++++++++++++++++++ .../controller/DesignerController.java | 43 +++ .../designer/feign/DesignerFeignClient.java | 24 ++ .../designer/service/DesignerService.java | 29 ++ .../service/impl/DesignerServiceImpl.java | 31 ++ .../module/designer/vo/DesignerSearchVO.java | 33 +++ .../module/designer/vo/DesignerShopVO.java | 26 ++ .../listing/controller/ListingController.java | 50 ++++ .../listing/dto/ListingMallQueryDTO.java | 34 +++ .../listing/dto/ListingShopQueryDTO.java | 27 ++ .../listing/feign/ListingFeignClient.java | 32 +++ .../listing/service/ListingService.java | 41 +++ .../service/impl/ListingServiceImpl.java | 63 ++++ .../module/listing/vo/ListingDetailVO.java | 44 +++ .../module/listing/vo/ListingMallVO.java | 25 ++ .../module/listing/vo/ListingPageVO.java | 26 ++ .../aida/buyer/util/DesensitizationUtil.java | 41 +++ .../com/aida/buyer/util/LoginCacheUtil.java | 32 +++ .../java/com/aida/buyer/util/MinioUtil.java | 150 ++++++++++ .../java/com/aida/buyer/util/RandomsUtil.java | 10 + .../com/aida/buyer/util/RedisLoginUtil.java | 30 ++ .../java/com/aida/buyer/util/RedisUtil.java | 92 ++++++ .../com/aida/buyer/util/SendEmailUtil.java | 76 +++++ .../aida/buyer/util/TokenGenerateUtils.java | 55 ++++ .../com/aida/buyer/util/ValidationUtil.java | 40 +++ src/main/resources/application.yml | 24 ++ src/main/resources/bootstrap.yml | 31 ++ src/main/resources/db/schema.sql | 23 ++ src/main/resources/logback-spring.xml | 61 ++++ 69 files changed, 2696 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/aida/buyer/AidaBuyerApplication.java create mode 100644 src/main/java/com/aida/buyer/common/constants/BuyerConstants.java create mode 100644 src/main/java/com/aida/buyer/common/constants/CommonConstants.java create mode 100644 src/main/java/com/aida/buyer/common/constants/MinioFileConstants.java create mode 100644 src/main/java/com/aida/buyer/common/constants/StatusConstants.java create mode 100644 src/main/java/com/aida/buyer/common/context/UserContext.java create mode 100644 src/main/java/com/aida/buyer/common/exception/BusinessException.java create mode 100644 src/main/java/com/aida/buyer/common/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/aida/buyer/common/exception/UnauthorizedException.java create mode 100644 src/main/java/com/aida/buyer/common/interceptor/UserContextInterceptor.java create mode 100644 src/main/java/com/aida/buyer/common/result/PageResponse.java create mode 100644 src/main/java/com/aida/buyer/common/result/Response.java create mode 100644 src/main/java/com/aida/buyer/common/result/ResultEnum.java create mode 100644 src/main/java/com/aida/buyer/config/GatewayAuthProperties.java create mode 100644 src/main/java/com/aida/buyer/config/JwtConfig.java create mode 100644 src/main/java/com/aida/buyer/config/MinioConfig.java create mode 100644 src/main/java/com/aida/buyer/config/MyBatisPlusConfig.java create mode 100644 src/main/java/com/aida/buyer/config/MyMetaObjectHandler.java create mode 100644 src/main/java/com/aida/buyer/config/RedisConfig.java create mode 100644 src/main/java/com/aida/buyer/config/SwaggerConfig.java create mode 100644 src/main/java/com/aida/buyer/config/WebConfig.java create mode 100644 src/main/java/com/aida/buyer/model/vo/AuthPrincipalVo.java create mode 100644 src/main/java/com/aida/buyer/module/account/controller/BuyerAccountController.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/BindEmailDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/BindEmailVO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/LoginDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/PreLoginDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/PreLoginVO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/RegisterDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/RegisterVO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/ResetPasswordDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/SendCodeDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/SendEmailCodeDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/VerifyCodeDTO.java create mode 100644 src/main/java/com/aida/buyer/module/account/dto/VerifyCodeVO.java create mode 100644 src/main/java/com/aida/buyer/module/account/entity/BuyerAccount.java create mode 100644 src/main/java/com/aida/buyer/module/account/mapper/BuyerAccountMapper.java create mode 100644 src/main/java/com/aida/buyer/module/account/service/IBuyerAccountService.java create mode 100644 src/main/java/com/aida/buyer/module/account/service/impl/BuyerAccountServiceImpl.java create mode 100644 src/main/java/com/aida/buyer/module/designer/controller/DesignerController.java create mode 100644 src/main/java/com/aida/buyer/module/designer/feign/DesignerFeignClient.java create mode 100644 src/main/java/com/aida/buyer/module/designer/service/DesignerService.java create mode 100644 src/main/java/com/aida/buyer/module/designer/service/impl/DesignerServiceImpl.java create mode 100644 src/main/java/com/aida/buyer/module/designer/vo/DesignerSearchVO.java create mode 100644 src/main/java/com/aida/buyer/module/designer/vo/DesignerShopVO.java create mode 100644 src/main/java/com/aida/buyer/module/listing/controller/ListingController.java create mode 100644 src/main/java/com/aida/buyer/module/listing/dto/ListingMallQueryDTO.java create mode 100644 src/main/java/com/aida/buyer/module/listing/dto/ListingShopQueryDTO.java create mode 100644 src/main/java/com/aida/buyer/module/listing/feign/ListingFeignClient.java create mode 100644 src/main/java/com/aida/buyer/module/listing/service/ListingService.java create mode 100644 src/main/java/com/aida/buyer/module/listing/service/impl/ListingServiceImpl.java create mode 100644 src/main/java/com/aida/buyer/module/listing/vo/ListingDetailVO.java create mode 100644 src/main/java/com/aida/buyer/module/listing/vo/ListingMallVO.java create mode 100644 src/main/java/com/aida/buyer/module/listing/vo/ListingPageVO.java create mode 100644 src/main/java/com/aida/buyer/util/DesensitizationUtil.java create mode 100644 src/main/java/com/aida/buyer/util/LoginCacheUtil.java create mode 100644 src/main/java/com/aida/buyer/util/MinioUtil.java create mode 100644 src/main/java/com/aida/buyer/util/RandomsUtil.java create mode 100644 src/main/java/com/aida/buyer/util/RedisLoginUtil.java create mode 100644 src/main/java/com/aida/buyer/util/RedisUtil.java create mode 100644 src/main/java/com/aida/buyer/util/SendEmailUtil.java create mode 100644 src/main/java/com/aida/buyer/util/TokenGenerateUtils.java create mode 100644 src/main/java/com/aida/buyer/util/ValidationUtil.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/bootstrap.yml create mode 100644 src/main/resources/db/schema.sql create mode 100644 src/main/resources/logback-spring.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa6f3a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target/ +/aida-buyer.iml +/.idea/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..617b5bc --- /dev/null +++ b/pom.xml @@ -0,0 +1,227 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.aida + aida-buyer + 1.0.0 + jar + AiDA Buyer + AiDA Buyer Service + + + 21 + 21 + 21 + UTF-8 + + 3.5.7 + 8.0.3 + 0.12.3 + 5.8.23 + 3.13.0 + 4.4.0 + + 2023.0.3.4 + 2023.0.4 + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + + com.mysql + mysql-connector-j + + + + + io.minio + minio + ${minio.version} + + + + + io.jsonwebtoken + jjwt-api + ${jwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jwt.version} + runtime + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + + com.alibaba + druid-spring-boot-3-starter + 1.2.21 + + + + + com.alibaba + fastjson + 2.0.43 + + + + + com.baomidou + dynamic-datasource-spring-boot3-starter + 4.3.1 + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + com.tencentcloudapi + tencentcloud-sdk-java-ses + 3.1.572 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + ${project.basedir} + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/aida/buyer/AidaBuyerApplication.java b/src/main/java/com/aida/buyer/AidaBuyerApplication.java new file mode 100644 index 0000000..45a1976 --- /dev/null +++ b/src/main/java/com/aida/buyer/AidaBuyerApplication.java @@ -0,0 +1,21 @@ +package com.aida.buyer; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@MapperScan("com.aida.buyer.module.*.mapper") +@EnableFeignClients +@EnableDiscoveryClient +@ComponentScan(basePackages = "com.aida.buyer") +public class AidaBuyerApplication { + + public static void main(String[] args) { + SpringApplication.run(AidaBuyerApplication.class, args); + System.out.println("AidaBuyerApplication 启动完成!"); + } +} diff --git a/src/main/java/com/aida/buyer/common/constants/BuyerConstants.java b/src/main/java/com/aida/buyer/common/constants/BuyerConstants.java new file mode 100644 index 0000000..0a0c449 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/constants/BuyerConstants.java @@ -0,0 +1,13 @@ +package com.aida.buyer.common.constants; + +public class BuyerConstants { + + public static final Integer FAVORITE_STATUS_ACTIVE = 1; + public static final Integer FAVORITE_STATUS_REMOVED = 0; + + public static final Integer CART_STATUS_ACTIVE = 1; + public static final Integer CART_STATUS_REMOVED = 0; + + public static final Integer ADDRESS_STATUS_DEFAULT = 1; + public static final Integer ADDRESS_STATUS_NORMAL = 0; +} diff --git a/src/main/java/com/aida/buyer/common/constants/CommonConstants.java b/src/main/java/com/aida/buyer/common/constants/CommonConstants.java new file mode 100644 index 0000000..efe2ee8 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/constants/CommonConstants.java @@ -0,0 +1,8 @@ +package com.aida.buyer.common.constants; + +public class CommonConstants { + + public static final int MINIO_PATH_TIMEOUT = 7 * 24 * 60 * 60; // minio图片临时访问地址 7 天过期(second) + + public static final int TOKEN_EXPIRE_TIME = 7 * 24; // token 7 天过期(Hour) +} diff --git a/src/main/java/com/aida/buyer/common/constants/MinioFileConstants.java b/src/main/java/com/aida/buyer/common/constants/MinioFileConstants.java new file mode 100644 index 0000000..16d6a31 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/constants/MinioFileConstants.java @@ -0,0 +1,19 @@ +package com.aida.buyer.common.constants; + +public class MinioFileConstants { + + public static final String PATH_SEPARATOR = "/"; + + public static final String FILE_TYPE_PNG = "png"; + public static final String FILE_TYPE_JPG = "jpg"; + public static final String FILE_TYPE_JPEG = "jpeg"; + public static final String FILE_TYPE_GIF = "gif"; + public static final String FILE_TYPE_WEBP = "webp"; + public static final String FILE_TYPE_PDF = "pdf"; + + public static final String CONTENT_TYPE_PNG = "image/png"; + public static final String CONTENT_TYPE_JPG = "image/jpeg"; + public static final String CONTENT_TYPE_GIF = "image/gif"; + public static final String CONTENT_TYPE_WEBP = "image/webp"; + public static final String CONTENT_TYPE_PDF = "application/pdf"; +} diff --git a/src/main/java/com/aida/buyer/common/constants/StatusConstants.java b/src/main/java/com/aida/buyer/common/constants/StatusConstants.java new file mode 100644 index 0000000..34a57d2 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/constants/StatusConstants.java @@ -0,0 +1,12 @@ +package com.aida.buyer.common.constants; + +public class StatusConstants { + + public static final Integer ENABLE = 1; + public static final Integer DISABLE = 0; + public static final Integer DELETE = 1; + public static final Integer NOT_DELETE = 0; + public static final Integer AUDIT_PENDING = 0; + public static final Integer AUDIT_APPROVED = 1; + public static final Integer AUDIT_REJECTED = 2; +} diff --git a/src/main/java/com/aida/buyer/common/context/UserContext.java b/src/main/java/com/aida/buyer/common/context/UserContext.java new file mode 100644 index 0000000..1ce254e --- /dev/null +++ b/src/main/java/com/aida/buyer/common/context/UserContext.java @@ -0,0 +1,54 @@ +package com.aida.buyer.common.context; + +import com.aida.buyer.common.exception.UnauthorizedException; +import com.aida.buyer.model.vo.AuthPrincipalVo; + +public class UserContext { + private static final ThreadLocal userHolder = new ThreadLocal<>(); + private static final ThreadLocal optionalAuth = ThreadLocal.withInitial(() -> false); + + public static void setUserHolder(AuthPrincipalVo authPrincipalVo) { + userHolder.set(authPrincipalVo); + } + + public static void setOptionalAuth(boolean value) { + optionalAuth.set(value); + } + + public static AuthPrincipalVo getUserHolder() { + AuthPrincipalVo holder = userHolder.get(); + if (holder == null) { + if (optionalAuth.get()) { + return null; + } + throw new UnauthorizedException("Gateway token verification failed"); + } + if (!"AIDA".equals(holder.getSource())) { + throw new UnauthorizedException("Gateway token verification failed"); + } + return holder; + } + + public static void delete() { + userHolder.remove(); + optionalAuth.remove(); + } + + public static Long getUserId() { + return getUserHolder() == null ? null : getUserHolder().getId(); + } + + public static Long getBuyerId() { + AuthPrincipalVo holder = userHolder.get(); + if (holder == null) { + if (optionalAuth.get()) { + return null; + } + throw new UnauthorizedException("Gateway token verification failed"); + } + if (!"BUYER".equals(holder.getSource())) { + throw new UnauthorizedException("Gateway token verification failed"); + } + return holder.getId(); + } +} diff --git a/src/main/java/com/aida/buyer/common/exception/BusinessException.java b/src/main/java/com/aida/buyer/common/exception/BusinessException.java new file mode 100644 index 0000000..3d2b096 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/exception/BusinessException.java @@ -0,0 +1,37 @@ +package com.aida.buyer.common.exception; + +import com.aida.buyer.common.result.ResultEnum; +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final Integer code; + private final String msg; + + public BusinessException(ResultEnum resultEnum) { + super(resultEnum.getMessage()); + this.code = resultEnum.getCode(); + this.msg = resultEnum.getMessage(); + } + + public BusinessException(Integer code, String msg) { + super(msg); + this.code = code; + this.msg = msg; + } + + public BusinessException(String message) { + super(message); + this.code = -1; + this.msg = message; + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + this.code = -1; + this.msg = message; + } +} diff --git a/src/main/java/com/aida/buyer/common/exception/GlobalExceptionHandler.java b/src/main/java/com/aida/buyer/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a78194f --- /dev/null +++ b/src/main/java/com/aida/buyer/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package com.aida.buyer.common.exception; + +import com.aida.buyer.common.result.Response; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(UnauthorizedException.class) + public ResponseEntity> handleUnauthorizedException(UnauthorizedException e) { + log.error("Unauthorized: {}", e.getMessage()); + return new ResponseEntity<>( + Response.fail(401, e.getMessage()), + HttpStatus.UNAUTHORIZED + ); + } + + @ExceptionHandler(BusinessException.class) + public Response handleBusinessException(BusinessException e) { + return Response.fail(e.getCode(), e.getMsg()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public Response handleValidationException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .findFirst() + .orElse("validation error"); + return Response.fail(-2, message); + } + + @ExceptionHandler(BindException.class) + public Response handleBindException(BindException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .findFirst() + .orElse("bind error"); + return Response.fail(-2, message); + } + + @ExceptionHandler(Exception.class) + public Response handleException(Exception e) { + log.error("system error: ", e); + return Response.error("系统繁忙"); + } +} diff --git a/src/main/java/com/aida/buyer/common/exception/UnauthorizedException.java b/src/main/java/com/aida/buyer/common/exception/UnauthorizedException.java new file mode 100644 index 0000000..ea91122 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/exception/UnauthorizedException.java @@ -0,0 +1,14 @@ +package com.aida.buyer.common.exception; + +public class UnauthorizedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public UnauthorizedException(String message) { + super(message); + } + + public UnauthorizedException() { + super("Gateway token verification failed"); + } +} diff --git a/src/main/java/com/aida/buyer/common/interceptor/UserContextInterceptor.java b/src/main/java/com/aida/buyer/common/interceptor/UserContextInterceptor.java new file mode 100644 index 0000000..3cbb775 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/interceptor/UserContextInterceptor.java @@ -0,0 +1,79 @@ +package com.aida.buyer.common.interceptor; + +import com.aida.buyer.common.context.UserContext; +import com.aida.buyer.config.GatewayAuthProperties; +import com.aida.buyer.model.vo.AuthPrincipalVo; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class UserContextInterceptor implements HandlerInterceptor { + + private static final String USER_ID_HEADER = "X-User-Id"; + private static final String USER_INFO_HEADER = "X-User-Info"; + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + private final List localOptionalAuthPaths; + + public UserContextInterceptor(GatewayAuthProperties gatewayAuthProperties) { + this.localOptionalAuthPaths = gatewayAuthProperties.getOptionalAuthPaths().stream() + .map(this::toLocalPath) + .collect(Collectors.toList()); + log.info("Local optional auth paths: {}", localOptionalAuthPaths); + } + + private String toLocalPath(String gatewayPath) { + if (gatewayPath == null) { + return gatewayPath; + } + if (gatewayPath.startsWith("/buyer")) { + String local = gatewayPath.substring("/buyer".length()); + return local.isEmpty() ? "/" : local; + } + return gatewayPath; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String userInfoJson = request.getHeader(USER_INFO_HEADER); + if (userInfoJson != null && !userInfoJson.isBlank()) { + try { + AuthPrincipalVo principal = objectMapper.readValue(userInfoJson, AuthPrincipalVo.class); + UserContext.setUserHolder(principal); + } catch (Exception e) { + log.warn("Failed to parse X-User-Info header: {}", e.getMessage()); + } + } else if (isOptionalAuthPath(request.getRequestURI())) { + UserContext.setOptionalAuth(true); + } + return true; + } + + private boolean isOptionalAuthPath(String requestUri) { + if (localOptionalAuthPaths == null || localOptionalAuthPaths.isEmpty()) { + return false; + } + for (String pattern : localOptionalAuthPaths) { + if (pathMatcher.match(pattern, requestUri)) { + return true; + } + } + return false; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) { + UserContext.delete(); + } +} diff --git a/src/main/java/com/aida/buyer/common/result/PageResponse.java b/src/main/java/com/aida/buyer/common/result/PageResponse.java new file mode 100644 index 0000000..26bac93 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/result/PageResponse.java @@ -0,0 +1,29 @@ +package com.aida.buyer.common.result; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@Data +public class PageResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long page; + private Long size; + private Long pages; + private Long total; + private List content; + + public static PageResponse success(IPage page) { + PageResponse response = new PageResponse<>(); + response.setPage(page.getCurrent()); + response.setSize(page.getSize()); + response.setPages(page.getPages()); + response.setTotal(page.getTotal()); + response.setContent(page.getRecords()); + return response; + } +} diff --git a/src/main/java/com/aida/buyer/common/result/Response.java b/src/main/java/com/aida/buyer/common/result/Response.java new file mode 100644 index 0000000..6ac6ca2 --- /dev/null +++ b/src/main/java/com/aida/buyer/common/result/Response.java @@ -0,0 +1,48 @@ +package com.aida.buyer.common.result; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class Response implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer errCode; + private String errMsg; + private T data; + + public static Response success() { + return success(null); + } + + public static Response success(T data) { + Response response = new Response<>(); + response.setErrCode(ResultEnum.SUCCESS.getCode()); + response.setErrMsg(ResultEnum.SUCCESS.getMessage()); + response.setData(data); + return response; + } + + public static Response fail(String errMsg) { + Response response = new Response<>(); + response.setErrCode(ResultEnum.FAIL.getCode()); + response.setErrMsg(errMsg); + return response; + } + + public static Response fail(Integer errCode, String errMsg) { + Response response = new Response<>(); + response.setErrCode(errCode); + response.setErrMsg(errMsg); + return response; + } + + public static Response error(String errMsg) { + Response response = new Response<>(); + response.setErrCode(ResultEnum.FAIL.getCode()); + response.setErrMsg(errMsg); + return response; + } +} diff --git a/src/main/java/com/aida/buyer/common/result/ResultEnum.java b/src/main/java/com/aida/buyer/common/result/ResultEnum.java new file mode 100644 index 0000000..6269b8f --- /dev/null +++ b/src/main/java/com/aida/buyer/common/result/ResultEnum.java @@ -0,0 +1,19 @@ +package com.aida.buyer.common.result; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ResultEnum { + + SUCCESS(0, "success"), + FAIL(-1, "fail"), + PARAMETER_ERROR(-2, "parameter error"), + NO_LOGIN(-100, "no login"), + NO_PERMISSION(-200, "no permission"), + ACCOUNT_LOCK(-300, "account locked"); + + private final Integer code; + private final String message; +} diff --git a/src/main/java/com/aida/buyer/config/GatewayAuthProperties.java b/src/main/java/com/aida/buyer/config/GatewayAuthProperties.java new file mode 100644 index 0000000..3745956 --- /dev/null +++ b/src/main/java/com/aida/buyer/config/GatewayAuthProperties.java @@ -0,0 +1,16 @@ +package com.aida.buyer.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Configuration +@ConfigurationProperties(prefix = "gateway.auth") +public class GatewayAuthProperties { + + private List optionalAuthPaths = new ArrayList<>(); +} diff --git a/src/main/java/com/aida/buyer/config/JwtConfig.java b/src/main/java/com/aida/buyer/config/JwtConfig.java new file mode 100644 index 0000000..aa6c2da --- /dev/null +++ b/src/main/java/com/aida/buyer/config/JwtConfig.java @@ -0,0 +1,14 @@ +package com.aida.buyer.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "spring.security") +public class JwtConfig { + + private String jwtSecret; + private Long jwtExpiration; +} diff --git a/src/main/java/com/aida/buyer/config/MinioConfig.java b/src/main/java/com/aida/buyer/config/MinioConfig.java new file mode 100644 index 0000000..dde2e14 --- /dev/null +++ b/src/main/java/com/aida/buyer/config/MinioConfig.java @@ -0,0 +1,27 @@ +package com.aida.buyer.config; + +import io.minio.MinioClient; +import io.minio.MinioClient.Builder; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "minio") +public class MinioConfig { + + private String endpoint; + private String accessKey; + private String secretKey; + private String bucketName; + + @Bean + public MinioClient minioClient() { + return new Builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + } +} diff --git a/src/main/java/com/aida/buyer/config/MyBatisPlusConfig.java b/src/main/java/com/aida/buyer/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..640c642 --- /dev/null +++ b/src/main/java/com/aida/buyer/config/MyBatisPlusConfig.java @@ -0,0 +1,18 @@ +package com.aida.buyer.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MyBatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } +} diff --git a/src/main/java/com/aida/buyer/config/MyMetaObjectHandler.java b/src/main/java/com/aida/buyer/config/MyMetaObjectHandler.java new file mode 100644 index 0000000..3505f73 --- /dev/null +++ b/src/main/java/com/aida/buyer/config/MyMetaObjectHandler.java @@ -0,0 +1,22 @@ +package com.aida.buyer.config; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class MyMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); + this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + } + + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + } +} diff --git a/src/main/java/com/aida/buyer/config/RedisConfig.java b/src/main/java/com/aida/buyer/config/RedisConfig.java new file mode 100644 index 0000000..01b7e9f --- /dev/null +++ b/src/main/java/com/aida/buyer/config/RedisConfig.java @@ -0,0 +1,29 @@ +package com.aida.buyer.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 connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + StringRedisSerializer stringSerializer = new StringRedisSerializer(); + GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); + + template.setKeySerializer(stringSerializer); + template.setValueSerializer(jsonSerializer); + template.setHashKeySerializer(stringSerializer); + template.setHashValueSerializer(jsonSerializer); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/com/aida/buyer/config/SwaggerConfig.java b/src/main/java/com/aida/buyer/config/SwaggerConfig.java new file mode 100644 index 0000000..7b86367 --- /dev/null +++ b/src/main/java/com/aida/buyer/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package com.aida.buyer.config; + +import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableKnife4j +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("AiDA买家后台API文档") + .description("AiDA买家后台服务接口文档") + .version("1.0.0") + .contact(new Contact().name("AiDA Team"))); + } +} diff --git a/src/main/java/com/aida/buyer/config/WebConfig.java b/src/main/java/com/aida/buyer/config/WebConfig.java new file mode 100644 index 0000000..abcd0b3 --- /dev/null +++ b/src/main/java/com/aida/buyer/config/WebConfig.java @@ -0,0 +1,33 @@ +package com.aida.buyer.config; + +import com.aida.buyer.common.interceptor.UserContextInterceptor; +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.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final UserContextInterceptor userContextInterceptor; + + public WebConfig(UserContextInterceptor userContextInterceptor) { + this.userContextInterceptor = userContextInterceptor; + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(userContextInterceptor) + .addPathPatterns("/**"); + } +} diff --git a/src/main/java/com/aida/buyer/model/vo/AuthPrincipalVo.java b/src/main/java/com/aida/buyer/model/vo/AuthPrincipalVo.java new file mode 100644 index 0000000..e1c6645 --- /dev/null +++ b/src/main/java/com/aida/buyer/model/vo/AuthPrincipalVo.java @@ -0,0 +1,24 @@ +package com.aida.buyer.model.vo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuthPrincipalVo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String username; + private String avatarUrl; + private Boolean isAdmin; + private String source; + private Integer status; + private String language; + private String country; + private List authorities; +} diff --git a/src/main/java/com/aida/buyer/module/account/controller/BuyerAccountController.java b/src/main/java/com/aida/buyer/module/account/controller/BuyerAccountController.java new file mode 100644 index 0000000..6ea5003 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/controller/BuyerAccountController.java @@ -0,0 +1,69 @@ +package com.aida.buyer.module.account.controller; + +import com.aida.buyer.module.account.dto.*; +import com.aida.buyer.module.account.service.IBuyerAccountService; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/account") +@RequiredArgsConstructor +@Tag(name = "Buyer Account", description = "Buyer registration and login") +public class BuyerAccountController { + + private final IBuyerAccountService buyerAccountService; + + @PostMapping("/sendCode") + @Operation(summary = "忘记密码:发送邮箱验证码") + public Boolean sendCode(@Valid @RequestBody SendCodeDTO dto) { + return buyerAccountService.sendCode(dto); + } + + @PostMapping("/register") + @Operation(summary = "注册") + public Boolean register(@Valid @RequestBody RegisterDTO dto) { + return buyerAccountService.register(dto); + } + + @PostMapping("/preLogin") + @Operation(summary = "登录第一步:校验密码并发送验证码") + public PreLoginVO preLogin(@Valid @RequestBody PreLoginDTO dto) { + return buyerAccountService.preLogin(dto); + } + + @PostMapping("/login") + @Operation(summary = "登录第二步:校验验证码并返回token") + public RegisterVO login(@Valid @RequestBody LoginDTO dto) { + return buyerAccountService.login(dto); + } + + @PostMapping("/verifyCode") + @Operation(summary = "通用验证码校验(根据operationType区分:FORGET_PWD, BIND_MAILBOX)") + public VerifyCodeVO verifyCode(@Valid @RequestBody VerifyCodeDTO dto) { + return buyerAccountService.verifyCode(dto); + } + + @PostMapping("/resetPassword") + @Operation(summary = "忘记密码:重置密码") + public Boolean resetPassword(@Valid @RequestBody ResetPasswordDTO dto) { + return buyerAccountService.resetPassword(dto); + } + + @PostMapping("/sendEmailChangeCode") + @Operation(summary = "变更邮箱:发送新邮箱验证码") + public Boolean sendEmailChangeCode(@Valid @RequestBody SendEmailCodeDTO dto) { + return buyerAccountService.sendEmailChangeCode(dto); + } + + @PostMapping("/bindEmail") + @Operation(summary = "变更邮箱:绑定新邮箱") + public BindEmailVO bindEmail(@Valid @RequestBody BindEmailDTO dto) { + return buyerAccountService.bindEmail(dto); + } +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/BindEmailDTO.java b/src/main/java/com/aida/buyer/module/account/dto/BindEmailDTO.java new file mode 100644 index 0000000..36cc76d --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/BindEmailDTO.java @@ -0,0 +1,22 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "绑定新邮箱请求") +public class BindEmailDTO { + + @NotBlank(message = "oldEmail is required") + @Schema(description = "当前邮箱") + private String oldEmail; + + @NotBlank(message = "newEmail is required") + @Schema(description = "新邮箱") + private String newEmail; + + @NotBlank(message = "newEmailVerifyCode is required") + @Schema(description = "新邮箱验证码") + private String newEmailVerifyCode; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/BindEmailVO.java b/src/main/java/com/aida/buyer/module/account/dto/BindEmailVO.java new file mode 100644 index 0000000..fe2b04b --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/BindEmailVO.java @@ -0,0 +1,19 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "绑定新邮箱响应") +public class BindEmailVO { + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "新邮箱") + private String newEmail; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/LoginDTO.java b/src/main/java/com/aida/buyer/module/account/dto/LoginDTO.java new file mode 100644 index 0000000..9c134d0 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/LoginDTO.java @@ -0,0 +1,22 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "登录第二步请求") +public class LoginDTO { + + @NotBlank(message = "email is required") + @Schema(description = "邮箱") + private String email; + + @NotBlank(message = "password is required") + @Schema(description = "密码") + private String password; + + @NotBlank(message = "emailVerifyCode is required") + @Schema(description = "邮箱验证码") + private String emailVerifyCode; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/PreLoginDTO.java b/src/main/java/com/aida/buyer/module/account/dto/PreLoginDTO.java new file mode 100644 index 0000000..0185a68 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/PreLoginDTO.java @@ -0,0 +1,18 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "登录第一步请求") +public class PreLoginDTO { + + @NotBlank(message = "email is required") + @Schema(description = "邮箱") + private String email; + + @NotBlank(message = "password is required") + @Schema(description = "密码") + private String password; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/PreLoginVO.java b/src/main/java/com/aida/buyer/module/account/dto/PreLoginVO.java new file mode 100644 index 0000000..bd0e160 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/PreLoginVO.java @@ -0,0 +1,16 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "登录第一步响应") +public class PreLoginVO { + + @Schema(description = "用户ID") + private Long userId; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/RegisterDTO.java b/src/main/java/com/aida/buyer/module/account/dto/RegisterDTO.java new file mode 100644 index 0000000..a15a7ba --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/RegisterDTO.java @@ -0,0 +1,30 @@ +package com.aida.buyer.module.account.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 RegisterDTO { + + @NotBlank(message = "email is required") + @Email(message = "invalid email format") + @Schema(description = "邮箱") + private String email; + + @NotBlank(message = "password is required") + @Size(min = 6, message = "password must be at least 6 characters") + @Schema(description = "密码") + private String password; + + @NotBlank(message = "username is required") + @Schema(description = "用户名") + private String username; + + @NotBlank(message = "emailVerifyCode is required") + @Schema(description = "邮箱验证码") + private String emailVerifyCode; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/RegisterVO.java b/src/main/java/com/aida/buyer/module/account/dto/RegisterVO.java new file mode 100644 index 0000000..8caab8c --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/RegisterVO.java @@ -0,0 +1,30 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "注册响应") +public class RegisterVO { + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "过期时间(秒)") + private Long expiresIn; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/ResetPasswordDTO.java b/src/main/java/com/aida/buyer/module/account/dto/ResetPasswordDTO.java new file mode 100644 index 0000000..52e604d --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/ResetPasswordDTO.java @@ -0,0 +1,22 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "重置密码请求") +public class ResetPasswordDTO { + + @NotBlank(message = "email is required") + @Schema(description = "邮箱") + private String email; + + @NotBlank(message = "emailVerifyCode is required") + @Schema(description = "邮箱验证码") + private String emailVerifyCode; + + @NotBlank(message = "password is required") + @Schema(description = "新密码") + private String password; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/SendCodeDTO.java b/src/main/java/com/aida/buyer/module/account/dto/SendCodeDTO.java new file mode 100644 index 0000000..ab74bb7 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/SendCodeDTO.java @@ -0,0 +1,14 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "发送验证码请求") +public class SendCodeDTO { + + @NotBlank(message = "email is required") + @Schema(description = "邮箱") + private String email; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/SendEmailCodeDTO.java b/src/main/java/com/aida/buyer/module/account/dto/SendEmailCodeDTO.java new file mode 100644 index 0000000..df161ba --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/SendEmailCodeDTO.java @@ -0,0 +1,18 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "发送邮箱变更验证码请求") +public class SendEmailCodeDTO { + + @NotBlank(message = "oldEmail is required") + @Schema(description = "当前邮箱") + private String oldEmail; + + @NotBlank(message = "newEmail is required") + @Schema(description = "新邮箱") + private String newEmail; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/VerifyCodeDTO.java b/src/main/java/com/aida/buyer/module/account/dto/VerifyCodeDTO.java new file mode 100644 index 0000000..11e2143 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/VerifyCodeDTO.java @@ -0,0 +1,22 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "验证验证码请求") +public class VerifyCodeDTO { + + @NotBlank(message = "email is required") + @Schema(description = "邮箱") + private String email; + + @NotBlank(message = "emailVerifyCode is required") + @Schema(description = "邮箱验证码") + private String emailVerifyCode; + + @NotBlank(message = "operationType is required") + @Schema(description = "操作类型:FORGET_PWD, BIND_MAILBOX") + private String operationType; +} diff --git a/src/main/java/com/aida/buyer/module/account/dto/VerifyCodeVO.java b/src/main/java/com/aida/buyer/module/account/dto/VerifyCodeVO.java new file mode 100644 index 0000000..c07272a --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/dto/VerifyCodeVO.java @@ -0,0 +1,16 @@ +package com.aida.buyer.module.account.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "验证验证码响应") +public class VerifyCodeVO { + + @Schema(description = "是否验证通过") + private Boolean verified; +} diff --git a/src/main/java/com/aida/buyer/module/account/entity/BuyerAccount.java b/src/main/java/com/aida/buyer/module/account/entity/BuyerAccount.java new file mode 100644 index 0000000..c2ccbb7 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/entity/BuyerAccount.java @@ -0,0 +1,40 @@ +package com.aida.buyer.module.account.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +@TableName("buyer_account") +public class BuyerAccount implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private String email; + + private String password; + + private String username; + + private String avatar; + + private String language; + + private String country; + + private String occupation; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + private Integer deleted; +} diff --git a/src/main/java/com/aida/buyer/module/account/mapper/BuyerAccountMapper.java b/src/main/java/com/aida/buyer/module/account/mapper/BuyerAccountMapper.java new file mode 100644 index 0000000..78f4fb4 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/mapper/BuyerAccountMapper.java @@ -0,0 +1,11 @@ +package com.aida.buyer.module.account.mapper; + +import com.aida.buyer.module.account.entity.BuyerAccount; +import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +@DS("buyer") +public interface BuyerAccountMapper extends BaseMapper { +} diff --git a/src/main/java/com/aida/buyer/module/account/service/IBuyerAccountService.java b/src/main/java/com/aida/buyer/module/account/service/IBuyerAccountService.java new file mode 100644 index 0000000..8d2c1f3 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/service/IBuyerAccountService.java @@ -0,0 +1,23 @@ +package com.aida.buyer.module.account.service; + + +import com.aida.buyer.module.account.dto.*; + +public interface IBuyerAccountService { + + Boolean sendCode(SendCodeDTO dto); + + Boolean register(RegisterDTO dto); + + PreLoginVO preLogin(PreLoginDTO dto); + + RegisterVO login(LoginDTO dto); + + VerifyCodeVO verifyCode(VerifyCodeDTO dto); + + Boolean resetPassword(ResetPasswordDTO dto); + + Boolean sendEmailChangeCode(SendEmailCodeDTO dto); + + BindEmailVO bindEmail(BindEmailDTO dto); +} diff --git a/src/main/java/com/aida/buyer/module/account/service/impl/BuyerAccountServiceImpl.java b/src/main/java/com/aida/buyer/module/account/service/impl/BuyerAccountServiceImpl.java new file mode 100644 index 0000000..6a4a029 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/account/service/impl/BuyerAccountServiceImpl.java @@ -0,0 +1,269 @@ +package com.aida.buyer.module.account.service.impl; + +import com.aida.buyer.module.account.dto.*; +import com.aida.buyer.common.exception.BusinessException; +import com.aida.buyer.model.vo.AuthPrincipalVo; +import com.aida.buyer.module.account.entity.BuyerAccount; +import com.aida.buyer.module.account.mapper.BuyerAccountMapper; +import com.aida.buyer.module.account.service.IBuyerAccountService; +import com.aida.buyer.util.LoginCacheUtil; +import com.aida.buyer.util.RandomsUtil; +import com.aida.buyer.util.RedisLoginUtil; +import com.aida.buyer.util.SendEmailUtil; +import com.aida.buyer.util.TokenGenerateUtils; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BuyerAccountServiceImpl implements IBuyerAccountService { + + private final BuyerAccountMapper buyerAccountMapper; + private final TokenGenerateUtils tokenGenerateUtils; + private final RedisLoginUtil redisLoginUtil; + + @Override + public Boolean sendCode(SendCodeDTO dto) { + String email = dto.getEmail(); + + String code = RandomsUtil.generateVerifyCode(100000L, 999999L); + String cacheKey = "FORGET_PWD_" + email; + LoginCacheUtil.setVerifyCodeCache(cacheKey, code); + + SendEmailUtil.sendVerifyCode(email, code); + log.info("Email verification code sent to: {}", email); + return Boolean.TRUE; + } + + @Override + public Boolean register(RegisterDTO dto) { + String email = dto.getEmail(); + String code = dto.getEmailVerifyCode(); + + String cacheKey = "REGISTER_" + email; + String cachedCode = LoginCacheUtil.getVerifyCodeCache(cacheKey); + if (!code.equals(cachedCode)) { + throw new BusinessException("verification.code.error"); + } + + LambdaQueryWrapper emailQuery = new LambdaQueryWrapper<>(); + emailQuery.eq(BuyerAccount::getEmail, email); + if (buyerAccountMapper.selectCount(emailQuery) > 0) { + throw new BusinessException("email.already.registered"); + } + + BuyerAccount account = new BuyerAccount(); + account.setEmail(email); + account.setPassword(dto.getPassword()); + account.setUsername(dto.getUsername()); + buyerAccountMapper.insert(account); + + LoginCacheUtil.invalidateCache(cacheKey); + log.info("Buyer account registered: {}", email); + return Boolean.TRUE; + } + + @Override + public PreLoginVO preLogin(PreLoginDTO dto) { + String email = dto.getEmail(); + String password = dto.getPassword(); + + BuyerAccount account = findByEmail(email); + if (account == null) { + throw new BusinessException("account.not.found"); + } + if (!password.equals(account.getPassword())) { + throw new BusinessException("password.error"); + } + + String code = RandomsUtil.generateVerifyCode(100000L, 999999L); + String cacheKey = "LOGIN_" + email; + LoginCacheUtil.setVerifyCodeCache(cacheKey, code); + + SendEmailUtil.sendVerifyCode(email, code); + log.info("PreLogin verification code sent to: {}", email); + + PreLoginVO vo = new PreLoginVO(); + vo.setUserId(account.getId()); + return vo; + } + + @Override + public RegisterVO login(LoginDTO dto) { + String email = dto.getEmail(); + String code = dto.getEmailVerifyCode(); + + BuyerAccount account = findByEmail(email); + if (account == null) { + throw new BusinessException("account.not.found"); + } + + String cacheKey = "LOGIN_" + email; + String cachedCode = LoginCacheUtil.getVerifyCodeCache(cacheKey); + if (StringUtils.isEmpty(cachedCode)) { + throw new BusinessException("verification.code.expired"); + } + if (!code.equals(cachedCode)) { + throw new BusinessException("verification.code.error"); + } + + AuthPrincipalVo principal = new AuthPrincipalVo(); + principal.setId(account.getId()); + principal.setUsername(account.getUsername()); + principal.setSource("BUYER"); + principal.setLanguage(account.getLanguage()); + principal.setCountry(account.getCountry()); + + String token = tokenGenerateUtils.createToken(principal); + long ttlMs = tokenGenerateUtils.getJwtExpiration(); + redisLoginUtil.setLoginToken(account.getId(), token, ttlMs); + + LoginCacheUtil.invalidateCache(cacheKey); + + return RegisterVO.builder() + .userId(account.getId()) + .email(account.getEmail()) + .username(account.getUsername()) + .accessToken(token) + .expiresIn(ttlMs / 1000) + .build(); + } + + private BuyerAccount findByEmail(String email) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(BuyerAccount::getEmail, email); + return buyerAccountMapper.selectOne(query); + } + + @Override + public VerifyCodeVO verifyCode(VerifyCodeDTO dto) { + String email = dto.getEmail(); + String code = dto.getEmailVerifyCode(); + String operationType = dto.getOperationType(); + + if ("FORGET_PWD".equals(operationType)) { + BuyerAccount account = findByEmail(email); + if (account == null) { + throw new BusinessException("account.not.found"); + } + } + + String cacheKey = operationType + "_" + email; + String cachedCode = LoginCacheUtil.getVerifyCodeCache(cacheKey); + if (StringUtils.isEmpty(cachedCode)) { + throw new BusinessException("verification.code.expired"); + } + if (!code.equals(cachedCode)) { + throw new BusinessException("verification.code.error"); + } + + return new VerifyCodeVO(Boolean.TRUE); + } + + @Override + public Boolean resetPassword(ResetPasswordDTO dto) { + String email = dto.getEmail(); + String code = dto.getEmailVerifyCode(); + String newPassword = dto.getPassword(); + + BuyerAccount account = findByEmail(email); + if (account == null) { + throw new BusinessException("account.not.found"); + } + + String cacheKey = "FORGET_PWD_" + email; + String cachedCode = LoginCacheUtil.getVerifyCodeCache(cacheKey); + if (StringUtils.isEmpty(cachedCode)) { + throw new BusinessException("verification.code.expired"); + } + if (!code.equals(cachedCode)) { + throw new BusinessException("verification.code.error"); + } + + BuyerAccount updateAccount = new BuyerAccount(); + updateAccount.setId(account.getId()); + updateAccount.setPassword(newPassword); + buyerAccountMapper.updateById(updateAccount); + + LoginCacheUtil.invalidateCache(cacheKey); + log.info("Password reset successfully for: {}", email); + return Boolean.TRUE; + } + + @Override + public Boolean sendEmailChangeCode(SendEmailCodeDTO dto) { + String oldEmail = dto.getOldEmail(); + String newEmail = dto.getNewEmail(); + + if (oldEmail.equalsIgnoreCase(newEmail)) { + throw new BusinessException("new.email.cannot.same.as.old"); + } + + LambdaQueryWrapper oldEmailQuery = new LambdaQueryWrapper<>(); + oldEmailQuery.eq(BuyerAccount::getEmail, oldEmail); + if (buyerAccountMapper.selectCount(oldEmailQuery) == 0) { + throw new BusinessException("account.not.found"); + } + + LambdaQueryWrapper newEmailQuery = new LambdaQueryWrapper<>(); + newEmailQuery.eq(BuyerAccount::getEmail, newEmail); + if (buyerAccountMapper.selectCount(newEmailQuery) > 0) { + throw new BusinessException("email.already.registered"); + } + + String code = RandomsUtil.generateVerifyCode(100000L, 999999L); + String cacheKey = "BIND_MAILBOX_" + newEmail; + LoginCacheUtil.setVerifyCodeCache(cacheKey, code); + + SendEmailUtil.sendVerifyCode(newEmail, code); + log.info("Email change code sent to new email: {}", newEmail); + return Boolean.TRUE; + } + + @Override + public BindEmailVO bindEmail(BindEmailDTO dto) { + String oldEmail = dto.getOldEmail(); + String newEmail = dto.getNewEmail(); + String code = dto.getNewEmailVerifyCode(); + + if (oldEmail.equalsIgnoreCase(newEmail)) { + throw new BusinessException("new.email.cannot.same.as.old"); + } + + LambdaQueryWrapper oldEmailQuery = new LambdaQueryWrapper<>(); + oldEmailQuery.eq(BuyerAccount::getEmail, oldEmail); + BuyerAccount account = buyerAccountMapper.selectOne(oldEmailQuery); + if (account == null) { + throw new BusinessException("account.not.found"); + } + + LambdaQueryWrapper newEmailQuery = new LambdaQueryWrapper<>(); + newEmailQuery.eq(BuyerAccount::getEmail, newEmail); + if (buyerAccountMapper.selectCount(newEmailQuery) > 0) { + throw new BusinessException("email.already.registered"); + } + + String cacheKey = "BIND_MAILBOX_" + newEmail; + String cachedCode = LoginCacheUtil.getVerifyCodeCache(cacheKey); + if (StringUtils.isEmpty(cachedCode)) { + throw new BusinessException("verification.code.expired"); + } + if (!code.equals(cachedCode)) { + throw new BusinessException("verification.code.error"); + } + + BuyerAccount updateAccount = new BuyerAccount(); + updateAccount.setId(account.getId()); + updateAccount.setEmail(newEmail); + buyerAccountMapper.updateById(updateAccount); + + LoginCacheUtil.invalidateCache(cacheKey); + log.info("Email changed from {} to {}", oldEmail, newEmail); + + return new BindEmailVO(account.getId(), newEmail); + } +} diff --git a/src/main/java/com/aida/buyer/module/designer/controller/DesignerController.java b/src/main/java/com/aida/buyer/module/designer/controller/DesignerController.java new file mode 100644 index 0000000..fef2a6c --- /dev/null +++ b/src/main/java/com/aida/buyer/module/designer/controller/DesignerController.java @@ -0,0 +1,43 @@ +package com.aida.buyer.module.designer.controller; + +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.designer.service.DesignerService; +import com.aida.buyer.module.designer.vo.DesignerSearchVO; +import com.aida.buyer.module.designer.vo.DesignerShopVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 设计师 Controller + */ +@Tag(name = "Designer - 设计师") +@RestController +@RequestMapping("/designer") +@RequiredArgsConstructor +public class DesignerController { + + private final DesignerService designerService; + + @Operation(summary = "获取商城店铺详情", description = "根据 sellerId 获取店铺公开信息,供买家端店铺主页调用") + @GetMapping("/shop/{sellerId}") + public Response getShopDetail( + @Parameter(description = "设计师用户ID") @PathVariable Long sellerId) { + return designerService.getShopDetail(sellerId); + } + + @Operation(summary = "搜索设计师", description = "根据关键词不区分大小写同时匹配店铺名称和所有者姓名,返回设计师信息、最近5张商品封面图(按updateTime倒序)、商品总数") + @GetMapping("/search") + public Response> search( + @Parameter(description = "关键词(同时匹配店铺名称和所有者姓名,不区分大小写)") @RequestParam String keyword) { + return designerService.searchDesigners(keyword); + } +} diff --git a/src/main/java/com/aida/buyer/module/designer/feign/DesignerFeignClient.java b/src/main/java/com/aida/buyer/module/designer/feign/DesignerFeignClient.java new file mode 100644 index 0000000..60fbc1c --- /dev/null +++ b/src/main/java/com/aida/buyer/module/designer/feign/DesignerFeignClient.java @@ -0,0 +1,24 @@ +package com.aida.buyer.module.designer.feign; + +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.designer.vo.DesignerSearchVO; +import com.aida.buyer.module.designer.vo.DesignerShopVO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * 设计师服务 Feign Client + */ +@FeignClient(name = "aida-seller", contextId = "designer", path = "/designer") +public interface DesignerFeignClient { + + @GetMapping("/shop/{sellerId}") + Response getShopDetail(@PathVariable Long sellerId); + + @GetMapping("/search") + Response> searchDesigners(@RequestParam String keyword); +} diff --git a/src/main/java/com/aida/buyer/module/designer/service/DesignerService.java b/src/main/java/com/aida/buyer/module/designer/service/DesignerService.java new file mode 100644 index 0000000..6045042 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/designer/service/DesignerService.java @@ -0,0 +1,29 @@ +package com.aida.buyer.module.designer.service; + +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.designer.vo.DesignerSearchVO; +import com.aida.buyer.module.designer.vo.DesignerShopVO; + +import java.util.List; + +/** + * 设计师 Service 接口 + */ +public interface DesignerService { + + /** + * 获取店铺详情 + * + * @param sellerId 设计师用户ID + * @return 店铺详情 + */ + Response getShopDetail(Long sellerId); + + /** + * 搜索设计师 + * + * @param keyword 关键词(同时匹配店铺名称和所有者姓名,不区分大小写) + * @return 设计师搜索结果列表 + */ + Response> searchDesigners(String keyword); +} diff --git a/src/main/java/com/aida/buyer/module/designer/service/impl/DesignerServiceImpl.java b/src/main/java/com/aida/buyer/module/designer/service/impl/DesignerServiceImpl.java new file mode 100644 index 0000000..4d3af37 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/designer/service/impl/DesignerServiceImpl.java @@ -0,0 +1,31 @@ +package com.aida.buyer.module.designer.service.impl; + +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.designer.feign.DesignerFeignClient; +import com.aida.buyer.module.designer.service.DesignerService; +import com.aida.buyer.module.designer.vo.DesignerSearchVO; +import com.aida.buyer.module.designer.vo.DesignerShopVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 设计师 Service 实现 + */ +@Service +@RequiredArgsConstructor +public class DesignerServiceImpl implements DesignerService { + + private final DesignerFeignClient designerFeignClient; + + @Override + public Response getShopDetail(Long sellerId) { + return designerFeignClient.getShopDetail(sellerId); + } + + @Override + public Response> searchDesigners(String keyword) { + return designerFeignClient.searchDesigners(keyword); + } +} diff --git a/src/main/java/com/aida/buyer/module/designer/vo/DesignerSearchVO.java b/src/main/java/com/aida/buyer/module/designer/vo/DesignerSearchVO.java new file mode 100644 index 0000000..04f4213 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/designer/vo/DesignerSearchVO.java @@ -0,0 +1,33 @@ +package com.aida.buyer.module.designer.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 设计师搜索结果VO + */ +@Data +public class DesignerSearchVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 用户ID */ + private Long sellerId; + + /** 店铺名称 */ + private String shopName; + + /** 所有者全名 */ + private String ownerName; + + /** 店铺头像URL */ + private String avatar; + + /** 商品封面图列表(最多5张,按更新时间倒序) */ + private List covers; + + /** 该设计师的商品总数 */ + private Long listingTotal; +} diff --git a/src/main/java/com/aida/buyer/module/designer/vo/DesignerShopVO.java b/src/main/java/com/aida/buyer/module/designer/vo/DesignerShopVO.java new file mode 100644 index 0000000..09e60e3 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/designer/vo/DesignerShopVO.java @@ -0,0 +1,26 @@ +package com.aida.buyer.module.designer.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 设计师店铺详情VO(买家端店铺主页) + */ +@Data +public class DesignerShopVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private String shopName; + + private String avatar; + + private String brandBanner; + + private String ownerName; + + private String description; + + private String socialLinks; +} diff --git a/src/main/java/com/aida/buyer/module/listing/controller/ListingController.java b/src/main/java/com/aida/buyer/module/listing/controller/ListingController.java new file mode 100644 index 0000000..7d93416 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/controller/ListingController.java @@ -0,0 +1,50 @@ +package com.aida.buyer.module.listing.controller; + +import com.aida.buyer.common.result.PageResponse; +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.listing.dto.ListingMallQueryDTO; +import com.aida.buyer.module.listing.service.ListingService; +import com.aida.buyer.module.listing.vo.ListingDetailVO; +import com.aida.buyer.module.listing.vo.ListingMallVO; +import com.aida.buyer.module.listing.vo.ListingPageVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 商品管理 Controller + */ +@Tag(name = "Listing - 商品管理") +@RestController +@RequestMapping("/listing") +@RequiredArgsConstructor +public class ListingController { + + private final ListingService listingService; + + @Operation(summary = "商城首页商品分页列表", description = "面向所有卖家,支持分类筛选、多字段排序、分页") + @PostMapping("/mall") + public PageResponse getMallListings( + @RequestBody ListingMallQueryDTO dto) { + return listingService.getMallListings(dto); + } + + @Operation(summary = "商品详情(商城详情页)", description = "关联图片与店铺信息") + @GetMapping("/mall/detail") + public Response getListingDetail( + @Parameter(description = "商品ID") @RequestParam Long id) { + return listingService.getListingDetail(id); + } + + @Operation(summary = "获取店铺商品列表", description = "按 status=1、deleted=0、designFor 筛选,返回店铺已发布商品分页列表") + @GetMapping("/shop/seller") + public PageResponse getShopListings( + @Parameter(description = "设计师用户ID") @RequestParam Long sellerId, + @Parameter(description = "适用性别 female/male/all") @RequestParam String designFor, + @Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum, + @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") int pageSize) { + return listingService.getShopListings(sellerId, designFor, pageNum, pageSize); + } +} diff --git a/src/main/java/com/aida/buyer/module/listing/dto/ListingMallQueryDTO.java b/src/main/java/com/aida/buyer/module/listing/dto/ListingMallQueryDTO.java new file mode 100644 index 0000000..09ff07a --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/dto/ListingMallQueryDTO.java @@ -0,0 +1,34 @@ +package com.aida.buyer.module.listing.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 商城首页商品查询 DTO + */ +@Data +public class ListingMallQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "适用性别 female/male/all,不传表示全部") + private String designFor; + + @Schema(description = "商品分类列表,支持多选") + private List categories; + + @Schema(description = "排序字段:price/salesVolume/updateTime/viewCount/createTime,默认 updateTime") + private String sortField = "updateTime"; + + @Schema(description = "排序方向:asc/desc,默认 desc") + private String sortOrder = "desc"; + + @Schema(description = "页码,默认1") + private Integer pageNum = 1; + + @Schema(description = "每页数量,默认10") + private Integer pageSize = 10; +} diff --git a/src/main/java/com/aida/buyer/module/listing/dto/ListingShopQueryDTO.java b/src/main/java/com/aida/buyer/module/listing/dto/ListingShopQueryDTO.java new file mode 100644 index 0000000..7d72ac9 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/dto/ListingShopQueryDTO.java @@ -0,0 +1,27 @@ +package com.aida.buyer.module.listing.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 店铺商品查询 DTO + */ +@Data +public class ListingShopQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "设计师用户ID") + private Long sellerId; + + @Schema(description = "适用性别 female/male/all") + private String designFor; + + @Schema(description = "页码,默认1") + private Integer pageNum = 1; + + @Schema(description = "每页数量,默认10") + private Integer pageSize = 10; +} diff --git a/src/main/java/com/aida/buyer/module/listing/feign/ListingFeignClient.java b/src/main/java/com/aida/buyer/module/listing/feign/ListingFeignClient.java new file mode 100644 index 0000000..afff78a --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/feign/ListingFeignClient.java @@ -0,0 +1,32 @@ +package com.aida.buyer.module.listing.feign; + +import com.aida.buyer.common.result.PageResponse; +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.listing.dto.ListingMallQueryDTO; +import com.aida.buyer.module.listing.vo.ListingDetailVO; +import com.aida.buyer.module.listing.vo.ListingMallVO; +import com.aida.buyer.module.listing.vo.ListingPageVO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 商品服务 Feign Client + */ +@FeignClient(name = "aida-seller", contextId = "listing", path = "/listing") +public interface ListingFeignClient { + + @PostMapping("/mall") + Response> getMallListings(ListingMallQueryDTO dto); + + @GetMapping("/mall/detail") + Response getListingDetail(@RequestParam Long id); + + @GetMapping("/shop/seller") + Response> getShopListings( + @RequestParam Long sellerId, + @RequestParam String designFor, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize); +} diff --git a/src/main/java/com/aida/buyer/module/listing/service/ListingService.java b/src/main/java/com/aida/buyer/module/listing/service/ListingService.java new file mode 100644 index 0000000..5cb935a --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/service/ListingService.java @@ -0,0 +1,41 @@ +package com.aida.buyer.module.listing.service; + +import com.aida.buyer.common.result.PageResponse; +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.listing.dto.ListingMallQueryDTO; +import com.aida.buyer.module.listing.vo.ListingDetailVO; +import com.aida.buyer.module.listing.vo.ListingMallVO; +import com.aida.buyer.module.listing.vo.ListingPageVO; + +/** + * 商品 Service 接口 + */ +public interface ListingService { + + /** + * 商城首页商品分页列表 + * + * @param dto 查询条件 + * @return 分页结果 + */ + PageResponse getMallListings(ListingMallQueryDTO dto); + + /** + * 获取商品详情(商城详情页) + * + * @param id 商品ID + * @return 商品详情 + */ + Response getListingDetail(Long id); + + /** + * 获取店铺商品列表(已发布商品分页) + * + * @param sellerId 设计师用户ID + * @param designFor 适用性别 female/male/all + * @param pageNum 页码 + * @param pageSize 每页数量 + * @return 店铺商品分页列表 + */ + PageResponse getShopListings(Long sellerId, String designFor, int pageNum, int pageSize); +} diff --git a/src/main/java/com/aida/buyer/module/listing/service/impl/ListingServiceImpl.java b/src/main/java/com/aida/buyer/module/listing/service/impl/ListingServiceImpl.java new file mode 100644 index 0000000..5a98c96 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/service/impl/ListingServiceImpl.java @@ -0,0 +1,63 @@ +package com.aida.buyer.module.listing.service.impl; + +import com.aida.buyer.common.context.UserContext; +import com.aida.buyer.common.result.PageResponse; +import com.aida.buyer.common.result.Response; +import com.aida.buyer.module.listing.dto.ListingMallQueryDTO; +import com.aida.buyer.module.listing.feign.ListingFeignClient; +import com.aida.buyer.module.listing.service.ListingService; +import com.aida.buyer.module.listing.vo.ListingDetailVO; +import com.aida.buyer.module.listing.vo.ListingMallVO; +import com.aida.buyer.module.listing.vo.ListingPageVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 商品 Service 实现 + */ +@Service +@RequiredArgsConstructor +public class ListingServiceImpl implements ListingService { + + private final ListingFeignClient listingFeignClient; + + @Override + public PageResponse getMallListings(ListingMallQueryDTO dto) { + Response> response = listingFeignClient.getMallListings(dto); + PageResponse pageResponse = response != null ? response.getData() : null; + if (pageResponse == null) { + return new PageResponse<>(); + } + Long buyerId = UserContext.getBuyerId(); + if (buyerId == null && pageResponse.getContent() != null) { + pageResponse.getContent().forEach(vo -> vo.setPrice(null)); + } + return pageResponse; + } + + @Override + public Response getListingDetail(Long id) { + Response response = listingFeignClient.getListingDetail(id); + if (response != null && response.getData() != null) { + Long buyerId = UserContext.getBuyerId(); + if (buyerId == null) { + response.getData().setPrice(null); + } + } + return response; + } + + @Override + public PageResponse getShopListings(Long sellerId, String designFor, int pageNum, int pageSize) { + Response> response = listingFeignClient.getShopListings(sellerId, designFor, pageNum, pageSize); + PageResponse pageResponse = response != null ? response.getData() : null; + if (pageResponse == null) { + return new PageResponse<>(); + } + Long buyerId = UserContext.getBuyerId(); + if (buyerId == null && pageResponse.getContent() != null && !pageResponse.getContent().isEmpty()) { + pageResponse.getContent().forEach(vo -> vo.setPrice(null)); + } + return pageResponse; + } +} diff --git a/src/main/java/com/aida/buyer/module/listing/vo/ListingDetailVO.java b/src/main/java/com/aida/buyer/module/listing/vo/ListingDetailVO.java new file mode 100644 index 0000000..4f80972 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/vo/ListingDetailVO.java @@ -0,0 +1,44 @@ +package com.aida.buyer.module.listing.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 商品详情 VO(商城详情页) + */ +@Data +@Schema(description = "商品详情") +public class ListingDetailVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "商品ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long id; + + @Schema(description = "商品标题") + private String title; + + @Schema(description = "商品描述") + private String description; + + @Schema(description = "价格") + private BigDecimal price; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "店铺名称") + private String shopName; + + @Schema(description = "图片列表,key 为 category,value 为该分类下所有图片 URL") + private Map> images; +} diff --git a/src/main/java/com/aida/buyer/module/listing/vo/ListingMallVO.java b/src/main/java/com/aida/buyer/module/listing/vo/ListingMallVO.java new file mode 100644 index 0000000..439f6a8 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/vo/ListingMallVO.java @@ -0,0 +1,25 @@ +package com.aida.buyer.module.listing.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 商城首页商品 VO + */ +@Data +public class ListingMallVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String cover; + + private String title; + + private BigDecimal price; +} diff --git a/src/main/java/com/aida/buyer/module/listing/vo/ListingPageVO.java b/src/main/java/com/aida/buyer/module/listing/vo/ListingPageVO.java new file mode 100644 index 0000000..84e2636 --- /dev/null +++ b/src/main/java/com/aida/buyer/module/listing/vo/ListingPageVO.java @@ -0,0 +1,26 @@ +package com.aida.buyer.module.listing.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 商品分页列表 VO(店铺商品页) + */ +@Data +public class ListingPageVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String cover; + + private String title; + + private BigDecimal price; + + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/aida/buyer/util/DesensitizationUtil.java b/src/main/java/com/aida/buyer/util/DesensitizationUtil.java new file mode 100644 index 0000000..3a54e52 --- /dev/null +++ b/src/main/java/com/aida/buyer/util/DesensitizationUtil.java @@ -0,0 +1,41 @@ +package com.aida.buyer.util; + +import cn.hutool.core.util.DesensitizedUtil; + +public class DesensitizationUtil { + + public static String mobile(String mobile) { + if (mobile == null || mobile.length() < 7) { + return mobile; + } + return DesensitizedUtil.mobilePhone(mobile); + } + + public static String email(String email) { + if (email == null || !email.contains("@")) { + return email; + } + return DesensitizedUtil.email(email); + } + + public static String idCard(String idCard) { + if (idCard == null || idCard.length() < 10) { + return idCard; + } + return DesensitizedUtil.idCardNum(idCard, 4, 4); + } + + public static String bankCard(String bankCard) { + if (bankCard == null || bankCard.length() < 8) { + return bankCard; + } + return DesensitizedUtil.bankCard(bankCard); + } + + public static String chineseName(String chineseName) { + if (chineseName == null || chineseName.length() < 2) { + return chineseName; + } + return DesensitizedUtil.chineseName(chineseName); + } +} diff --git a/src/main/java/com/aida/buyer/util/LoginCacheUtil.java b/src/main/java/com/aida/buyer/util/LoginCacheUtil.java new file mode 100644 index 0000000..88d6636 --- /dev/null +++ b/src/main/java/com/aida/buyer/util/LoginCacheUtil.java @@ -0,0 +1,32 @@ +package com.aida.buyer.util; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.util.concurrent.TimeUnit; + +public class LoginCacheUtil { + + private static final LoadingCache emailCache = CacheBuilder.newBuilder() + .expireAfterWrite(30, TimeUnit.MINUTES) + .maximumSize(10000) + .build(new CacheLoader<>() { + @Override + public String load(String key) { + return null; + } + }); + + public static void setVerifyCodeCache(String key, String code) { + emailCache.put(key, code); + } + + public static String getVerifyCodeCache(String key) { + return emailCache.getIfPresent(key); + } + + public static void invalidateCache(String key) { + emailCache.invalidate(key); + } +} diff --git a/src/main/java/com/aida/buyer/util/MinioUtil.java b/src/main/java/com/aida/buyer/util/MinioUtil.java new file mode 100644 index 0000000..99ef59b --- /dev/null +++ b/src/main/java/com/aida/buyer/util/MinioUtil.java @@ -0,0 +1,150 @@ +package com.aida.buyer.util; + +import com.aida.buyer.common.constants.CommonConstants; +import com.aida.buyer.common.exception.BusinessException; +import com.aida.buyer.config.MinioConfig; +import io.minio.*; +import io.minio.http.Method; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MinioUtil { + + private final MinioClient minioClient; + private final MinioConfig minioConfig; + + public String uploadImage(String fileName, InputStream inputStream, long size, String contentType) { + try { + minioClient.putObject( + PutObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(fileName) + .stream(inputStream, size, -1) + .contentType(contentType) + .build() + ); + return minioConfig.getBucketName() + "/" + fileName; + } catch (Exception e) { + log.error("upload image error: {}", fileName, e); + throw new BusinessException("文件上传失败: " + e.getMessage()); + } + } + + public String uploadBytes(String fileName, byte[] data, String contentType) { + try { + minioClient.putObject( + PutObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(fileName) + .stream(new java.io.ByteArrayInputStream(data), data.length, -1) + .contentType(contentType) + .build() + ); + return minioConfig.getBucketName() + "/" + fileName; + } catch (Exception e) { + log.error("upload bytes error: {}", fileName, e); + throw new BusinessException("文件上传失败: " + e.getMessage()); + } + } + + public String getImageUrl(String objectName) { + try { + boolean exists = minioClient.statObject( + StatObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .build() + ) != null; + if (!exists) { + return objectName; + } + return minioClient.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(minioConfig.getBucketName()) + .object(objectName) + .expiry(CommonConstants.MINIO_PATH_TIMEOUT, TimeUnit.SECONDS) + .build() + ); + } catch (Exception e) { + log.error("get image url error: {}", objectName, e); + return objectName; + } + } + + public void deleteImage(String objectName) { + try { + minioClient.removeObject( + RemoveObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .build() + ); + } catch (Exception e) { + log.error("delete image error: {}", objectName, e); + } + } + + public void deleteImages(List objectNames) { + if (objectNames == null || objectNames.isEmpty()) { + return; + } + for (String objectName : objectNames) { + deleteImage(objectName); + } + } + + public InputStream downloadFile(String objectName) { + try { + return minioClient.getObject( + GetObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .build() + ); + } catch (Exception e) { + log.error("download file error: {}", objectName, e); + throw new BusinessException("文件下载失败: " + e.getMessage()); + } + } + + public boolean isPresignedUrl(String path) { + return path != null && path.contains("X-Amz-Expires"); + } + + public boolean isMinioLogicalPath(String path) { + return path != null && path.startsWith(minioConfig.getBucketName() + "/"); + } + + public String convertToLogicalPath(String path) { + if (path == null) { + return null; + } + if (path.startsWith(minioConfig.getEndpoint())) { + String suffix = path.substring(minioConfig.getEndpoint().length()); + if (suffix.startsWith("/")) { + return suffix.substring(1); + } + return suffix; + } + return path; + } + + public String processMinioResource(String path) { + if (path == null) { + return null; + } + if (isPresignedUrl(path) || !isMinioLogicalPath(path)) { + return path; + } + return getImageUrl(path); + } +} diff --git a/src/main/java/com/aida/buyer/util/RandomsUtil.java b/src/main/java/com/aida/buyer/util/RandomsUtil.java new file mode 100644 index 0000000..aee90f3 --- /dev/null +++ b/src/main/java/com/aida/buyer/util/RandomsUtil.java @@ -0,0 +1,10 @@ +package com.aida.buyer.util; + +import cn.hutool.core.util.RandomUtil; + +public class RandomsUtil { + + public static String generateVerifyCode(Long randomStart, Long randomEnd) { + return String.valueOf(RandomUtil.randomLong(randomStart, randomEnd)); + } +} diff --git a/src/main/java/com/aida/buyer/util/RedisLoginUtil.java b/src/main/java/com/aida/buyer/util/RedisLoginUtil.java new file mode 100644 index 0000000..fb6843a --- /dev/null +++ b/src/main/java/com/aida/buyer/util/RedisLoginUtil.java @@ -0,0 +1,30 @@ +package com.aida.buyer.util; + +import com.aida.buyer.util.RedisUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class RedisLoginUtil { + + private static final String TOKEN_KEY_PREFIX = "login:token:"; + + private final RedisUtil redisUtil; + + public void setLoginToken(Long userId, String token, long ttlMs) { + long ttlSeconds = ttlMs / 1000; + redisUtil.setWithExpire(TOKEN_KEY_PREFIX + userId, token, ttlSeconds); + } + + public String getLoginToken(Long userId) { + Object token = redisUtil.get(TOKEN_KEY_PREFIX + userId); + return token == null ? null : String.valueOf(token); + } + + public void deleteLoginToken(Long userId) { + redisUtil.delete(TOKEN_KEY_PREFIX + userId); + } +} diff --git a/src/main/java/com/aida/buyer/util/RedisUtil.java b/src/main/java/com/aida/buyer/util/RedisUtil.java new file mode 100644 index 0000000..e2b9dde --- /dev/null +++ b/src/main/java/com/aida/buyer/util/RedisUtil.java @@ -0,0 +1,92 @@ +package com.aida.buyer.util; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Component +public class RedisUtil { + + @Autowired + private RedisTemplate redisTemplate; + + public void set(String key, Object value) { + redisTemplate.opsForValue().set(key, value); + } + + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + public Boolean setWithExpire(String key, Object value, long time) { + return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS); + } + + public Boolean setWithExpire(String key, Object value, long time, TimeUnit timeUnit) { + return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit); + } + + public Boolean increment(String key) { + return redisTemplate.opsForValue().increment(key) != null; + } + + public Long increment(String key, long delta) { + return redisTemplate.opsForValue().increment(key, delta); + } + + public Long decrement(String key) { + return redisTemplate.opsForValue().decrement(key); + } + + public Long decrement(String key, long delta) { + return redisTemplate.opsForValue().decrement(key, delta); + } + + public Boolean delete(String key) { + return redisTemplate.delete(key); + } + + public Long delete(Collection keys) { + return redisTemplate.delete(keys); + } + + public void hSet(String key, String hashKey, Object value) { + redisTemplate.opsForHash().put(key, hashKey, value); + } + + public Object hGet(String key, String hashKey) { + return redisTemplate.opsForHash().get(key, hashKey); + } + + public Map hGetAll(String key) { + return redisTemplate.opsForHash().entries(key); + } + + public Boolean hDelete(String key, Object... hashKeys) { + return redisTemplate.opsForHash().delete(key, hashKeys) > 0; + } + + public Long hIncrement(String key, String hashKey, long delta) { + return redisTemplate.opsForHash().increment(key, hashKey, delta); + } + + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + public Boolean expire(String key, long time) { + return redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + + public Long getExpire(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + public Collection keys(String pattern) { + return redisTemplate.keys(pattern); + } +} diff --git a/src/main/java/com/aida/buyer/util/SendEmailUtil.java b/src/main/java/com/aida/buyer/util/SendEmailUtil.java new file mode 100644 index 0000000..152670a --- /dev/null +++ b/src/main/java/com/aida/buyer/util/SendEmailUtil.java @@ -0,0 +1,76 @@ +package com.aida.buyer.util; + +import com.alibaba.fastjson.JSONObject; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.ses.v20201002.SesClient; +import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest; +import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse; +import com.tencentcloudapi.ses.v20201002.models.Template; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SendEmailUtil { + + private static final String SECRET_ID = "AKID52lRwDIBsLaZLtDI9m9LJMAj36wYw50i"; + private static final String SECRET_KEY = "XqujLlywhHfrqcCYfYVHtNgmeIiwxkKf"; + private static final String SEND_ADDRESS = "info@aida.com.hk"; + + private static final Long LOGIN_TEMPLATE_ID = 58020L; + private static final Long PORTFOLIO_REGISTER_ID = 124847L; + private static final Long UPDATE_PWD_TEMPLATE_ID = 58022L; + private static final Long BIND_MAILBOX_TEMPLATE_ID = 132754L; + + private static final String LOGIN_SUBJECT = "Log on"; + private static final String REGISTER_SUBJECT = "Tourist registration"; + private static final String FORGET_PWD_SUBJECT = "Reset password"; + private static final String BIND_MAILBOX_SUBJECT = "\u7ed1\u5b9a\u90ae\u7bb1"; + + public static Boolean sendVerifyCode(String receiverAddress, String verifyCode) { + return send(receiverAddress, null, PORTFOLIO_REGISTER_ID, verifyCode, REGISTER_SUBJECT); + } + + public static Boolean send(String receiverAddress, String ip, Long templateId, String verifyCode) { + String subject = LOGIN_SUBJECT; + if (UPDATE_PWD_TEMPLATE_ID.equals(templateId)) { + subject = FORGET_PWD_SUBJECT; + } else if (BIND_MAILBOX_TEMPLATE_ID.equals(templateId)) { + subject = BIND_MAILBOX_SUBJECT; + } + return send(receiverAddress, ip, templateId, verifyCode, subject); + } + + private static Boolean send(String receiverAddress, String ip, Long templateId, String verifyCode, String subject) { + try { + Credential cred = new Credential(SECRET_ID, SECRET_KEY); + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("ses.tencentcloudapi.com"); + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + SesClient client = new SesClient(cred, "ap-hongkong", clientProfile); + + SendEmailRequest req = new SendEmailRequest(); + req.setFromEmailAddress(SEND_ADDRESS); + req.setDestination(new String[]{receiverAddress}); + req.setSubject(subject); + req.setTemplate(contractTemplate(templateId, verifyCode, ip)); + + SendEmailResponse resp = client.SendEmail(req); + log.info("Email sent result: {}", SendEmailResponse.toJsonString(resp)); + return Boolean.TRUE; + } catch (Exception e) { + log.info("Email send failed: {}", e.toString()); + throw new RuntimeException("Failed to send email", e); + } + } + + private static Template contractTemplate(Long templateId, String verifyCode, String ip) { + Template template = new Template(); + template.setTemplateID(templateId); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("code", verifyCode); + template.setTemplateData(jsonObject.toJSONString()); + return template; + } +} diff --git a/src/main/java/com/aida/buyer/util/TokenGenerateUtils.java b/src/main/java/com/aida/buyer/util/TokenGenerateUtils.java new file mode 100644 index 0000000..a507e9e --- /dev/null +++ b/src/main/java/com/aida/buyer/util/TokenGenerateUtils.java @@ -0,0 +1,55 @@ +package com.aida.buyer.util; + +import cn.hutool.crypto.digest.DigestUtil; +import com.aida.buyer.config.JwtConfig; +import com.aida.buyer.model.vo.AuthPrincipalVo; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TokenGenerateUtils { + + private static final String ISSUER = "BUYER"; + + private final JwtConfig jwtConfig; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public String createToken(AuthPrincipalVo principal) { + SecretKey key = buildSigningKey(); + try { + String token = Jwts.builder() + .id(String.valueOf(principal.getId())) + .subject(objectMapper.writeValueAsString(principal)) + .issuedAt(new Date()) + .issuer(ISSUER) + .expiration(new Date(System.currentTimeMillis() + jwtConfig.getJwtExpiration())) + .signWith(key) + .compact(); + return token; + } catch (Exception e) { + throw new RuntimeException("Failed to create JWT token", e); + } + } + + public long getJwtExpiration() { + return jwtConfig.getJwtExpiration(); + } + + private SecretKey buildSigningKey() { + byte[] raw = jwtConfig.getJwtSecret().getBytes(StandardCharsets.UTF_8); + if (raw.length < 32) { + raw = DigestUtil.sha256(raw); + } + return Keys.hmacShaKeyFor(raw); + } +} diff --git a/src/main/java/com/aida/buyer/util/ValidationUtil.java b/src/main/java/com/aida/buyer/util/ValidationUtil.java new file mode 100644 index 0000000..07dbcec --- /dev/null +++ b/src/main/java/com/aida/buyer/util/ValidationUtil.java @@ -0,0 +1,40 @@ +package com.aida.buyer.util; + +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +public class ValidationUtil { + + private static final String MOBILE_REGEX = "^1[3-9]\\d{9}$"; + private static final String EMAIL_REGEX = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"; + private static final String ID_CARD_REGEX = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"; + private static final String URL_REGEX = "^https?://[^\\s/$.?#].[^\\s]*$"; + + public static boolean isMobile(String mobile) { + if (StrUtil.isBlank(mobile)) { + return false; + } + return ReUtil.isMatch(MOBILE_REGEX, mobile); + } + + public static boolean isEmail(String email) { + if (StrUtil.isBlank(email)) { + return false; + } + return ReUtil.isMatch(EMAIL_REGEX, email); + } + + public static boolean isIdCard(String idCard) { + if (StrUtil.isBlank(idCard)) { + return false; + } + return ReUtil.isMatch(ID_CARD_REGEX, idCard); + } + + public static boolean isUrl(String url) { + if (StrUtil.isBlank(url)) { + return false; + } + return ReUtil.isMatch(URL_REGEX, url); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..3c1da61 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,24 @@ +server: + port: 10095 + +spring: + application: + name: aida-buyer + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + type-aliases-package: com.aida.buyer.module.*.entity + global-config: + db-config: + id-type: assign_id + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +minio: + default-bucket: aida-user + +logging: diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..cc6b401 --- /dev/null +++ b/src/main/resources/bootstrap.yml @@ -0,0 +1,31 @@ +# ============================================================ +# aida-buyer - Bootstrap +# 通过 NACOS_NAMESPACE 环境变量切换命名空间(dev / test / prod) +# ============================================================ + +nacos: + namespace: ltx + host: 18.167.251.121:28848 + username: nacos + password: Aidlab123123! + +spring: + application: + name: aida-buyer + config: + import: optional:nacos:aida-public-${nacos.namespace}.yml + cloud: + nacos: + discovery: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + username: ${nacos.username} + password: ${nacos.password} + port: 10095 + + config: + server-addr: ${nacos.host} + namespace: ${nacos.namespace} + file-extension: yaml + username: ${nacos.username} + password: ${nacos.password} diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql new file mode 100644 index 0000000..bf134ff --- /dev/null +++ b/src/main/resources/db/schema.sql @@ -0,0 +1,23 @@ +-- 创建 buyer 数据库 +CREATE DATABASE IF NOT EXISTS `buyer` + DEFAULT CHARACTER SET utf8mb4 + DEFAULT COLLATE utf8mb4_general_ci; + +USE `buyer`; + +-- 创建 buyer_account 表 +CREATE TABLE IF NOT EXISTS `buyer_account` ( + `id` BIGINT NOT NULL COMMENT '主键ID' PRIMARY KEY, + `email` VARCHAR(255) NOT NULL COMMENT '邮箱(唯一)' UNIQUE, + `password` VARCHAR(255) NOT NULL COMMENT '密码', + `username` VARCHAR(100) COMMENT '用户名', + `avatar` VARCHAR(500) COMMENT '头像URL', + `language` VARCHAR(20) DEFAULT 'ENGLISH' COMMENT '语言:ENGLISH / CHINESE_SIMPLIFIED / CHINESE_TRADITIONAL', + `country` VARCHAR(50) COMMENT '国家', + `occupation` VARCHAR(100) COMMENT '职业', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除', + INDEX `idx_email` (`email`), + INDEX `idx_deleted` (`deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='买家账号表'; diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..77b3827 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,61 @@ + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + ${LOG_HOME}/${APP_NAME}-info.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + INFO + ACCEPT + DENY + + + ${LOG_HOME}/${APP_NAME}-info-%d{yyyy-MM-dd}.%i.log + + 100MB + + 60 + + + + + ${LOG_HOME}/${APP_NAME}-error.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + ERROR + ACCEPT + DENY + + + ${LOG_HOME}/${APP_NAME}-error-%d{yyyy-MM-dd}.%i.log + + 100MB + + 60 + + + + + + + + + + + +