This commit is contained in:
litianxiang
2026-05-15 15:09:21 +08:00
commit ac7de27099
69 changed files with 2696 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target/
/aida-buyer.iml
/.idea/

227
pom.xml Normal file
View 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>

View 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 启动完成!");
}
}

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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";
}

View File

@@ -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;
}

View 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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("系统繁忙");
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View File

@@ -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<>();
}

View 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;
}

View 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();
}
}

View 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;
}
}

View 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());
}
}

View 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;
}
}

View 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")));
}
}

View 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("/**");
}
}

View 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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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 为 categoryvalue 为该分类下所有图片 URL")
private Map<String, List<String>> images;
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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));
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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:

View 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}

View 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='买家账号表';

View 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>