From 09457ecebd6a2d7dbd3085bf8641457c6fd47d57 Mon Sep 17 00:00:00 2001 From: Leo Date: Wed, 8 Oct 2025 02:06:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Eheritage-plugins?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=8F=92=E4=BB=B6=EF=BC=88=E7=AC=AC1?= =?UTF-8?q?=E9=83=A8=E5=88=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增heritage-resultex:统一结果封装和全局异常处理插件 - 新增heritage-desensitize:数据脱敏插件,支持手机号、身份证等 - 新增heritage-dict:字典翻译插件,自动翻译字典值 - 新增plugins父POM配置 --- heritage-plugins/heritage-desensitize/pom.xml | 31 ++ .../desensitize/anno/CoderDesensitize.java | 39 ++ .../anno/EnableCoderDesensitize.java | 17 + .../config/DesensitizeJsonSerializer.java | 124 ++++++ .../enums/DesensitizeRuleEnum.java | 67 ++++ heritage-plugins/heritage-dict/pom.xml | 35 ++ .../heritage/dict/anno/EnableCoderDict.java | 16 + .../heritage/dict/aspect/CoderDictAspect.java | 212 ++++++++++ .../heritage/dict/aspect/CoderDictModel.java | 21 + heritage-plugins/heritage-resultex/pom.xml | 61 +++ .../resultex/anno/EnableResultEx.java | 17 + .../handler/GlobalExceptionHandler.java | 362 ++++++++++++++++++ .../handler/ResultResponseHandler.java | 104 +++++ heritage-plugins/pom.xml | 30 ++ 14 files changed, 1136 insertions(+) create mode 100644 heritage-plugins/heritage-desensitize/pom.xml create mode 100755 heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/CoderDesensitize.java create mode 100755 heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/EnableCoderDesensitize.java create mode 100755 heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/config/DesensitizeJsonSerializer.java create mode 100755 heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/enums/DesensitizeRuleEnum.java create mode 100644 heritage-plugins/heritage-dict/pom.xml create mode 100644 heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/anno/EnableCoderDict.java create mode 100644 heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictAspect.java create mode 100644 heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictModel.java create mode 100644 heritage-plugins/heritage-resultex/pom.xml create mode 100755 heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/anno/EnableResultEx.java create mode 100755 heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/GlobalExceptionHandler.java create mode 100755 heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/ResultResponseHandler.java create mode 100644 heritage-plugins/pom.xml diff --git a/heritage-plugins/heritage-desensitize/pom.xml b/heritage-plugins/heritage-desensitize/pom.xml new file mode 100644 index 0000000..73bc194 --- /dev/null +++ b/heritage-plugins/heritage-desensitize/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-plugins + ${revision} + + + + heritage-desensitize + heritage-desensitize + 数据脱敏插件 + + + + + org.leocoder.heritage + heritage-common + ${revision} + + + + org.springframework.boot + spring-boot-starter-aop + + + + \ No newline at end of file diff --git a/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/CoderDesensitize.java b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/CoderDesensitize.java new file mode 100755 index 0000000..9393e10 --- /dev/null +++ b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/CoderDesensitize.java @@ -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; + + +} diff --git a/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/EnableCoderDesensitize.java b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/EnableCoderDesensitize.java new file mode 100755 index 0000000..f60348f --- /dev/null +++ b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/anno/EnableCoderDesensitize.java @@ -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 { + +} diff --git a/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/config/DesensitizeJsonSerializer.java b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/config/DesensitizeJsonSerializer.java new file mode 100755 index 0000000..94bf487 --- /dev/null +++ b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/config/DesensitizeJsonSerializer.java @@ -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 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); + } + +} diff --git a/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/enums/DesensitizeRuleEnum.java b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/enums/DesensitizeRuleEnum.java new file mode 100755 index 0000000..9d527ec --- /dev/null +++ b/heritage-plugins/heritage-desensitize/src/main/java/org/leocoder/heritage/desensitize/enums/DesensitizeRuleEnum.java @@ -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 + +} diff --git a/heritage-plugins/heritage-dict/pom.xml b/heritage-plugins/heritage-dict/pom.xml new file mode 100644 index 0000000..97686a6 --- /dev/null +++ b/heritage-plugins/heritage-dict/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-plugins + ${revision} + + + heritage-dict + heritage-dict + 字典翻译插件 + + + + + org.leocoder.heritage + heritage-common + ${revision} + + + + org.leocoder.heritage + heritage-model + ${revision} + + + + org.springframework.boot + spring-boot-starter-aop + + + \ No newline at end of file diff --git a/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/anno/EnableCoderDict.java b/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/anno/EnableCoderDict.java new file mode 100644 index 0000000..1fba66b --- /dev/null +++ b/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/anno/EnableCoderDict.java @@ -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 { + +} \ No newline at end of file diff --git a/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictAspect.java b/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictAspect.java new file mode 100644 index 0000000..5f4a1cf --- /dev/null +++ b/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictAspect.java @@ -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 dictMap = new HashMap<>(); + + @Resource + private RedisUtil redisUtil; + + @Pointcut("@annotation(org.leocoder.heritage.common.anno.CoderDictClass)") + public void logPointCut() { + } + + /** + * @description [翻译数据] + * 需要处理的数据类型: + * 1、分页数据R> + * 2、普通列表R> + * 3、普通数据R + * @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 list = (List) 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 dictModelList = getCoderDict(objectData.getClass()); + // dictModelList.forEach(System.out::println); + // CoderDictModel(dictKey=sys_user_sex, dictValue=sex) + // 如果没有字典映射,则直接返回原始结果 + if (dictModelList.isEmpty()) { + return; + } + + // 获取所有的字典数据 + List 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 listDictCache(List dictModelList) { + List dictDataList = new ArrayList<>(); + List dictKeyList = getDictKey(dictModelList); + for (String dictKey : dictKeyList) { + Boolean hasKey = redisUtil.hasKey(CoderCacheConstants.DICT_REDIS_KEY + dictKey); + if(hasKey) { + List 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 dictModelList, Map 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 getDictKey(List CoderDictBos) { + // 创建一个字符串列表用于存储dictKey + List 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 getCoderDict(Class classData) { + Field[] fields = classData.getDeclaredFields(); // 获取类classData中声明的所有字段 + List 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; + } + +} \ No newline at end of file diff --git a/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictModel.java b/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictModel.java new file mode 100644 index 0000000..3b1c59c --- /dev/null +++ b/heritage-plugins/heritage-dict/src/main/java/org/leocoder/heritage/dict/aspect/CoderDictModel.java @@ -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; +} \ No newline at end of file diff --git a/heritage-plugins/heritage-resultex/pom.xml b/heritage-plugins/heritage-resultex/pom.xml new file mode 100644 index 0000000..9b082cd --- /dev/null +++ b/heritage-plugins/heritage-resultex/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-plugins + ${revision} + + + + heritage-resultex + heritage-resultex + 全局统一返回 和 全局异常类 + + + + + org.leocoder.heritage + heritage-model + ${revision} + + + + org.leocoder.heritage + heritage-mybatisplus + ${revision} + + + + + + + + + + org.leocoder.heritage + heritage-limit + ${revision} + + + + org.leocoder.heritage + heritage-repect + ${revision} + + + + org.leocoder.heritage + heritage-easyexcel + ${revision} + + + + cn.dev33 + sa-token-spring-boot3-starter + + + + \ No newline at end of file diff --git a/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/anno/EnableResultEx.java b/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/anno/EnableResultEx.java new file mode 100755 index 0000000..8433a03 --- /dev/null +++ b/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/anno/EnableResultEx.java @@ -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 { + + +} diff --git a/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/GlobalExceptionHandler.java b/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/GlobalExceptionHandler.java new file mode 100755 index 0000000..9fb997b --- /dev/null +++ b/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/GlobalExceptionHandler.java @@ -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> toValidatorMsg(List fieldErrorList) { +// List> mapList = new ArrayList<>(); +// for (FieldError fieldError : fieldErrorList) { +// Map 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()); + } + +} diff --git a/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/ResultResponseHandler.java b/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/ResultResponseHandler.java new file mode 100755 index 0000000..3342950 --- /dev/null +++ b/heritage-plugins/heritage-resultex/src/main/java/org/leocoder/heritage/resultex/handler/ResultResponseHandler.java @@ -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 { + + /** + * 是否支持advice功能,true是支持 false是不支持 + */ + @Override + public boolean supports(@Nullable MethodParameter methodParameter, @Nullable Class> 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> 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); + } + +} diff --git a/heritage-plugins/pom.xml b/heritage-plugins/pom.xml new file mode 100644 index 0000000..228c459 --- /dev/null +++ b/heritage-plugins/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-backend + ${revision} + + + + heritage-plugins + pom + 插件模块 + + + heritage-resultex + heritage-sa-token + heritage-desensitize + heritage-easyexcel + heritage-repect + heritage-limit + heritage-oper-logs + heritage-oss + heritage-dict + heritage-job + + + \ No newline at end of file