1
This commit is contained in:
19
src/main/java/com/aida/seller/AidaSellerApplication.java
Normal file
19
src/main/java/com/aida/seller/AidaSellerApplication.java
Normal file
@@ -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 启动成功.");
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/aida/seller/common/config/RedisConfig.java
Normal file
29
src/main/java/com/aida/seller/common/config/RedisConfig.java
Normal file
@@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.aida.seller.common.constants;
|
||||
|
||||
public class MinioBucketNameConstants {
|
||||
//默认桶名
|
||||
public static final String USER = "aida-user";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.aida.seller.common.context;
|
||||
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
|
||||
public class UserContext {
|
||||
private static final ThreadLocal<AuthPrincipalVo> 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;
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/aida/seller/common/dto/LoginDTO.java
Normal file
24
src/main/java/com/aida/seller/common/dto/LoginDTO.java
Normal file
@@ -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;
|
||||
}
|
||||
31
src/main/java/com/aida/seller/common/dto/LoginVO.java
Normal file
31
src/main/java/com/aida/seller/common/dto/LoginVO.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<T> extends Response<List<T>> {
|
||||
@Schema(description = "页码")
|
||||
private long page;
|
||||
@Schema(description = "每页数量")
|
||||
private long size;
|
||||
@Schema(description = "总页数")
|
||||
private long pages;
|
||||
@Schema(description = "总条数")
|
||||
private long total;
|
||||
@Schema(description = "结果集")
|
||||
private List<T> content;
|
||||
|
||||
public PageResponse(Response<List<T>> 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 <T> PageResponse<T> success(IPage<T> page) {
|
||||
Response<List<T>> response = success(page.getRecords());
|
||||
return new PageResponse<>(response, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
}
|
||||
98
src/main/java/com/aida/seller/common/result/Response.java
Normal file
98
src/main/java/com/aida/seller/common/result/Response.java
Normal file
@@ -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<T> implements Serializable {
|
||||
|
||||
@Schema(description = "响应状态码 0:成功 -1:失败")
|
||||
private int errCode;
|
||||
@Schema(description = "提示消息")
|
||||
private String errMsg;
|
||||
@Schema(description = "数据结果")
|
||||
private T data;
|
||||
|
||||
public static <T> Response<T> success() {
|
||||
return success(ResultEnum.SUCCESS, null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(T data) {
|
||||
return success(ResultEnum.SUCCESS, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(ResultEnum resultEnum, T data) {
|
||||
return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(int code, T data) {
|
||||
return success(code, ResultEnum.SUCCESS.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(int code, String msg, T data) {
|
||||
return getResponse(code, msg, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(String msg) {
|
||||
return fail(ResultEnum.FAIL.getCode(), msg);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(T data) {
|
||||
return fail(ResultEnum.FAIL, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(ResultEnum resultEnum) {
|
||||
return fail(resultEnum.getCode(), resultEnum.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(ResultEnum resultEnum, T data) {
|
||||
return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(int code, String msg) {
|
||||
return fail(code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(int code, String msg, T data) {
|
||||
return getResponse(code, msg, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(String msg) {
|
||||
return error(ResultEnum.ERROR.getCode(), msg);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(T data) {
|
||||
return error(ResultEnum.ERROR, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(ResultEnum resultEnum) {
|
||||
return error(resultEnum.getCode(), resultEnum.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(ResultEnum resultEnum, T data) {
|
||||
return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(int code, String msg) {
|
||||
return error(code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(int code, String msg, T data) {
|
||||
return getResponse(code, msg, data);
|
||||
}
|
||||
|
||||
private static <T> Response<T> getResponse(int code, String msg, T data) {
|
||||
return new Response<>(code, msg, data);
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/aida/seller/common/result/ResultEnum.java
Normal file
55
src/main/java/com/aida/seller/common/result/ResultEnum.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String> 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 + "\"}");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Long, CacheEntry> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/main/java/com/aida/seller/config/FilterConfig.java
Normal file
20
src/main/java/com/aida/seller/config/FilterConfig.java
Normal file
@@ -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<SellerAuthenticationFilter> authFilterRegistration(
|
||||
SellerAuthenticationFilter sellerAuthenticationFilter) {
|
||||
FilterRegistrationBean<SellerAuthenticationFilter> registration = new FilterRegistrationBean<>();
|
||||
registration.setFilter(sellerAuthenticationFilter);
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setOrder(1);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
14
src/main/java/com/aida/seller/config/JwtConfig.java
Normal file
14
src/main/java/com/aida/seller/config/JwtConfig.java
Normal file
@@ -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;
|
||||
}
|
||||
26
src/main/java/com/aida/seller/config/MinioConfig.java
Normal file
26
src/main/java/com/aida/seller/config/MinioConfig.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/aida/seller/config/MyBatisPlusConfig.java
Normal file
18
src/main/java/com/aida/seller/config/MyBatisPlusConfig.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/aida/seller/config/SwaggerConfig.java
Normal file
25
src/main/java/com/aida/seller/config/SwaggerConfig.java
Normal file
@@ -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")));
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/aida/seller/config/WebConfig.java
Normal file
25
src/main/java/com/aida/seller/config/WebConfig.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/aida/seller/model/vo/AuthPrincipalVo.java
Normal file
21
src/main/java/com/aida/seller/model/vo/AuthPrincipalVo.java
Normal file
@@ -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<String> authorities;
|
||||
}
|
||||
@@ -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<Boolean> check(
|
||||
@Parameter(description = "用户ID") @RequestParam Long userId) {
|
||||
boolean hasQualification = designerService.checkQualification(userId);
|
||||
return Response.success(hasQualification);
|
||||
}
|
||||
|
||||
@Operation(summary = "提交设计师入驻申请", description = "设计师提交入驻申请,系统自动设置为待审核状态")
|
||||
@PostMapping("/apply")
|
||||
public Response<Void> apply(
|
||||
@Parameter(description = "入驻申请表单") @RequestBody DesignerApplyDTO dto) {
|
||||
designerService.submitApply(dto);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "分页获取设计师入驻申请列表")
|
||||
@GetMapping("/apply/page")
|
||||
public Response<PageResponse<DesignerEntity>> 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<DesignerEntity> pageResponse = PageResponse.success(designerService.getApplyPage(page, size, filter));
|
||||
return Response.success(pageResponse);
|
||||
}
|
||||
|
||||
// @Operation(summary = "获取设计师入驻申请详情")
|
||||
// @GetMapping("/apply/{id}")
|
||||
// public Response<DesignerApplyDetailVo> 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<Void> audit(
|
||||
@Parameter(description = "审核表单") @RequestBody DesignerAuditDTO dto) {
|
||||
designerService.audit(dto);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "清理用户登录缓存", description = "供 aida-back 登出时远程调用,清除 seller 本地 token 缓存")
|
||||
@PostMapping("/cache/clear")
|
||||
public Response<Void> clearCache(
|
||||
@Parameter(description = "用户ID") @RequestParam Long userId) {
|
||||
com.aida.seller.common.security.utils.SellerLocalCacheUtils.delTokenCache(userId);
|
||||
return Response.success();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<DesignerEntity> {
|
||||
}
|
||||
@@ -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<DesignerEntity> {
|
||||
|
||||
/**
|
||||
* 检查设计师是否有售卖资格
|
||||
*/
|
||||
Boolean checkQualification(Long userId);
|
||||
|
||||
/**
|
||||
* 提交设计师入驻申请
|
||||
*/
|
||||
void submitApply(DesignerApplyDTO dto);
|
||||
|
||||
/**
|
||||
* 分页查询设计师申请列表
|
||||
* @param filter 筛选条件: all-全部, audited-已审核(通过+拒绝), pending-待审核
|
||||
*/
|
||||
IPage<DesignerEntity> getApplyPage(long page, long size, String filter);
|
||||
|
||||
/**
|
||||
* 获取设计师申请详情
|
||||
*/
|
||||
// DesignerApplyDetailVo getApplyDetail(Long id);
|
||||
|
||||
/**
|
||||
* 审核设计师入驻申请
|
||||
*/
|
||||
void audit(DesignerAuditDTO dto);
|
||||
}
|
||||
@@ -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<DesignerMapper, DesignerEntity> implements DesignerService {
|
||||
|
||||
@Override
|
||||
public Boolean checkQualification(Long userId) {
|
||||
DesignerEntity entity = this.getOne(
|
||||
new LambdaQueryWrapper<DesignerEntity>()
|
||||
.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<DesignerEntity>()
|
||||
.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<DesignerEntity> getApplyPage(long page, long size, String filter) {
|
||||
Page<DesignerEntity> pageParam = new Page<>(page, size);
|
||||
LambdaQueryWrapper<DesignerEntity> 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<DesignerEntity>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<OrderSummaryVO> 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<PageResponse<OrderVO>> 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<OrderVO> orderPage = orderService.getOrderPage(dto, sellerId);
|
||||
return Response.success(PageResponse.success(orderPage));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<OrderInfoEntity> {
|
||||
}
|
||||
@@ -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<OrderItemEntity> {
|
||||
}
|
||||
@@ -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<OrderVO> getOrderPage(OrderListDTO dto, Long sellerId);
|
||||
}
|
||||
@@ -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<OrderInfoMapper, OrderInfoEntity> implements OrderService {
|
||||
|
||||
private final OrderItemMapper orderItemMapper;
|
||||
|
||||
/**
|
||||
* 查询指定卖家的订单汇总数据
|
||||
* <p>遍历该卖家所有订单,累加金额和浏览量</p>
|
||||
*/
|
||||
@Override
|
||||
public OrderSummaryVO getSummary(Long sellerId) {
|
||||
LambdaQueryWrapper<OrderInfoEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(OrderInfoEntity::getSellerId, sellerId);
|
||||
|
||||
List<OrderInfoEntity> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询订单列表,支持按商品名或订单号搜索
|
||||
* <p>搜索逻辑:先匹配 order_id 模糊查询,再通过子查询匹配 order_item 中的 product_name</p>
|
||||
* <p>关联查询:每个订单附带其商品明细列表(用于前端展示缩略图和商品名)</p>
|
||||
*/
|
||||
@Override
|
||||
public IPage<OrderVO> getOrderPage(OrderListDTO dto, Long sellerId) {
|
||||
Page<OrderInfoEntity> pageParam = new Page<>(dto.getPage(), dto.getSize());
|
||||
|
||||
LambdaQueryWrapper<OrderInfoEntity> 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<OrderInfoEntity> page = this.page(pageParam, queryWrapper);
|
||||
|
||||
List<String> orderIds = page.getRecords().stream()
|
||||
.map(OrderInfoEntity::getOrderId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, List<OrderItemEntity>> itemsMap = orderIds.isEmpty()
|
||||
? Collections.emptyMap()
|
||||
: orderItemMapper.selectList(
|
||||
new LambdaQueryWrapper<OrderItemEntity>()
|
||||
.in(OrderItemEntity::getOrderId, orderIds)
|
||||
).stream()
|
||||
.collect(Collectors.groupingBy(OrderItemEntity::getOrderId));
|
||||
|
||||
List<OrderVO> 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<OrderItemEntity> items = itemsMap.getOrDefault(order.getOrderId(), new ArrayList<>());
|
||||
List<OrderVO.ItemVO> 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<OrderVO> resultPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
resultPage.setRecords(voList);
|
||||
return resultPage;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
47
src/main/java/com/aida/seller/module/order/vo/OrderVO.java
Normal file
47
src/main/java/com/aida/seller/module/order/vo/OrderVO.java
Normal file
@@ -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<ItemVO> 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;
|
||||
}
|
||||
}
|
||||
44
src/main/java/com/aida/seller/util/DesensitizationUtil.java
Normal file
44
src/main/java/com/aida/seller/util/DesensitizationUtil.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
99
src/main/java/com/aida/seller/util/JwtUtil.java
Normal file
99
src/main/java/com/aida/seller/util/JwtUtil.java
Normal file
@@ -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<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("username", username);
|
||||
return createToken(claims, username);
|
||||
}
|
||||
|
||||
public String generateToken(Long userId, String username, Map<String, Object> additionalClaims) {
|
||||
Map<String, Object> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
460
src/main/java/com/aida/seller/util/MinioUtil.java
Normal file
460
src/main/java/com/aida/seller/util/MinioUtil.java
Normal file
@@ -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<String> 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<DeleteObject> 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<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.objects(deleteObjects)
|
||||
.build());
|
||||
|
||||
for (Result<DeleteError> 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<Map.Entry<String, JsonNode>> fields = objectNode.fields();
|
||||
while (fields.hasNext()) {
|
||||
Map.Entry<String, JsonNode> 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<String> urls) {
|
||||
if (urls == null || urls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (String url : urls) {
|
||||
String logicalPath = convertToLogicalPath(url);
|
||||
deleteImage(logicalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/main/java/com/aida/seller/util/RedisUtil.java
Normal file
203
src/main/java/com/aida/seller/util/RedisUtil.java
Normal file
@@ -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<String, Object> 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> T get(String key, Class<T> 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<String> 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<String> keys(String pattern) {
|
||||
try {
|
||||
return redisTemplate.keys(pattern);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis keys error, pattern: {}, error: {}", pattern, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/aida/seller/util/ValidationUtil.java
Normal file
43
src/main/java/com/aida/seller/util/ValidationUtil.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
82
src/main/resources/application.yml
Normal file
82
src/main/resources/application.yml
Normal file
@@ -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"
|
||||
10
src/main/resources/bootstrap.yml
Normal file
10
src/main/resources/bootstrap.yml
Normal file
@@ -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
|
||||
87
src/main/resources/db/schema.sql
Normal file
87
src/main/resources/db/schema.sql
Normal file
@@ -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='订单项表';
|
||||
Reference in New Issue
Block a user