feat: 新增heritage-plugins基础插件(第1部分)

- 新增heritage-resultex:统一结果封装和全局异常处理插件
- 新增heritage-desensitize:数据脱敏插件,支持手机号、身份证等
- 新增heritage-dict:字典翻译插件,自动翻译字典值
- 新增plugins父POM配置
This commit is contained in:
Leo 2025-10-08 02:06:46 +08:00
parent 6e36d9ab83
commit 09457ecebd
14 changed files with 1136 additions and 0 deletions

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-desensitize</name>
<artifactId>heritage-desensitize</artifactId>
<description>数据脱敏插件</description>
<dependencies>
<!-- 全局公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- SpringBoot Aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,39 @@
package org.leocoder.heritage.desensitize.anno;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.leocoder.heritage.desensitize.config.DesensitizeJsonSerializer;
import org.leocoder.heritage.desensitize.enums.DesensitizeRuleEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Leocoder
* @description [CoderDesensitize]
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizeJsonSerializer.class)
public @interface CoderDesensitize {
/**
* 脱敏数据类型在MY_RULE的时候startInclude和endExclude生效
*/
DesensitizeRuleEnum rule() default DesensitizeRuleEnum.CODER_RULE;
/**
* 脱敏开始位置[不包含]
*/
int beginExclude() default 0;
/**
* 脱敏结束位置[包含]
*/
int endInclude() default 0;
}

View File

@ -0,0 +1,17 @@
package org.leocoder.heritage.desensitize.anno;
import org.leocoder.heritage.desensitize.config.DesensitizeJsonSerializer;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
// 可用在字段上
@Target({ElementType.TYPE})
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({ DesensitizeJsonSerializer.class })
public @interface EnableCoderDesensitize {
}

View File

@ -0,0 +1,124 @@
package org.leocoder.heritage.desensitize.config;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.DesensitizedUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.desensitize.anno.CoderDesensitize;
import org.leocoder.heritage.desensitize.enums.DesensitizeRuleEnum;
import java.io.IOException;
import java.util.Objects;
/**
* @author Leocoder
* @description [DesensitizeSerialize自定义序列化类]
*/
@AllArgsConstructor
@NoArgsConstructor
public class DesensitizeJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private DesensitizeRuleEnum ruleEnum;
private Integer beginExclude;
private Integer endInclude;
public DesensitizeJsonSerializer(DesensitizeRuleEnum rule, int beginExclude, int endInclude, String[] strings) {
}
@Override
public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// 绑定的需要脱敏的部门ID
try {
boolean isCoderAdmin = CoderLoginUtil.getIsCoderAdmin();
if(isCoderAdmin) {
// 无脱敏部门数据直接写入原始值
jsonGenerator.writeString(str);
return;
}
} catch (Exception ignored) {
}
switch (ruleEnum) {
// 自定义类型脱敏
case CODER_RULE:
jsonGenerator.writeString(CharSequenceUtil.hide(str, beginExclude, endInclude));
break;
// userId脱敏
case USER_ID:
jsonGenerator.writeString(String.valueOf(DesensitizedUtil.userId()));
break;
// 中文姓名脱敏
case CHINESE_NAME:
jsonGenerator.writeString(DesensitizedUtil.chineseName(String.valueOf(str)));
break;
// 身份证脱敏
case ID_CARD:
jsonGenerator.writeString(DesensitizedUtil.idCardNum(String.valueOf(str), 3, 4));
break;
// 固定电话脱敏
case FIXED_PHONE:
jsonGenerator.writeString(DesensitizedUtil.fixedPhone(String.valueOf(str)));
break;
// 手机号脱敏
case MOBILE_PHONE:
jsonGenerator.writeString(DesensitizedUtil.mobilePhone(String.valueOf(str)));
break;
// 地址脱敏
case ADDRESS:
jsonGenerator.writeString(DesensitizedUtil.address(String.valueOf(str), 8));
break;
// 邮箱脱敏
case EMAIL:
jsonGenerator.writeString(DesensitizedUtil.email(String.valueOf(str)));
break;
// 密码脱敏
case PASSWORD:
jsonGenerator.writeString(DesensitizedUtil.password(String.valueOf(str)));
break;
// 中国车牌脱敏
case CAR_LICENSE:
jsonGenerator.writeString(DesensitizedUtil.carLicense(String.valueOf(str)));
break;
// 银行卡脱敏
case BANK_CARD:
jsonGenerator.writeString(DesensitizedUtil.bankCard(String.valueOf(str)));
break;
default:
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
// 判断数据类型是否为String类型
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
// 获取定义的注解
CoderDesensitize desensitize = beanProperty.getAnnotation(CoderDesensitize.class);
// 为null
if (desensitize == null) {
desensitize = beanProperty.getContextAnnotation(CoderDesensitize.class);
}
// 不为null
if (desensitize != null) {
// 创建定义的序列化类的实例并且返回入参为注解定义的type开始位置结束位置
return new DesensitizeJsonSerializer(desensitize.rule(), desensitize.beginExclude(),
desensitize.endInclude());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
}

View File

@ -0,0 +1,67 @@
package org.leocoder.heritage.desensitize.enums;
import lombok.AllArgsConstructor;
/**
* @author Leocoder
* @description [DesensitizeRuleEnum脱敏策略]
*/
@AllArgsConstructor
public enum DesensitizeRuleEnum {
/**
* 自定义规则
*/
CODER_RULE,
/**
* 用户id
*/
USER_ID,
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份证号
*/
ID_CARD,
/**
* 固定电话座机号
*/
FIXED_PHONE,
/**
* 手机号
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 电子邮件
*/
EMAIL,
/**
* 密码
*/
PASSWORD,
/**
* 中国大陆车牌包含普通车辆新能源车辆
*/
CAR_LICENSE,
/**
* 银行卡
*/
BANK_CARD
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-dict</name>
<artifactId>heritage-dict</artifactId>
<description>字典翻译插件</description>
<dependencies>
<!-- 通用公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据模型模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-model</artifactId>
<version>${revision}</version>
</dependency>
<!-- Aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
package org.leocoder.heritage.dict.anno;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.dict.aspect.CoderDictAspect;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({CoderDictAspect.class, RedisUtil.class})
public @interface EnableCoderDict {
}

View File

@ -0,0 +1,212 @@
package org.leocoder.heritage.dict.aspect;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.leocoder.heritage.common.anno.CoderDict;
import org.leocoder.heritage.common.anno.CoderDictClass;
import org.leocoder.heritage.common.constants.CoderCacheConstants;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.domain.pojo.system.SysDictData;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [CoderDictAspect]
*/
@Aspect
@Order(4)
@Component
@Slf4j
public class CoderDictAspect {
private Map<String, String> dictMap = new HashMap<>();
@Resource
private RedisUtil redisUtil;
@Pointcut("@annotation(org.leocoder.heritage.common.anno.CoderDictClass)")
public void logPointCut() {
}
/**
* @description [翻译数据]
* 需要处理的数据类型
* 1分页数据R<IPage<T>>
* 2普通列表R<List<T>>
* 3普通数据R<T>
* @author Leocoder
*/
@Around("@annotation(CoderDictClass)")
public Object CoderTypeDictTranslation(final ProceedingJoinPoint proceedingJoinPoint, CoderDictClass CoderDictClass) throws Throwable {
Object proceed = proceedingJoinPoint.proceed();
if (ObjectUtils.isEmpty(proceed)) {
return proceed;
}
if (proceed instanceof List) {
// 数据字典翻译
List<Object> list = (List<Object>) proceed;
CoderDictTranslate(list);
return list;
} else if (proceed instanceof Page) {
// Page 类型处理逻辑
IPage page = (IPage) proceed;
List records = page.getRecords();
// 数据字典翻译
CoderDictTranslate(records);
page.setRecords(records);
// 返回修改后的分页对象
return page;
} else {
// 其他类型处理逻辑或错误处理
return proceed;
}
}
/**
* @description [获取不同类型返回结果数据进行翻译]
* @author Leocoder
*/
private void CoderDictTranslate(Object resultData) {
Object objectData;
// 检查输入的result是否是List或者ArrayList的实例
if (resultData instanceof List) {
// 如果是列表则获取第一个元素
List CoderList = ((List) resultData);
// 如果列表为空则直接返回原始结果
if (CoderList.isEmpty()) {
return;
}
// 获取集合的第一条数据
objectData = CoderList.get(0);
} else {
// 否则直接使用result对象
objectData = resultData;
}
// 获取数据字典key 实体类字段名称
List<CoderDictModel> dictModelList = getCoderDict(objectData.getClass());
// dictModelList.forEach(System.out::println);
// CoderDictModel(dictKey=sys_user_sex, dictValue=sex)
// 如果没有字典映射则直接返回原始结果
if (dictModelList.isEmpty()) {
return;
}
// 获取所有的字典数据
List<SysDictData> dictDataList = listDictCache(dictModelList);
// 如果字典数据是空则直接返回
if (CollectionUtils.isEmpty(dictDataList)) {
return;
}
// 将字典值转换成map形式
for (SysDictData dictData : dictDataList) {
dictMap.put(dictData.getDictType() + "_" + dictData.getDictValue(), dictData.getDictLabel());
}
// 根据对象类型为返回数据的每一个对象赋予字典值
if (resultData instanceof List) {
for (Object entity : (List) resultData) {
assignDictValue(entity, dictModelList, dictMap);
}
} else {
assignDictValue(resultData, dictModelList, dictMap);
}
}
/**
* @description [获取字典数据缓存]
* @author Leocoder
*/
private List<SysDictData> listDictCache(List<CoderDictModel> dictModelList) {
List<SysDictData> dictDataList = new ArrayList<>();
List<String> dictKeyList = getDictKey(dictModelList);
for (String dictKey : dictKeyList) {
Boolean hasKey = redisUtil.hasKey(CoderCacheConstants.DICT_REDIS_KEY + dictKey);
if(hasKey) {
List<SysDictData> dictDataKeyList = redisUtil.getKey(CoderCacheConstants.DICT_REDIS_KEY + dictKey);
dictDataList.addAll(dictDataKeyList);
}
}
return dictDataList;
}
/**
* @param entity 返回List集合数据循环出来的实体类对象转换字典的核心代码
* @param dictModelList CoderDictModel(dictKey=sys_user_sex, dictValue=sex) 获取实体类的@CoderDict注解值
* @param dictMap 所有字典数据缓存
*/
public void assignDictValue(Object entity, List<CoderDictModel> dictModelList, Map<String, String> dictMap) {
try {
// 遍历每个CoderDictModel对象
for (CoderDictModel dictModel : dictModelList) {
String dictKey = dictModel.getDictKey(); // 获取字典类型
String fieldName = dictModel.getDictValue(); // 获取需要赋值的字段名
Class<?> classData = entity.getClass(); // 获取实体类的类
Field fieldData = classData.getDeclaredField(fieldName); // 获取需要赋值的字段
fieldData.setAccessible(true); // 设置字段可访问性
Object originalValue = fieldData.get(entity); // 获取字段的原始值
if (ObjectUtils.isNotEmpty(originalValue)) {
// 如果原始值不为空则将字典值赋给字段
String dictValue = dictMap.getOrDefault(dictKey + "_" + originalValue, originalValue.toString());
fieldData.set(entity, dictValue);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取实体中所有需要翻译的dictKey返回需要翻译的dictKey列表
*/
private List<String> getDictKey(List<CoderDictModel> CoderDictBos) {
// 创建一个字符串列表用于存储dictKey
List<String> dictKeyList = new ArrayList<>();
// 如果传入的CoderDictBo为空则直接返回空列表
if (CollectionUtils.isEmpty(CoderDictBos)) {
return dictKeyList;
}
for (CoderDictModel dictBo : CoderDictBos) {
// 将每个CoderDictBo对象的key添加到列表中
dictKeyList.add(dictBo.getDictKey());
}
return dictKeyList;
}
/**
* 获取实体类中配置的dictKey 对应的字段名称
*/
private List<CoderDictModel> getCoderDict(Class classData) {
Field[] fields = classData.getDeclaredFields(); // 获取类classData中声明的所有字段
List<CoderDictModel> list = new ArrayList<>(); // 创建一个CoderDictBo列表
CoderDictModel CoderDictBo;
CoderDict CoderDict;
for (Field field : fields) {
if (field.isAnnotationPresent(CoderDict.class)) { // 检查字段是否带有CoderDict注解
CoderDict = field.getAnnotation(CoderDict.class); // 获取CoderDict注解
CoderDictBo = new CoderDictModel(CoderDict.dictKey(), field.getName()); // 创建CoderDictBo对象存储dictKey和字段名
list.add(CoderDictBo);
}
}
// 返回包含配置的dictKey和对应字段内容的列表
return list;
}
}

View File

@ -0,0 +1,21 @@
package org.leocoder.heritage.dict.aspect;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Leocoder
* @description [CoderDictModel-@CoderDict注解字段]
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CoderDictModel {
/** 字典Key值 */
private String dictKey;
/** 实体类字段名称 */
private String dictValue;
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-resultex</name>
<artifactId>heritage-resultex</artifactId>
<description>全局统一返回 和 全局异常类</description>
<dependencies>
<!-- Model模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-model</artifactId>
<version>${revision}</version>
</dependency>
<!-- MyBatisPlus模块[可删除coder-dict插件已经依赖] -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-mybatisplus</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据字典翻译 -->
<!-- <dependency> -->
<!-- <groupId>org.leocoder.heritage</groupId> -->
<!-- <artifactId>heritage-dict</artifactId> -->
<!-- <version>${revision}</version> -->
<!-- </dependency> -->
<!-- 限流(工具类模块已包含里面,其他插件都使用) -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-limit</artifactId>
<version>${revision}</version>
</dependency>
<!-- 重复提交插件-->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-repect</artifactId>
<version>${revision}</version>
</dependency>
<!-- EasyExcel插件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-easyexcel</artifactId>
<version>${revision}</version>
</dependency>
<!-- Sa-Token 权限认证用来处理全局拦截sa-token异常使用在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,17 @@
package org.leocoder.heritage.resultex.anno;
import org.leocoder.heritage.resultex.handler.GlobalExceptionHandler;
import org.leocoder.heritage.resultex.handler.ResultResponseHandler;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({ GlobalExceptionHandler.class, ResultResponseHandler.class })
public @interface EnableResultEx {
}

View File

@ -0,0 +1,362 @@
package org.leocoder.heritage.resultex.handler;
import cn.dev33.satoken.exception.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.enmus.common.IResultEnum;
import org.leocoder.heritage.common.exception.BusinessException;
import org.leocoder.heritage.common.exception.RateLimiterException;
import org.leocoder.heritage.common.exception.RepeatSubmitException;
import org.leocoder.heritage.common.exception.coder.ParamsException;
import org.leocoder.heritage.common.resultex.ErrorHandler;
import org.leocoder.heritage.common.utils.json.JsonUtil;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.yaml.snakeyaml.constructor.DuplicateKeyException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Leocoder
* @description [全局异常拦截-所有异常必须放置这个类中]
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* @description [拦截所有程序异常]
* @author Leocoder
*/
@ExceptionHandler(value = Exception.class)
public ErrorHandler errorHandler(HttpServletRequest request, Exception ex) {
log.error("Coder-ADMIN未知异常{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, "系统异常,请联系管理员!", request.getRequestURL().toString());
}
/**
* @description [自定义异常]
* @author Leocoder
*/
@ExceptionHandler(value = ParamsException.class)
public ErrorHandler errorHandlerParamsException(HttpServletRequest request, ParamsException ex) {
log.error("Coder-ADMIN自定义异常ParamsException{},请求地址:{}", ex.getMessage(), request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(ex.getStatus(), ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [自定义业务异常]
* @author Leocoder
*/
@ExceptionHandler(value = BusinessException.class)
public ErrorHandler errorHandlerBusinessEx(HttpServletRequest request, BusinessException ex) {
log.error("业务异常:{},请求地址:{}", ex.getMessage(), request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(ex.getStatus(), ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [Sa-Token 登录失败异常]
* @author Leocoder
*/
@ExceptionHandler(NotLoginException.class)
public ErrorHandler handlerNotLoginException(HttpServletRequest request, NotLoginException ex) {
// 不同异常返回不同状态码
String message = "";
if (ex.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未提供Token";
} else if (ex.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "未提供有效的Token";
} else if (ex.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "登录信息已过期,请重新登录";
} else if (ex.getType().equals(NotLoginException.BE_REPLACED)) {
message = "您的账户已在另一台设备上登录,如非本人操作,请立即修改密码";
} else if (ex.getType().equals(NotLoginException.KICK_OUT)) {
message = "已被系统强制下线";
} else {
message = "当前会话未登录";
}
log.error("Sa-Token异常提示{}", ex.getMessage());
// 返回给前端
return ErrorHandler.error(401, message, request.getRequestURL().toString());
}
@ExceptionHandler
public ErrorHandler handlerNotRoleException(HttpServletRequest request, NotRoleException ex) {
log.error("Sa-Token提示无此角色{}", ex.getRole());
return ErrorHandler.error(401, "无此角色:" + ex.getRole(), request.getRequestURL().toString());
}
@ExceptionHandler
public ErrorHandler handlerNotPermissionException(HttpServletRequest request, NotPermissionException ex) {
log.error("Sa-Token提示无此权限{}", ex.getPermission());
return ErrorHandler.error(401, "无此权限:" + ex.getPermission(), request.getRequestURL().toString());
}
@ExceptionHandler
public ErrorHandler handlerDisableLoginException(HttpServletRequest request, DisableServiceException ex) {
log.error("Sa-Token提示账户被封禁{}秒后解封", ex.getDisableTime());
return ErrorHandler.error(401, "账户被封禁:" + ex.getDisableTime() + "秒后解封", request.getRequestURL().toString());
}
@ExceptionHandler
public ErrorHandler handlerNotSafeException(HttpServletRequest request, NotSafeException ex) {
log.error("Sa-Token提示二级认证异常{}", ex.getMessage());
return ErrorHandler.error(401, "二级认证异常:" + ex.getMessage(), request.getRequestURL().toString());
}
@ExceptionHandler
public ErrorHandler handlerStopMatchException(HttpServletRequest request, StopMatchException ex) {
log.error("Sa-Token提示路由匹配异常{}", ex.getMessage());
return ErrorHandler.error(500, "路由匹配异常", request.getRequestURL().toString());
}
@ExceptionHandler
public ErrorHandler handlerBackResultException(HttpServletRequest request, BackResultException ex) {
log.error("Sa-Token提示停止匹配{}", ex.getMessage());
return ErrorHandler.error(500, "停止匹配", request.getRequestURL().toString());
}
/**
* @description [处理Get请求中,使用@Valid 验证路径中请求实体校验失败后抛出的异常]
* @Validated @Valid[仅对于表单提交有效对于以json格式提交将会失效]
* @author Leocoder
*/
@ExceptionHandler(BindException.class)
public ErrorHandler handleBindException(BindException e) {
log.error("自定义验证异常BindException{}", e.getMessage());
String message = e.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return ErrorHandler.error(IResultEnum.SERVER_ERROR, JsonUtil.toJsonString(message));
}
/**
* @description [@Validated @Valid 前端提交的方式为json格式有效]
* @author Leocoder
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorHandler handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("自定义验证异常MethodArgumentNotValidException{}", e.getMessage());
String message = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
return ErrorHandler.error(IResultEnum.SERVER_ERROR, JsonUtil.toJsonString(message));
}
/**
* @description [处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException]
* 针对@NotBlank @NotNull @NotEmpty
* @author Leocoder
*/
@ExceptionHandler(ConstraintViolationException.class)
public ErrorHandler handlerConstraintViolationException(ConstraintViolationException e) {
log.error("自定义验证异常ConstraintViolationException{}", e.getMessage());
String errorMessages = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
return ErrorHandler.error(IResultEnum.SERVER_ERROR, errorMessages);
}
/**
* 对验证异常进行统一处理,List集合方式
*/
// private List<Map<String, String>> toValidatorMsg(List<FieldError> fieldErrorList) {
// List<Map<String, String>> mapList = new ArrayList<>();
// for (FieldError fieldError : fieldErrorList) {
// Map<String, String> map = new HashMap<>();
// map.put("field", fieldError.getField());
// map.put("msg", fieldError.getDefaultMessage());
// mapList.add(map);
// }
// return mapList;
// }
/**
* @description [IllegalArgumentException]
* @author Leocoder
*/
@ExceptionHandler(IllegalArgumentException.class)
public ErrorHandler handlerIllegalArgumentException(HttpServletRequest request, IllegalArgumentException ex) {
log.error("IllegalArgumentException异常{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [lua限流异常]
* @author Leocoder
*/
@ExceptionHandler(RateLimiterException.class)
public ErrorHandler RateLimitException(HttpServletRequest request, RateLimiterException ex) {
log.error("lua限流异常{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [重复提交异常]
* @author Leocoder
*/
@ExceptionHandler(RepeatSubmitException.class)
public ErrorHandler RepeatSubmitException(HttpServletRequest request, RepeatSubmitException ex) {
log.error("重复提交异常:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [处理空指针异常]
* @author Leocoder
*/
@ExceptionHandler(NullPointerException.class)
public ErrorHandler exceptionHandler(HttpServletRequest request, NullPointerException ex) {
log.error("空指针异常,请联系管理员核实:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, "空指针异常", request.getRequestURL().toString());
}
/**
* @description [DB主键冲突异常]
* @author Leocoder
*/
@ExceptionHandler(DuplicateKeyException.class)
public ErrorHandler handleDuplicateKeyException(HttpServletRequest request, DuplicateKeyException ex) {
log.error("数据库主键冲突异常,请联系管理员核实:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [运算异常]
* @author Leocoder
*/
@ExceptionHandler(ArithmeticException.class)
public ErrorHandler arithmeticExceptionHandler(HttpServletRequest request, ArithmeticException ex) {
log.error("运算异常,请联系管理员核实:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [类型转换异常]
* @author Leocoder
*/
@ExceptionHandler(ClassCastException.class)
public ErrorHandler classCastExceptionHandler(HttpServletRequest request, ClassCastException ex) {
log.error("类型转换异常,请联系管理员核实:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [数据下标越界异常]
* @author Leocoder
*/
@ExceptionHandler(IndexOutOfBoundsException.class)
public ErrorHandler indexOutOfBoundsExceptionHandler(HttpServletRequest request, IndexOutOfBoundsException ex) {
log.error("数据下标越界异常,请联系管理员核实:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [文件未找到异常]
* @author Leocoder
*/
@ExceptionHandler(FileNotFoundException.class)
public ErrorHandler fileNotFoundExceptionHandler(HttpServletRequest request, FileNotFoundException ex) {
log.error("文件未找到异常,请联系管理员核实:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [IO异常]
* @author Leocoder
*/
@ExceptionHandler(IOException.class)
public ErrorHandler IOExceptionHandler(HttpServletRequest request, IOException ex) {
log.error("IO异常请联系管理员核实{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [参数类型不匹配]
* @author Leocoder
*/
@ExceptionHandler({MethodArgumentTypeMismatchException.class})
public ErrorHandler requestTypeMismatch(HttpServletRequest request, MethodArgumentTypeMismatchException ex) {
log.error("参数类型不匹配异常,请联系管理员核实:{},请求地址:{}", ex, request.getRequestURL().toString());
ex.printStackTrace();
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [文件上传大小超限异常]
* @author Leocoder
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ErrorHandler handleMaxUploadSizeExceededException(HttpServletRequest request, MaxUploadSizeExceededException ex) {
log.error("文件上传大小超限异常:{},请求地址:{}", ex.getMessage(), request.getRequestURL().toString());
// 提取文件大小限制信息提供更友好的错误提示
String message = "文件大小超出限制请上传小于2MB的文件";
// 尝试从异常信息中提取具体的大小限制
String exceptionMsg = ex.getMessage();
if (exceptionMsg != null && exceptionMsg.contains("maximum permitted size")) {
if (exceptionMsg.contains("1048576")) {
message = "文件大小超出限制最大允许上传1MB的文件";
} else if (exceptionMsg.contains("2097152")) {
message = "文件大小超出限制最大允许上传2MB的文件";
} else if (exceptionMsg.contains("5242880")) {
message = "文件大小超出限制最大允许上传5MB的文件";
} else if (exceptionMsg.contains("10485760")) {
message = "文件大小超出限制最大允许上传10MB的文件";
}
}
return ErrorHandler.error(413, message, request.getRequestURL().toString());
}
/**
* @description [文件上传异常]
* @author Leocoder
*/
@ExceptionHandler(MultipartException.class)
public ErrorHandler handleMultipartException(HttpServletRequest request, MultipartException ex) {
log.error("文件上传异常:{},请求地址:{}", ex.getMessage(), request.getRequestURL().toString());
String message = "文件上传失败";
String exceptionMsg = ex.getMessage();
if (exceptionMsg != null) {
if (exceptionMsg.contains("size")) {
message = "文件大小超出限制,请选择较小的文件";
} else if (exceptionMsg.contains("format") || exceptionMsg.contains("type")) {
message = "文件格式不支持,请选择正确的文件格式";
} else if (exceptionMsg.contains("empty")) {
message = "请选择要上传的文件";
}
}
return ErrorHandler.error(400, message, request.getRequestURL().toString());
}
}

View File

@ -0,0 +1,104 @@
package org.leocoder.heritage.resultex.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Nullable;
import net.dreamlu.mica.core.result.R;
import org.leocoder.heritage.common.anno.CoderIgnoreR;
import org.leocoder.heritage.common.resultex.ErrorHandler;
import org.leocoder.heritage.common.resultex.ResultUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.reflect.Method;
/**
* @author leocoder
* @description 全局统一返回类
* bug: (basePackages = "org.leocoder")建议扫包
* 为什么
* 如果你项目中没有使用Swagger你可以扫包也可以不扫都是正常的
* 但是如果你项目使用了Swagger因为Swagger本身也是一个springmvc的项目他里面也是一个个http请求
* 这个请求的时候如果你项目中配置了拦截器或者一些通知类xxxAdvice那么就会把Swagger都会进行拦截
* 就会造成Swagger失效
* 解决knife4j失效问题(basePackages = { "org.leocoder" }, annotations = { RestController.class })
*/
@RestControllerAdvice(basePackages = { "org.leocoder" }, annotations = { RestController.class })
public class ResultResponseHandler implements ResponseBodyAdvice<Object> {
/**
* 是否支持advice功能true是支持 false是不支持
*/
@Override
public boolean supports(@Nullable MethodParameter methodParameter, @Nullable Class<? extends HttpMessageConverter<?>> CoderClass) {
// 排除Swagger相关路径避免干扰OpenAPI文档生成
if (methodParameter != null && methodParameter.getMethod() != null) {
String className = methodParameter.getMethod().getDeclaringClass().getName();
// 排除SpringDoc相关的Controller
if (className.contains("springdoc") || className.contains("swagger")) {
return false;
}
}
return true;
}
@Override
public Object beforeBodyWrite(Object body, @Nullable MethodParameter methodParameter, @Nullable MediaType mediaType, Class<? extends HttpMessageConverter<?>> coderClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
// 参数body 代表其实就是SpringMvc的请求的方法的结果
// 对请求的结果在这里统一返回和处理
// 排除Swagger相关路径避免包装OpenAPI文档
String requestPath = serverHttpRequest.getURI().getPath();
if (requestPath.startsWith("/v3/api-docs") ||
requestPath.startsWith("/swagger-ui") ||
requestPath.contains("/swagger") ||
requestPath.contains("/api-docs")) {
return body;
}
if (body instanceof ErrorHandler errorHandler) {
// 如果返回的结果是一个异常的结果就把异常返回的结构数据倒腾到R.error里面即可
return ResultUtils.error(errorHandler.getStatus(), errorHandler.getMsg());
}
// 检查是否有 CoderIgnoreR 注解如果有则不进行封装直接返回原始数据body
if (methodParameter != null) {
Method method = methodParameter.getMethod();
Class<?> declaringClass = method.getDeclaringClass();
// 检查方法级别的注解
if (method.isAnnotationPresent(CoderIgnoreR.class)) {
return body;
}
// 检查类级别的注解
if (declaringClass.isAnnotationPresent(CoderIgnoreR.class)) {
return body;
}
}
if (body instanceof R) {
return body;
}
if (body instanceof String) {
try {
// 因为SpringMVC数据转换器对String是有特殊处理 StringHttpMessageConverter解决String类型的返回
ObjectMapper objectMapper = new ObjectMapper();
R r = R.success(body);
return objectMapper.writeValueAsString(r);
} catch (Exception e) {
e.printStackTrace();
}
}
// 对于其他类型的对象直接封装到 R
return R.success(body);
}
}

30
heritage-plugins/pom.xml Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-backend</artifactId>
<version>${revision}</version>
</parent>
<artifactId>heritage-plugins</artifactId>
<packaging>pom</packaging>
<description>插件模块</description>
<modules>
<module>heritage-resultex</module>
<module>heritage-sa-token</module>
<module>heritage-desensitize</module>
<module>heritage-easyexcel</module>
<module>heritage-repect</module>
<module>heritage-limit</module>
<module>heritage-oper-logs</module>
<module>heritage-oss</module>
<module>heritage-dict</module>
<module>heritage-job</module>
</modules>
</project>