chore(assets): 参赛提交规范红线修复(ASCII 化 + 相对路径)

按《参赛成果物提交规范·赛道一》§6 红线:
- samples/ 目录改名 sample/(git mv,保留历史)
- 10 个中日文样本文件 + docs 参赛手册 PDF 重命名为 ASCII
  (requirements_*/template_*/rules_*/contestant-handbook.pdf)
- tests/test_zh_template.py 硬编码绝对路径 D:\00_project\Genesis 改为相对路径
- 全局更新 21 个活动文件引用;历史日志/审查文档不改(追加说明记录)
全量 pytest 431 passed / 99.15%
This commit is contained in:
lhl
2026-08-26 14:15:52 +08:00
parent 1925ef7239
commit becd3e1f57
398 changed files with 286 additions and 72 deletions
+103
View File
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>1.0.0</version>
<name>trade-common</name>
<description>trade-common</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Swagger Annotations -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.20</version>
</dependency>
<!-- JJWT (Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- SpringDoc OpenAPI UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<!-- Spring Security Core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.7.11</version>
</dependency>
<!-- Spring Security Config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.7.11</version>
</dependency>
<!-- Spring Security Web -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.7.11</version>
</dependency>
<!-- PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,49 @@
package com.stock.common.dal.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础 DO 类,所有模块的 DO 类都应继承此类
*
* @author TraeAI
*/
@Data
public abstract class BaseDO implements Serializable {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建者,例如:userId
* TODO: 考虑从上下文中自动获取创建者
*/
@TableField(fill = FieldFill.INSERT)
private String creator;
/**
* 更新者,例如:userId
* TODO: 考虑从上下文中自动获取更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updater;
/**
* 是否删除
*/
private Boolean deleted;
}
@@ -0,0 +1,46 @@
package com.stock.common.pojo;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import java.io.Serializable;
/**
* 通用返回结果
*
* @param <T> 数据泛型
*/
@Data
@Validated
public class CommonResult<T> implements Serializable {
/**
* 错误码
*/
private Integer code;
/**
* 返回数据
*/
private T data;
/**
* 错误提示
*/
private String msg;
public static <T> CommonResult<T> success(T data) {
CommonResult<T> result = new CommonResult<>();
result.setCode(200); // 默认成功码为200
result.setData(data);
result.setMsg("成功");
return result;
}
public static <T> CommonResult<T> error(Integer code, String message) {
CommonResult<T> result = new CommonResult<>();
result.setCode(code);
result.setMsg(message);
return result;
}
// 可根据需要添加更多静态方法,例如处理特定错误码等
}
@@ -0,0 +1,102 @@
package com.stock.trading.common.api;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 通用API返回对象
* @param <T>
*/
@Data
@NoArgsConstructor
public class CommonResult<T> {
private long code;
private String message;
private T data;
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) {
return new CommonResult<T>(errorCode.getCode(), message, null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
}
@@ -0,0 +1,10 @@
package com.stock.trading.common.api;
/**
* 封装API的错误码
*/
public interface IErrorCode {
long getCode();
String getMessage();
}
@@ -0,0 +1,33 @@
package com.stock.trading.common.api;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分页数据封装类
*/
@Data
@NoArgsConstructor
public class PageResult<T> {
private Long pageNum;
private Long pageSize;
private Long totalPage;
private Long total;
private List<T> list;
/**
* 将MyBatisPageHelper分页后的list转为分页信息
*/
public static <T> PageResult<T> restPage(List<T> list) {
PageResult<T> result = new PageResult<T>();
//PageInfo<T> pageInfo = new PageInfo<T>(list);
//result.setTotalPage(pageInfo.getPages());
//result.setPageNum(pageInfo.getPageNum());
//result.setPageSize(pageInfo.getPageSize());
//result.setTotal(pageInfo.getTotal());
//result.setList(pageInfo.getList());
return result;
}
}
@@ -0,0 +1,27 @@
package com.stock.trading.common.api;
/**
* 常用API返回对象状态码
*/
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(404, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限");
private long code;
private String message;
private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
public long getCode() {
return code;
}
public String getMessage() {
return message;
}
}
@@ -0,0 +1,29 @@
package com.stock.trading.common.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 基础实体类,包含创建时间和更新时间
*/
@Data
public abstract class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
@@ -0,0 +1,31 @@
package com.stock.trading.common.exception;
import com.stock.trading.common.api.IErrorCode;
/**
* 自定义API异常
*/
public class ApiException extends RuntimeException {
private IErrorCode errorCode;
public ApiException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ApiException(String message) {
super(message);
}
public ApiException(Throwable cause) {
super(cause);
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
public IErrorCode getErrorCode() {
return errorCode;
}
}
@@ -0,0 +1,25 @@
package com.stock.trading.common.exception;
import com.stock.trading.common.api.CommonResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 全局异常处理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义异常
*/
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public CommonResult<String> handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
@@ -0,0 +1,34 @@
package com.stock.trading.common.util;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 日期工具类
*/
public class DateUtil {
/**
* 格式化日期时间
*/
public static String formatDateTime(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
/**
* 格式化日期
*/
public static String formatDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
/**
* 格式化时间
*/
public static String formatTime(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(date);
}
}
@@ -0,0 +1,143 @@
package com.stock.trading.common.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {
* "alg": "HS512",
* "typ": "JWT"
* }
* payload的格式(用户名、创建时间、过期时间):
* {
* "sub": "wang",
* "created": 1489079981398,
* "exp": 1489689981
* }
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败: {}", token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 判断token是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
@@ -0,0 +1,49 @@
package com.trade.common.api;
import com.github.pagehelper.PageInfo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 分页数据封装类
* @author Trade Team
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class CommonPage<T> {
private Integer pageNum;
private Integer pageSize;
private Integer totalPage;
private Long total;
private List<T> list;
/**
* 将PageHelper分页后的list转为分页信息
*/
public static <T> CommonPage<T> restPage(List<T> list) {
CommonPage<T> result = new CommonPage<>();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> pageInfo = page;
result.setTotalPage((int)pageInfo.getPages());
result.setPageNum((int)pageInfo.getCurrent());
result.setPageSize((int)pageInfo.getSize());
result.setTotal(pageInfo.getTotal());
result.setList(pageInfo.getRecords());
return result;
}
/**
* 将MyBatis Plus分页后的Page转为分页信息
*/
public static <T> CommonPage<T> restPage(com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> page) {
CommonPage<T> result = new CommonPage<>();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> pageInfo = page;
result.setTotalPage((int)pageInfo.getPages());
result.setPageNum((int)pageInfo.getCurrent());
result.setPageSize((int)pageInfo.getSize());
result.setTotal(pageInfo.getTotal());
result.setList(pageInfo.getRecords());
return result;
}
}
@@ -0,0 +1,105 @@
package com.trade.common.api;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通用返回对象
* @author Trade Team
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class CommonResult<T> {
private long code;
private String message;
private T data;
protected CommonResult() {
}
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) {
return new CommonResult<T>(errorCode.getCode(), message, null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
}
@@ -0,0 +1,11 @@
package com.trade.common.api;
/**
* 封装API的错误码
* @author Trade Team
*/
public interface IErrorCode {
long getCode();
String getMessage();
}
@@ -0,0 +1,30 @@
package com.trade.common.api;
/**
* 枚举常用API操作码
* @author Trade Team
*/
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(404, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限");
private long code;
private String message;
private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
@Override
public long getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
@@ -0,0 +1,46 @@
package com.trade.common.config;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
/**
* Swagger基础配置
* @author Trade Team
*/
public abstract class BaseSwaggerConfig {
/**
* 自定义Swagger配置
*/
public abstract SwaggerProperties swaggerProperties();
@Bean
public GroupedOpenApi publicApi() {
SwaggerProperties swaggerProperties = swaggerProperties();
return GroupedOpenApi.builder()
.group(swaggerProperties.getGroup())
.pathsToMatch(swaggerProperties.getApiBasePackage())
.build();
}
@Bean
public OpenAPI springShopOpenAPI() {
SwaggerProperties swaggerProperties = swaggerProperties();
return new OpenAPI()
.info(new Info().title(swaggerProperties.getTitle())
.description(swaggerProperties.getDescription())
.version(swaggerProperties.getVersion())
.license(new License().name(swaggerProperties.getLicense()).url(swaggerProperties.getLicenseUrl())))
.externalDocs(new ExternalDocumentation()
.description(swaggerProperties.getDocDescription())
.url(swaggerProperties.getDocUrl()))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("BearerAuth",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")));
}
}
@@ -0,0 +1,29 @@
package com.trade.common.config;
import com.trade.common.exception.ApiException;
import com.trade.common.response.CommonResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 全局异常处理
* @author Trade Team
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义API异常
* @param e API异常
* @return 统一结果封装
*/
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public CommonResult handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
@@ -0,0 +1,13 @@
package com.trade.common.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis配置类
* @author Trade Team
*/
@Configuration
@MapperScan({"com.trade.common.mapper"})
public class MyBatisConfig {
}
@@ -0,0 +1,58 @@
package com.trade.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* <p>MyBatis Plus 配置类</p>
*
* @author creator
* @since 2024-01-01
*/
@Configuration
@EnableTransactionManagement // 开启事务管理
@MapperScan("com.trade.*.mapper") // 扫描 Mapper 接口,根据实际模块调整或在各模块单独配置
public class MyBatisPlusConfig {
/**
* 配置 MyBatis Plus 拦截器
*
* @return MybatisPlusInterceptor 实例
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 添加分页插件
// DbType 用于指定数据库类型,MySQL为例
// optimizeJoin 是否优化left join连接查询,默认为false,建议保持false以保证结果正确性
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInnerInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInnerInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// 2. 添加乐观锁插件 (如果需要)
// 当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新
// 需要在实体类的字段上加上 @Version 注解
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 3. 防止全表更新与删除插件 (如果需要, 注意在生产环境谨慎使用,因为它会阻止没有 WHERE 条件的 UPDATE 和 DELETE 操作)
// interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
// 如果使用了 MyBatis Plus 的逻辑删除功能,可以在这里配置全局的逻辑删除字段等
// 例如,在 application.yml/properties 中配置:
// mybatis-plus.global-config.db-config.logic-delete-field=deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
// mybatis-plus.global-config.db-config.logic-delete-value=1 # 逻辑已删除值(默认为 1)
// mybatis-plus.global-config.db-config.logic-not-delete-value=0 # 逻辑未删除值(默认为 0)
}
@@ -0,0 +1,46 @@
package com.trade.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
* @author Trade Team
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(om);
// String序列化配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
redisTemplate.setValueSerializer(serializer);
// hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@@ -0,0 +1,38 @@
package com.trade.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Spring Security配置
* @author Trade Team
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login", "/register", "/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest()
.authenticated();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@@ -0,0 +1,101 @@
package com.trade.common.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* <p>SpringDoc OpenAPI 配置类</p>
* <p>用于生成API接口文档</p>
*
* @author creator
* @since 2024-01-01
*/
@Configuration
public class SpringDocConfig {
@Value("${spring.application.name:default-service}")
private String applicationName;
/**
* 配置 OpenAPI 基本信息
*
* @return OpenAPI 实例
*/
@Bean
public OpenAPI customOpenAPI(
@Value("${springdoc.version:1.0.0}") String appVersion,
@Value("${springdoc.title:API Documentation}") String title,
@Value("${springdoc.description:API documentation for the service}") String description,
@Value("${springdoc.contact.name:API Support}") String contactName,
@Value("${springdoc.contact.email:[email protected]}") String contactEmail,
@Value("${springdoc.contact.url:https://example.com}") String contactUrl,
@Value("${springdoc.license.name:Apache 2.0}") String licenseName,
@Value("${springdoc.license.url:https://www.apache.org/licenses/LICENSE-2.0.html}") String licenseUrl) {
// 优化:将 applicationName 用于 title 和 description (如果它们是默认值)
String effectiveTitle = title.equals("API Documentation") ? applicationName + " API Documentation" : title;
String effectiveDescription = description.equals("API documentation for the service") ?
"API documentation for the " + applicationName + " service." : description;
return new OpenAPI()
.info(new Info()
.title(effectiveTitle)
.version(appVersion)
.description(effectiveDescription)
.contact(new Contact()
.name(contactName)
.email(contactEmail)
.url(contactUrl))
.license(new License()
.name(licenseName)
.url(licenseUrl)));
}
/**
* 创建一个默认的 API 分组
* <p>
* 可以根据需要创建多个 GroupedOpenApi Bean 来对 API 进行分组展示。
* 例如,可以按模块、按版本或按访问权限进行分组。
* </p>
*
* @return GroupedOpenApi 实例
*/
@Bean
public GroupedOpenApi defaultApiGroup() {
// 优化:使用 applicationName 作为 group 名称,使其更具辨识度
String groupName = applicationName.replace("-service", "") + "-apis";
return GroupedOpenApi.builder()
.group(groupName) // API 分组的名称
.packagesToScan("com.trade."+ applicationName.replace("trade-","") + ".controller") // 指定扫描的包路径,需要根据模块名动态调整
// .pathsToMatch("/api/**") // 可以通过路径匹配来包含特定的API
// .displayName(applicationName + " APIs") // 分组的显示名称
.build();
}
// 示例:为特定模块创建API分组 (如果需要)
// @Bean
// public GroupedOpenApi userApiGroup() {
// return GroupedOpenApi.builder()
// .group("user-management")
// .packagesToScan("com.trade.user.controller") // 假设用户模块的controller在此包下
// .pathsToMatch("/user/**")
// .displayName("User Management APIs")
// .build();
// }
// @Bean
// public GroupedOpenApi productApiGroup() {
// return GroupedOpenApi.builder()
// .group("product-catalog")
// .packagesToScan("com.trade.product.controller") // 假设产品模块的controller在此包下
// .pathsToMatch("/product/**")
// .displayName("Product Catalog APIs")
// .build();
// }
}
@@ -0,0 +1,55 @@
package com.trade.common.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Swagger属性配置
* @author Trade Team
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperties {
/**
* API文档生成基础路径
*/
private String apiBasePackage;
/**
* 是否开启Swagger
*/
private boolean enable;
/**
* 页面标题
*/
private String title;
/**
* 页面描述
*/
private String description;
/**
* 版本号
*/
private String version;
/**
* 许可证名称
*/
private String license;
/**
* 许可证URL
*/
private String licenseUrl;
/**
* 外部文档地址
*/
private String docUrl;
/**
* 外部文档描述
*/
private String docDescription;
/**
* 分组名称
*/
private String group;
}
@@ -0,0 +1,119 @@
package com.trade.common.constant;
/**
* <p>通用常量类</p>
*
* @author creator
* @since 2024-01-01
*/
public final class CommonConstant {
private CommonConstant() {
// 私有构造函数,防止实例化
}
// --------------------------- 符号常量 ---------------------------
public static final String EMPTY_STRING = "";
public static final String SPACE = " ";
public static final String COMMA = ",";
public static final String PERIOD = ".";
public static final String COLON = ":";
public static final String SEMICOLON = ";";
public static final String HYPHEN = "-";
public static final String UNDERSCORE = "_";
public static final String SLASH = "/";
public static final String BACKSLASH = "\\";
public static final String PIPE = "|";
// --------------------------- HTTP 相关常量 ---------------------------
/**
* HTTP 请求头:认证 (Authorization)
*/
public static final String HTTP_HEADER_AUTHORIZATION = "Authorization";
/**
* HTTP 请求头:Bearer Token 前缀
*/
public static final String HTTP_HEADER_BEARER_PREFIX = "Bearer ";
/**
* HTTP 请求头:内容类型 (Content-Type)
*/
public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type";
/**
* HTTP 内容类型:JSON
*/
public static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8";
/**
* HTTP 内容类型:表单
*/
public static final String CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8";
/**
* HTTP 内容类型:文件上传
*/
public static final String CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
// --------------------------- 编码常量 ---------------------------
public static final String CHARSET_UTF8 = "UTF-8";
public static final String CHARSET_GBK = "GBK";
// --------------------------- 日期时间格式常量 ---------------------------
public static final String DATETIME_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss";
public static final String DATE_FORMAT_DEFAULT = "yyyy-MM-dd";
public static final String TIME_FORMAT_DEFAULT = "HH:mm:ss";
public static final String DATETIME_FORMAT_NO_SEPARATOR = "yyyyMMddHHmmss";
public static final String DATE_FORMAT_NO_SEPARATOR = "yyyyMMdd";
// --------------------------- 缓存相关常量 ---------------------------
/**
* 缓存键分隔符
*/
public static final String CACHE_KEY_SEPARATOR = "::";
/**
* 用户信息缓存键前缀
*/
public static final String CACHE_USER_PREFIX = "user";
/**
* Token 缓存键前缀
*/
public static final String CACHE_TOKEN_PREFIX = "token";
// --------------------------- 逻辑删除状态 ---------------------------
/**
* 逻辑未删除
*/
public static final Integer LOGIC_NOT_DELETED = 0;
/**
* 逻辑已删除
*/
public static final Integer LOGIC_DELETED = 1;
// --------------------------- 通用状态 (启用/禁用) ---------------------------
/**
* 状态:启用
*/
public static final Integer STATUS_ENABLED = 1;
/**
* 状态:禁用
*/
public static final Integer STATUS_DISABLED = 0;
// --------------------------- 默认值常量 ---------------------------
/**
* 默认分页大小
*/
public static final int DEFAULT_PAGE_SIZE = 10;
/**
* 默认当前页码
*/
public static final int DEFAULT_PAGE_NUM = 1;
// --------------------------- 其他常量 ---------------------------
/**
* 默认的超级管理员用户ID (示例)
*/
public static final Long SUPER_ADMIN_ID = 1L;
/**
* 默认的超级管理员角色代码 (示例)
*/
public static final String ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN";
}
@@ -0,0 +1,71 @@
package com.trade.common.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 登录用户信息
* @author Trade Team
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserDto implements UserDetails {
private Long id;
private String username;
private String password;
private Integer status;
private List<String> permissionList;
public UserDto(Long id, String username, String password, Integer status, List<String> permissionList) {
this.id = id;
this.username = username;
this.password = password;
this.status = status;
this.permissionList = permissionList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissionList.stream()
.filter(permission -> permission!=null)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return status == 1;
}
}
@@ -0,0 +1,32 @@
package com.trade.common.exception;
/**
* 自定义API异常
* @author Trade Team
*/
import com.trade.common.api.IErrorCode;
public class ApiException extends RuntimeException {
private IErrorCode errorCode;
public ApiException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ApiException(String message) {
super(message);
}
public ApiException(Throwable cause) {
super(cause);
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
public IErrorCode getErrorCode() {
return errorCode;
}
}
@@ -0,0 +1,17 @@
package com.trade.common.exception;
import com.trade.common.api.IErrorCode;
/**
* 断言处理类,用于抛出各种API异常
* @author Trade Team
*/
public class Asserts {
public static void fail(String message) {
throw new ApiException(message);
}
public static void fail(IErrorCode errorCode) {
throw new ApiException(errorCode);
}
}
@@ -0,0 +1,97 @@
package com.trade.common.exception;
import com.trade.common.vo.ResultCodeEnum;
import lombok.Getter;
/**
* <p>自定义业务异常类</p>
*
* @author creator
* @since 2024-01-01
*/
@Getter
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private final int code;
/**
* 构造器,使用自定义消息
*
* @param message 异常消息
*/
public BusinessException(String message) {
super(message);
this.code = ResultCodeEnum.OPERATION_FAILED.getCode(); // 默认为操作失败
}
/**
* 构造器,使用自定义错误码和消息
*
* @param code 错误码
* @param message 异常消息
*/
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
/**
* 构造器,使用 ResultCodeEnum 定义的错误
*
* @param resultCodeEnum 结果代码枚举
*/
public BusinessException(ResultCodeEnum resultCodeEnum) {
super(resultCodeEnum.getMessage());
this.code = resultCodeEnum.getCode();
}
/**
* 构造器,使用 ResultCodeEnum 定义的错误,并附加额外信息
*
* @param resultCodeEnum 结果代码枚举
* @param detailMessage 详细错误信息,将追加到枚举定义的消息后
*/
public BusinessException(ResultCodeEnum resultCodeEnum, String detailMessage) {
super(resultCodeEnum.getMessage() + ": " + detailMessage);
this.code = resultCodeEnum.getCode();
}
/**
* 构造器,包装另一个异常
*
* @param message 异常消息
* @param cause 原始异常
*/
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(); // 默认为内部服务器错误
}
/**
* 构造器,使用自定义错误码、消息并包装另一个异常
*
* @param code 错误码
* @param message 异常消息
* @param cause 原始异常
*/
public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
/**
* 构造器,使用 ResultCodeEnum 并包装另一个异常
*
* @param resultCodeEnum 结果代码枚举
* @param cause 原始异常
*/
public BusinessException(ResultCodeEnum resultCodeEnum, Throwable cause) {
super(resultCodeEnum.getMessage(), cause);
this.code = resultCodeEnum.getCode();
}
}
@@ -0,0 +1,10 @@
package com.trade.common.exception;
/**
* 封装API的错误码
* @author Trade Team
*/
public interface ErrorCode {
long getCode();
String getMessage();
}
@@ -0,0 +1,205 @@
package com.trade.common.exception;
import com.trade.common.vo.ResultCodeEnum;
import com.trade.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* <p>全局异常处理器</p>
* <p>捕获controller层抛出的异常,并返回统一的JSON格式响应</p>
*
* @author creator
* @since 2024-01-01
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常 BusinessException
*
* @param e BusinessException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK) // 通常业务异常返回200,通过code和message区分
public Result<?> handleBusinessException(BusinessException e) {
log.error("业务异常: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常 (Query Param / Path Variable)
*
* @param e ConstraintViolationException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
String message = violations.stream()
.map(violation -> String.format("%s: %s", getFieldName(violation), violation.getMessage()))
.collect(Collectors.joining("; "));
log.warn("参数校验失败 (ConstraintViolationException): {}", message);
return Result.error(ResultCodeEnum.BAD_REQUEST, message);
}
private String getFieldName(ConstraintViolation<?> violation) {
String propertyPath = violation.getPropertyPath().toString();
// propertyPath 通常是 "methodName.argName.fieldName" 或 "fieldName"
// 我们尝试获取最后一个点之后的部分作为字段名
int lastDotIndex = propertyPath.lastIndexOf('.');
return (lastDotIndex == -1) ? propertyPath : propertyPath.substring(lastDotIndex + 1);
}
/**
* 处理参数校验异常 (RequestBody @Valid)
*
* @param e MethodArgumentNotValidException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String message = fieldErrors.stream()
.map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
.collect(Collectors.joining("; "));
log.warn("参数校验失败 (MethodArgumentNotValidException): {}", message);
return Result.error(ResultCodeEnum.BAD_REQUEST, message);
}
/**
* 处理参数绑定异常 (form-data/x-www-form-urlencoded @Valid)
*
* @param e BindException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleBindException(BindException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String message = fieldErrors.stream()
.map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
.collect(Collectors.joining("; "));
log.warn("参数绑定失败 (BindException): {}", message);
return Result.error(ResultCodeEnum.BAD_REQUEST, message);
}
/**
* 处理缺少请求参数异常
*
* @param e MissingServletRequestParameterException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
String message = String.format("缺少必要的请求参数: %s (类型: %s)", e.getParameterName(), e.getParameterType());
log.warn(message);
return Result.error(ResultCodeEnum.BAD_REQUEST, message);
}
/**
* 处理参数类型不匹配异常
*
* @param e MethodArgumentTypeMismatchException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
String message = String.format("参数类型不匹配: 参数 '%s' 需要类型 '%s', 但提供了值 '%s'",
e.getName(), e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "N/A", e.getValue());
log.warn(message, e);
return Result.error(ResultCodeEnum.BAD_REQUEST, message);
}
/**
* 处理HTTP请求方法不支持异常
*
* @param e HttpRequestMethodNotSupportedException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Result<?> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
String message = String.format("不支持的请求方法: %s. 支持的方法有: %s", e.getMethod(), e.getSupportedHttpMethods());
log.warn(message);
return Result.error(ResultCodeEnum.FORBIDDEN, message);
}
/**
* 处理404 NoHandlerFoundException
* 需要在 application.properties 中配置 spring.mvc.throw-exception-if-no-handler-found=true
* 和 spring.web.resources.add-mappings=false (如果使用了静态资源映射)
*
* @param e NoHandlerFoundException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<?> handleNoHandlerFoundException(NoHandlerFoundException e) {
String message = String.format("接口 [%s %s] 不存在", e.getHttpMethod(), e.getRequestURL());
log.warn(message, e);
return Result.error(ResultCodeEnum.NOT_FOUND, message);
}
/**
* 处理请求体不可读异常
*
* @param e HttpMessageNotReadableException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.warn("请求体不可读或JSON格式错误: {}", e.getMessage());
return Result.error(ResultCodeEnum.BAD_REQUEST, "请求体不可读或JSON格式错误");
}
/**
* 处理其他所有未捕获的运行时异常
*
* @param e RuntimeException 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleRuntimeException(RuntimeException e) {
log.error("服务器发生运行时异常: {}", e.getMessage(), e);
return Result.error(ResultCodeEnum.INTERNAL_SERVER_ERROR, "服务器发生运行时异常,请联系管理员");
}
/**
* 处理其他所有未捕获的顶级异常 Exception
*
* @param e Exception 实例
* @return Result 封装的错误响应
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleException(Exception e) {
log.error("服务器发生未知异常: {}", e.getMessage(), e);
return Result.error(ResultCodeEnum.INTERNAL_SERVER_ERROR, "服务器发生未知异常,请联系管理员");
}
}
@@ -0,0 +1,126 @@
package com.trade.common.response;
import com.trade.common.api.IErrorCode;
/**
* 通用返回对象
* @author Trade Team
*/
public class CommonResult<T> {
private long code;
private String message;
private T data;
protected CommonResult() {
}
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) {
return new CommonResult<T>(errorCode.getCode(), message, null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
public long getCode() {
return code;
}
public void setCode(long code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
@@ -0,0 +1,34 @@
package com.trade.common.response;
import com.trade.common.api.IErrorCode;
import com.trade.common.exception.ErrorCode;
/**
* 枚举常用API操作码
* @author Trade Team
*/
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(404, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限");
private long code;
private String message;
private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
@Override
public long getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
@@ -0,0 +1,132 @@
package com.trade.common.service;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* redis操作Service,
* 对象和数组都以json形式进行存储
* @author Trade Team
*/
public interface RedisService {
/**
* 存储数据
*/
void set(String key, Object value);
/**
* 存储数据并设置过期时间
*/
void set(String key, Object value, long expire);
/**
* 获取数据
*/
Object get(String key);
/**
* 设置过期时间
*/
boolean expire(String key, long expire);
/**
* 删除数据
*/
void del(String key);
/**
* 批量删除数据
*/
void del(List<String> keys);
/**
* 批量删除key
*/
Long delByPrefix(String prefix);
/**
* 对某个key的value进行递增操作
*/
Long increment(String key, Long delta);
/**
* 对某个key的value进行递减操作
*/
Long decrement(String key, Long delta);
/**
* 获取所有key
*/
Set<String> keys(String pattern);
/**
* 判断key是否存在
*/
Boolean hasKey(String key);
/**
* 将数据放入redis的list中
*/
Long lPush(String key, Object value);
/**
* 将数据放入redis的list中并设置过期时间
*/
Long lPush(String key, Object value, long expire);
/**
* 从redis的list中获取数据
*/
Object lPop(String key);
/**
* 将数据放入redis的set中
*/
Long sAdd(String key, Object... values);
/**
* 将数据放入redis的set中并设置过期时间
*/
Long sAdd(String key, long expire, Object... values);
/**
* 从redis的set中获取数据
*/
Set<Object> sMembers(String key);
/**
* 将数据放入redis的hash中
*/
void hPut(String key, String hashKey, Object value);
/**
* 将数据放入redis的hash中并设置过期时间
*/
void hPut(String key, String hashKey, Object value, long expire);
/**
* 从redis的hash中获取数据
*/
Object hGet(String key, String hashKey);
/**
* 将map放入redis的hash中
*/
void hPutAll(String key, Map<String, Object> map);
/**
* 将map放入redis的hash中并设置过期时间
*/
void hPutAll(String key, Map<String, Object> map, long expire);
/**
* 从redis的hash中获取所有数据
*/
Map<String, Object> hGetAll(String key);
/**
* 删除redis的hash中的数据
*/
void hDel(String key, Object... hashKeys);
}
@@ -0,0 +1,151 @@
package com.trade.common.service.impl;
import com.trade.common.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* redis操作Service的实现类
* @author Trade Team
*/
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public void set(String key, Object value, long expire) {
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public boolean expire(String key, long expire) {
return redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
@Override
public void del(String key) {
redisTemplate.delete(key);
}
@Override
public void del(List<String> keys) {
redisTemplate.delete(keys);
}
@Override
public Long delByPrefix(String prefix) {
Set<String> keys = redisTemplate.keys(prefix + "*");
if (keys != null && !keys.isEmpty()) {
return redisTemplate.delete(keys);
}
return 0L;
}
@Override
public Long increment(String key, Long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
@Override
public Long decrement(String key, Long delta) {
return redisTemplate.opsForValue().decrement(key, delta);
}
@Override
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
@Override
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
@Override
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().leftPush(key, value);
}
@Override
public Long lPush(String key, Object value, long expire) {
Long result = redisTemplate.opsForList().leftPush(key, value);
expire(key, expire);
return result;
}
@Override
public Object lPop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
@Override
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
@Override
public Long sAdd(String key, long expire, Object... values) {
Long result = redisTemplate.opsForSet().add(key, values);
expire(key, expire);
return result;
}
@Override
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
@Override
public void hPut(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
@Override
public void hPut(String key, String hashKey, Object value, long expire) {
redisTemplate.opsForHash().put(key, hashKey, value);
expire(key, expire);
}
@Override
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
@Override
public void hPutAll(String key, Map<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
}
@Override
public void hPutAll(String key, Map<String, Object> map, long expire) {
redisTemplate.opsForHash().putAll(key, map);
expire(key, expire);
}
@Override
public Map<String, Object> hGetAll(String key) {
return (Map<String, Object>) (Map) redisTemplate.opsForHash().entries(key);
}
@Override
public void hDel(String key, Object... hashKeys) {
redisTemplate.opsForHash().delete(key, hashKeys);
}
}
@@ -0,0 +1,48 @@
package com.trade.common.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 日期工具类
* @author Trade Team
*/
public class DateUtil {
/**
* 将LocalDateTime格式化为指定字符串
* @param dateTime LocalDateTime对象
* @param pattern 格式模式,如 "yyyy-MM-dd HH:mm:ss"
* @return 格式化后的日期字符串
*/
public static String formatLocalDateTime(LocalDateTime dateTime, String pattern) {
if (dateTime == null || pattern == null || pattern.isEmpty()) {
return null;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return dateTime.format(formatter);
}
/**
* 将当前LocalDateTime格式化为默认字符串 "yyyy-MM-dd HH:mm:ss"
* @param dateTime LocalDateTime对象
* @return 格式化后的日期字符串
*/
public static String formatLocalDateTime(LocalDateTime dateTime) {
return formatLocalDateTime(dateTime, "yyyy-MM-dd HH:mm:ss");
}
/**
* 将字符串解析为LocalDateTime对象
* @param dateString 日期字符串
* @param pattern 格式模式,如 "yyyy-MM-dd HH:mm:ss"
* @return 解析后的LocalDateTime对象
*/
public static LocalDateTime parseLocalDateTime(String dateString, String pattern) {
if (dateString == null || dateString.isEmpty() || pattern == null || pattern.isEmpty()) {
return null;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return LocalDateTime.parse(dateString, formatter);
}
}
@@ -0,0 +1,143 @@
package com.trade.common.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {
* "alg": "HS512",
* "typ": "JWT"
* }
* payload的格式(用户名、创建时间、生成时间):
* {
* "sub":"wang",
* "created":1489079981398
* }
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
* @author Trade Team
*/
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败: {}", token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 判断token是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
@@ -0,0 +1,231 @@
package com.trade.common.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* <p>JWT (JSON Web Token) 工具类</p>
* <p>用于生成、解析和验证JWT</p>
*
* @author creator
* @since 2024-01-01
*/
@Slf4j
@Component // 使其可以被Spring管理,方便注入配置
public class JwtUtils {
/**
* JWT 密钥,从配置文件读取,必须足够复杂以保证安全
* 建议长度至少为256位 (32个ASCII字符)
*/
@Value("${jwt.secret:defaultSecretKey_must_be_at_least_32_characters_long_for_HS256}")
private String secret;
/**
* JWT 过期时间(毫秒),从配置文件读取,默认为1小时
*/
@Value("${jwt.expiration:3600000}")
private long expiration;
private SecretKey secretKey;
/**
* 初始化密钥
*/
private SecretKey getSecretKey() {
if (secretKey == null) {
// 优化:确保密钥长度符合HS256要求,如果不足则进行提示或使用默认安全密钥
if (!StringUtils.hasText(secret) || secret.length() < 32) {
log.warn("JWT secret key is not configured or too short (must be at least 32 chars for HS256). Using a default secure key. THIS IS NOT RECOMMENDED FOR PRODUCTION!");
// Keys.secretKeyFor(SignatureAlgorithm.HS256) 会生成一个安全的随机密钥
this.secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
} else {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
}
}
return secretKey;
}
/**
* 生成JWT Token
*
* @param subject 主题,通常是用户ID或其他唯一标识
* @param claims 自定义声明 (payload)
* @return 生成的JWT字符串
*/
public String generateToken(String subject, Map<String, Object> claims) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims) // 设置自定义声明
.setSubject(subject) // 设置主题
.setId(UUID.randomUUID().toString()) // JWT的唯一身份标识
.setIssuedAt(now) // 设置签发时间
.setExpiration(expiryDate) // 设置过期时间
.signWith(getSecretKey(), SignatureAlgorithm.HS256) // 设置签名算法和密钥
.compact();
}
/**
* 生成JWT Token,仅包含 subject
*
* @param subject 主题,通常是用户ID或其他唯一标识
* @return 生成的JWT字符串
*/
public String generateToken(String subject) {
return generateToken(subject, null);
}
/**
* 从JWT Token中获取 Claims (Payload)
*
* @param token JWT字符串
* @return Claims 对象
* @throws ExpiredJwtException 如果JWT已过期
* @throws UnsupportedJwtException 如果JWT格式不受支持
* @throws MalformedJwtException 如果JWT格式错误
* @throws SignatureException 如果签名验证失败
* @throws IllegalArgumentException 如果token为空或无效
*/
private Claims getClaimsFromToken(String token) {
if (!StringUtils.hasText(token)) {
throw new IllegalArgumentException("JWT token cannot be null or empty.");
}
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 从JWT Token中获取主题 (Subject)
*
* @param token JWT字符串
* @return 主题
*/
public String getSubjectFromToken(String token) {
try {
return getClaimsFromToken(token).getSubject();
} catch (Exception e) {
log.debug("Failed to get subject from token: {}", e.getMessage());
return null;
}
}
/**
* 从JWT Token中获取指定的 Claim 值
*
* @param token JWT字符串
* @param claimKey Claim的键
* @param type Claim值的类型
* @param <T> Claim值的泛型
* @return Claim值,如果不存在或类型不匹配则返回null
*/
public <T> T getClaimFromToken(String token, String claimKey, Class<T> type) {
try {
Claims claims = getClaimsFromToken(token);
return claims.get(claimKey, type);
} catch (Exception e) {
log.debug("Failed to get claim '{}' from token: {}", claimKey, e.getMessage());
return null;
}
}
/**
* 验证JWT Token是否有效
*
* @param token JWT字符串
* @return 如果token有效返回true,否则返回false
*/
public boolean validateToken(String token) {
if (!StringUtils.hasText(token)) {
return false;
}
try {
Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature: {}", ex.getMessage());
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token: {}", ex.getMessage());
} catch (ExpiredJwtException ex) {
log.warn("Expired JWT token: {}", ex.getMessage());
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token: {}", ex.getMessage());
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty: {}", ex.getMessage());
}
return false;
}
/**
* 判断JWT Token是否已过期
*
* @param token JWT字符串
* @return 如果已过期返回true,否则返回false。如果token无效也返回true。
*/
public boolean isTokenExpired(String token) {
try {
Date expirationDate = getClaimsFromToken(token).getExpiration();
return expirationDate.before(new Date());
} catch (ExpiredJwtException e) {
return true; // 明确已过期
} catch (Exception e) {
return true; // 其他解析错误,视为无效或已过期
}
}
/**
* 刷新JWT Token的过期时间
* <p>
* 注意:这实际上是重新签发了一个新的Token,包含了原有Token的claims和subject。
* </p>
*
* @param token 旧的JWT字符串
* @return 新的JWT字符串,如果旧token无效则返回null
*/
public String refreshToken(String token) {
if (!StringUtils.hasText(token)) {
return null;
}
try {
Claims claims = getClaimsFromToken(token);
// 检查是否允许刷新 (例如,可以在claims中设置一个特定的刷新标记或时间窗口)
// 此处简单实现为只要未过期即可刷新
if (claims.getExpiration().after(new Date())) {
return generateToken(claims.getSubject(), claims);
}
} catch (ExpiredJwtException ex) {
log.warn("Cannot refresh an already expired JWT token: {}", ex.getMessage());
} catch (Exception e) {
log.error("Error refreshing JWT token: {}", e.getMessage());
}
return null;
}
/**
* 获取JWT的过期时间
*
* @return 过期时间(毫秒)
*/
public long getExpiration() {
return expiration;
}
}
@@ -0,0 +1,77 @@
package com.trade.common.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 请求工具类
* @author Trade Team
*/
public class RequestUtil {
/**
* 获取当前请求的HttpServletRequest对象
* @return HttpServletRequest对象,如果不在请求上下文中则返回null
*/
public static HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return null;
}
return attributes.getRequest();
}
/**
* 获取请求的IP地址
* @return IP地址字符串
*/
public static String getRequestIp() {
HttpServletRequest request = getCurrentRequest();
if (request == null) {
return "";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
/**
* 获取请求的User-Agent
* @return User-Agent字符串
*/
public static String getUserAgent() {
HttpServletRequest request = getCurrentRequest();
if (request == null) {
return "";
}
return request.getHeader("User-Agent");
}
/**
* 获取请求的Referer
* @return Referer字符串
*/
public static String getReferer() {
HttpServletRequest request = getCurrentRequest();
if (request == null) {
return "";
}
return request.getHeader("Referer");
}
}
@@ -0,0 +1,61 @@
package com.trade.common.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Spring工具类,用于获取Spring上下文中的Bean
* @author Trade Team
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
/**
* 获取applicationContext
* @return ApplicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过name获取 Bean.
* @param name Bean的名称
* @return Object
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
* @param clazz Bean的类型
* @param <T> 泛型
* @return T
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
* @param name Bean的名称
* @param clazz Bean的类型
* @param <T> 泛型
* @return T
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
@@ -0,0 +1,50 @@
package com.trade.common.util;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
/**
* 参数校验工具类
* @author Trade Team
*/
public class ValidationUtil {
private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
/**
* 校验对象
* @param obj 待校验对象
* @param <T> 对象类型
* @throws IllegalArgumentException 如果校验失败,则抛出此异常
*/
public static <T> void validate(T obj) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validate(obj);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<T> violation : violations) {
sb.append(violation.getMessage()).append(";");
}
throw new IllegalArgumentException(sb.toString());
}
}
/**
* 校验对象的指定属性
* @param obj 待校验对象
* @param propertyName 待校验属性名
* @param <T> 对象类型
* @throws IllegalArgumentException 如果校验失败,则抛出此异常
*/
public static <T> void validateProperty(T obj, String propertyName) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validateProperty(obj, propertyName);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<T> violation : violations) {
sb.append(violation.getMessage()).append(";");
}
throw new IllegalArgumentException(sb.toString());
}
}
}
@@ -0,0 +1,128 @@
package com.trade.common.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* <p>通用API接口返回结果封装类</p>
*
* @author creator
* @since 2024-01-01
*/
@Data
@Schema(description = "通用API接口返回结果")
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 成功标志
*/
@Schema(description = "成功标志", example = "true")
private boolean success = true;
/**
* 返回处理消息
*/
@Schema(description = "返回处理消息", example = "操作成功!")
private String message = "操作成功!";
/**
* 返回代码
*/
@Schema(description = "返回代码", example = "200")
private Integer code = 200;
/**
* 返回数据对象 data
*/
@Schema(description = "返回数据对象")
private T result;
/**
* 时间戳
*/
@Schema(description = "时间戳", example = "1609459200000")
private long timestamp = System.currentTimeMillis();
public Result() {
}
public Result(boolean success, String message, Integer code) {
this.success = success;
this.message = message;
this.code = code;
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> ok() {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
return r;
}
public static <T> Result<T> ok(String msg) {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(msg);
return r;
}
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
r.setResult(data);
return r;
}
public static <T> Result<T> ok(String msg, T data) {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(msg);
r.setResult(data);
return r;
}
public static <T> Result<T> error(String msg) {
return error(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), msg);
}
public static <T> Result<T> error(Integer code, String msg) {
Result<T> r = new Result<>();
r.setSuccess(false);
r.setCode(code);
r.setMessage(msg);
return r;
}
public static <T> Result<T> error(ResultCodeEnum resultCodeEnum) {
Result<T> r = new Result<>();
r.setSuccess(false);
r.setCode(resultCodeEnum.getCode());
r.setMessage(resultCodeEnum.getMessage());
return r;
}
public static <T> Result<T> error(ResultCodeEnum resultCodeEnum, String msg) {
Result<T> r = new Result<>();
r.setSuccess(false);
r.setCode(resultCodeEnum.getCode());
r.setMessage(msg); // Use custom message
return r;
}
/**
* 无权限访问返回结果
*/
public static <T> Result<T> noauth(String msg) {
return error(ResultCodeEnum.UNAUTHORIZED.getCode(), msg);
}
}
@@ -0,0 +1,30 @@
package com.trade.common.vo;
import lombok.Getter;
/**
* <p>结果代码枚举</p>
*
* @author creator
* @since 2024-01-01
*/
@Getter
public enum ResultCodeEnum {
SUCCESS(200, "操作成功"),
OPERATION_FAILED(5001, "操作失败"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未经授权"),
FORBIDDEN(403, "访问被拒绝"),
NOT_FOUND(404, "资源不存在"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
SERVICE_UNAVAILABLE(503, "服务不可用");
private final Integer code;
private final String message;
ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}