From 85c1779debb85e991e55fb0a71cee71fc5cc63f4 Mon Sep 17 00:00:00 2001 From: litianxiang Date: Tue, 21 Apr 2026 10:25:39 +0800 Subject: [PATCH] 1 --- pom.xml | 199 ++++++++ .../aida/seller/AidaSellerApplication.java | 19 + .../seller/common/config/RedisConfig.java | 29 ++ .../common/constants/CommonConstants.java | 14 + .../constants/MinioBucketNameConstants.java | 6 + .../common/constants/MinioFileConstants.java | 68 +++ .../common/constants/OrderConstants.java | 13 + .../common/constants/ProductConstants.java | 11 + .../common/constants/StatusConstants.java | 16 + .../seller/common/context/UserContext.java | 24 + .../com/aida/seller/common/dto/LoginDTO.java | 24 + .../com/aida/seller/common/dto/LoginVO.java | 31 ++ .../common/exception/BusinessException.java | 42 ++ .../exception/GlobalExceptionHandler.java | 64 +++ .../common/exception/MinioException.java | 78 +++ .../seller/common/result/PageResponse.java | 46 ++ .../aida/seller/common/result/Response.java | 98 ++++ .../aida/seller/common/result/ResultEnum.java | 55 +++ .../security/config/SecurityProperties.java | 21 + .../filter/SellerAuthenticationFilter.java | 134 +++++ .../common/security/jwt/SellerJwtHelper.java | 67 +++ .../security/utils/SellerLocalCacheUtils.java | 57 +++ .../security/utils/SellerRedisUtil.java | 40 ++ .../com/aida/seller/config/FilterConfig.java | 20 + .../com/aida/seller/config/JwtConfig.java | 14 + .../com/aida/seller/config/MinioConfig.java | 26 + .../aida/seller/config/MyBatisPlusConfig.java | 18 + .../com/aida/seller/config/SwaggerConfig.java | 25 + .../com/aida/seller/config/WebConfig.java | 25 + .../aida/seller/model/vo/AuthPrincipalVo.java | 21 + .../controller/DesignerController.java | 72 +++ .../module/designer/dto/DesignerApplyDTO.java | 48 ++ .../module/designer/dto/DesignerAuditDTO.java | 28 ++ .../designer/entity/DesignerEntity.java | 68 +++ .../enums/DesignerApplyStatusEnum.java | 27 + .../designer/mapper/DesignerMapper.java | 9 + .../designer/service/DesignerService.java | 36 ++ .../designer/service/DesignerServiceImpl.java | 133 +++++ .../designer/vo/DesignerApplyDetailVo.java | 55 +++ .../order/controller/OrderController.java | 64 +++ .../seller/module/order/dto/OrderListDTO.java | 18 + .../module/order/entity/OrderInfoEntity.java | 48 ++ .../module/order/entity/OrderItemEntity.java | 48 ++ .../module/order/mapper/OrderInfoMapper.java | 9 + .../module/order/mapper/OrderItemMapper.java | 9 + .../module/order/service/OrderService.java | 29 ++ .../order/service/OrderServiceImpl.java | 122 +++++ .../module/order/vo/OrderSummaryVO.java | 27 + .../aida/seller/module/order/vo/OrderVO.java | 47 ++ .../aida/seller/util/DesensitizationUtil.java | 44 ++ .../java/com/aida/seller/util/JwtUtil.java | 99 ++++ .../java/com/aida/seller/util/MinioUtil.java | 460 ++++++++++++++++++ .../java/com/aida/seller/util/RedisUtil.java | 203 ++++++++ .../com/aida/seller/util/ValidationUtil.java | 43 ++ src/main/resources/application.yml | 82 ++++ src/main/resources/bootstrap.yml | 10 + src/main/resources/db/schema.sql | 87 ++++ 57 files changed, 3230 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/aida/seller/AidaSellerApplication.java create mode 100644 src/main/java/com/aida/seller/common/config/RedisConfig.java create mode 100644 src/main/java/com/aida/seller/common/constants/CommonConstants.java create mode 100644 src/main/java/com/aida/seller/common/constants/MinioBucketNameConstants.java create mode 100644 src/main/java/com/aida/seller/common/constants/MinioFileConstants.java create mode 100644 src/main/java/com/aida/seller/common/constants/OrderConstants.java create mode 100644 src/main/java/com/aida/seller/common/constants/ProductConstants.java create mode 100644 src/main/java/com/aida/seller/common/constants/StatusConstants.java create mode 100644 src/main/java/com/aida/seller/common/context/UserContext.java create mode 100644 src/main/java/com/aida/seller/common/dto/LoginDTO.java create mode 100644 src/main/java/com/aida/seller/common/dto/LoginVO.java create mode 100644 src/main/java/com/aida/seller/common/exception/BusinessException.java create mode 100644 src/main/java/com/aida/seller/common/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/aida/seller/common/exception/MinioException.java create mode 100644 src/main/java/com/aida/seller/common/result/PageResponse.java create mode 100644 src/main/java/com/aida/seller/common/result/Response.java create mode 100644 src/main/java/com/aida/seller/common/result/ResultEnum.java create mode 100644 src/main/java/com/aida/seller/common/security/config/SecurityProperties.java create mode 100644 src/main/java/com/aida/seller/common/security/filter/SellerAuthenticationFilter.java create mode 100644 src/main/java/com/aida/seller/common/security/jwt/SellerJwtHelper.java create mode 100644 src/main/java/com/aida/seller/common/security/utils/SellerLocalCacheUtils.java create mode 100644 src/main/java/com/aida/seller/common/security/utils/SellerRedisUtil.java create mode 100644 src/main/java/com/aida/seller/config/FilterConfig.java create mode 100644 src/main/java/com/aida/seller/config/JwtConfig.java create mode 100644 src/main/java/com/aida/seller/config/MinioConfig.java create mode 100644 src/main/java/com/aida/seller/config/MyBatisPlusConfig.java create mode 100644 src/main/java/com/aida/seller/config/SwaggerConfig.java create mode 100644 src/main/java/com/aida/seller/config/WebConfig.java create mode 100644 src/main/java/com/aida/seller/model/vo/AuthPrincipalVo.java create mode 100644 src/main/java/com/aida/seller/module/designer/controller/DesignerController.java create mode 100644 src/main/java/com/aida/seller/module/designer/dto/DesignerApplyDTO.java create mode 100644 src/main/java/com/aida/seller/module/designer/dto/DesignerAuditDTO.java create mode 100644 src/main/java/com/aida/seller/module/designer/entity/DesignerEntity.java create mode 100644 src/main/java/com/aida/seller/module/designer/enums/DesignerApplyStatusEnum.java create mode 100644 src/main/java/com/aida/seller/module/designer/mapper/DesignerMapper.java create mode 100644 src/main/java/com/aida/seller/module/designer/service/DesignerService.java create mode 100644 src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java create mode 100644 src/main/java/com/aida/seller/module/designer/vo/DesignerApplyDetailVo.java create mode 100644 src/main/java/com/aida/seller/module/order/controller/OrderController.java create mode 100644 src/main/java/com/aida/seller/module/order/dto/OrderListDTO.java create mode 100644 src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java create mode 100644 src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java create mode 100644 src/main/java/com/aida/seller/module/order/mapper/OrderInfoMapper.java create mode 100644 src/main/java/com/aida/seller/module/order/mapper/OrderItemMapper.java create mode 100644 src/main/java/com/aida/seller/module/order/service/OrderService.java create mode 100644 src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java create mode 100644 src/main/java/com/aida/seller/module/order/vo/OrderSummaryVO.java create mode 100644 src/main/java/com/aida/seller/module/order/vo/OrderVO.java create mode 100644 src/main/java/com/aida/seller/util/DesensitizationUtil.java create mode 100644 src/main/java/com/aida/seller/util/JwtUtil.java create mode 100644 src/main/java/com/aida/seller/util/MinioUtil.java create mode 100644 src/main/java/com/aida/seller/util/RedisUtil.java create mode 100644 src/main/java/com/aida/seller/util/ValidationUtil.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/bootstrap.yml create mode 100644 src/main/resources/db/schema.sql diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e08001d --- /dev/null +++ b/pom.xml @@ -0,0 +1,199 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.aida + aida-seller + 1.0.0 + jar + AiDA Seller + AiDA平台电商卖家后台管理系统 + + + 21 + 21 + 21 + UTF-8 + + 3.5.6 + 8.5.7 + 0.12.3 + 5.8.26 + 3.13.0 + 4.5.0 + + 2023.0.1.0 + 2023.0.0 + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + + com.mysql + mysql-connector-j + runtime + + + + + io.minio + minio + ${minio.version} + + + + + io.jsonwebtoken + jjwt-api + ${jwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jwt.version} + runtime + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/aida/seller/AidaSellerApplication.java b/src/main/java/com/aida/seller/AidaSellerApplication.java new file mode 100644 index 0000000..a2e5e6e --- /dev/null +++ b/src/main/java/com/aida/seller/AidaSellerApplication.java @@ -0,0 +1,19 @@ +package com.aida.seller; + +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; + +@SpringBootApplication +@MapperScan("com.aida.seller.module.*.mapper") +@EnableFeignClients +@EnableDiscoveryClient +public class AidaSellerApplication { + + public static void main(String[] args) { + SpringApplication.run(AidaSellerApplication.class, args); + System.out.println("AidaSellerApplication 启动成功."); + } +} diff --git a/src/main/java/com/aida/seller/common/config/RedisConfig.java b/src/main/java/com/aida/seller/common/config/RedisConfig.java new file mode 100644 index 0000000..4a75097 --- /dev/null +++ b/src/main/java/com/aida/seller/common/config/RedisConfig.java @@ -0,0 +1,29 @@ +package com.aida.seller.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + StringRedisSerializer stringSerializer = new StringRedisSerializer(); + GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); + + template.setKeySerializer(stringSerializer); + template.setValueSerializer(jsonSerializer); + template.setHashKeySerializer(stringSerializer); + template.setHashValueSerializer(jsonSerializer); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/com/aida/seller/common/constants/CommonConstants.java b/src/main/java/com/aida/seller/common/constants/CommonConstants.java new file mode 100644 index 0000000..d35c591 --- /dev/null +++ b/src/main/java/com/aida/seller/common/constants/CommonConstants.java @@ -0,0 +1,14 @@ +package com.aida.seller.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) + + + + + + +} \ No newline at end of file diff --git a/src/main/java/com/aida/seller/common/constants/MinioBucketNameConstants.java b/src/main/java/com/aida/seller/common/constants/MinioBucketNameConstants.java new file mode 100644 index 0000000..b371fea --- /dev/null +++ b/src/main/java/com/aida/seller/common/constants/MinioBucketNameConstants.java @@ -0,0 +1,6 @@ +package com.aida.seller.common.constants; + +public class MinioBucketNameConstants { + //默认桶名 + public static final String USER = "aida-user"; +} diff --git a/src/main/java/com/aida/seller/common/constants/MinioFileConstants.java b/src/main/java/com/aida/seller/common/constants/MinioFileConstants.java new file mode 100644 index 0000000..1bf2e6a --- /dev/null +++ b/src/main/java/com/aida/seller/common/constants/MinioFileConstants.java @@ -0,0 +1,68 @@ +package com.aida.seller.common.constants; + +import java.util.UUID; + +/** + * MinIO文件命名常量类 + * 统一管理不同类型图片的命名规范 + * + * @author Fida Team + * @date 2026-03-09 + */ +public class MinioFileConstants { + + /** + * 文件路径分隔符 + */ + public static final String PATH_SEPARATOR = "/"; + + /** + * 图片文件扩展名 + */ + public static final String PNG_EXTENSION = ".png"; + public static final String JPG_EXTENSION = ".jpg"; + public static final String JPEG_EXTENSION = ".jpeg"; + + /** + * 生成to_real_style图片文件名(仅路径部分) + * 格式: to_real_style/UUID.png + * + * @return 文件路径(不含桶名) + */ + public static String generateToRealStyleObjectName() { + return FileType.TO_REAL_STYLE.getDir() + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION; + } + + /** + * 根据文件类型生成对应的对象名称 + * + * @param fileType 文件类型 + * @return 对象名称 + */ + public static String generateObjectNameByType(FileType fileType) { + return switch (fileType) { + case TO_REAL_STYLE -> generateToRealStyleObjectName(); + + }; + } + + + + /** + * 文件类型枚举 + */ + public enum FileType { + + TO_REAL_STYLE("to_real_style"); + + private final String dir; + + FileType(String dir) { + this.dir = dir; + } + + public String getDir() { + return dir; + } + } +} diff --git a/src/main/java/com/aida/seller/common/constants/OrderConstants.java b/src/main/java/com/aida/seller/common/constants/OrderConstants.java new file mode 100644 index 0000000..c535443 --- /dev/null +++ b/src/main/java/com/aida/seller/common/constants/OrderConstants.java @@ -0,0 +1,13 @@ +package com.aida.seller.common.constants; + +public class OrderConstants { + + private OrderConstants() {} + + public static final Integer ORDER_PENDING = 0; + public static final Integer ORDER_PAID = 1; + public static final Integer ORDER_SHIPPED = 2; + public static final Integer ORDER_COMPLETED = 3; + public static final Integer ORDER_CANCELLED = 4; + public static final Integer ORDER_REFUND = 5; +} diff --git a/src/main/java/com/aida/seller/common/constants/ProductConstants.java b/src/main/java/com/aida/seller/common/constants/ProductConstants.java new file mode 100644 index 0000000..70b2651 --- /dev/null +++ b/src/main/java/com/aida/seller/common/constants/ProductConstants.java @@ -0,0 +1,11 @@ +package com.aida.seller.common.constants; + +public class ProductConstants { + + private ProductConstants() {} + + public static final Integer PRODUCT_DRAFT = 0; + public static final Integer PRODUCT_ON_SALE = 1; + public static final Integer PRODUCT_OFF_SALE = 2; + public static final Integer PRODUCT_AUDITING = 3; +} diff --git a/src/main/java/com/aida/seller/common/constants/StatusConstants.java b/src/main/java/com/aida/seller/common/constants/StatusConstants.java new file mode 100644 index 0000000..a6ef1a5 --- /dev/null +++ b/src/main/java/com/aida/seller/common/constants/StatusConstants.java @@ -0,0 +1,16 @@ +package com.aida.seller.common.constants; + +public class StatusConstants { + + private 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_PASS = 1; + public static final Integer AUDIT_REJECT = 2; +} diff --git a/src/main/java/com/aida/seller/common/context/UserContext.java b/src/main/java/com/aida/seller/common/context/UserContext.java new file mode 100644 index 0000000..40f9043 --- /dev/null +++ b/src/main/java/com/aida/seller/common/context/UserContext.java @@ -0,0 +1,24 @@ +package com.aida.seller.common.context; + +import com.aida.seller.model.vo.AuthPrincipalVo; + +public class UserContext { + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static AuthPrincipalVo getUserHolder() { + return userHolder.get(); + } + + public static void delete() { + userHolder.remove(); + } + + public static void setUserHolder(AuthPrincipalVo authPrincipalVo) { + userHolder.set(authPrincipalVo); + } + + public static Long getUserId() { + AuthPrincipalVo holder = userHolder.get(); + return holder != null ? holder.getId() : null; + } +} diff --git a/src/main/java/com/aida/seller/common/dto/LoginDTO.java b/src/main/java/com/aida/seller/common/dto/LoginDTO.java new file mode 100644 index 0000000..55cd9b4 --- /dev/null +++ b/src/main/java/com/aida/seller/common/dto/LoginDTO.java @@ -0,0 +1,24 @@ +package com.aida.seller.common.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "登录请求") +public class LoginDTO { + + @Schema(description = "用户名") + @NotBlank(message = "用户名不能为空") + private String username; + + @Schema(description = "密码") + @NotBlank(message = "密码不能为空") + private String password; + + @Schema(description = "验证码") + private String captcha; + + @Schema(description = "验证码Key") + private String captchaKey; +} diff --git a/src/main/java/com/aida/seller/common/dto/LoginVO.java b/src/main/java/com/aida/seller/common/dto/LoginVO.java new file mode 100644 index 0000000..d68f2c0 --- /dev/null +++ b/src/main/java/com/aida/seller/common/dto/LoginVO.java @@ -0,0 +1,31 @@ +package com.aida.seller.common.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 LoginVO { + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "令牌类型") + private String tokenType = "Bearer"; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "商家ID") + private Long sellerId; + + @Schema(description = "过期时间(毫秒)") + private Long expiresIn; +} diff --git a/src/main/java/com/aida/seller/common/exception/BusinessException.java b/src/main/java/com/aida/seller/common/exception/BusinessException.java new file mode 100644 index 0000000..e8d645d --- /dev/null +++ b/src/main/java/com/aida/seller/common/exception/BusinessException.java @@ -0,0 +1,42 @@ +package com.aida.seller.common.exception; + +import com.aida.seller.common.result.ResultEnum; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +/** + * @author: dwjian + * @description: 业务异常 + */ +@Data +@Slf4j +public class BusinessException extends RuntimeException { + + private Integer code; + private String msg; + + public BusinessException(ResultEnum resultEnum) { + this.code = resultEnum.getCode(); + this.msg = resultEnum.getMsg(); + } + + public BusinessException(String msg) { + this.code = ResultEnum.FAIL.getCode(); + this.msg = msg; + } + + public BusinessException(String msg, Integer code) { + this.code = code; + this.msg = msg; + } + + public BusinessException(Throwable cause) { + this.code = ResultEnum.FAIL.getCode(); + this.msg = cause.getMessage(); + } + + public BusinessException(ResultEnum resultEnum, String customMsg) { + this.code = resultEnum.getCode(); + this.msg = customMsg; + } +} diff --git a/src/main/java/com/aida/seller/common/exception/GlobalExceptionHandler.java b/src/main/java/com/aida/seller/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..70deaa9 --- /dev/null +++ b/src/main/java/com/aida/seller/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,64 @@ +package com.aida.seller.common.exception; + +import com.aida.seller.common.result.Response; +import com.aida.seller.common.result.ResultEnum; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public Response handleBusinessException(BusinessException e) { + log.error("业务异常: code={}, msg={}", e.getCode(), e.getMsg()); + return Response.fail(e.getCode(), e.getMsg()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public Response handleValidationException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数校验异常: {}", message); + return Response.fail(ResultEnum.PARAMETER_ERROR.getCode(), message); + } + + @ExceptionHandler(BindException.class) + public Response handleBindException(BindException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数绑定异常: {}", message); + return Response.fail(ResultEnum.PARAMETER_ERROR.getCode(), message); + } + + @ExceptionHandler(Exception.class) + public Response handleException(Exception e) { + log.error("系统异常: ", e); + return Response.error("系统繁忙,请稍后再试"); + } + /** + * 处理MinIO异常 + */ + @ExceptionHandler(MinioException.class) + public ResponseEntity handleMinioException(MinioException e) { + log.error("[MinioException] {}", e.getMessage(), e); + String message = e.getMessage(); + if (message != null && (message.contains("文件不能为空") || message.contains("不能为空"))) { + Response response = Response.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "File cannot be empty"); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + Response response = Response.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "File storage service error"); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/aida/seller/common/exception/MinioException.java b/src/main/java/com/aida/seller/common/exception/MinioException.java new file mode 100644 index 0000000..1549f1b --- /dev/null +++ b/src/main/java/com/aida/seller/common/exception/MinioException.java @@ -0,0 +1,78 @@ +package com.aida.seller.common.exception; + +/** + * MinIO 操作异常类 + * 用于处理 MinIO 相关的业务异常 + * + * @author Aida + * @since 2024-01-01 + */ +public class MinioException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private String errorCode; + + /** + * 构造函数 + * + * @param message 异常信息 + */ + public MinioException(String message) { + super(message); + } + + /** + * 构造函数 + * + * @param message 异常信息 + * @param cause 原因 + */ + public MinioException(String message, Throwable cause) { + super(message, cause); + } + + /** + * 构造函数 + * + * @param errorCode 错误码 + * @param message 异常信息 + */ + public MinioException(String errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + /** + * 构造函数 + * + * @param errorCode 错误码 + * @param message 异常信息 + * @param cause 原因 + */ + public MinioException(String errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + /** + * 获取错误码 + * + * @return 错误码 + */ + public String getErrorCode() { + return errorCode; + } + + /** + * 设置错误码 + * + * @param errorCode 错误码 + */ + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } +} \ No newline at end of file diff --git a/src/main/java/com/aida/seller/common/result/PageResponse.java b/src/main/java/com/aida/seller/common/result/PageResponse.java new file mode 100644 index 0000000..a78a36f --- /dev/null +++ b/src/main/java/com/aida/seller/common/result/PageResponse.java @@ -0,0 +1,46 @@ +package com.aida.seller.common.result; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @ClassName PageResponse + * @Description 分页响应 + */ +@Data +@NoArgsConstructor +@Schema(description = "分页响应结果") +public class PageResponse extends Response> { + @Schema(description = "页码") + private long page; + @Schema(description = "每页数量") + private long size; + @Schema(description = "总页数") + private long pages; + @Schema(description = "总条数") + private long total; + @Schema(description = "结果集") + private List content; + + public PageResponse(Response> response, long page, long size, long total, long pages) { + if (response != null) { + this.setData(response.getData()); + this.setErrCode(response.getErrCode()); + this.setErrMsg(response.getErrMsg()); + } + this.page = page; + this.size = size; + this.total = total; + this.pages = pages; + this.content = response.getData(); + } + + public static PageResponse success(IPage page) { + Response> response = success(page.getRecords()); + return new PageResponse<>(response, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); + } +} diff --git a/src/main/java/com/aida/seller/common/result/Response.java b/src/main/java/com/aida/seller/common/result/Response.java new file mode 100644 index 0000000..ef4bb53 --- /dev/null +++ b/src/main/java/com/aida/seller/common/result/Response.java @@ -0,0 +1,98 @@ +package com.aida.seller.common.result; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @ClassName Response + * @Description success代表响应成功 fail代表主动响应失败 error代表系统异常 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "响应结果") +public class Response implements Serializable { + + @Schema(description = "响应状态码 0:成功 -1:失败") + private int errCode; + @Schema(description = "提示消息") + private String errMsg; + @Schema(description = "数据结果") + private T data; + + public static Response success() { + return success(ResultEnum.SUCCESS, null); + } + + public static Response success(T data) { + return success(ResultEnum.SUCCESS, data); + } + + public static Response success(ResultEnum resultEnum, T data) { + return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data); + } + + public static Response success(int code, T data) { + return success(code, ResultEnum.SUCCESS.getMsg(), data); + } + + public static Response success(int code, String msg, T data) { + return getResponse(code, msg, data); + } + + public static Response fail(String msg) { + return fail(ResultEnum.FAIL.getCode(), msg); + } + + public static Response fail(T data) { + return fail(ResultEnum.FAIL, data); + } + + public static Response fail(ResultEnum resultEnum) { + return fail(resultEnum.getCode(), resultEnum.getMsg(), null); + } + + public static Response fail(ResultEnum resultEnum, T data) { + return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data); + } + + public static Response fail(int code, String msg) { + return fail(code, msg, null); + } + + public static Response fail(int code, String msg, T data) { + return getResponse(code, msg, data); + } + + public static Response error(String msg) { + return error(ResultEnum.ERROR.getCode(), msg); + } + + public static Response error(T data) { + return error(ResultEnum.ERROR, data); + } + + public static Response error(ResultEnum resultEnum) { + return error(resultEnum.getCode(), resultEnum.getMsg(), null); + } + + public static Response error(ResultEnum resultEnum, T data) { + return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data); + } + + public static Response error(int code, String msg) { + return error(code, msg, null); + } + + public static Response error(int code, String msg, T data) { + return getResponse(code, msg, data); + } + + private static Response getResponse(int code, String msg, T data) { + return new Response<>(code, msg, data); + } +} diff --git a/src/main/java/com/aida/seller/common/result/ResultEnum.java b/src/main/java/com/aida/seller/common/result/ResultEnum.java new file mode 100644 index 0000000..e140a73 --- /dev/null +++ b/src/main/java/com/aida/seller/common/result/ResultEnum.java @@ -0,0 +1,55 @@ +package com.aida.seller.common.result; + +/** + * @ClassName ResultEnum + * @Description 响应结果枚举 + */ +public enum ResultEnum { + + SUCCESS(true, 0, "SUCCESS"), + FAIL(false, -1, "FAIL"), + ERROR(false, -1, "system error!"), + PARAMETER_ERROR(false, -2, "parameter error!"), + + NO_LOGIN(false, -100, "User not logged in"), + NO_PERMISSION(false, -200, "No access"), + ACCOUNT_LOCK(false, -300, "Account frozen"), + + PROMPT(false, 1, "Prompt"), + WARNING(false, 2, "Warning"), + ; + + private int code; + private String msg; + private boolean isOK; + + ResultEnum(boolean isOK, int code, String msg) { + this.isOK = isOK; + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public boolean isOK() { + return isOK; + } + + public void setOK(boolean OK) { + isOK = OK; + } +} diff --git a/src/main/java/com/aida/seller/common/security/config/SecurityProperties.java b/src/main/java/com/aida/seller/common/security/config/SecurityProperties.java new file mode 100644 index 0000000..b36e6bf --- /dev/null +++ b/src/main/java/com/aida/seller/common/security/config/SecurityProperties.java @@ -0,0 +1,21 @@ +package com.aida.seller.common.security.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 SecurityProperties { + + private String jwtSecret; + + private String jwtTokenHeader = "Authorization"; + + private String jwtTokenPrefix = "Bearer-"; + + private long jwtExpiration; + + private String[] ignorePaths; +} diff --git a/src/main/java/com/aida/seller/common/security/filter/SellerAuthenticationFilter.java b/src/main/java/com/aida/seller/common/security/filter/SellerAuthenticationFilter.java new file mode 100644 index 0000000..ab89633 --- /dev/null +++ b/src/main/java/com/aida/seller/common/security/filter/SellerAuthenticationFilter.java @@ -0,0 +1,134 @@ +package com.aida.seller.common.security.filter; + +import cn.hutool.core.util.StrUtil; +import com.aida.seller.common.context.UserContext; +import com.aida.seller.common.security.config.SecurityProperties; +import com.aida.seller.common.security.jwt.SellerJwtHelper; +import com.aida.seller.common.security.utils.SellerRedisUtil; +import com.aida.seller.model.vo.AuthPrincipalVo; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import com.aida.seller.common.security.utils.SellerLocalCacheUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SellerAuthenticationFilter extends OncePerRequestFilter { + + private final SellerJwtHelper jwtHelper; + private final SecurityProperties securityProperties; + private final SellerRedisUtil redisUtil; + + private static final List FILTER_URL = Arrays.asList( + "/favicon.ico", "/doc.html", "/swagger-ui.html", + "/swagger-resources", "/swagger-resources/", "/swagger-resources/configuration/ui", + "/swagger-resources/configuration/security", "/webjars/", "/v2/api-docs", + "/v3/api-docs", "/v3/api-docs/swagger-config", + "/api/account/login", "/api/account/preLogin", + "/api/designer/check" + ); + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + String requestURI = request.getRequestURI(); + + if (isIgnoredPath(requestURI)) { + filterChain.doFilter(request, response); + return; + } + + String jwtToken = request.getHeader(securityProperties.getJwtTokenHeader()); + if (StrUtil.isBlank(jwtToken)) { + writeUnauthorized(response, "请传入token!"); + return; + } + + try { + // 1. JWT 签名验证 + if (!jwtHelper.validateToken(jwtToken)) { + writeUnauthorized(response, "Token无效!"); + return; + } + + // 2. 解析用户信息 + AuthPrincipalVo principal = jwtHelper.parserToUser(jwtToken); + if (principal == null || principal.getId() == null) { + writeUnauthorized(response, "Token解析失败!"); + return; + } + + // 3. 本地缓存比对 + String cacheToken = SellerLocalCacheUtils.getTokenCache(principal.getId()); + if (StrUtil.isNotBlank(cacheToken)) { + // 本地缓存有,直接比对 + if (!cacheToken.equals(jwtToken)) { + writeUnauthorized(response, "Token已被顶替,请重新登录!"); + return; + } + } else { + // 本地缓存没有,查 Redis + String redisToken = redisUtil.getLoginToken(principal.getId()); + if (!StrUtil.isNotBlank(redisToken)) { + // Redis 也没有,说明真的失效了 + writeUnauthorized(response, "Token已过期,请重新登录!"); + return; + } + if (!redisToken.equals(jwtToken)) { + writeUnauthorized(response, "Token已被顶替,请重新登录!"); + return; + } + // Redis 有, 回填到本地缓存,减少后续 Redis 访问 + SellerLocalCacheUtils.setTokenCache(principal.getId(), jwtToken); + } + + // 4. 设置用户上下文 + UserContext.delete(); + UserContext.setUserHolder(principal); + filterChain.doFilter(request, response); + + } catch (Exception e) { + log.error("JWT验证失败: {}", e.getMessage()); + writeUnauthorized(response, "Token已过期,请重新登录!"); + } + } + + private boolean isIgnoredPath(String requestURI) { + // 检查配置文件中的白名单 + if (securityProperties.getIgnorePaths() != null) { + for (String path : securityProperties.getIgnorePaths()) { + String pattern = path.replace("/**", "").replace("*", ""); + if (requestURI.contains(pattern)) { + return true; + } + } + } + // 检查硬编码的白名单 + for (String url : FILTER_URL) { + if (requestURI.contains(url)) { + return true; + } + } + return false; + } + + private void writeUnauthorized(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write("{\"code\":401,\"message\":\"" + message + "\"}"); + } +} diff --git a/src/main/java/com/aida/seller/common/security/jwt/SellerJwtHelper.java b/src/main/java/com/aida/seller/common/security/jwt/SellerJwtHelper.java new file mode 100644 index 0000000..30254ba --- /dev/null +++ b/src/main/java/com/aida/seller/common/security/jwt/SellerJwtHelper.java @@ -0,0 +1,67 @@ +package com.aida.seller.common.security.jwt; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.aida.seller.common.security.config.SecurityProperties; +import com.aida.seller.model.vo.AuthPrincipalVo; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +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; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SellerJwtHelper { + + private final SecurityProperties securityProperties; + private final ObjectMapper objectMapper; + + private static final String ISSUER = "DWJ"; + + public boolean validateToken(String token) { + try { + Claims claims = parser(token); + return claims != null && StrUtil.isNotEmpty(claims.getSubject()); + } catch (Exception e) { + log.error("JWT签名验证失败: {}", e.getMessage()); + return false; + } + } + + public AuthPrincipalVo parserToUser(String token) { + try { + String subject = parser(token).getSubject(); + if (StrUtil.isNotEmpty(subject)) { + return objectMapper.readValue(subject, AuthPrincipalVo.class); + } + } catch (Exception e) { + log.error("JWT解析用户信息失败: {}", e.getMessage()); + } + return null; + } + + public Claims parser(String token) { + token = token.replaceAll(securityProperties.getJwtTokenPrefix(), ""); + SecretKey key = buildSigningKey(); + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private SecretKey buildSigningKey() { + byte[] raw = securityProperties.getJwtSecret().getBytes(StandardCharsets.UTF_8); + if (raw.length < 32) { + raw = DigestUtil.sha256(raw); + } + return Keys.hmacShaKeyFor(raw); + } +} diff --git a/src/main/java/com/aida/seller/common/security/utils/SellerLocalCacheUtils.java b/src/main/java/com/aida/seller/common/security/utils/SellerLocalCacheUtils.java new file mode 100644 index 0000000..786767b --- /dev/null +++ b/src/main/java/com/aida/seller/common/security/utils/SellerLocalCacheUtils.java @@ -0,0 +1,57 @@ +package com.aida.seller.common.security.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class SellerLocalCacheUtils { + + private static final ConcurrentHashMap TOKEN_CACHE = new ConcurrentHashMap<>(); + + private static final long EXPIRE_HOURS = 24 * 7 - 1; + + private static class CacheEntry { + private final String token; + private final long expireTime; + + CacheEntry(String token, long expireTime) { + this.token = token; + this.expireTime = expireTime; + } + + String getToken() { + return token; + } + + boolean isExpired() { + return System.currentTimeMillis() > expireTime; + } + } + + public static void setTokenCache(Long userId, String token) { + long expireTime = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(EXPIRE_HOURS); + TOKEN_CACHE.put(userId, new CacheEntry(token, expireTime)); + } + + public static String getTokenCache(Long userId) { + CacheEntry entry = TOKEN_CACHE.get(userId); + if (entry == null) { + return null; + } + if (entry.isExpired()) { + TOKEN_CACHE.remove(userId); + return null; + } + return entry.getToken(); + } + + public static void delTokenCache(Long userId) { + TOKEN_CACHE.remove(userId); + } + + public static void clearAll() { + TOKEN_CACHE.clear(); + } +} diff --git a/src/main/java/com/aida/seller/common/security/utils/SellerRedisUtil.java b/src/main/java/com/aida/seller/common/security/utils/SellerRedisUtil.java new file mode 100644 index 0000000..e153285 --- /dev/null +++ b/src/main/java/com/aida/seller/common/security/utils/SellerRedisUtil.java @@ -0,0 +1,40 @@ +package com.aida.seller.common.security.utils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SellerRedisUtil { + + private final RedisTemplate redisTemplate; + + public static final String LOGIN_TOKEN_KEY = "login:token:"; + + public String getLoginToken(Long userId) { + try { + Object value = redisTemplate.opsForValue().get(LOGIN_TOKEN_KEY + userId); + return value != null ? value.toString() : null; + } catch (Exception e) { + log.error("Redis getLoginToken error, userId: {}, error: {}", userId, e.getMessage()); + return null; + } + } + + public void setLoginToken(Long userId, String token, long expireMillis) { + try { + long expireSeconds = expireMillis / 1000; + if (expireSeconds <= 0) { + expireSeconds = 1; + } + redisTemplate.opsForValue().set(LOGIN_TOKEN_KEY + userId, token, expireSeconds, TimeUnit.SECONDS); + } catch (Exception e) { + log.error("Redis setLoginToken error, userId: {}, error: {}", userId, e.getMessage()); + } + } +} diff --git a/src/main/java/com/aida/seller/config/FilterConfig.java b/src/main/java/com/aida/seller/config/FilterConfig.java new file mode 100644 index 0000000..86177d6 --- /dev/null +++ b/src/main/java/com/aida/seller/config/FilterConfig.java @@ -0,0 +1,20 @@ +package com.aida.seller.config; + +import com.aida.seller.common.security.filter.SellerAuthenticationFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean authFilterRegistration( + SellerAuthenticationFilter sellerAuthenticationFilter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(sellerAuthenticationFilter); + registration.addUrlPatterns("/*"); + registration.setOrder(1); + return registration; + } +} diff --git a/src/main/java/com/aida/seller/config/JwtConfig.java b/src/main/java/com/aida/seller/config/JwtConfig.java new file mode 100644 index 0000000..9daf83c --- /dev/null +++ b/src/main/java/com/aida/seller/config/JwtConfig.java @@ -0,0 +1,14 @@ +package com.aida.seller.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "jwt") +public class JwtConfig { + + private String secret; + private Long expiration; +} diff --git a/src/main/java/com/aida/seller/config/MinioConfig.java b/src/main/java/com/aida/seller/config/MinioConfig.java new file mode 100644 index 0000000..6d45636 --- /dev/null +++ b/src/main/java/com/aida/seller/config/MinioConfig.java @@ -0,0 +1,26 @@ +package com.aida.seller.config; + +import io.minio.MinioClient; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@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 MinioClient.builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + } +} diff --git a/src/main/java/com/aida/seller/config/MyBatisPlusConfig.java b/src/main/java/com/aida/seller/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..2900f1b --- /dev/null +++ b/src/main/java/com/aida/seller/config/MyBatisPlusConfig.java @@ -0,0 +1,18 @@ +package com.aida.seller.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MyBatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } +} diff --git a/src/main/java/com/aida/seller/config/SwaggerConfig.java b/src/main/java/com/aida/seller/config/SwaggerConfig.java new file mode 100644 index 0000000..8092ce8 --- /dev/null +++ b/src/main/java/com/aida/seller/config/SwaggerConfig.java @@ -0,0 +1,25 @@ +package com.aida.seller.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") + .email("support@aida.com"))); + } +} diff --git a/src/main/java/com/aida/seller/config/WebConfig.java b/src/main/java/com/aida/seller/config/WebConfig.java new file mode 100644 index 0000000..71b685a --- /dev/null +++ b/src/main/java/com/aida/seller/config/WebConfig.java @@ -0,0 +1,25 @@ +package com.aida.seller.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class WebConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/src/main/java/com/aida/seller/model/vo/AuthPrincipalVo.java b/src/main/java/com/aida/seller/model/vo/AuthPrincipalVo.java new file mode 100644 index 0000000..aaf4340 --- /dev/null +++ b/src/main/java/com/aida/seller/model/vo/AuthPrincipalVo.java @@ -0,0 +1,21 @@ +package com.aida.seller.model.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@Data +public class AuthPrincipalVo implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + private String username; + private String avatarUrl; + private Boolean isAdmin; + private String source; + private Integer status; + private String language; + private String country; + private List authorities; +} diff --git a/src/main/java/com/aida/seller/module/designer/controller/DesignerController.java b/src/main/java/com/aida/seller/module/designer/controller/DesignerController.java new file mode 100644 index 0000000..77ccf4e --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/controller/DesignerController.java @@ -0,0 +1,72 @@ +package com.aida.seller.module.designer.controller; + +import com.aida.seller.common.result.PageResponse; +import com.aida.seller.common.result.Response; +import com.aida.seller.module.designer.dto.DesignerApplyDTO; +import com.aida.seller.module.designer.dto.DesignerAuditDTO; +import com.aida.seller.module.designer.entity.DesignerEntity; +import com.aida.seller.module.designer.service.DesignerService; +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.*; + +@Tag(name = "设计师入驻管理") +@RestController +@RequestMapping("/designer") +@RequiredArgsConstructor +public class DesignerController { + + private final DesignerService designerService; + + @Operation(summary = "查询设计师是否有售卖资格") + @GetMapping("/check") + public Response check( + @Parameter(description = "用户ID") @RequestParam Long userId) { + boolean hasQualification = designerService.checkQualification(userId); + return Response.success(hasQualification); + } + + @Operation(summary = "提交设计师入驻申请", description = "设计师提交入驻申请,系统自动设置为待审核状态") + @PostMapping("/apply") + public Response apply( + @Parameter(description = "入驻申请表单") @RequestBody DesignerApplyDTO dto) { + designerService.submitApply(dto); + return Response.success(); + } + + @Operation(summary = "分页获取设计师入驻申请列表") + @GetMapping("/apply/page") + public Response> getApplyPage( + @Parameter(description = "页码,默认1") @RequestParam(defaultValue = "1") long page, + @Parameter(description = "每页数量,默认10") @RequestParam(defaultValue = "10") long size, + @Parameter(description = "筛选条件: all-全部, audited-已审核, pending-待审核") @RequestParam(defaultValue = "all") String filter) { + PageResponse pageResponse = PageResponse.success(designerService.getApplyPage(page, size, filter)); + return Response.success(pageResponse); + } + +// @Operation(summary = "获取设计师入驻申请详情") +// @GetMapping("/apply/{id}") +// public Response getApplyDetail( +// @Parameter(description = "申请记录ID") @PathVariable Long id) { +// DesignerApplyDetailVo vo = designerService.getApplyDetail(id); +// return Response.success(vo); +// } + + @Operation(summary = "审核设计师入驻申请", description = "根据用户ID审核设计师入驻申请,审核通过后设计师自动获得售卖资格,审核拒绝时可填写拒绝原因") + @PostMapping("/apply/audit") + public Response audit( + @Parameter(description = "审核表单") @RequestBody DesignerAuditDTO dto) { + designerService.audit(dto); + return Response.success(); + } + + @Operation(summary = "清理用户登录缓存", description = "供 aida-back 登出时远程调用,清除 seller 本地 token 缓存") + @PostMapping("/cache/clear") + public Response clearCache( + @Parameter(description = "用户ID") @RequestParam Long userId) { + com.aida.seller.common.security.utils.SellerLocalCacheUtils.delTokenCache(userId); + return Response.success(); + } +} diff --git a/src/main/java/com/aida/seller/module/designer/dto/DesignerApplyDTO.java b/src/main/java/com/aida/seller/module/designer/dto/DesignerApplyDTO.java new file mode 100644 index 0000000..7a40b88 --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/dto/DesignerApplyDTO.java @@ -0,0 +1,48 @@ +package com.aida.seller.module.designer.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import java.io.Serializable; + +/** + * 设计师入驻申请DTO + */ +@Data +@Schema(description = "设计师入驻申请表单") +public class DesignerApplyDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "店铺名称") + @NotBlank(message = "店铺名称不能为空") + private String shopName; + + @Schema(description = "店铺头像URL") + private String avatar; + + @Schema(description = "品牌Banner URL") + private String brandBanner; + + @Schema(description = "所有者全名") + @NotBlank(message = "所有者全名不能为空") + private String ownerName; + + @Schema(description = "邮箱") + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + private String email; + + @Schema(description = "手机号") + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String mobile; + + @Schema(description = "作品集/社交媒体链接(JSON数组)") + private String socialLinks; +} diff --git a/src/main/java/com/aida/seller/module/designer/dto/DesignerAuditDTO.java b/src/main/java/com/aida/seller/module/designer/dto/DesignerAuditDTO.java new file mode 100644 index 0000000..cbe79fd --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/dto/DesignerAuditDTO.java @@ -0,0 +1,28 @@ +package com.aida.seller.module.designer.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import java.io.Serializable; + +/** + * 设计师入驻审核DTO + */ +@Data +@Schema(description = "设计师入驻审核表单") +public class DesignerAuditDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "用户ID") + @NotNull(message = "用户ID不能为空") + private Long userId; + + @Schema(description = "审核结果: 1-通过, 2-拒绝") + @NotNull(message = "审核结果不能为空") + private Integer auditStatus; + + @Schema(description = "审核备注") + private String auditRemark; +} diff --git a/src/main/java/com/aida/seller/module/designer/entity/DesignerEntity.java b/src/main/java/com/aida/seller/module/designer/entity/DesignerEntity.java new file mode 100644 index 0000000..26acdda --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/entity/DesignerEntity.java @@ -0,0 +1,68 @@ +package com.aida.seller.module.designer.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 设计师表实体类 + */ +@Data +@TableName("designer") +public class DesignerEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 设计师ID */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 用户ID(关联用户表) */ + private Long userId; + + /** 店铺名称 */ + private String shopName; + + /** 店铺头像URL */ + private String avatar; + + /** 品牌Banner URL */ + private String brandBanner; + + /** 所有者全名 */ + private String ownerName; + + /** 邮箱 */ + private String email; + + /** 手机号 */ + private String mobile; + + /** 作品集/社交媒体链接(JSON数组) */ + private String socialLinks; + + /** 申请状态: 0-待审核, 1-审核通过, 2-审核拒绝 */ + private Integer applyStatus; + + /** 审核备注 */ + private String auditRemark; + + /** 审核时间 */ + private LocalDateTime auditTime; + + /** 状态: 0-禁用, 1-启用 */ + private Integer status; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + /** 是否删除: 0-否, 1-是 */ + @TableLogic + private Integer deleted; +} diff --git a/src/main/java/com/aida/seller/module/designer/enums/DesignerApplyStatusEnum.java b/src/main/java/com/aida/seller/module/designer/enums/DesignerApplyStatusEnum.java new file mode 100644 index 0000000..f7f9948 --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/enums/DesignerApplyStatusEnum.java @@ -0,0 +1,27 @@ +package com.aida.seller.module.designer.enums; + +/** + * 设计师申请状态枚举 + */ +public enum DesignerApplyStatusEnum { + + PENDING(0, "待审核"), + APPROVED(1, "审核通过"), + REJECTED(2, "审核拒绝"); + + private final Integer code; + private final String desc; + + DesignerApplyStatusEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + public Integer getCode() { + return code; + } + + public String getDesc() { + return desc; + } +} diff --git a/src/main/java/com/aida/seller/module/designer/mapper/DesignerMapper.java b/src/main/java/com/aida/seller/module/designer/mapper/DesignerMapper.java new file mode 100644 index 0000000..488f91c --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/mapper/DesignerMapper.java @@ -0,0 +1,9 @@ +package com.aida.seller.module.designer.mapper; + +import com.aida.seller.module.designer.entity.DesignerEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DesignerMapper extends BaseMapper { +} diff --git a/src/main/java/com/aida/seller/module/designer/service/DesignerService.java b/src/main/java/com/aida/seller/module/designer/service/DesignerService.java new file mode 100644 index 0000000..76fe20b --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/service/DesignerService.java @@ -0,0 +1,36 @@ +package com.aida.seller.module.designer.service; + +import com.aida.seller.module.designer.dto.DesignerApplyDTO; +import com.aida.seller.module.designer.dto.DesignerAuditDTO; +import com.aida.seller.module.designer.entity.DesignerEntity; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface DesignerService extends IService { + + /** + * 检查设计师是否有售卖资格 + */ + Boolean checkQualification(Long userId); + + /** + * 提交设计师入驻申请 + */ + void submitApply(DesignerApplyDTO dto); + + /** + * 分页查询设计师申请列表 + * @param filter 筛选条件: all-全部, audited-已审核(通过+拒绝), pending-待审核 + */ + IPage getApplyPage(long page, long size, String filter); + + /** + * 获取设计师申请详情 + */ +// DesignerApplyDetailVo getApplyDetail(Long id); + + /** + * 审核设计师入驻申请 + */ + void audit(DesignerAuditDTO dto); +} diff --git a/src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java b/src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java new file mode 100644 index 0000000..5f13be2 --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/service/DesignerServiceImpl.java @@ -0,0 +1,133 @@ +package com.aida.seller.module.designer.service; + +import com.aida.seller.common.exception.BusinessException; +import com.aida.seller.module.designer.dto.DesignerApplyDTO; +import com.aida.seller.module.designer.dto.DesignerAuditDTO; +import com.aida.seller.module.designer.entity.DesignerEntity; +import com.aida.seller.module.designer.enums.DesignerApplyStatusEnum; +import com.aida.seller.module.designer.mapper.DesignerMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class DesignerServiceImpl extends ServiceImpl implements DesignerService { + + @Override + public Boolean checkQualification(Long userId) { + DesignerEntity entity = this.getOne( + new LambdaQueryWrapper() + .eq(DesignerEntity::getUserId, userId) + .last("LIMIT 1") + ); + + if (entity == null) { + return false; + } + + return DesignerApplyStatusEnum.APPROVED.getCode().equals(entity.getApplyStatus()) + && entity.getStatus() != null && entity.getStatus() == 1; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submitApply(DesignerApplyDTO dto) { + DesignerEntity existDesigner = this.getOne( + new LambdaQueryWrapper() + .eq(DesignerEntity::getUserId, dto.getUserId()) + .last("LIMIT 1") + ); + + if (existDesigner != null) { + throw new BusinessException("该用户已提交过申请或已入驻"); + } + + DesignerEntity entity = new DesignerEntity(); + entity.setUserId(dto.getUserId()); + entity.setShopName(dto.getShopName()); + entity.setAvatar(dto.getAvatar()); + entity.setBrandBanner(dto.getBrandBanner()); + entity.setOwnerName(dto.getOwnerName()); + entity.setEmail(dto.getEmail()); + entity.setMobile(dto.getMobile()); + entity.setSocialLinks(dto.getSocialLinks()); + entity.setApplyStatus(DesignerApplyStatusEnum.PENDING.getCode()); + entity.setStatus(0); + + this.save(entity); + } + + @Override + public IPage getApplyPage(long page, long size, String filter) { + Page pageParam = new Page<>(page, size); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + if ("audited".equals(filter)) { + queryWrapper.in(DesignerEntity::getApplyStatus, + DesignerApplyStatusEnum.APPROVED.getCode(), + DesignerApplyStatusEnum.REJECTED.getCode()); + } else if ("pending".equals(filter)) { + queryWrapper.eq(DesignerEntity::getApplyStatus, DesignerApplyStatusEnum.PENDING.getCode()); + } + + queryWrapper.orderByDesc(DesignerEntity::getCreateTime); + return this.page(pageParam, queryWrapper); + } + + // @Override + // public DesignerApplyDetailVo getApplyDetail(Long id) { + // DesignerEntity entity = this.getById(id); + // if (entity == null) { + // throw new BusinessException("申请记录不存在"); + // } + + // DesignerApplyDetailVo vo = new DesignerApplyDetailVo(); + // vo.setId(entity.getId()); + // vo.setShopName(entity.getShopName()); + // vo.setOwnerName(entity.getOwnerName()); + // vo.setEmail(entity.getEmail()); + // vo.setMobile(entity.getMobile()); + // vo.setSocialLinks(entity.getSocialLinks()); + // vo.setApplyStatus(entity.getApplyStatus()); + // vo.setAuditRemark(entity.getAuditRemark()); + // vo.setAuditTime(entity.getAuditTime()); + // vo.setStatus(entity.getStatus()); + // vo.setCreateTime(entity.getCreateTime()); + + // return vo; + // } + + @Override + @Transactional(rollbackFor = Exception.class) + public void audit(DesignerAuditDTO dto) { + DesignerEntity entity = this.getOne( + new LambdaQueryWrapper() + .eq(DesignerEntity::getUserId, dto.getUserId()) + .last("LIMIT 1") + ); + if (entity == null) { + throw new BusinessException("申请记录不存在"); + } + + if (!DesignerApplyStatusEnum.PENDING.getCode().equals(entity.getApplyStatus())) { + throw new BusinessException("当前状态不支持审核操作"); + } + + entity.setApplyStatus(dto.getAuditStatus()); + entity.setAuditRemark(dto.getAuditRemark()); + entity.setAuditTime(LocalDateTime.now()); + + if (DesignerApplyStatusEnum.APPROVED.getCode().equals(dto.getAuditStatus())) { + entity.setStatus(1); + } + + this.updateById(entity); + } +} diff --git a/src/main/java/com/aida/seller/module/designer/vo/DesignerApplyDetailVo.java b/src/main/java/com/aida/seller/module/designer/vo/DesignerApplyDetailVo.java new file mode 100644 index 0000000..fc8ea27 --- /dev/null +++ b/src/main/java/com/aida/seller/module/designer/vo/DesignerApplyDetailVo.java @@ -0,0 +1,55 @@ +package com.aida.seller.module.designer.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 设计师入驻申请详情VO + */ +@Data +@Schema(description = "设计师入驻申请详情") +public class DesignerApplyDetailVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "设计师ID") + private Long id; + + @Schema(description = "店铺名称") + private String shopName; + + @Schema(description = "店铺头像URL") + private String avatar; + + @Schema(description = "品牌Banner URL") + private String brandBanner; + + @Schema(description = "所有者全名") + private String ownerName; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "手机号") + private String mobile; + + @Schema(description = "作品集/社交媒体链接") + private String socialLinks; + + @Schema(description = "申请状态: 0-待审核, 1-审核通过, 2-审核拒绝") + private Integer applyStatus; + + @Schema(description = "审核备注") + private String auditRemark; + + @Schema(description = "审核时间") + private LocalDateTime auditTime; + + @Schema(description = "状态: 0-禁用, 1-启用") + private Integer status; + + @Schema(description = "创建时间") + private LocalDateTime createTime; +} diff --git a/src/main/java/com/aida/seller/module/order/controller/OrderController.java b/src/main/java/com/aida/seller/module/order/controller/OrderController.java new file mode 100644 index 0000000..5d06246 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/controller/OrderController.java @@ -0,0 +1,64 @@ +package com.aida.seller.module.order.controller; + +import com.aida.seller.common.context.UserContext; +import com.aida.seller.common.result.PageResponse; +import com.aida.seller.common.result.Response; +import com.aida.seller.module.order.dto.OrderListDTO; +import com.aida.seller.module.order.service.OrderService; +import com.aida.seller.module.order.vo.OrderSummaryVO; +import com.aida.seller.module.order.vo.OrderVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * My Orders - 订单管理控制器 + */ +@Tag(name = "My Orders - 订单管理") +@RestController +@RequestMapping("/order") +@RequiredArgsConstructor +public class OrderController { + + private final OrderService orderService; + + /** + * 获取订单数据总览 + * + * @return 顶部三个数据卡:总收入、总订单数、总浏览量 + */ + @Operation(summary = "获取订单数据总览", description = "返回顶部三个数据卡:总收入、总订单数、总浏览量") + @GetMapping("/summary") + public Response getSummary() { + Long sellerId = UserContext.getUserId(); + return Response.success(orderService.getSummary(sellerId)); + } + + /** + * 分页获取订单列表 + * + * @param keyword 搜索关键字(商品名或 Order ID) + * @param page 页码,默认 1 + * @param size 每页数量,默认 10 + * @return 订单分页列表,默认按下单时间降序排列 + */ + @Operation(summary = "分页获取订单列表", description = "搜索商品名或 Order ID,默认按 Date 降序排列") + @GetMapping("/page") + public Response> getOrderPage( + @RequestParam(required = false) String keyword, + @RequestParam(defaultValue = "1") long page, + @RequestParam(defaultValue = "10") long size) { + OrderListDTO dto = new OrderListDTO(); + dto.setKeyword(keyword); + dto.setPage(page); + dto.setSize(size); + Long sellerId = UserContext.getUserId(); + IPage orderPage = orderService.getOrderPage(dto, sellerId); + return Response.success(PageResponse.success(orderPage)); + } +} diff --git a/src/main/java/com/aida/seller/module/order/dto/OrderListDTO.java b/src/main/java/com/aida/seller/module/order/dto/OrderListDTO.java new file mode 100644 index 0000000..b26176d --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/dto/OrderListDTO.java @@ -0,0 +1,18 @@ +package com.aida.seller.module.order.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "订单列表查询参数") +public class OrderListDTO { + + @Schema(description = "搜索关键字(商品名或Order ID)") + private String keyword; + + @Schema(description = "页码,默认1") + private long page = 1; + + @Schema(description = "每页数量,默认10") + private long size = 10; +} diff --git a/src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java b/src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java new file mode 100644 index 0000000..5a391eb --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/entity/OrderInfoEntity.java @@ -0,0 +1,48 @@ +package com.aida.seller.module.order.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 订单主表 + */ +@Data +@TableName("order_info") +public class OrderInfoEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 订单唯一标识(如 SP897772698) */ + @TableId(type = IdType.INPUT) + private String orderId; + + /** 卖家ID */ + private Long sellerId; + + /** 订单总金额(HK$) */ + private BigDecimal totalPrice; + + /** 买家账号 */ + private String buyerUsername; + + /** 商品总数量 */ + private Integer totalItems; + + /** 总浏览量 */ + private Long totalViews; + + /** 下单时间 */ + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + /** 是否删除:0-否,1-是 */ + @TableLogic + private Integer deleted; +} diff --git a/src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java b/src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java new file mode 100644 index 0000000..8b582c7 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/entity/OrderItemEntity.java @@ -0,0 +1,48 @@ +package com.aida.seller.module.order.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 订单商品明细表 + */ +@Data +@TableName("order_item") +public class OrderItemEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 订单ID(关联 order_info) */ + private String orderId; + + /** 商品ID */ + private Long productId; + + /** 商品名称 */ + private String productName; + + /** 商品缩略图URL */ + private String thumbnailUrl; + + /** 成交单价(HK$) */ + private BigDecimal price; + + /** 购买数量 */ + private Integer quantity; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 是否删除:0-否,1-是 */ + @TableLogic + private Integer deleted; +} diff --git a/src/main/java/com/aida/seller/module/order/mapper/OrderInfoMapper.java b/src/main/java/com/aida/seller/module/order/mapper/OrderInfoMapper.java new file mode 100644 index 0000000..aaeb5c1 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/mapper/OrderInfoMapper.java @@ -0,0 +1,9 @@ +package com.aida.seller.module.order.mapper; + +import com.aida.seller.module.order.entity.OrderInfoEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OrderInfoMapper extends BaseMapper { +} diff --git a/src/main/java/com/aida/seller/module/order/mapper/OrderItemMapper.java b/src/main/java/com/aida/seller/module/order/mapper/OrderItemMapper.java new file mode 100644 index 0000000..f04c007 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/mapper/OrderItemMapper.java @@ -0,0 +1,9 @@ +package com.aida.seller.module.order.mapper; + +import com.aida.seller.module.order.entity.OrderItemEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OrderItemMapper extends BaseMapper { +} diff --git a/src/main/java/com/aida/seller/module/order/service/OrderService.java b/src/main/java/com/aida/seller/module/order/service/OrderService.java new file mode 100644 index 0000000..d3cca47 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/service/OrderService.java @@ -0,0 +1,29 @@ +package com.aida.seller.module.order.service; + +import com.aida.seller.module.order.dto.OrderListDTO; +import com.aida.seller.module.order.vo.OrderSummaryVO; +import com.aida.seller.module.order.vo.OrderVO; +import com.baomidou.mybatisplus.core.metadata.IPage; + +/** + * 订单服务接口 + */ +public interface OrderService { + + /** + * 获取卖家订单数据总览 + * + * @param sellerId 卖家ID + * @return 包含总收入、总订单数、总浏览量的汇总数据 + */ + OrderSummaryVO getSummary(Long sellerId); + + /** + * 分页查询订单列表 + * + * @param dto 查询参数(搜索关键字、分页信息) + * @param sellerId 卖家ID + * @return 分页后的订单列表,按下单时间降序排列 + */ + IPage getOrderPage(OrderListDTO dto, Long sellerId); +} diff --git a/src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java b/src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java new file mode 100644 index 0000000..98f9c63 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/service/OrderServiceImpl.java @@ -0,0 +1,122 @@ +package com.aida.seller.module.order.service; + +import com.aida.seller.module.order.dto.OrderListDTO; +import com.aida.seller.module.order.entity.OrderInfoEntity; +import com.aida.seller.module.order.entity.OrderItemEntity; +import com.aida.seller.module.order.mapper.OrderInfoMapper; +import com.aida.seller.module.order.mapper.OrderItemMapper; +import com.aida.seller.module.order.vo.OrderSummaryVO; +import com.aida.seller.module.order.vo.OrderVO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 订单服务实现 + */ +@Service +@RequiredArgsConstructor +public class OrderServiceImpl extends ServiceImpl implements OrderService { + + private final OrderItemMapper orderItemMapper; + + /** + * 查询指定卖家的订单汇总数据 + *

遍历该卖家所有订单,累加金额和浏览量

+ */ + @Override + public OrderSummaryVO getSummary(Long sellerId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OrderInfoEntity::getSellerId, sellerId); + + List orders = this.list(wrapper); + + BigDecimal totalRevenue = orders.stream() + .map(OrderInfoEntity::getTotalPrice) + .filter(p -> p != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Integer totalPurchases = orders.size(); + + Long totalViews = orders.stream() + .map(OrderInfoEntity::getTotalViews) + .filter(v -> v != null) + .reduce(0L, Long::sum); + + return new OrderSummaryVO(totalRevenue, totalPurchases, totalViews); + } + + /** + * 分页查询订单列表,支持按商品名或订单号搜索 + *

搜索逻辑:先匹配 order_id 模糊查询,再通过子查询匹配 order_item 中的 product_name

+ *

关联查询:每个订单附带其商品明细列表(用于前端展示缩略图和商品名)

+ */ + @Override + public IPage getOrderPage(OrderListDTO dto, Long sellerId) { + Page pageParam = new Page<>(dto.getPage(), dto.getSize()); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(OrderInfoEntity::getSellerId, sellerId); + + if (StringUtils.hasText(dto.getKeyword())) { + String keyword = dto.getKeyword().trim(); + queryWrapper.and(w -> w + .like(OrderInfoEntity::getOrderId, keyword) + .or() + .inSql(OrderInfoEntity::getOrderId, + "SELECT order_id FROM order_item WHERE product_name LIKE '%" + keyword + "%'") + ); + } + + queryWrapper.orderByDesc(OrderInfoEntity::getCreateTime); + + Page page = this.page(pageParam, queryWrapper); + + List orderIds = page.getRecords().stream() + .map(OrderInfoEntity::getOrderId) + .collect(Collectors.toList()); + + Map> itemsMap = orderIds.isEmpty() + ? Collections.emptyMap() + : orderItemMapper.selectList( + new LambdaQueryWrapper() + .in(OrderItemEntity::getOrderId, orderIds) + ).stream() + .collect(Collectors.groupingBy(OrderItemEntity::getOrderId)); + + List voList = page.getRecords().stream().map(order -> { + OrderVO vo = new OrderVO(); + vo.setOrderId(order.getOrderId()); + vo.setPrice(order.getTotalPrice()); + vo.setBuyerUsername("@" + (order.getBuyerUsername() != null ? order.getBuyerUsername() : "")); + vo.setDate(order.getCreateTime()); + + List items = itemsMap.getOrDefault(order.getOrderId(), new ArrayList<>()); + List itemVOs = items.stream().map(item -> { + OrderVO.ItemVO itemVO = new OrderVO.ItemVO(); + itemVO.setProductId(item.getProductId()); + itemVO.setProductName(item.getProductName()); + itemVO.setThumbnailUrl(item.getThumbnailUrl()); + return itemVO; + }).collect(Collectors.toList()); + vo.setItems(itemVOs); + + return vo; + }).collect(Collectors.toList()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + resultPage.setRecords(voList); + return resultPage; + } +} diff --git a/src/main/java/com/aida/seller/module/order/vo/OrderSummaryVO.java b/src/main/java/com/aida/seller/module/order/vo/OrderSummaryVO.java new file mode 100644 index 0000000..9b3471b --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/vo/OrderSummaryVO.java @@ -0,0 +1,27 @@ +package com.aida.seller.module.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "订单数据总览") +public class OrderSummaryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "总收入(HK$)", example = "12345.00") + private BigDecimal totalRevenue; + + @Schema(description = "总订单数", example = "100") + private Integer totalPurchases; + + @Schema(description = "总浏览量", example = "5000") + private Long totalViews; +} diff --git a/src/main/java/com/aida/seller/module/order/vo/OrderVO.java b/src/main/java/com/aida/seller/module/order/vo/OrderVO.java new file mode 100644 index 0000000..7917fe4 --- /dev/null +++ b/src/main/java/com/aida/seller/module/order/vo/OrderVO.java @@ -0,0 +1,47 @@ +package com.aida.seller.module.order.vo; + +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; + +@Data +@Schema(description = "订单信息") +public class OrderVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "订单唯一标识", example = "SP897772698") + private String orderId; + + @Schema(description = "商品明细列表") + private List items; + + @Schema(description = "成交价格(HK$)", example = "299.00") + private BigDecimal price; + + @Schema(description = "买家账号", example = "@john_doe") + private String buyerUsername; + + @Schema(description = "下单时间", example = "Mar 18, 2026 2:32 PM") + private LocalDateTime date; + + @Data + @Schema(description = "订单商品明细") + public static class ItemVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "商品ID") + private Long productId; + + @Schema(description = "商品名") + private String productName; + + @Schema(description = "商品缩略图URL") + private String thumbnailUrl; + } +} diff --git a/src/main/java/com/aida/seller/util/DesensitizationUtil.java b/src/main/java/com/aida/seller/util/DesensitizationUtil.java new file mode 100644 index 0000000..22b6c20 --- /dev/null +++ b/src/main/java/com/aida/seller/util/DesensitizationUtil.java @@ -0,0 +1,44 @@ +package com.aida.seller.util; + +import cn.hutool.core.util.DesensitizedUtil; +import cn.hutool.core.util.StrUtil; + +public class DesensitizationUtil { + + private DesensitizationUtil() {} + + public static String mobile(String mobile) { + if (StrUtil.isBlank(mobile)) { + return ""; + } + return DesensitizedUtil.mobilePhone(mobile); + } + + public static String email(String email) { + if (StrUtil.isBlank(email)) { + return ""; + } + return DesensitizedUtil.email(email); + } + + public static String idCard(String idCard) { + if (StrUtil.isBlank(idCard)) { + return ""; + } + return DesensitizedUtil.idCardNum(idCard, 4, 4); + } + + public static String bankCard(String bankCard) { + if (StrUtil.isBlank(bankCard)) { + return ""; + } + return DesensitizedUtil.bankCard(bankCard); + } + + public static String chineseName(String name) { + if (StrUtil.isBlank(name)) { + return ""; + } + return DesensitizedUtil.chineseName(name); + } +} diff --git a/src/main/java/com/aida/seller/util/JwtUtil.java b/src/main/java/com/aida/seller/util/JwtUtil.java new file mode 100644 index 0000000..ff5eee4 --- /dev/null +++ b/src/main/java/com/aida/seller/util/JwtUtil.java @@ -0,0 +1,99 @@ +package com.aida.seller.util; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.aida.seller.config.JwtConfig; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class JwtUtil { + + private final JwtConfig jwtConfig; + + private SecretKey getSecretKey() { + return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(Long userId, String username) { + Map claims = new HashMap<>(); + claims.put("userId", userId); + claims.put("username", username); + return createToken(claims, username); + } + + public String generateToken(Long userId, String username, Map additionalClaims) { + Map claims = new HashMap<>(); + claims.put("userId", userId); + claims.put("username", username); + if (additionalClaims != null) { + claims.putAll(additionalClaims); + } + return createToken(claims, username); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .claims(claims) + .subject(subject) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + jwtConfig.getExpiration())) + .signWith(getSecretKey()) + .compact(); + } + + public Claims parseToken(String token) { + return Jwts.parser() + .verifyWith(getSecretKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public String getUsernameFromToken(String token) { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + public Long getUserIdFromToken(String token) { + Claims claims = parseToken(token); + return claims.get("userId", Long.class); + } + + public boolean isTokenExpired(String token) { + try { + Claims claims = parseToken(token); + return claims.getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + return true; + } + } + + public boolean validateToken(String token) { + if (StrUtil.isBlank(token)) { + return false; + } + try { + parseToken(token); + return true; + } catch (JwtException e) { + return false; + } + } + + public String refreshToken(String token) { + Claims claims = parseToken(token); + String username = claims.getSubject(); + Long userId = claims.get("userId", Long.class); + return generateToken(userId, username); + } +} diff --git a/src/main/java/com/aida/seller/util/MinioUtil.java b/src/main/java/com/aida/seller/util/MinioUtil.java new file mode 100644 index 0000000..fd89f1b --- /dev/null +++ b/src/main/java/com/aida/seller/util/MinioUtil.java @@ -0,0 +1,460 @@ +package com.aida.seller.util; + +import com.aida.seller.common.constants.MinioFileConstants; +import com.aida.seller.common.exception.MinioException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.minio.*; +import io.minio.http.Method; +import io.minio.messages.DeleteError; +import io.minio.messages.DeleteObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URL; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MinioUtil { + + @Autowired + private MinioClient minioClient; + + @Autowired + private RedisUtil redisUtil; + + private static final String REDIS_MINIO_URL_PREFIX = "minio:url:"; + private static final long URL_CACHE_EXPIRE_SECONDS = 24 * 60 * 60; + + @Value("${minio.default-bucket}") + private String defaultBucketName; + + @Value("${minio.endpoint}") + private String endpoint; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public String uploadImage(MultipartFile file, String bucketName, String userId) { + try { + if (bucketName == null || bucketName.isEmpty()) { + bucketName = defaultBucketName; + } + + if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } + + String originalFilename = file.getOriginalFilename(); + String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); + String fileName = UUID.randomUUID().toString() + fileExtension; + String filePath = (userId != null && !userId.isEmpty()) ? userId + "/" + fileName : fileName; + + minioClient.putObject(PutObjectArgs.builder() + .bucket(bucketName) + .object(filePath) + .stream(file.getInputStream(), file.getSize(), -1) + .contentType(file.getContentType()) + .build()); + + log.info("文件上传成功,桶名: {}, 文件路径: {}", bucketName, filePath); + return bucketName + "/" + filePath; + } catch (Exception e) { + log.error("文件上传失败: {}", e.getMessage(), e); + throw new MinioException("文件上传失败", e); + } + } + + public String uploadImage(MultipartFile file, String userId) { + return uploadImage(file, null, userId); + } + + public String uploadImage(MultipartFile file) { + return uploadImage(file, null, null); + } + + public String getImageUrl(String path, int expires) { + if (!path.contains("/")) { + } + int index = path.indexOf("/"); + String bucketName = path.substring(0, index); + String fileName = path.substring(index + 1); + return getImageUrl(bucketName, fileName, expires); + } + + public String getImageUrl(String bucketName, String filePath, int expires) { + String cacheKey = REDIS_MINIO_URL_PREFIX + bucketName + "/" + filePath; + Object cachedUrl = redisUtil.get(cacheKey); + if (cachedUrl != null) { + return cachedUrl.toString(); + } + + try { + String url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(bucketName) + .object(filePath) + .expiry(expires) + .build()); + + redisUtil.setWithExpire(cacheKey, url, URL_CACHE_EXPIRE_SECONDS); + return url; + } catch (Exception e) { + log.error("获取临时访问地址失败: {}", e.getMessage(), e); + throw new MinioException("获取临时访问地址失败", e); + } + } + + public String getImageUrl(String bucketName, String filePath) { + return getImageUrl(bucketName, filePath, 7 * 24 * 60 * 60); + } + + public void deleteImage(String objectPath) { + try { + int index = objectPath.indexOf("/"); + if (index == -1) { + throw new MinioException("无效的对象路径,格式应为 bucketName/filePath"); + } + String bucketName = objectPath.substring(0, index); + String filePath = objectPath.substring(index + 1); + + minioClient.removeObject(RemoveObjectArgs.builder() + .bucket(bucketName) + .object(filePath) + .build()); + + log.info("文件删除成功,桶名: {}, 文件路径: {}", bucketName, filePath); + } catch (Exception e) { + log.error("文件删除失败: {}", e.getMessage(), e); + throw new MinioException("文件删除失败", e); + } + } + + public void deleteImages(List objectPaths) { + if (objectPaths == null || objectPaths.isEmpty()) { + return; + } + + try { + String firstPath = objectPaths.get(0); + int index = firstPath.indexOf("/"); + if (index == -1) { + throw new MinioException("无效的对象路径,格式应为 bucketName/filePath"); + } + String bucketName = firstPath.substring(0, index); + + List deleteObjects = new ArrayList<>(); + for (String objectPath : objectPaths) { + int sepIndex = objectPath.indexOf("/"); + if (sepIndex != -1) { + String filePath = objectPath.substring(sepIndex + 1); + deleteObjects.add(new DeleteObject(filePath)); + } + } + + Iterable> results = minioClient.removeObjects(RemoveObjectsArgs.builder() + .bucket(bucketName) + .objects(deleteObjects) + .build()); + + for (Result result : results) { + DeleteError error = result.get(); + log.error("文件删除失败,桶名: {}, 文件路径: {}, 错误信息: {}", bucketName, error.objectName(), error.message()); + } + + log.info("批量删除文件成功,桶名: {}, 文件数量: {}", bucketName, objectPaths.size()); + } catch (Exception e) { + log.error("批量删除文件失败: {}", e.getMessage(), e); + throw new MinioException("批量删除文件失败", e); + } + } + + public String uploadBase64Image(String base64Image, String bucketName, String filePath) { + try { + String[] base64Parts = base64Image.split(","); + String imageData = base64Parts[1]; + String contentType = base64Parts[0].split(":")[1].split(";")[0]; + + byte[] imageBytes = java.util.Base64.getDecoder().decode(imageData); + + if (bucketName == null || bucketName.isEmpty()) { + bucketName = defaultBucketName; + } + + if (filePath == null || filePath.isEmpty()) { + String fileExtension = contentType.split("/")[1]; + filePath = UUID.randomUUID().toString() + "." + fileExtension; + } + + return uploadImage(imageBytes, bucketName, filePath, contentType); + } catch (Exception e) { + log.error("base64图片上传失败: {}", e.getMessage(), e); + throw new MinioException("base64图片上传失败", e); + } + } + + public String uploadBase64Image(String base64Image, String bucketName) { + return uploadBase64Image(base64Image, bucketName, null); + } + + public String uploadBase64Image(String base64Image) { + return uploadBase64Image(base64Image, null, null); + } + + private String uploadImage(byte[] bytes, String bucketName, String filePath, String contentType) { + try { + if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } + + minioClient.putObject(PutObjectArgs.builder() + .bucket(bucketName) + .object(filePath) + .stream(new ByteArrayInputStream(bytes), bytes.length, -1) + .contentType(contentType) + .build()); + + return bucketName + "/" + filePath; + } catch (Exception e) { + log.error("文件上传失败: {}", e.getMessage(), e); + throw new MinioException("文件上传失败", e); + } + } + + public String uploadBytes(byte[] bytes, MinioFileConstants.FileType fileType, String contentType, String bucketName) { + String objectName = MinioFileConstants.generateObjectNameByType(fileType); + return uploadBytes(bytes, objectName, contentType, bucketName); + } + + public String uploadBytes(byte[] bytes, String objectName, String contentType, String bucketName) { + if (bytes == null || bytes.length == 0) { + throw new MinioException("文件内容不能为空"); + } + + try { + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectName) + .stream(new ByteArrayInputStream(bytes), bytes.length, -1) + .contentType(contentType) + .build() + ); + + log.info("字节数组上传成功: {}/{}", bucketName, objectName); + return bucketName + "/" + objectName; + } catch (Exception e) { + log.error("字节数组上传失败: {}", e.getMessage(), e); + throw new MinioException("字节数组上传失败", e); + } + } + + public InputStream downloadFile(String logicalPath) { + int index = logicalPath.indexOf("/"); + if (index <= 0) { + throw new MinioException("逻辑路径格式错误,应包含桶名: " + logicalPath); + } + + String bucketName = logicalPath.substring(0, index); + String objectName = logicalPath.substring(index + 1); + + try { + boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + if (!bucketExists) { + throw new MinioException("桶不存在: " + bucketName); + } + } catch (Exception e) { + log.error("验证桶存在性失败: {}", e.getMessage(), e); + throw new MinioException("验证桶存在性失败bucketName:{}", bucketName); + } + + try { + return minioClient.getObject( + GetObjectArgs.builder() + .bucket(bucketName) + .object(objectName) + .build() + ); + } catch (Exception e) { + log.error("文件下载失败: {}", e.getMessage(), e); + throw new MinioException("文件下载失败", e); + } + } + + public String getLogicalPathFromPresignedUrl(String presignedUrl) { + try { + URL url = new URL(presignedUrl); + + String path = url.getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + + int firstSlashIndex = path.indexOf("/"); + if (firstSlashIndex <= 0) { + throw new MinioException("预签名URL路径格式无效,应包含桶名和对象名: " + presignedUrl); + } + + String bucketName = path.substring(0, firstSlashIndex); + String objectName = path.substring(firstSlashIndex + 1); + + return bucketName + "/" + objectName; + } catch (Exception e) { + log.error("预签名URL解析失败: {}", e.getMessage(), e); + throw new MinioException("预签名URL解析失败", e); + } + } + + public boolean isPresignedUrl(String str) { + if (str == null || str.isEmpty()) { + return false; + } + try { + URL url = new URL(str); + String host = url.getHost(); + String endpointHost = endpoint; + if (endpointHost.startsWith("http://")) { + endpointHost = endpointHost.substring(7); + } else if (endpointHost.startsWith("https://")) { + endpointHost = endpointHost.substring(8); + } + if (endpointHost.contains(":")) { + endpointHost = endpointHost.substring(0, endpointHost.indexOf(":")); + } + return host.equals(endpointHost); + } catch (Exception e) { + return false; + } + } + + public boolean isMinioLogicalPath(String str) { + if (str == null || str.isEmpty()) { + return false; + } + if (!(str instanceof String)) { + return false; + } + String trimStr = str.trim(); + if (trimStr.startsWith("http://") || trimStr.startsWith("https://")) { + return false; + } + if (!trimStr.contains("/")) { + return false; + } + if (trimStr.contains(" ") || trimStr.contains("\n") || trimStr.contains("\t")) { + return false; + } + return true; + } + + public boolean isMinioResource(String str) { + return isPresignedUrl(str) || isMinioLogicalPath(str); + } + + public String processJsonPresignedUrls(String jsonString, int expires) { + if (jsonString == null || jsonString.isEmpty()) { + return jsonString; + } + + try { + JsonNode rootNode = objectMapper.readTree(jsonString); + JsonNode processedNode = processNode(rootNode, expires); + return objectMapper.writeValueAsString(processedNode); + } catch (Exception e) { + log.error("处理JSON中的预签名URL失败: {}", e.getMessage(), e); + return jsonString; + } + } + + private JsonNode processNode(JsonNode node, int expires) { + if (node.isObject()) { + ObjectNode objectNode = (ObjectNode) node; + Iterator> fields = objectNode.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + JsonNode value = field.getValue(); + if (value.isTextual()) { + String text = value.asText(); + if (isMinioResource(text)) { + String newUrl = processMinioResource(text, expires); + objectNode.put(field.getKey(), newUrl); + } + } else if (!value.isNull()) { + JsonNode processedValue = processNode(value, expires); + objectNode.set(field.getKey(), processedValue); + } + } + return objectNode; + } else if (node.isArray()) { + ArrayNode arrayNode = (ArrayNode) node; + for (int i = 0; i < arrayNode.size(); i++) { + JsonNode element = arrayNode.get(i); + if (element.isTextual()) { + String text = element.asText(); + if (isMinioResource(text)) { + String newUrl = processMinioResource(text, expires); + arrayNode.set(i, newUrl); + } + } else if (!element.isNull()) { + JsonNode processedElement = processNode(element, expires); + arrayNode.set(i, processedElement); + } + } + return arrayNode; + } else { + return node; + } + } + + public String processMinioResource(String resource, int expires) { + try { + String logicalPath; + if (isPresignedUrl(resource)) { + logicalPath = getLogicalPathFromPresignedUrl(resource); + } else if (isMinioLogicalPath(resource)) { + logicalPath = resource.trim(); + } else { + log.warn("未识别的MinIO资源格式: {}", resource); + return resource; + } + + return getImageUrl(logicalPath, expires); + } catch (Exception e) { + log.error("处理MinIO资源失败: {}, error: {}", resource, e.getMessage(), e); + return resource; + } + } + + public String convertToLogicalPath(String url) { + if (url == null || url.isEmpty()) { + throw new MinioException("URL不能为空"); + } + if (isMinioLogicalPath(url)) { + return url.trim(); + } else if (isPresignedUrl(url)) { + return getLogicalPathFromPresignedUrl(url); + } else { + throw new MinioException("无法识别的MinIO资源格式: " + url + ",请提供有效的预签名URL或逻辑路径"); + } + } + + public void deleteImagesByUrls(Collection urls) { + if (urls == null || urls.isEmpty()) { + return; + } + for (String url : urls) { + String logicalPath = convertToLogicalPath(url); + deleteImage(logicalPath); + } + } +} diff --git a/src/main/java/com/aida/seller/util/RedisUtil.java b/src/main/java/com/aida/seller/util/RedisUtil.java new file mode 100644 index 0000000..277a718 --- /dev/null +++ b/src/main/java/com/aida/seller/util/RedisUtil.java @@ -0,0 +1,203 @@ +package com.aida.seller.util; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisUtil { + + private final RedisTemplate redisTemplate; + + public void set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + } catch (Exception e) { + log.error("Redis set error, key: {}, error: {}", key, e.getMessage()); + } + } + + public void set(String key, Object value, long timeout, TimeUnit unit) { + try { + redisTemplate.opsForValue().set(key, value, timeout, unit); + } catch (Exception e) { + log.error("Redis set with expiry error, key: {}, error: {}", key, e.getMessage()); + } + } + + public void setWithExpire(String key, Object value, long seconds) { + set(key, value, seconds, TimeUnit.SECONDS); + } + + public Object get(String key) { + try { + return redisTemplate.opsForValue().get(key); + } catch (Exception e) { + log.error("Redis get error, key: {}, error: {}", key, e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + public T get(String key, Class type) { + try { + Object value = redisTemplate.opsForValue().get(key); + if (value != null && type.isInstance(value)) { + return (T) value; + } + return null; + } catch (Exception e) { + log.error("Redis get with type error, key: {}, error: {}", key, e.getMessage()); + return null; + } + } + + public Long increment(String key) { + try { + return redisTemplate.opsForValue().increment(key); + } catch (Exception e) { + log.error("Redis increment error, key: {}, error: {}", key, e.getMessage()); + return null; + } + } + + public Long increment(String key, long delta) { + try { + return redisTemplate.opsForValue().increment(key, delta); + } catch (Exception e) { + log.error("Redis increment error, key: {}, delta: {}, error: {}", key, delta, e.getMessage()); + return null; + } + } + + public Long decrement(String key) { + try { + return redisTemplate.opsForValue().decrement(key); + } catch (Exception e) { + log.error("Redis decrement error, key: {}, error: {}", key, e.getMessage()); + return null; + } + } + + public Long decrement(String key, long delta) { + try { + return redisTemplate.opsForValue().decrement(key, delta); + } catch (Exception e) { + log.error("Redis decrement error, key: {}, delta: {}, error: {}", key, delta, e.getMessage()); + return null; + } + } + + public Boolean delete(String key) { + try { + return redisTemplate.delete(key); + } catch (Exception e) { + log.error("Redis delete error, key: {}, error: {}", key, e.getMessage()); + return false; + } + } + + public Long delete(Collection keys) { + try { + return redisTemplate.delete(keys); + } catch (Exception e) { + log.error("Redis batch delete error, error: {}", e.getMessage()); + return 0L; + } + } + + public void hSet(String key, String hashKey, Object value) { + try { + redisTemplate.opsForHash().put(key, hashKey, value); + } catch (Exception e) { + log.error("Redis hSet error, key: {}, hashKey: {}, error: {}", key, hashKey, e.getMessage()); + } + } + + public Object hGet(String key, String hashKey) { + try { + return redisTemplate.opsForHash().get(key, hashKey); + } catch (Exception e) { + log.error("Redis hGet error, key: {}, hashKey: {}, error: {}", key, hashKey, e.getMessage()); + return null; + } + } + + public Object hGetAll(String key) { + try { + return redisTemplate.opsForHash().entries(key); + } catch (Exception e) { + log.error("Redis hGetAll error, key: {}, error: {}", key, e.getMessage()); + return null; + } + } + + public Long hDelete(String key, Object... hashKeys) { + try { + return redisTemplate.opsForHash().delete(key, hashKeys); + } catch (Exception e) { + log.error("Redis hDelete error, key: {}, error: {}", key, e.getMessage()); + return 0L; + } + } + + public Long hIncrement(String key, String hashKey, long delta) { + try { + return redisTemplate.opsForHash().increment(key, hashKey, delta); + } catch (Exception e) { + log.error("Redis hIncrement error, key: {}, hashKey: {}, error: {}", key, hashKey, e.getMessage()); + return null; + } + } + + public Boolean hasKey(String key) { + try { + return redisTemplate.hasKey(key); + } catch (Exception e) { + log.error("Redis hasKey error, key: {}, error: {}", key, e.getMessage()); + return false; + } + } + + public Boolean expire(String key, long timeout, TimeUnit unit) { + try { + return redisTemplate.expire(key, timeout, unit); + } catch (Exception e) { + log.error("Redis expire error, key: {}, error: {}", key, e.getMessage()); + return false; + } + } + + public Boolean expire(String key, long seconds) { + return expire(key, seconds, TimeUnit.SECONDS); + } + + public Long getExpire(String key, TimeUnit unit) { + try { + return redisTemplate.getExpire(key, unit); + } catch (Exception e) { + log.error("Redis getExpire error, key: {}, error: {}", key, e.getMessage()); + return null; + } + } + + public Long getExpire(String key) { + return getExpire(key, TimeUnit.SECONDS); + } + + public Set keys(String pattern) { + try { + return redisTemplate.keys(pattern); + } catch (Exception e) { + log.error("Redis keys error, pattern: {}, error: {}", pattern, e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/aida/seller/util/ValidationUtil.java b/src/main/java/com/aida/seller/util/ValidationUtil.java new file mode 100644 index 0000000..0fec9c4 --- /dev/null +++ b/src/main/java/com/aida/seller/util/ValidationUtil.java @@ -0,0 +1,43 @@ +package com.aida.seller.util; + +import cn.hutool.core.util.StrUtil; + +import java.util.regex.Pattern; + +public class ValidationUtil { + + private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); + private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); + private static final Pattern ID_CARD_PATTERN = Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$"); + private static final Pattern URL_PATTERN = Pattern.compile("^(https?|ftp)://[^\\s/$.?#].[^\\s]*$"); + + private ValidationUtil() {} + + public static boolean isMobile(String mobile) { + if (StrUtil.isBlank(mobile)) { + return false; + } + return MOBILE_PATTERN.matcher(mobile).matches(); + } + + public static boolean isEmail(String email) { + if (StrUtil.isBlank(email)) { + return false; + } + return EMAIL_PATTERN.matcher(email).matches(); + } + + public static boolean isIdCard(String idCard) { + if (StrUtil.isBlank(idCard)) { + return false; + } + return ID_CARD_PATTERN.matcher(idCard).matches(); + } + + public static boolean isUrl(String url) { + if (StrUtil.isBlank(url)) { + return false; + } + return URL_PATTERN.matcher(url).matches(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..9f8d5ef --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,82 @@ +server: + port: 5568 + servlet: + context-path: /api + +spring: + application: + name: aida-seller + profiles: + active: dev + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:aida_seller}?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: ${DB_USER:root} + password: ${DB_PASSWORD:root} + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 10MB + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + database: 0 + security: + jwt-secret: ${BACK_JWT_SECRET:JWTSECRET} + jwt-token-header: Authorization + jwt-token-prefix: Bearer- + jwt-expiration: ${BACK_JWT_EXPIRATION:8640000000} + ignore-paths: + - /favicon.ico + - /doc.html + - /swagger-ui.html + - /swagger-ui/** + - /swagger-resources/** + - /v2/api-docs + - /v3/api-docs/** + - /webjars/** + - /api/account/login + - /api/account/preLogin + - /api/designer/check + - /api/global-award/contestants/export + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + type-aliases-package: com.aida.seller.module.*.entity + global-config: + db-config: + id-type: auto + 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 配置 +minio: + endpoint: https://www.minio-api.aida.com.hk + access-key: admin + secret-key: Aidlab123123! + default-bucket: aida-user + +# JWT 配置 +jwt: + secret: ${JWT_SECRET:YourSuperSecretKeyForJWTTokenGenerationMustBeAtLeast256Bits} + expiration: ${JWT_EXPIRATION:86400000} + +# Knife4j 配置 +knife4j: + enable: true + setting: + language: zh_cn + +# 日志配置 +logging: + level: + com.aida: debug + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n" diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..7a42ffa --- /dev/null +++ b/src/main/resources/bootstrap.yml @@ -0,0 +1,10 @@ +spring: + application: + name: aida-seller + config: + import: "optional:nacos:${spring.application.name}.yml" + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 + namespace: dev diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql new file mode 100644 index 0000000..6615d3d --- /dev/null +++ b/src/main/resources/db/schema.sql @@ -0,0 +1,87 @@ + + +CREATE DATABASE IF NOT EXISTS aida_seller DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +USE aida_seller; + +-- ==================== 1. 设计师表 ==================== +DROP TABLE IF EXISTS `designer`; +CREATE TABLE `designer` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '设计师ID', + `user_id` BIGINT DEFAULT NULL COMMENT '用户ID(关联用户表)', + `shop_name` VARCHAR(100) NOT NULL COMMENT '店铺名称', + `owner_name` VARCHAR(50) NOT NULL COMMENT '所有者全名', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `mobile` VARCHAR(20) NOT NULL COMMENT '手机号', + `social_links` TEXT DEFAULT NULL COMMENT '作品集/社交媒体链接(JSON数组)', + `apply_status` TINYINT NOT NULL DEFAULT 0 COMMENT '申请状态: 0-待审核, 1-审核通过, 2-审核拒绝', + `audit_remark` VARCHAR(500) DEFAULT NULL COMMENT '审核备注', + `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-禁用, 1-启用', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是', + PRIMARY KEY (`id`), + KEY `idx_mobile` (`mobile`), + KEY `idx_email` (`email`), + KEY `idx_apply_status` (`apply_status`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设计师表'; + +-- ==================== 2. 订单表 ==================== +-- 注意: 代码中 OrderInfoEntity 使用 @TableName("order_info"), +-- 若生产库表名为 "orders" 请改为 "order_info",列名 "order_no" 建议改为 "order_id" +DROP TABLE IF EXISTS `orders`; +CREATE TABLE `orders` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `order_id` VARCHAR(32) NOT NULL COMMENT '订单号', + `seller_id` BIGINT NOT NULL COMMENT '商家ID', + `buyer_id` BIGINT NOT NULL COMMENT '买家ID', + `buyer_name` VARCHAR(100) DEFAULT NULL COMMENT '买家名称', + `total_price` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品总金额', + `order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态: 0-待支付, 1-已支付, 2-已发货, 3-已完成, 4-已取消, 5-退款中', + `total_views` BIGINT NOT NULL DEFAULT 0 COMMENT '商品浏览量(订单关联商品的总浏览数)', + `shipping_address` TEXT DEFAULT NULL COMMENT '收货地址', + `receiver_name` VARCHAR(50) DEFAULT NULL COMMENT '收货人', + `receiver_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + `receiver_address` VARCHAR(500) DEFAULT NULL COMMENT '详细地址', + `tracking_number` VARCHAR(100) DEFAULT NULL COMMENT '快递单号', + `tracking_company` VARCHAR(100) DEFAULT NULL COMMENT '快递公司', + `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间', + `ship_time` DATETIME DEFAULT NULL COMMENT '发货时间', + `receive_time` DATETIME DEFAULT NULL COMMENT '收货时间', + `cancel_time` DATETIME DEFAULT NULL COMMENT '取消时间', + `cancel_reason` VARCHAR(500) DEFAULT NULL COMMENT '取消原因', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_order_id` (`order_id`), + KEY `idx_seller_id` (`seller_id`), + KEY `idx_buyer_id` (`buyer_id`), + KEY `idx_order_status` (`order_status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; + +-- ==================== 3. 订单项表 ==================== +DROP TABLE IF EXISTS `order_item`; +CREATE TABLE `order_item` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单项ID', + `order_id` BIGINT NOT NULL COMMENT '订单ID(关联orders.id)', + `order_no` VARCHAR(32) NOT NULL COMMENT '订单号', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `sku_id` BIGINT DEFAULT NULL COMMENT 'SKU ID', + `product_name` VARCHAR(200) NOT NULL COMMENT '商品名称', + `sku_name` VARCHAR(200) DEFAULT NULL COMMENT 'SKU名称', + `product_image` VARCHAR(500) DEFAULT NULL COMMENT '商品图片', + `price` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品单价', + `quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量', + `total_amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '小计金额', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是', + PRIMARY KEY (`id`), + KEY `idx_order_id` (`order_id`), + KEY `idx_order_no` (`order_no`), + KEY `idx_product_id` (`product_id`), + KEY `idx_sku_id` (`sku_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表';