first
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target/
|
||||
/aida-buyer.iml
|
||||
/.idea/
|
||||
227
pom.xml
Normal file
227
pom.xml
Normal file
@@ -0,0 +1,227 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.aida</groupId>
|
||||
<artifactId>aida-buyer</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>AiDA Buyer</name>
|
||||
<description>AiDA Buyer Service</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
<minio.version>8.0.3</minio.version>
|
||||
<jwt.version>0.12.3</jwt.version>
|
||||
<hutool.version>5.8.23</hutool.version>
|
||||
<commons-lang3.version>3.13.0</commons-lang3.version>
|
||||
<knife4j.version>4.4.0</knife4j.version>
|
||||
|
||||
<spring-cloud-alibaba.version>2023.0.3.4</spring-cloud-alibaba.version>
|
||||
<spring-cloud.version>2023.0.4</spring-cloud.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- Spring Cloud 统一版本 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>${spring-cloud.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Spring Cloud Alibaba 统一版本 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
|
||||
<version>${spring-cloud-alibaba.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Logging(显式引入,确保 logback 正确初始化) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus Spring Boot3 专用 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MinIO -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>${minio.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Hutool 工具库 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Knife4j OpenAPI3 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Druid -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
<version>1.2.21</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Fastjson -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>2.0.43</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 动态数据源 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
<version>4.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test & DevTools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 微服务 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 腾讯云 SES -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-ses</artifactId>
|
||||
<version>3.1.572</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<workingDirectory>${project.basedir}</workingDirectory>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
21
src/main/java/com/aida/buyer/AidaBuyerApplication.java
Normal file
21
src/main/java/com/aida/buyer/AidaBuyerApplication.java
Normal file
@@ -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 启动完成!");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
54
src/main/java/com/aida/buyer/common/context/UserContext.java
Normal file
54
src/main/java/com/aida/buyer/common/context/UserContext.java
Normal file
@@ -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<AuthPrincipalVo> userHolder = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Boolean> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Response<Void>> handleUnauthorizedException(UnauthorizedException e) {
|
||||
log.error("Unauthorized: {}", e.getMessage());
|
||||
return new ResponseEntity<>(
|
||||
Response.fail(401, e.getMessage()),
|
||||
HttpStatus.UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Response<Void> handleBusinessException(BusinessException e) {
|
||||
return Response.fail(e.getCode(), e.getMsg());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Response<Void> 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<Void> 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<Void> handleException(Exception e) {
|
||||
log.error("system error: ", e);
|
||||
return Response.error("系统繁忙");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/aida/buyer/common/result/PageResponse.java
Normal file
29
src/main/java/com/aida/buyer/common/result/PageResponse.java
Normal file
@@ -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<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long page;
|
||||
private Long size;
|
||||
private Long pages;
|
||||
private Long total;
|
||||
private List<T> content;
|
||||
|
||||
public static <T> PageResponse<T> success(IPage<T> page) {
|
||||
PageResponse<T> 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;
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/aida/buyer/common/result/Response.java
Normal file
48
src/main/java/com/aida/buyer/common/result/Response.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.aida.buyer.common.result;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
public class Response<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer errCode;
|
||||
private String errMsg;
|
||||
private T data;
|
||||
|
||||
public static <T> Response<T> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(T data) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setErrCode(ResultEnum.SUCCESS.getCode());
|
||||
response.setErrMsg(ResultEnum.SUCCESS.getMessage());
|
||||
response.setData(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(String errMsg) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setErrCode(ResultEnum.FAIL.getCode());
|
||||
response.setErrMsg(errMsg);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(Integer errCode, String errMsg) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setErrCode(errCode);
|
||||
response.setErrMsg(errMsg);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(String errMsg) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setErrCode(ResultEnum.FAIL.getCode());
|
||||
response.setErrMsg(errMsg);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
19
src/main/java/com/aida/buyer/common/result/ResultEnum.java
Normal file
19
src/main/java/com/aida/buyer/common/result/ResultEnum.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<String> optionalAuthPaths = new ArrayList<>();
|
||||
}
|
||||
14
src/main/java/com/aida/buyer/config/JwtConfig.java
Normal file
14
src/main/java/com/aida/buyer/config/JwtConfig.java
Normal file
@@ -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;
|
||||
}
|
||||
27
src/main/java/com/aida/buyer/config/MinioConfig.java
Normal file
27
src/main/java/com/aida/buyer/config/MinioConfig.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/aida/buyer/config/MyBatisPlusConfig.java
Normal file
18
src/main/java/com/aida/buyer/config/MyBatisPlusConfig.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/aida/buyer/config/MyMetaObjectHandler.java
Normal file
22
src/main/java/com/aida/buyer/config/MyMetaObjectHandler.java
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/aida/buyer/config/RedisConfig.java
Normal file
29
src/main/java/com/aida/buyer/config/RedisConfig.java
Normal file
@@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> 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;
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/aida/buyer/config/SwaggerConfig.java
Normal file
23
src/main/java/com/aida/buyer/config/SwaggerConfig.java
Normal file
@@ -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")));
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/aida/buyer/config/WebConfig.java
Normal file
33
src/main/java/com/aida/buyer/config/WebConfig.java
Normal file
@@ -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("/**");
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/aida/buyer/model/vo/AuthPrincipalVo.java
Normal file
24
src/main/java/com/aida/buyer/model/vo/AuthPrincipalVo.java
Normal file
@@ -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<String> authorities;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<BuyerAccount> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<BuyerAccount> 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<BuyerAccount> 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<BuyerAccount> oldEmailQuery = new LambdaQueryWrapper<>();
|
||||
oldEmailQuery.eq(BuyerAccount::getEmail, oldEmail);
|
||||
if (buyerAccountMapper.selectCount(oldEmailQuery) == 0) {
|
||||
throw new BusinessException("account.not.found");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<BuyerAccount> 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<BuyerAccount> oldEmailQuery = new LambdaQueryWrapper<>();
|
||||
oldEmailQuery.eq(BuyerAccount::getEmail, oldEmail);
|
||||
BuyerAccount account = buyerAccountMapper.selectOne(oldEmailQuery);
|
||||
if (account == null) {
|
||||
throw new BusinessException("account.not.found");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<BuyerAccount> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<DesignerShopVO> getShopDetail(
|
||||
@Parameter(description = "设计师用户ID") @PathVariable Long sellerId) {
|
||||
return designerService.getShopDetail(sellerId);
|
||||
}
|
||||
|
||||
@Operation(summary = "搜索设计师", description = "根据关键词不区分大小写同时匹配店铺名称和所有者姓名,返回设计师信息、最近5张商品封面图(按updateTime倒序)、商品总数")
|
||||
@GetMapping("/search")
|
||||
public Response<List<DesignerSearchVO>> search(
|
||||
@Parameter(description = "关键词(同时匹配店铺名称和所有者姓名,不区分大小写)") @RequestParam String keyword) {
|
||||
return designerService.searchDesigners(keyword);
|
||||
}
|
||||
}
|
||||
@@ -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<DesignerShopVO> getShopDetail(@PathVariable Long sellerId);
|
||||
|
||||
@GetMapping("/search")
|
||||
Response<List<DesignerSearchVO>> searchDesigners(@RequestParam String keyword);
|
||||
}
|
||||
@@ -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<DesignerShopVO> getShopDetail(Long sellerId);
|
||||
|
||||
/**
|
||||
* 搜索设计师
|
||||
*
|
||||
* @param keyword 关键词(同时匹配店铺名称和所有者姓名,不区分大小写)
|
||||
* @return 设计师搜索结果列表
|
||||
*/
|
||||
Response<List<DesignerSearchVO>> searchDesigners(String keyword);
|
||||
}
|
||||
@@ -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<DesignerShopVO> getShopDetail(Long sellerId) {
|
||||
return designerFeignClient.getShopDetail(sellerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<List<DesignerSearchVO>> searchDesigners(String keyword) {
|
||||
return designerFeignClient.searchDesigners(keyword);
|
||||
}
|
||||
}
|
||||
@@ -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<String> covers;
|
||||
|
||||
/** 该设计师的商品总数 */
|
||||
private Long listingTotal;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<ListingMallVO> getMallListings(
|
||||
@RequestBody ListingMallQueryDTO dto) {
|
||||
return listingService.getMallListings(dto);
|
||||
}
|
||||
|
||||
@Operation(summary = "商品详情(商城详情页)", description = "关联图片与店铺信息")
|
||||
@GetMapping("/mall/detail")
|
||||
public Response<ListingDetailVO> getListingDetail(
|
||||
@Parameter(description = "商品ID") @RequestParam Long id) {
|
||||
return listingService.getListingDetail(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取店铺商品列表", description = "按 status=1、deleted=0、designFor 筛选,返回店铺已发布商品分页列表")
|
||||
@GetMapping("/shop/seller")
|
||||
public PageResponse<ListingPageVO> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<PageResponse<ListingMallVO>> getMallListings(ListingMallQueryDTO dto);
|
||||
|
||||
@GetMapping("/mall/detail")
|
||||
Response<ListingDetailVO> getListingDetail(@RequestParam Long id);
|
||||
|
||||
@GetMapping("/shop/seller")
|
||||
Response<PageResponse<ListingPageVO>> getShopListings(
|
||||
@RequestParam Long sellerId,
|
||||
@RequestParam String designFor,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize);
|
||||
}
|
||||
@@ -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<ListingMallVO> getMallListings(ListingMallQueryDTO dto);
|
||||
|
||||
/**
|
||||
* 获取商品详情(商城详情页)
|
||||
*
|
||||
* @param id 商品ID
|
||||
* @return 商品详情
|
||||
*/
|
||||
Response<ListingDetailVO> getListingDetail(Long id);
|
||||
|
||||
/**
|
||||
* 获取店铺商品列表(已发布商品分页)
|
||||
*
|
||||
* @param sellerId 设计师用户ID
|
||||
* @param designFor 适用性别 female/male/all
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 店铺商品分页列表
|
||||
*/
|
||||
PageResponse<ListingPageVO> getShopListings(Long sellerId, String designFor, int pageNum, int pageSize);
|
||||
}
|
||||
@@ -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<ListingMallVO> getMallListings(ListingMallQueryDTO dto) {
|
||||
Response<PageResponse<ListingMallVO>> response = listingFeignClient.getMallListings(dto);
|
||||
PageResponse<ListingMallVO> 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<ListingDetailVO> getListingDetail(Long id) {
|
||||
Response<ListingDetailVO> 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<ListingPageVO> getShopListings(Long sellerId, String designFor, int pageNum, int pageSize) {
|
||||
Response<PageResponse<ListingPageVO>> response = listingFeignClient.getShopListings(sellerId, designFor, pageNum, pageSize);
|
||||
PageResponse<ListingPageVO> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, List<String>> images;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
41
src/main/java/com/aida/buyer/util/DesensitizationUtil.java
Normal file
41
src/main/java/com/aida/buyer/util/DesensitizationUtil.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
32
src/main/java/com/aida/buyer/util/LoginCacheUtil.java
Normal file
32
src/main/java/com/aida/buyer/util/LoginCacheUtil.java
Normal file
@@ -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<String, String> 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);
|
||||
}
|
||||
}
|
||||
150
src/main/java/com/aida/buyer/util/MinioUtil.java
Normal file
150
src/main/java/com/aida/buyer/util/MinioUtil.java
Normal file
@@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
10
src/main/java/com/aida/buyer/util/RandomsUtil.java
Normal file
10
src/main/java/com/aida/buyer/util/RandomsUtil.java
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/aida/buyer/util/RedisLoginUtil.java
Normal file
30
src/main/java/com/aida/buyer/util/RedisLoginUtil.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
92
src/main/java/com/aida/buyer/util/RedisUtil.java
Normal file
92
src/main/java/com/aida/buyer/util/RedisUtil.java
Normal file
@@ -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<String, Object> 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<String> 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<Object, Object> 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<String> keys(String pattern) {
|
||||
return redisTemplate.keys(pattern);
|
||||
}
|
||||
}
|
||||
76
src/main/java/com/aida/buyer/util/SendEmailUtil.java
Normal file
76
src/main/java/com/aida/buyer/util/SendEmailUtil.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/aida/buyer/util/TokenGenerateUtils.java
Normal file
55
src/main/java/com/aida/buyer/util/TokenGenerateUtils.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/aida/buyer/util/ValidationUtil.java
Normal file
40
src/main/java/com/aida/buyer/util/ValidationUtil.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
24
src/main/resources/application.yml
Normal file
24
src/main/resources/application.yml
Normal file
@@ -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:
|
||||
31
src/main/resources/bootstrap.yml
Normal file
31
src/main/resources/bootstrap.yml
Normal file
@@ -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}
|
||||
23
src/main/resources/db/schema.sql
Normal file
23
src/main/resources/db/schema.sql
Normal file
@@ -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='买家账号表';
|
||||
61
src/main/resources/logback-spring.xml
Normal file
61
src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<property name="LOG_HOME" value="./log"/>
|
||||
<property name="APP_NAME" value="aida-buyer"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${APP_NAME}-info.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>INFO</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${APP_NAME}-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${APP_NAME}-error.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${APP_NAME}-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<logger name="com.aida" level="DEBUG"/>
|
||||
<logger name="com.aida.buyer.mapper" level="INFO"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="INFO_FILE"/>
|
||||
<appender-ref ref="ERROR_FILE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user