houzhongjian
5 天以前 152781b05131e48bf6e94d71cc72dd54af52a3fb
恢复iailab-framework
已添加400个文件
21490 ■■■■■ 文件已修改
iailab-framework/iailab-common-biz-tenant/pom.xml 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantAutoConfiguration.java 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantRpcAutoConfiguration.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/TenantProperties.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnore.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnoreAspect.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/DataContextHolder.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/TenantContextHolder.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantBaseDO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantDatabaseInterceptor.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/DataDS.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDS.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDsProcessor.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJob.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJobAspect.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/redis/TenantRedisCacheManager.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/rpc/TenantRequestInterceptor.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/security/TenantSecurityWebFilter.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkService.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkServiceImpl.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/util/TenantUtils.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/web/TenantContextWebFilter.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/package-info.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring.factories 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/pom.xml 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvEnvironmentPostProcessor.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvProperties.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvRpcAutoConfiguration.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvWebAutoConfiguration.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/context/EnvContextHolder.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClient.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClientFactory.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvRequestInterceptor.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/package-info.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/util/EnvUtils.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/web/EnvWebFilter.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/package-info.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/resources/META-INF/spring.factories 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-env/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/pom.xml 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictAutoConfiguration.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictRpcAutoConfiguration.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/core/DictFrameworkUtils.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/DictFormat.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/ExcelColumnSelect.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/AreaConvert.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/DictConvert.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/JsonConvert.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/MoneyConvert.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/function/ExcelColumnSelectFunction.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/handler/SelectSheetWriteHandler.java 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/util/ExcelUtils.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-excel/src/test/java/com/iailab/framework/dict/core/util/DictFrameworkUtilsTest.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-job/pom.xml 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabAsyncAutoConfiguration.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabXxlJobAutoConfiguration.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/XxlJobProperties.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/package-info.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/pom.xml 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabMetricsAutoConfiguration.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabTracerAutoConfiguration.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/TracerProperties.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/annotation/BizTrace.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/aop/BizTraceAspect.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/filter/TraceFilter.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/util/TracerFrameworkUtils.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/pom.xml 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/common/RoutingConstant.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/config/IailabRabbitMQAutoConfiguration.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/core/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQConsumerAutoConfiguration.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQProducerAutoConfiguration.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/RedisMQTemplate.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/job/RedisPendingMessageResendJob.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/message/AbstractRedisMessage.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/pom.xml 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/dao/BaseDao.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/entity/BaseEntity.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/package-info.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/page/PageData.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/BaseService.java 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/CrudService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/BaseServiceImpl.java 219 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/CrudServiceImpl.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/config/IailabDataSourceAutoConfiguration.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/enums/DataSourceEnum.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/filter/DruidAdRemoveFilter.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/package-info.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IailabMybatisAutoConfiguration.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/MyBatisConfiguration.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/dataobject/BaseDO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/DbTypeEnum.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/SqlConstants.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/DefaultDBFieldHandler.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/MybatisHandler.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/mapper/BaseMapperX.java 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/LambdaQueryWrapperX.java 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/MPJLambdaWrapperX.java 313 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/QueryWrapperX.java 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/EncryptTypeHandler.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/IntegerListTypeHandler.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/JsonLongSetTypeHandler.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/LongListTypeHandler.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/StringListTypeHandler.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/JdbcUtils.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/MyBatisUtils.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataFilterInterceptor.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataScope.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/config/IailabTranslateAutoConfiguration.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/core/TranslateUtils.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring.factories 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/pom.xml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/config/IailabIdempotentConfiguration.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/annotation/Idempotent.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/aop/IdempotentAspect.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/redis/IdempotentRedisDAO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/package-info.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/config/IailabLock4jConfiguration.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/DefaultLockFailureStrategy.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/Lock4jRedisKeyConstants.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/config/IailabRateLimiterConfiguration.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/annotation/RateLimiter.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/aop/RateLimiterAspect.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/config/IailabApiSignatureAutoConfiguration.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/annotation/ApiSignature.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/aop/ApiSignatureAspect.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/redis/ApiSignatureRedisDAO.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-protection/src/test/java/com/iailab/framework/signature/core/ApiSignatureTest.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-redis/pom.xml 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheAutoConfiguration.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheProperties.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabRedisAutoConfiguration.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/core/TimeoutRedisCacheManager.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-rpc/pom.xml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/config/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/core/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/pom.xml 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogConfiguration.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogRpcAutoConfiguration.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/service/LogRecordServiceImpl.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/package-info.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/AuthorizeRequestsCustomizer.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityAutoConfiguration.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityRpcAutoConfiguration.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabWebSecurityConfigurerAdapter.java 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/SecurityProperties.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/annotations/PreAuthenticated.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/aop/PreAuthenticatedAspect.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/filter/TokenAuthenticationFilter.java 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AccessDeniedHandlerImpl.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AuthenticationEntryPointImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/rpc/LoginUserRequestInterceptor.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkService.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkServiceImpl.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/util/SecurityFrameworkUtils.java 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/package-info.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/resources/assembly.xml 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/pom.xml 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/RedisTestConfiguration.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/SqlInitializationTestConfiguration.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbAndRedisUnitTest.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbUnitTest.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseMockitoUnitTest.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseRedisUnitTest.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/AssertUtils.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/RandomUtils.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/pom.xml 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogAutoConfiguration.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogRpcAutoConfiguration.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/annotation/ApiAccessLog.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/enums/OperateTypeEnum.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/filter/ApiAccessLogFilter.java 251 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/package-info.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/config/IailabBannerAutoConfiguration.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/core/BannerApplicationRunner.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/annotation/DesensitizeBy.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/handler/DesensitizationHandler.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/EmailDesensitize.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/RegexDesensitize.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/BankCardDesensitize.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/IdCardDesensitize.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/MobileDesensitize.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/PasswordDesensitize.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/SliderDesensitize.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/BankCardDesensitization.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/IdCardDesensitization.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/MobileDesensitization.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/PasswordDesensitization.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/config/IailabJacksonAutoConfiguration.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/NumberSerializer.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/package-info.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/IailabSwaggerAutoConfiguration.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/SwaggerProperties.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/IailabWebAutoConfiguration.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/WebProperties.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/ApiRequestFilter.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyFilter.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyWrapper.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/DemoFilter.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalExceptionHandler.java 356 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/main/resources/banner.txt 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/pom.xml 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/pom.xml 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java 406 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java 338 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java 309 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java 385 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/FileUtils.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/IoUtils.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/JsonUtils.java 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/NumberSerializer.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/monitor/TracerUtils.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/MoneyUtils.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/NumberUtils.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/BeanUtils.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ConvertUtils.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ObjectUtils.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/PageUtils.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/package-info.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/servlet/ServletUtils.java 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringContextUtils.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringExpressionUtils.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringUtils.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/string/StrUtils.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/validation/ValidationUtils.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnum.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumCollectionValidator.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumValidator.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Mobile.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/MobileValidator.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Telephone.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/TelephoneValidator.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/AddGroup.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/DefaultGroup.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/Group.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/UpdateGroup.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common/src/test/java/com/iailab/framework/common/util/collection/CollectionUtilsTest.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/pom.xml 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-tenant/pom.xml
对比新文件
@@ -0,0 +1,96 @@
<?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">
    <parent>
        <artifactId>iailab-framework</artifactId>
        <groupId>com.iailab</groupId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-biz-tenant</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>多租户</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-security</artifactId>
        </dependency>
        <!-- DB 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-mybatis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>
        <!-- RPC 远程调用相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-rpc</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Job 定时任务相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-job</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 消息队列相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-mq</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 工具类相关 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantAutoConfiguration.java
对比新文件
@@ -0,0 +1,160 @@
package com.iailab.framework.tenant.config;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
import com.iailab.framework.common.enums.WebFilterOrderEnum;
import com.iailab.framework.mybatis.core.util.MyBatisUtils;
import com.iailab.framework.redis.config.IailabCacheProperties;
import com.iailab.framework.tenant.core.aop.TenantIgnoreAspect;
import com.iailab.framework.tenant.core.db.TenantDatabaseInterceptor;
import com.iailab.framework.tenant.core.db.dynamic.TenantDsProcessor;
import com.iailab.framework.tenant.core.job.TenantJobAspect;
import com.iailab.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
import com.iailab.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor;
import com.iailab.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
import com.iailab.framework.tenant.core.redis.TenantRedisCacheManager;
import com.iailab.framework.tenant.core.security.TenantSecurityWebFilter;
import com.iailab.framework.tenant.core.service.TenantFrameworkService;
import com.iailab.framework.tenant.core.service.TenantFrameworkServiceImpl;
import com.iailab.framework.tenant.core.web.TenantContextWebFilter;
import com.iailab.framework.web.config.WebProperties;
import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
import com.iailab.module.system.api.tenant.TenantApi;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Objects;
@AutoConfiguration
@ConditionalOnProperty(prefix = "iailab.tenant", value = "enable", matchIfMissing = true) // 允许使用 iailab.tenant.enable=false 禁用多租户
@EnableConfigurationProperties(TenantProperties.class)
public class IailabTenantAutoConfiguration {
    @Bean
    public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) {
        return new TenantFrameworkServiceImpl(tenantApi);
    }
    // ========== AOP ==========
    @Bean
    public TenantIgnoreAspect tenantIgnoreAspect() {
        return new TenantIgnoreAspect();
    }
    // ========== DB ==========
    @Bean
    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
                                                                 MybatisPlusInterceptor interceptor) {
        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
        // 添加到 interceptor 中
        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
        MyBatisUtils.addInterceptor(interceptor, inner, 0);
        return inner;
    }
    @Bean
    public DsProcessor dsProcessor(
//            TenantFrameworkService tenantFrameworkService,
//                                   DataSource dataSource,
//                                   DefaultDataSourceCreator dataSourceCreator
    ) {
//        TenantDsProcessor tenantDsProcessor = new TenantDsProcessor(tenantFrameworkService, dataSourceCreator);
        TenantDsProcessor tenantDsProcessor = new TenantDsProcessor();
        tenantDsProcessor.setNextProcessor(new DsSpelExpressionProcessor());
        return tenantDsProcessor;
    }
    // ========== WEB ==========
    @Bean
    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
        FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new TenantContextWebFilter());
        registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
        return registrationBean;
    }
    // ========== Security ==========
    @Bean
    public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
                                                                                   WebProperties webProperties,
                                                                                   GlobalExceptionHandler globalExceptionHandler,
                                                                                   TenantFrameworkService tenantFrameworkService) {
        FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties,
                globalExceptionHandler, tenantFrameworkService));
        registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
        return registrationBean;
    }
    // ========== Job ==========
    @Bean
    @ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
    public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
        return new TenantJobAspect(tenantFrameworkService);
    }
    // ========== MQ ==========
    /**
     * 多租户 Redis 消息队列的配置类
     *
     * 为什么要单独一个配置类呢?如果直接把 TenantRedisMessageInterceptor Bean 的初始化放外面,会报 RedisMessageInterceptor 类不存在的错误
     */
    @Configuration
    @ConditionalOnClass(name = "com.iailab.framework.mq.redis.core.RedisMQTemplate")
    public static class TenantRedisMQAutoConfiguration {
        @Bean
        public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
            return new TenantRedisMessageInterceptor();
        }
    }
    @Bean
    @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
    public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
        return new TenantRabbitMQInitializer();
    }
    @Bean
    @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
    public TenantRocketMQInitializer tenantRocketMQInitializer() {
        return new TenantRocketMQInitializer();
    }
    // ========== Redis ==========
    @Bean
    @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
    public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
                                                     RedisCacheConfiguration redisCacheConfiguration,
                                                     IailabCacheProperties iailabCacheProperties,
                                                     TenantProperties tenantProperties) {
        // 创建 RedisCacheWriter 对象
        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
                BatchStrategies.scan(iailabCacheProperties.getRedisScanBatchSize()));
        // 创建 TenantRedisCacheManager 对象
        return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/IailabTenantRpcAutoConfiguration.java
对比新文件
@@ -0,0 +1,21 @@
package com.iailab.framework.tenant.config;
import com.iailab.framework.tenant.core.rpc.TenantRequestInterceptor;
import com.iailab.module.system.api.tenant.TenantApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@AutoConfiguration
@ConditionalOnProperty(prefix = "iailab.tenant", value = "enable", matchIfMissing = true) // 允许使用 iailab.tenant.enable=false 禁用多租户
@EnableFeignClients(clients = TenantApi.class) // 主要是引入相关的 API 服务
public class IailabTenantRpcAutoConfiguration {
    @Bean
    public TenantRequestInterceptor tenantRequestInterceptor() {
        return new TenantRequestInterceptor();
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/config/TenantProperties.java
对比新文件
@@ -0,0 +1,49 @@
package com.iailab.framework.tenant.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Collections;
import java.util.Set;
/**
 * 多租户配置
 *
 * @author iailab
 */
@ConfigurationProperties(prefix = "iailab.tenant")
@Data
public class TenantProperties {
    /**
     * 租户是否开启
     */
    private static final Boolean ENABLE_DEFAULT = true;
    /**
     * 是否开启
     */
    private Boolean enable = ENABLE_DEFAULT;
    /**
     * 需要忽略多租户的请求
     *
     * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
     */
    private Set<String> ignoreUrls = Collections.emptySet();
    /**
     * 需要忽略多租户的表
     *
     * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
     */
    private Set<String> ignoreTables = Collections.emptySet();
    /**
     * 需要忽略多租户的 Spring Cache 缓存
     *
     * 即默认所有缓存都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
     */
    private Set<String> ignoreCaches = Collections.emptySet();
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnore.java
对比新文件
@@ -0,0 +1,18 @@
package com.iailab.framework.tenant.core.aop;
import java.lang.annotation.*;
/**
 * 忽略租户,标记指定方法不进行租户的自动过滤
 *
 * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
 * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
 * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
 *
 * @author iailab
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TenantIgnore {
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/aop/TenantIgnoreAspect.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.tenant.core.aop;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.framework.tenant.core.util.TenantUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
 * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
 * 例如说,一个定时任务,读取所有数据,进行处理。
 * 又例如说,读取所有数据,进行缓存。
 *
 * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致
 *
 * @author iailab
 */
@Aspect
@Slf4j
public class TenantIgnoreAspect {
    @Around("@annotation(tenantIgnore)")
    public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
        Boolean oldIgnore = TenantContextHolder.isIgnore();
        try {
            TenantContextHolder.setIgnore(true);
            // 执行逻辑
            return joinPoint.proceed();
        } finally {
            TenantContextHolder.setIgnore(oldIgnore);
        }
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/DataContextHolder.java
对比新文件
@@ -0,0 +1,45 @@
package com.iailab.framework.tenant.core.context;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.iailab.framework.common.enums.DocumentEnum;
/**
 * 数据源上下文 Holder
 *
 * @author iailab
 */
public class DataContextHolder {
    /**
     * 数据源id
     */
    private static final ThreadLocal<Long> DATA_SOURCE_ID =  new TransmittableThreadLocal<>();
    /**
     * 数据源id
     *
     * @return 租户编号
     */
    public static Long getDataSourceId() {
        return DATA_SOURCE_ID.get();
    }
    /**
     * 数据源id。如果不存在,则抛出 NullPointerException 异常
     *
     * @return 租户编号
     */
    public static Long getRequiredDataSourceId() {
        Long dataSourceId = getDataSourceId();
        if (dataSourceId == null) {
            throw new NullPointerException("DataContextHolder 不存在数据源id!可参考文档:"
                + DocumentEnum.TENANT.getUrl());
        }
        return dataSourceId;
    }
    public static void setDataSourceId(Long dataSourceId) {
        DATA_SOURCE_ID.set(dataSourceId);
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/context/TenantContextHolder.java
对比新文件
@@ -0,0 +1,69 @@
package com.iailab.framework.tenant.core.context;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.enums.DocumentEnum;
import com.alibaba.ttl.TransmittableThreadLocal;
/**
 * 多租户上下文 Holder
 *
 * @author iailab
 */
public class TenantContextHolder {
    /**
     * 当前租户编号
     */
    private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
    /**
     * 是否忽略租户
     */
    private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
    /**
     * 获得租户编号
     *
     * @return 租户编号
     */
    public static Long getTenantId() {
        return TENANT_ID.get();
    }
    /**
     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
     *
     * @return 租户编号
     */
    public static Long getRequiredTenantId() {
        Long tenantId = getTenantId();
        if (tenantId == null) {
            throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
                + DocumentEnum.TENANT.getUrl());
        }
        return tenantId;
    }
    public static void setTenantId(Long tenantId) {
        TENANT_ID.set(tenantId);
    }
    public static void setIgnore(Boolean ignore) {
        IGNORE.set(ignore);
    }
    /**
     * 当前是否忽略租户
     *
     * @return 是否忽略
     */
    public static boolean isIgnore() {
        return Boolean.TRUE.equals(IGNORE.get());
    }
    public static void clear() {
        TENANT_ID.remove();
        IGNORE.remove();
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantBaseDO.java
对比新文件
@@ -0,0 +1,21 @@
package com.iailab.framework.tenant.core.db;
import com.iailab.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 拓展多租户的 BaseDO 基类
 *
 * @author iailab
 */
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class TenantBaseDO extends BaseDO {
    /**
     * 多租户编号
     */
    private Long tenantId;
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/TenantDatabaseInterceptor.java
对比新文件
@@ -0,0 +1,43 @@
package com.iailab.framework.tenant.core.db;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.tenant.config.TenantProperties;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import java.util.HashSet;
import java.util.Set;
/**
 * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
 *
 * @author iailab
 */
public class TenantDatabaseInterceptor implements TenantLineHandler {
    private final Set<String> ignoreTables = new HashSet<>();
    public TenantDatabaseInterceptor(TenantProperties properties) {
        // 不同 DB 下,大小写的习惯不同,所以需要都添加进去
        properties.getIgnoreTables().forEach(table -> {
            ignoreTables.add(table.toLowerCase());
            ignoreTables.add(table.toUpperCase());
        });
        // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
        ignoreTables.add("DUAL");
    }
    @Override
    public Expression getTenantId() {
        return new LongValue(TenantContextHolder.getRequiredTenantId());
    }
    @Override
    public boolean ignoreTable(String tableName) {
        return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
            || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/DataDS.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.tenant.core.db.dynamic;
import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;
/**
 * 使用数据源所在的数据源
 *
 * 使用方式:当我们希望一个表使用租户所在的数据源,可以在该表的 Mapper 上添加该注解
 *
 * @author 芋道源码
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS(DataDS.KEY)
public @interface DataDS {
    /**
     * 数据源的占位符
     */
    String KEY = "#context.dataSourceId";
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDS.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.tenant.core.db.dynamic;
import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;
/**
 * 使用租户所在的数据源
 *
 * 使用方式:当我们希望一个表使用租户所在的数据源,可以在该表的 Mapper 上添加该注解
 *
 * @author 芋道源码
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS(TenantDS.KEY)
public @interface TenantDS {
    /**
     * 租户对应的数据源的占位符
     */
    String KEY = "#context.tenantId";
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/db/dynamic/TenantDsProcessor.java
对比新文件
@@ -0,0 +1,106 @@
package com.iailab.framework.tenant.core.db.dynamic;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.iailab.framework.tenant.core.context.DataContextHolder;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.framework.tenant.core.service.TenantFrameworkService;
import com.iailab.module.infra.api.db.DataSourceConfigServiceApi;
import com.iailab.module.infra.api.db.dto.DataSourceConfigRespDTO;
import lombok.RequiredArgsConstructor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.annotation.Lazy;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Objects;
/**
 * 基于 {@link TenantDS} 的数据源处理器
 *
 * 1. 如果有 @TenantDS 注解,返回该租户的数据源
 * 2. 如果该租户的数据源未创建,则进行创建
 *
 * @author 芋道源码
 */
@RequiredArgsConstructor
public class TenantDsProcessor extends DsProcessor {
    /**
     * 用于获取租户数据源配置的 Service
     */
    @Resource
    @Lazy
    private TenantFrameworkService tenantFrameworkService;
    /**
     * 动态数据源
     */
    @Resource
    @Lazy  // 为什么添加 @Lazy 注解?因为它和 DynamicRoutingDataSource 相互依赖,导致无法初始化
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    /**
     * 用于创建租户数据源的 Creator
     */
    @Resource
    @Lazy
    private DefaultDataSourceCreator dataSourceCreator;
    @Resource
    @Lazy
    private DataSourceConfigServiceApi dataSourceConfigServiceApi;
    @Override
    public boolean matches(String key) {
        return Objects.equals(key, TenantDS.KEY) || Objects.equals(key, DataDS.KEY);
    }
    @Override
    public String doDetermineDatasource(MethodInvocation invocation, String key) {
        if (DataDS.KEY.equals(key)){
            // 获得数据源配置
            Long dataSourceId = DataContextHolder.getRequiredDataSourceId();
            DataSourceConfigRespDTO dataSourceConfigRespDTO = dataSourceConfigServiceApi.getDataSourceConfig(dataSourceId);
            DataSourceProperty dataSourceProperty = new DataSourceProperty();
            dataSourceProperty.setPoolName(dataSourceConfigRespDTO.getName());
            dataSourceProperty.setUrl(dataSourceConfigRespDTO.getUrl());
            dataSourceProperty.setUsername(dataSourceConfigRespDTO.getUsername());
            dataSourceProperty.setPassword(dataSourceConfigRespDTO.getPassword());
            // 创建 or 创建数据源,并返回数据源名字
            return createDatasourceIfAbsent(dataSourceProperty);
        }else if(TenantDS.KEY.equals(key)){
            // 获得数据源配置
            Long tenantId = TenantContextHolder.getRequiredTenantId();
            DataSourceProperty dataSourceProperty = tenantFrameworkService.getDataSourceProperty(tenantId);
            // 创建 or 创建数据源,并返回数据源名字
            return createDatasourceIfAbsent(dataSourceProperty);
        }
        return key;
    }
    private String createDatasourceIfAbsent(DataSourceProperty dataSourceProperty) {
        // 1. 重点:如果数据源不存在,则进行创建
        if (isDataSourceNotExist(dataSourceProperty)) {
            // 问题一:为什么要加锁?因为,如果多个线程同时执行到这里,会导致多次创建数据源
            // 问题二:为什么要使用 poolName 加锁?保证多个不同的 poolName 可以并发创建数据源
            // 问题三:为什么要使用 intern 方法?因为,intern 方法,会返回一个字符串的常量池中的引用
            // intern 的说明,可见 https://www.cnblogs.com/xrq730/p/6662232.html 文章
            synchronized (dataSourceProperty.getPoolName().intern()) {
                if (isDataSourceNotExist(dataSourceProperty)) {
                    DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
                    dynamicRoutingDataSource.addDataSource(dataSourceProperty.getPoolName(), dataSource);
                }
            }
        }
        // 2. 返回数据源的名字
        return dataSourceProperty.getPoolName();
    }
    private boolean isDataSourceNotExist(DataSourceProperty dataSourceProperty) {
        return !dynamicRoutingDataSource.getDataSources().containsKey(dataSourceProperty.getPoolName());
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJob.java
对比新文件
@@ -0,0 +1,14 @@
package com.iailab.framework.tenant.core.job;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 多租户 Job 注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TenantJob {
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/job/TenantJobAspect.java
对比新文件
@@ -0,0 +1,66 @@
package com.iailab.framework.tenant.core.job;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.tenant.core.service.TenantFrameworkService;
import com.iailab.framework.tenant.core.util.TenantUtils;
import com.xxl.job.core.context.XxlJobHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 多租户 JobHandler AOP
 * 任务执行时,会按照租户逐个执行 Job 的逻辑
 *
 * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
 *
 * @author iailab
 */
@Aspect
@RequiredArgsConstructor
@Slf4j
public class TenantJobAspect {
    private final TenantFrameworkService tenantFrameworkService;
    @Around("@annotation(tenantJob)")
    public void around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) {
        // 获得租户列表
        List<Long> tenantIds = tenantFrameworkService.getTenantIds();
        if (CollUtil.isEmpty(tenantIds)) {
            return;
        }
        // 逐个租户,执行 Job
        Map<Long, String> results = new ConcurrentHashMap<>();
        tenantIds.parallelStream().forEach(tenantId -> {
            // TODO iailab:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
            TenantUtils.execute(tenantId, () -> {
                try {
                    System.out.println("租户id:" + tenantId);
                    joinPoint.proceed();
                } catch (Throwable e) {
                    results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
                    // 打印异常
                    XxlJobHelper.log(StrUtil.format("[多租户({}) 执行任务({}),发生异常:{}]",
                            tenantId, joinPoint.getSignature(), ExceptionUtils.getStackTrace(e)));
                }
            });
        });
        // 如果 results 非空,说明发生了异常,标记 XXL-Job 执行失败
        if (CollUtil.isNotEmpty(results)) {
            XxlJobHelper.handleFail(JsonUtils.toJsonString(results));
        }
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.tenant.core.mq.kafka;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
/**
 * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类
 *
 * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器
 *
 * @author iailab
 */
@Slf4j
public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor {
    private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes";
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 添加 TenantKafkaProducerInterceptor 拦截器
        try {
            String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES);
            if (StrUtil.isEmpty(value)) {
                value = TenantKafkaProducerInterceptor.class.getName();
            } else {
                value += "," + TenantKafkaProducerInterceptor.class.getName();
            }
            environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value);
        } catch (NoClassDefFoundError ignore) {
            // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
        }
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java
对比新文件
@@ -0,0 +1,47 @@
package com.iailab.framework.tenant.core.mq.kafka;
import cn.hutool.core.util.ReflectUtil;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.header.Headers;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import java.util.Map;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类
 *
 * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
 * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
 *
 * @author iailab
 */
public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> {
    @Override
    public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) {
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
            headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes());
        }
        return record;
    }
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
    }
    @Override
    public void close() {
    }
    @Override
    public void configure(Map<String, ?> configs) {
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
对比新文件
@@ -0,0 +1,23 @@
package com.iailab.framework.tenant.core.mq.rabbitmq;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
 * 多租户的 RabbitMQ 初始化器
 *
 * @author iailab
 */
public class TenantRabbitMQInitializer implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof RabbitTemplate) {
            RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
            rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor());
        }
        return bean;
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java
对比新文件
@@ -0,0 +1,31 @@
package com.iailab.framework.tenant.core.mq.rabbitmq;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类
 *
 * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
 * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
 *
 * @author iailab
 */
public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor {
    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId);
        }
        return message;
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java
对比新文件
@@ -0,0 +1,42 @@
package com.iailab.framework.tenant.core.mq.redis;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * 多租户 {@link AbstractRedisMessage} 拦截器
 *
 * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
 * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
 *
 * @author iailab
 */
public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
    @Override
    public void sendMessageBefore(AbstractRedisMessage message) {
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            message.addHeader(HEADER_TENANT_ID, tenantId.toString());
        }
    }
    @Override
    public void consumeMessageBefore(AbstractRedisMessage message) {
        String tenantIdStr = message.getHeader(HEADER_TENANT_ID);
        if (StrUtil.isNotEmpty(tenantIdStr)) {
            TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
        }
    }
    @Override
    public void consumeMessageAfter(AbstractRedisMessage message) {
        // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
        TenantContextHolder.clear();
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.tenant.core.mq.rocketmq;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import org.apache.rocketmq.client.hook.ConsumeMessageContext;
import org.apache.rocketmq.client.hook.ConsumeMessageHook;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import java.util.List;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类
 *
 * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
 *
 * @author iailab
 */
public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook {
    @Override
    public String hookName() {
        return getClass().getSimpleName();
    }
    @Override
    public void consumeMessageBefore(ConsumeMessageContext context) {
        // 校验,消息必须是单条,不然设置租户可能不正确
        List<MessageExt> messages = context.getMsgList();
        Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size());
        // 设置租户编号
        String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID);
        if (StrUtil.isNotEmpty(tenantId)) {
            TenantContextHolder.setTenantId(Long.parseLong(tenantId));
        }
    }
    @Override
    public void consumeMessageAfter(ConsumeMessageContext context) {
        TenantContextHolder.clear();
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
对比新文件
@@ -0,0 +1,53 @@
package com.iailab.framework.tenant.core.mq.rocketmq;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl;
import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
 * 多租户的 RocketMQ 初始化器
 *
 * @author iailab
 */
public class TenantRocketMQInitializer implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof DefaultRocketMQListenerContainer) {
            DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
            initTenantConsumer(container.getConsumer());
        } else if (bean instanceof RocketMQTemplate) {
            RocketMQTemplate template = (RocketMQTemplate) bean;
            initTenantProducer(template.getProducer());
        }
        return bean;
    }
    private void initTenantProducer(DefaultMQProducer producer) {
        if (producer == null) {
            return;
        }
        DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl();
        if (producerImpl == null) {
            return;
        }
        producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook());
    }
    private void initTenantConsumer(DefaultMQPushConsumer consumer) {
        if (consumer == null) {
            return;
        }
        DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl();
        if (consumerImpl == null) {
            return;
        }
        consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.tenant.core.mq.rocketmq;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import org.apache.rocketmq.client.hook.SendMessageContext;
import org.apache.rocketmq.client.hook.SendMessageHook;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类
 *
 * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
 *
 * @author iailab
 */
public class TenantRocketMQSendMessageHook implements SendMessageHook {
    @Override
    public String hookName() {
        return getClass().getSimpleName();
    }
    @Override
    public void sendMessageBefore(SendMessageContext sendMessageContext) {
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            return;
        }
        sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString());
    }
    @Override
    public void sendMessageAfter(SendMessageContext sendMessageContext) {
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/redis/TenantRedisCacheManager.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.tenant.core.redis;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.redis.core.TimeoutRedisCacheManager;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.util.Set;
/**
 * 多租户的 {@link RedisCacheManager} 实现类
 *
 * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀
 *
 * @author airhead
 */
@Slf4j
public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
    private final Set<String> ignoreCaches;
    public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
                                   RedisCacheConfiguration defaultCacheConfiguration,
                                   Set<String> ignoreCaches) {
        super(cacheWriter, defaultCacheConfiguration);
        this.ignoreCaches = ignoreCaches;
    }
    @Override
    public Cache getCache(String name) {
        // 如果开启多租户,则 name 拼接租户后缀
        if (!TenantContextHolder.isIgnore()
                && TenantContextHolder.getTenantId() != null
                && !CollUtil.contains(ignoreCaches, name)) {
            name = name + ":" + TenantContextHolder.getTenantId();
        }
        // 继续基于父方法
        return super.getCache(name);
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/rpc/TenantRequestInterceptor.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.tenant.core.rpc;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * Tenant 的 RequestInterceptor 实现类:Feign 请求时,将 {@link TenantContextHolder} 设置到 header 中,继续透传给被调用的服务
 *
 * @author iailab
 */
public class TenantRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            requestTemplate.header(HEADER_TENANT_ID, String.valueOf(tenantId));
        }
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/security/TenantSecurityWebFilter.java
对比新文件
@@ -0,0 +1,117 @@
package com.iailab.framework.tenant.core.security;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.framework.tenant.config.TenantProperties;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.framework.tenant.core.service.TenantFrameworkService;
import com.iailab.framework.web.config.WebProperties;
import com.iailab.framework.web.core.filter.ApiRequestFilter;
import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
 * 多租户 Security Web 过滤器
 * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
 * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
 * 3. 校验租户是合法,例如说被禁用、到期
 *
 * @author iailab
 */
@Slf4j
public class TenantSecurityWebFilter extends ApiRequestFilter {
    private final TenantProperties tenantProperties;
    private final AntPathMatcher pathMatcher;
    private final GlobalExceptionHandler globalExceptionHandler;
    private final TenantFrameworkService tenantFrameworkService;
    public TenantSecurityWebFilter(TenantProperties tenantProperties,
                                   WebProperties webProperties,
                                   GlobalExceptionHandler globalExceptionHandler,
                                   TenantFrameworkService tenantFrameworkService) {
        super(webProperties);
        this.tenantProperties = tenantProperties;
        this.pathMatcher = new AntPathMatcher();
        this.globalExceptionHandler = globalExceptionHandler;
        this.tenantFrameworkService = tenantFrameworkService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        Long tenantId = TenantContextHolder.getTenantId();
        // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
        LoginUser user = SecurityFrameworkUtils.getLoginUser();
        if (user != null) {
            // 如果获取不到租户编号,则尝试使用登陆用户的租户编号
            if (tenantId == null) {
                tenantId = user.getTenantId();
                TenantContextHolder.setTenantId(tenantId);
            // 如果传递了租户编号,则进行比对租户编号,避免越权问题
            } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
                log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
                        user.getTenantId(), user.getId(), user.getUserType(),
                        TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
                        "您无权访问该租户的数据"));
                return;
            }
        }
        // 如果非允许忽略租户的 URL,则校验租户是否合法
        if (!isIgnoreUrl(request)) {
            // 2. 如果请求未带租户的编号,不允许访问。
            if (tenantId == null) {
                log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
                        "请求的租户标识未传递,请进行排查"));
                return;
            }
            // 3. 校验租户是合法,例如说被禁用、到期
            try {
                tenantFrameworkService.validTenant(tenantId);
            } catch (Throwable ex) {
                CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
                ServletUtils.writeJSON(response, result);
                return;
            }
        } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
            if (tenantId == null) {
                TenantContextHolder.setIgnore(true);
            }
        }
        // 继续过滤
        chain.doFilter(request, response);
    }
    private boolean isIgnoreUrl(HttpServletRequest request) {
        // 快速匹配,保证性能
        if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
            return true;
        }
        // 逐个 Ant 路径匹配
        for (String url : tenantProperties.getIgnoreUrls()) {
            if (pathMatcher.match(url, request.getRequestURI())) {
                return true;
            }
        }
        return false;
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkService.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.tenant.core.service;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import java.util.List;
/**
 * Tenant 框架 Service 接口,定义获取租户信息
 *
 * @author iailab
 */
public interface TenantFrameworkService {
    /**
     * 获得所有租户
     *
     * @return 租户编号数组
     */
    List<Long> getTenantIds();
    /**
     * 校验租户是否合法
     *
     * @param id 租户编号
     */
    void validTenant(Long id);
    /**
     * 获得租户对应的数据源配置
     *
     * @param id 租户编号
     * @return 数据源配置
     */
    DataSourceProperty getDataSourceProperty(Long id);
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/service/TenantFrameworkServiceImpl.java
对比新文件
@@ -0,0 +1,103 @@
package com.iailab.framework.tenant.core.service;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.cache.CacheUtils;
import com.iailab.module.system.api.tenant.TenantApi;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.iailab.module.system.api.tenant.dto.TenantDataSourceConfigRespDTO;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import java.time.Duration;
import java.util.List;
import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
/**
 * Tenant 框架 Service 实现类
 *
 * @author iailab
 */
@RequiredArgsConstructor
public class TenantFrameworkServiceImpl implements TenantFrameworkService {
    private final TenantApi tenantApi;
    /**
     * 针对 {@link #getTenantIds()} 的缓存
     */
    private final LoadingCache<Object, List<Long>> getTenantIdsCache = buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<Object, List<Long>>() {
                @Override
                public List<Long> load(Object key) {
                    return tenantApi.getTenantIdList().getCheckedData();
                }
            });
    /**
     * 针对 {@link #validTenant(Long)} 的缓存
     */
    private final LoadingCache<Long, CommonResult<Boolean>> validTenantCache = buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<Long, CommonResult<Boolean>>() {
                @Override
                public CommonResult<Boolean> load(Long id) {
                    return tenantApi.validTenant(id);
                }
            });
    /**
     * 针对 {@link #getDataSourceProperty(Long)} 的缓存
     */
    private final LoadingCache<Long, DataSourceProperty> dataSourcePropertyCache = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<Long, DataSourceProperty>() {
                @Override
                public DataSourceProperty load(Long id) {
                    // 获得租户对应的数据源配置
                    TenantDataSourceConfigRespDTO dataSourceConfig = tenantApi.getTenantDataSourceConfig(id);
                    if (dataSourceConfig == null) {
                        return null;
                    }
                    // 转换成 dynamic-datasource 配置
//                    return new DataSourceProperty()
//                            .setPoolName(dataSourceConfig.getName()).setUrl(dataSourceConfig.getUrl())
//                            .setUsername(dataSourceConfig.getUsername()).setPassword(dataSourceConfig.getPassword());
                    DataSourceProperty ds = new DataSourceProperty();
                    ds.setPoolName(dataSourceConfig.getName());
                    ds.setUrl(dataSourceConfig.getUrl());
                    ds.setUsername(dataSourceConfig.getUsername());
                    ds.setPassword(dataSourceConfig.getPassword());
                    return ds;
                }
            });
    @Override
    @SneakyThrows
    public List<Long> getTenantIds() {
        return getTenantIdsCache.get(Boolean.TRUE);
    }
    @Override
    @SneakyThrows
    public void validTenant(Long id) {
        validTenantCache.get(id).checkError();
    }
    @Override
    @SneakyThrows
    public DataSourceProperty getDataSourceProperty(Long id) {
        return dataSourcePropertyCache.get(id);
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/util/TenantUtils.java
对比新文件
@@ -0,0 +1,93 @@
package com.iailab.framework.tenant.core.util;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import java.util.Map;
import java.util.concurrent.Callable;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * 多租户 Util
 *
 * @author iailab
 */
public class TenantUtils {
    /**
     * 使用指定租户,执行对应的逻辑
     *
     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
     * 当然,执行完成后,还是会恢复回去
     *
     * @param tenantId 租户编号
     * @param runnable 逻辑
     */
    public static void execute(Long tenantId, Runnable runnable) {
        Long oldTenantId = TenantContextHolder.getTenantId();
        Boolean oldIgnore = TenantContextHolder.isIgnore();
        try {
            TenantContextHolder.setTenantId(tenantId);
            TenantContextHolder.setIgnore(false);
            // 执行逻辑
            runnable.run();
        } finally {
            TenantContextHolder.setTenantId(oldTenantId);
            TenantContextHolder.setIgnore(oldIgnore);
        }
    }
    /**
     * 使用指定租户,执行对应的逻辑
     *
     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
     * 当然,执行完成后,还是会恢复回去
     *
     * @param tenantId 租户编号
     * @param callable 逻辑
     */
    public static <V> V execute(Long tenantId, Callable<V> callable) {
        Long oldTenantId = TenantContextHolder.getTenantId();
        Boolean oldIgnore = TenantContextHolder.isIgnore();
        try {
            TenantContextHolder.setTenantId(tenantId);
            TenantContextHolder.setIgnore(false);
            // 执行逻辑
            return callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            TenantContextHolder.setTenantId(oldTenantId);
            TenantContextHolder.setIgnore(oldIgnore);
        }
    }
    /**
     * 忽略租户,执行对应的逻辑
     *
     * @param runnable 逻辑
     */
    public static void executeIgnore(Runnable runnable) {
        Boolean oldIgnore = TenantContextHolder.isIgnore();
        try {
            TenantContextHolder.setIgnore(true);
            // 执行逻辑
            runnable.run();
        } finally {
            TenantContextHolder.setIgnore(oldIgnore);
        }
    }
    /**
     * 将多租户编号,添加到 header 中
     *
     * @param headers HTTP 请求 headers
     * @param tenantId 租户编号
     */
    public static void addTenantHeader(Map<String, String> headers, Long tenantId) {
        if (tenantId != null) {
            headers.put(HEADER_TENANT_ID, tenantId.toString());
        }
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/core/web/TenantContextWebFilter.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.tenant.core.web;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 多租户 Context Web 过滤器
 * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
 *
 * @author iailab
 */
public class TenantContextWebFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        // 设置
        Long tenantId = WebFrameworkUtils.getTenantId(request);
        if (tenantId != null) {
            TenantContextHolder.setTenantId(tenantId);
        }
        try {
            chain.doFilter(request, response);
        } finally {
            // 清理
            TenantContextHolder.clear();
        }
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/java/com/iailab/framework/tenant/package-info.java
对比新文件
@@ -0,0 +1,17 @@
/**
 * 多租户,支持如下层面:
 * 1. DB:基于 MyBatis Plus 多租户的功能实现。
 * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
 * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。
 * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。
 * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
 * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。
 * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见:
 *      1)Spring Async:
 *          {@link com.iailab.framework.quartz.config.IailabAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()}
 *      2)Spring Security:
 *          TransmittableThreadLocalSecurityContextHolderStrategy
 *          和 IailabSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
 *
 */
package com.iailab.framework.tenant;
iailab-framework/iailab-common-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java
对比新文件
@@ -0,0 +1,269 @@
/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.messaging.handler.invocation;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.framework.tenant.core.util.TenantUtils;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * Extension of {@link HandlerMethod} that invokes the underlying method with
 * argument values resolved from the current HTTP request through a list of
 * {@link HandlerMethodArgumentResolver}.
 *
 * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中
 * TODO iailab:持续跟进,看看有没新的拓展点
 *
 * @author Rossen Stoyanchev
 * @author Juergen Hoeller
 * @since 4.0
 */
public class InvocableHandlerMethod extends HandlerMethod {
    private static final Object[] EMPTY_ARGS = new Object[0];
    private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
    private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    /**
     * Create an instance from a {@code HandlerMethod}.
     */
    public InvocableHandlerMethod(HandlerMethod handlerMethod) {
        super(handlerMethod);
    }
    /**
     * Create an instance from a bean instance and a method.
     */
    public InvocableHandlerMethod(Object bean, Method method) {
        super(bean, method);
    }
    /**
     * Construct a new handler method with the given bean instance, method name and parameters.
     * @param bean the object bean
     * @param methodName the method name
     * @param parameterTypes the method parameter types
     * @throws NoSuchMethodException when the method cannot be found
     */
    public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
            throws NoSuchMethodException {
        super(bean, methodName, parameterTypes);
    }
    /**
     * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values.
     */
    public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
        this.resolvers = argumentResolvers;
    }
    /**
     * Set the ParameterNameDiscoverer for resolving parameter names when needed
     * (e.g. default request attribute name).
     * <p>Default is a {@link DefaultParameterNameDiscoverer}.
     */
    public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
        this.parameterNameDiscoverer = parameterNameDiscoverer;
    }
    /**
     * Invoke the method after resolving its argument values in the context of the given message.
     * <p>Argument values are commonly resolved through
     * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
     * The {@code providedArgs} parameter however may supply argument values to be used directly,
     * i.e. without argument resolution.
     * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
     * resolved arguments.
     * @param message the current message being processed
     * @param providedArgs "given" arguments matched by type, not resolved
     * @return the raw value returned by the invoked method
     * @throws Exception raised if no suitable argument resolver can be found,
     * or if the method raised an exception
     * @see #getMethodArgumentValues
     * @see #doInvoke
     */
    @Nullable
    public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
        Object[] args = getMethodArgumentValues(message, providedArgs);
        if (logger.isTraceEnabled()) {
            logger.trace("Arguments: " + Arrays.toString(args));
        }
        // 注意:如下是本类的改动点!!!
        // 情况一:无租户编号的情况
        Long tenantId= parseTenantId(message);
        if (tenantId == null) {
            return doInvoke(args);
        }
        // 情况二:有租户的情况下
        return TenantUtils.execute(tenantId, () -> doInvoke(args));
    }
    private Long parseTenantId(Message<?> message) {
        Object tenantId = message.getHeaders().get(HEADER_TENANT_ID);
        if (tenantId == null) {
            return null;
        }
        if (tenantId instanceof Long) {
            return (Long) tenantId;
        }
        if (tenantId instanceof Number) {
            return ((Number) tenantId).longValue();
        }
        if (tenantId instanceof String) {
            return Long.parseLong((String) tenantId);
        }
        if (tenantId instanceof byte[]) {
            return Long.parseLong(new String((byte[]) tenantId));
        }
        throw new IllegalArgumentException("未知的数据类型:" + tenantId);
    }
    /**
     * Get the method argument values for the current message, checking the provided
     * argument values and falling back to the configured argument resolvers.
     * <p>The resulting array will be passed into {@link #doInvoke}.
     * @since 5.1.2
     */
    protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
        MethodParameter[] parameters = getMethodParameters();
        if (ObjectUtils.isEmpty(parameters)) {
            return EMPTY_ARGS;
        }
        Object[] args = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            MethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            args[i] = findProvidedArgument(parameter, providedArgs);
            if (args[i] != null) {
                continue;
            }
            if (!this.resolvers.supportsParameter(parameter)) {
                throw new MethodArgumentResolutionException(
                        message, parameter, formatArgumentError(parameter, "No suitable resolver"));
            }
            try {
                args[i] = this.resolvers.resolveArgument(parameter, message);
            }
            catch (Exception ex) {
                // Leave stack trace for later, exception may actually be resolved and handled...
                if (logger.isDebugEnabled()) {
                    String exMsg = ex.getMessage();
                    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                        logger.debug(formatArgumentError(parameter, exMsg));
                    }
                }
                throw ex;
            }
        }
        return args;
    }
    /**
     * Invoke the handler method with the given argument values.
     */
    @Nullable
    protected Object doInvoke(Object... args) throws Exception {
        try {
            return getBridgedMethod().invoke(getBean(), args);
        }
        catch (IllegalArgumentException ex) {
            assertTargetBean(getBridgedMethod(), getBean(), args);
            String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
            throw new IllegalStateException(formatInvokeError(text, args), ex);
        }
        catch (InvocationTargetException ex) {
            // Unwrap for HandlerExceptionResolvers ...
            Throwable targetException = ex.getTargetException();
            if (targetException instanceof RuntimeException) {
                throw (RuntimeException) targetException;
            }
            else if (targetException instanceof Error) {
                throw (Error) targetException;
            }
            else if (targetException instanceof Exception) {
                throw (Exception) targetException;
            }
            else {
                throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
            }
        }
    }
    MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
        return new AsyncResultMethodParameter(returnValue);
    }
    private class AsyncResultMethodParameter extends HandlerMethodParameter {
        @Nullable
        private final Object returnValue;
        private final ResolvableType returnType;
        public AsyncResultMethodParameter(@Nullable Object returnValue) {
            super(-1);
            this.returnValue = returnValue;
            this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
        }
        protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
            super(original);
            this.returnValue = original.returnValue;
            this.returnType = original.returnType;
        }
        @Override
        public Class<?> getParameterType() {
            if (this.returnValue != null) {
                return this.returnValue.getClass();
            }
            if (!ResolvableType.NONE.equals(this.returnType)) {
                return this.returnType.toClass();
            }
            return super.getParameterType();
        }
        @Override
        public Type getGenericParameterType() {
            return this.returnType.getType();
        }
        @Override
        public AsyncResultMethodParameter clone() {
            return new AsyncResultMethodParameter(this);
        }
    }
}
iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring.factories
对比新文件
@@ -0,0 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.iailab.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor
iailab-framework/iailab-common-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,2 @@
com.iailab.framework.tenant.config.IailabTenantRpcAutoConfiguration
com.iailab.framework.tenant.config.IailabTenantAutoConfiguration
iailab-framework/iailab-common-env/pom.xml
对比新文件
@@ -0,0 +1,66 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-env</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        开发环境拓展,实现类似阿里的特性环境的能力
        1. https://segmentfault.com/a/1190000018022987
    </description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
        </dependency>
        <!-- RPC 相关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-core</artifactId>
        </dependency>
        <!-- Registry 注册中心相关 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvEnvironmentPostProcessor.java
对比新文件
@@ -0,0 +1,50 @@
package com.iailab.framework.env.config;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.util.collection.SetUtils;
import com.iailab.framework.env.core.util.EnvUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.Set;
import static com.iailab.framework.env.core.util.EnvUtils.HOST_NAME_VALUE;
/**
 * 多环境的 {@link EnvEnvironmentPostProcessor} 实现类
 * 将 iailab.env.tag 设置到 nacos 等组件对应的 tag 配置项,当且仅当它们不存在时
 *
 * @author iailab
 */
public class EnvEnvironmentPostProcessor implements EnvironmentPostProcessor {
    private static final Set<String> TARGET_TAG_KEYS = SetUtils.asSet(
            "spring.cloud.nacos.discovery.metadata.tag" // Nacos 注册中心
            // MQ TODO
    );
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 0. 设置 ${HOST_NAME} 兜底的环境变量
        String hostNameKey = StrUtil.subBetween(HOST_NAME_VALUE, "{", "}");
        if (!environment.containsProperty(hostNameKey)) {
            environment.getSystemProperties().put(hostNameKey, EnvUtils.getHostName());
        }
        // 1.1 如果没有 iailab.env.tag 配置项,则不进行配置项的修改
        String tag = EnvUtils.getTag(environment);
        if (StrUtil.isEmpty(tag)) {
            return;
        }
        // 1.2 需要修改的配置项
        for (String targetTagKey : TARGET_TAG_KEYS) {
            String targetTagValue = environment.getProperty(targetTagKey);
            if (StrUtil.isNotEmpty(targetTagValue)) {
                continue;
            }
            environment.getSystemProperties().put(targetTagKey, tag);
        }
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/EnvProperties.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.env.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
 * 环境配置
 *
 * @author iailab
 */
@ConfigurationProperties(prefix = "iailab.env")
@Data
public class EnvProperties {
    public static final String TAG_KEY = "iailab.env.tag";
    /**
     * 环境标签
     */
    private String tag;
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvRpcAutoConfiguration.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.env.config;
import com.iailab.framework.env.core.fegin.EnvLoadBalancerClientFactory;
import com.iailab.framework.env.core.fegin.EnvRequestInterceptor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import java.util.Collections;
import java.util.List;
/**
 * 多环境的 RPC 组件的自动配置
 *
 * @author iailab
 */
@AutoConfiguration
@EnableConfigurationProperties(EnvProperties.class)
public class IailabEnvRpcAutoConfiguration {
    // ========== Feign 相关 ==========
    /**
     * 创建 {@link EnvLoadBalancerClientFactory} Bean
     *
     * 参考 {@link LoadBalancerAutoConfiguration#loadBalancerClientFactory(LoadBalancerClientsProperties)} 方法
     */
    @Bean
    public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties,
                                                               ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
        EnvLoadBalancerClientFactory clientFactory = new EnvLoadBalancerClientFactory(properties);
        clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
        return clientFactory;
    }
    @Bean
    public EnvRequestInterceptor envRequestInterceptor() {
        return new EnvRequestInterceptor();
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/config/IailabEnvWebAutoConfiguration.java
对比新文件
@@ -0,0 +1,32 @@
package com.iailab.framework.env.config;
import com.iailab.framework.common.enums.WebFilterOrderEnum;
import com.iailab.framework.env.core.web.EnvWebFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
/**
 * 多环境的 Web 组件的自动配置
 *
 * @author iailab
 */
@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableConfigurationProperties(EnvProperties.class)
public class IailabEnvWebAutoConfiguration {
    /**
     * 创建 {@link EnvWebFilter} Bean
     */
    @Bean
    public FilterRegistrationBean<EnvWebFilter> envWebFilterFilter() {
        EnvWebFilter filter = new EnvWebFilter();
        FilterRegistrationBean<EnvWebFilter> bean = new FilterRegistrationBean<>(filter);
        bean.setOrder(WebFilterOrderEnum.ENV_TAG_FILTER);
        return bean;
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/context/EnvContextHolder.java
对比新文件
@@ -0,0 +1,39 @@
package com.iailab.framework.env.core.context;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.ArrayList;
import java.util.List;
/**
 * 开发环境上下文
 *
 * @author iailab
 */
public class EnvContextHolder {
    /**
     * 标签的上下文
     *
     * 使用 {@link List} 的原因,可能存在多层设置或者清理
     */
    private static final ThreadLocal<List<String>> TAG_CONTEXT = TransmittableThreadLocal.withInitial(ArrayList::new);
    public static void setTag(String tag) {
        TAG_CONTEXT.get().add(tag);
    }
    public static String getTag() {
        return CollUtil.getLast(TAG_CONTEXT.get());
    }
    public static void removeTag() {
        List<String> tags = TAG_CONTEXT.get();
        if (CollUtil.isEmpty(tags)) {
            return;
        }
        tags.remove(tags.size() - 1);
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClient.java
对比新文件
@@ -0,0 +1,83 @@
package com.iailab.framework.env.core.fegin;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.util.collection.CollectionUtils;
import com.iailab.framework.env.core.context.EnvContextHolder;
import com.iailab.framework.env.core.util.EnvUtils;
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;
import java.util.List;
/**
 * 多环境的 {@link org.springframework.cloud.client.loadbalancer.LoadBalancerClient} 实现类
 * 在从服务实例列表选择时,优先选择 tag 匹配的服务实例
 *
 * @author iailab
 */
@RequiredArgsConstructor
@Slf4j
public class EnvLoadBalancerClient implements ReactorServiceInstanceLoadBalancer {
    /**
     * 用于获取 serviceId 对应的服务实例的列表
     */
    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    /**
     * 需要获取的服务实例名
     *
     * 暂时用于打印 logger 日志
     */
    private final String serviceId;
    /**
     * 被代理的 ReactiveLoadBalancer 对象
     */
    private final ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer;
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 情况一,没有 tag 时,使用默认的 reactiveLoadBalancer 实现负载均衡
        String tag = EnvContextHolder.getTag();
        if (StrUtil.isEmpty(tag)) {
            return Mono.from(reactiveLoadBalancer.choose(request));
        }
        // 情况二,有 tag 时,使用 tag 匹配服务实例
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map(list -> getInstanceResponse(list, tag));
    }
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, String tag) {
        // 如果服务实例为空,则直接返回
        if (CollUtil.isEmpty(instances)) {
            log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
            return new EmptyResponse();
        }
        // 筛选满足条件的实例列表
        List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
        if (CollUtil.isEmpty(chooseInstances)) {
            log.warn("[getInstanceResponse][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
            chooseInstances = instances;
        }
        // TODO iailab:https://juejin.cn/post/7056770721858469896 想通网段
        // 随机 + 权重获取实例列表 TODO iailab:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
        return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvLoadBalancerClientFactory.java
对比新文件
@@ -0,0 +1,30 @@
package com.iailab.framework.env.core.fegin;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
/**
 * 多环境的 {@link LoadBalancerClientFactory} 实现类
 * 目的:在创建 {@link ReactiveLoadBalancer} 时,会额外增加 {@link EnvLoadBalancerClient} 代理,用于 tag 过滤服务实例
 *
 * @author iailab
 */
public class EnvLoadBalancerClientFactory extends LoadBalancerClientFactory {
    public EnvLoadBalancerClientFactory(LoadBalancerClientsProperties properties) {
        super(properties);
    }
    @Override
    public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
        ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = super.getInstance(serviceId);
        // 参考 {@link com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration#nacosLoadBalancer(Environment, LoadBalancerClientFactory, NacosDiscoveryProperties)} 方法
        return new EnvLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
                serviceId, reactiveLoadBalancer);
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/fegin/EnvRequestInterceptor.java
对比新文件
@@ -0,0 +1,24 @@
package com.iailab.framework.env.core.fegin;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.env.core.context.EnvContextHolder;
import com.iailab.framework.env.core.util.EnvUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
 * 多环境的 {@link RequestInterceptor} 实现类:Feign 请求时,将 tag 设置到 header 中,继续透传给被调用的服务
 *
 * @author iailab
 */
public class EnvRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        String tag = EnvContextHolder.getTag();
        if (StrUtil.isNotEmpty(tag)) {
            EnvUtils.setTag(requestTemplate, tag);
        }
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/package-info.java
对比新文件
@@ -0,0 +1 @@
package com.iailab.framework.env.core;
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/util/EnvUtils.java
对比新文件
@@ -0,0 +1,56 @@
package com.iailab.framework.env.core.util;
import com.iailab.framework.env.config.EnvProperties;
import feign.RequestTemplate;
import lombok.SneakyThrows;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.env.Environment;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.util.Objects;
/**
 * 环境 Utils
 *
 * @author iailab
 */
public class EnvUtils {
    private static final String HEADER_TAG = "tag";
    public static final String HOST_NAME_VALUE = "${HOSTNAME}";
    public static String getTag(HttpServletRequest request) {
        String tag = request.getHeader(HEADER_TAG);
        // 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名
        // 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做
        return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag;
    }
    public static String getTag(ServiceInstance instance) {
        return instance.getMetadata().get(HEADER_TAG);
    }
    public static String getTag(Environment environment) {
        String tag = environment.getProperty(EnvProperties.TAG_KEY);
        // 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名
        // 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做
        return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag;
    }
    public static void setTag(RequestTemplate requestTemplate, String tag) {
        requestTemplate.header(HEADER_TAG, tag);
    }
    /**
     * 获得 hostname 主机名
     *
     * @return 主机名
     */
    @SneakyThrows
    public static String getHostName() {
        return InetAddress.getLocalHost().getHostName();
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/core/web/EnvWebFilter.java
对比新文件
@@ -0,0 +1,41 @@
package com.iailab.framework.env.core.web;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.env.core.context.EnvContextHolder;
import com.iailab.framework.env.core.util.EnvUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 环境的 {@link javax.servlet.Filter} 实现类
 * 当有 tag 请求头时,设置到 {@link EnvContextHolder} 的标签上下文
 *
 * @author iailab
 */
public class EnvWebFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        // 如果没有 tag,则走默认的流程
        String tag = EnvUtils.getTag(request);
        if (StrUtil.isEmpty(tag)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果有 tag,则设置到上下文
        EnvContextHolder.setTag(tag);
        try {
            chain.doFilter(request, response);
        } finally {
            EnvContextHolder.removeTag();
        }
    }
}
iailab-framework/iailab-common-env/src/main/java/com/iailab/framework/env/package-info.java
对比新文件
@@ -0,0 +1,7 @@
/**
 * 开发环境拓展,实现类似阿里的特性环境的能力
 * 1. https://segmentfault.com/a/1190000018022987
 *
 * @author iailab
 */
package com.iailab.framework.env;
iailab-framework/iailab-common-env/src/main/resources/META-INF/spring.factories
对比新文件
@@ -0,0 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
    com.iailab.framework.env.config.EnvEnvironmentPostProcessor
iailab-framework/iailab-common-env/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,2 @@
com.iailab.framework.env.config.IailabEnvWebAutoConfiguration
com.iailab.framework.env.config.IailabEnvRpcAutoConfiguration
iailab-framework/iailab-common-excel/pom.xml
对比新文件
@@ -0,0 +1,82 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-excel</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>Excel 拓展</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- RPC 远程调用相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-rpc</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 业务组件 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行 Dict 的查询 -->
            <version>${revision}</version>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 -->
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 -->
        </dependency>
        <!-- 工具类相关 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-biz-ip</artifactId>
            <optional>true</optional> <!-- 设置为 optional,只有在 AreaConvert 的时候使用 -->
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictAutoConfiguration.java
对比新文件
@@ -0,0 +1,18 @@
package com.iailab.framework.dict.config;
import com.iailab.framework.dict.core.DictFrameworkUtils;
import com.iailab.module.system.api.dict.DictDataApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class IailabDictAutoConfiguration {
    @Bean
    @SuppressWarnings("InstantiationOfUtilityClass")
    public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) {
        DictFrameworkUtils.init(dictDataApi);
        return new DictFrameworkUtils();
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/config/IailabDictRpcAutoConfiguration.java
对比新文件
@@ -0,0 +1,15 @@
package com.iailab.framework.dict.config;
import com.iailab.module.system.api.dict.DictDataApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
 * 字典用到 Feign 的配置项
 *
 * @author iailab
 */
@AutoConfiguration
@EnableFeignClients(clients = DictDataApi.class) // 主要是引入相关的 API 服务
public class IailabDictRpcAutoConfiguration {
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/core/DictFrameworkUtils.java
对比新文件
@@ -0,0 +1,96 @@
package com.iailab.framework.dict.core;
import cn.hutool.core.util.ObjectUtil;
import com.iailab.framework.common.core.KeyValue;
import com.iailab.framework.common.util.cache.CacheUtils;
import com.iailab.module.system.api.dict.DictDataApi;
import com.iailab.module.system.api.dict.dto.DictDataRespDTO;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.List;
/**
 * 字典工具类
 *
 * @author iailab
 */
@Slf4j
public class DictFrameworkUtils {
    private static DictDataApi dictDataApi;
    private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();
    // TODO @puhui999:GET_DICT_DATA_CACHE、GET_DICT_DATA_LIST_CACHE、PARSE_DICT_DATA_CACHE 这 3 个缓存是有点重叠,可以思考下,有没可能减少 1 个。微信讨论好私聊,再具体改哈
    /**
     * 针对 {@link #getDictDataLabel(String, String)} 的缓存
     */
    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
                @Override
                public DictDataRespDTO load(KeyValue<String, String> key) {
                    return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL);
                }
            });
    /**
     * 针对 {@link #getDictDataLabelList(String)} 的缓存
     */
    private static final LoadingCache<String, List<String>> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<String, List<String>>() {
                @Override
                public List<String> load(String dictType) {
                    return dictDataApi.getDictDataLabelList(dictType);
                }
            });
    /**
     * 针对 {@link #parseDictDataValue(String, String)} 的缓存
     */
    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
                @Override
                public DictDataRespDTO load(KeyValue<String, String> key) {
                    return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL);
                }
            });
    public static void init(DictDataApi dictDataApi) {
        DictFrameworkUtils.dictDataApi = dictDataApi;
        log.info("[init][初始化 DictFrameworkUtils 成功]");
    }
    @SneakyThrows
    public static String getDictDataLabel(String dictType, Integer value) {
        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel();
    }
    @SneakyThrows
    public static String getDictDataLabel(String dictType, String value) {
        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
    }
    @SneakyThrows
    public static List<String> getDictDataLabelList(String dictType) {
        return GET_DICT_DATA_LIST_CACHE.get(dictType);
    }
    @SneakyThrows
    public static String parseDictDataValue(String dictType, String label) {
        return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/dict/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * 字典数据模块,提供 {@link com.iailab.framework.dict.core.DictFrameworkUtils} 工具类
 *
 * 通过将字典缓存在内存中,保证性能
 */
package com.iailab.framework.dict;
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/DictFormat.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.excel.core.annotations;
import java.lang.annotation.*;
/**
 * 字典格式化
 *
 * 实现将字典数据的值,格式化成字典数据的标签
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DictFormat {
    /**
     * 例如说,SysDictTypeConstants、InfDictTypeConstants
     *
     * @return 字典类型
     */
    String value();
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/annotations/ExcelColumnSelect.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.excel.core.annotations;
import java.lang.annotation.*;
/**
 * 给 Excel 列添加下拉选择数据
 *
 * 其中 {@link #dictType()} 和 {@link #functionName()} 二选一
 *
 * @author HUIHUI
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelColumnSelect {
    /**
     * @return 字典类型
     */
    String dictType() default "";
    /**
     * @return 获取下拉数据源的方法名称
     */
    String functionName() default "";
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/AreaConvert.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.excel.core.convert;
import cn.hutool.core.convert.Convert;
import com.iailab.framework.ip.core.Area;
import com.iailab.framework.ip.core.utils.AreaUtils;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
/**
 * Excel 数据地区转换器
 *
 * @author HUIHUI
 */
@Slf4j
public class AreaConvert implements Converter<Object> {
    @Override
    public Class<?> supportJavaTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
                                    GlobalConfiguration globalConfiguration) {
        // 解析地区编号
        String label = readCellData.getStringValue();
        Area area = AreaUtils.parseArea(label);
        if (area == null) {
            log.error("[convertToJavaData][label({}) 解析不掉]", label);
            return null;
        }
        // 将 value 转换成对应的属性
        Class<?> fieldClazz = contentProperty.getField().getType();
        return Convert.convert(fieldClazz, area.getId());
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/DictConvert.java
对比新文件
@@ -0,0 +1,72 @@
package com.iailab.framework.excel.core.convert;
import cn.hutool.core.convert.Convert;
import com.iailab.framework.dict.core.DictFrameworkUtils;
import com.iailab.framework.excel.core.annotations.DictFormat;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
/**
 * Excel 数据字典转换器
 *
 * @author iailab
 */
@Slf4j
public class DictConvert implements Converter<Object> {
    @Override
    public Class<?> supportJavaTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
                                    GlobalConfiguration globalConfiguration) {
        // 使用字典解析
        String type = getType(contentProperty);
        String label = readCellData.getStringValue();
        String value = DictFrameworkUtils.parseDictDataValue(type, label);
        if (value == null) {
            log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
            return null;
        }
        // 将 String 的 value 转换成对应的属性
        Class<?> fieldClazz = contentProperty.getField().getType();
        return Convert.convert(fieldClazz, value);
    }
    @Override
    public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
                                                    GlobalConfiguration globalConfiguration) {
        // 空时,返回空
        if (object == null) {
            return new WriteCellData<>("");
        }
        // 使用字典格式化
        String type = getType(contentProperty);
        String value = String.valueOf(object);
        String label = DictFrameworkUtils.getDictDataLabel(type, value);
        if (label == null) {
            log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
            return new WriteCellData<>("");
        }
        // 生成 Excel 小表格
        return new WriteCellData<>(label);
    }
    private static String getType(ExcelContentProperty contentProperty) {
        return contentProperty.getField().getAnnotation(DictFormat.class).value();
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/JsonConvert.java
对比新文件
@@ -0,0 +1,34 @@
package com.iailab.framework.excel.core.convert;
import com.iailab.framework.common.util.json.JsonUtils;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
 * Excel Json 转换器
 *
 * @author iailab
 */
public class JsonConvert implements Converter<Object> {
    @Override
    public Class<?> supportJavaTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public WriteCellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
                                                    GlobalConfiguration globalConfiguration) {
        // 生成 Excel 小表格
        return new WriteCellData<>(JsonUtils.toJsonString(value));
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/convert/MoneyConvert.java
对比新文件
@@ -0,0 +1,39 @@
package com.iailab.framework.excel.core.convert;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
 * 金额转换器
 *
 * 金额单位:分
 *
 * @author iailab
 */
public class MoneyConvert implements Converter<Integer> {
    @Override
    public Class<?> supportJavaTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public WriteCellData<String> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
                                                    GlobalConfiguration globalConfiguration) {
        BigDecimal result = BigDecimal.valueOf(value)
                .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
        return new WriteCellData<>(result.toString());
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/function/ExcelColumnSelectFunction.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.excel.core.function;
import java.util.List;
/**
 * Excel 列下拉数据源获取接口
 *
 * 为什么不直接解析字典还搞个接口?考虑到有的下拉数据不是从字典中获取的所有需要做一个兼容
 * @author HUIHUI
 */
public interface ExcelColumnSelectFunction {
    /**
     * 获得方法名称
     *
     * @return 方法名称
     */
    String getName();
    /**
     * 获得列下拉数据源
     *
     * @return 下拉数据源
     */
    List<String> getOptions();
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/handler/SelectSheetWriteHandler.java
对比新文件
@@ -0,0 +1,186 @@
package com.iailab.framework.excel.core.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.poi.excel.ExcelUtil;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.iailab.framework.common.core.KeyValue;
import com.iailab.framework.dict.core.DictFrameworkUtils;
import com.iailab.framework.excel.core.annotations.ExcelColumnSelect;
import com.iailab.framework.excel.core.function.ExcelColumnSelectFunction;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFDataValidation;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import java.lang.reflect.Field;
import java.util.*;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
/**
 * 基于固定 sheet 实现下拉框
 *
 * @author HUIHUI
 */
@Slf4j
public class SelectSheetWriteHandler implements SheetWriteHandler {
    /**
     * 数据起始行从 0 开始
     * 约定:本项目第一行有标题所以从 1 开始如果您的 Excel 有多行标题请自行更改
     */
    public static final int FIRST_ROW = 1;
    /**
     * 下拉列需要创建下拉框的行数,默认两千行如需更多请自行调整
     */
    public static final int LAST_ROW = 2000;
    private static final String DICT_SHEET_NAME = "字典sheet";
    /**
     * key: 列 value: 下拉数据源
     */
    private final Map<Integer, List<String>> selectMap = new HashMap<>();
    private static Boolean ifSetSelect;
    public SelectSheetWriteHandler(Class<?> head, Boolean selectFlag) {
        ifSetSelect = selectFlag;
        // 加载下拉数据获取接口
        Map<String, ExcelColumnSelectFunction> beansMap = SpringUtil.getBeanFactory().getBeansOfType(ExcelColumnSelectFunction.class);
        if (MapUtil.isEmpty(beansMap)) {
            return;
        }
        List<Field> fields = new ArrayList<>();
        for (Class<?> c = head; c != null; c = c.getSuperclass()) {
            Collections.addAll(fields, c.getDeclaredFields());
        }
        // 解析下拉数据
        int colIndex = 0;
        for (Field field : fields) {
            if (field.isAnnotationPresent(ExcelColumnSelect.class)) {
                ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
                if (excelProperty != null && excelProperty.index() != -1) {
                    getSelectDataList(excelProperty.index(), field);
                }else{
                    getSelectDataList(colIndex, field);
                }
            }
            colIndex++;
        }
    }
    /**
     * 获得下拉数据,并添加到 {@link #selectMap} 中
     *
     * @param colIndex 列索引
     * @param field    字段
     */
    private void getSelectDataList(int colIndex, Field field) {
        ExcelColumnSelect columnSelect = field.getAnnotation(ExcelColumnSelect.class);
        String dictType = columnSelect.dictType();
        String functionName = columnSelect.functionName();
        Assert.isTrue(ObjectUtil.isNotEmpty(dictType) || ObjectUtil.isNotEmpty(functionName),
                "Field({}) 的 @ExcelColumnSelect 注解,dictType 和 functionName 不能同时为空", field.getName());
        // 情况一:使用 dictType 获得下拉数据
        if (StrUtil.isNotEmpty(dictType)) { // 情况一: 字典数据 (默认)
            selectMap.put(colIndex, DictFrameworkUtils.getDictDataLabelList(dictType));
            return;
        }
        // 情况二:使用 functionName 获得下拉数据
        Map<String, ExcelColumnSelectFunction> functionMap = SpringUtil.getApplicationContext().getBeansOfType(ExcelColumnSelectFunction.class);
        ExcelColumnSelectFunction function = CollUtil.findOne(functionMap.values(), item -> item.getName().equals(functionName));
        Assert.notNull(function, "未找到对应的 function({})", functionName);
        selectMap.put(colIndex, function.getOptions());
    }
    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        if (CollUtil.isEmpty(selectMap)) {
            return;
        }
        // 1. 获取相应操作对象
        DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手
        Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿
        List<KeyValue<Integer, List<String>>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue()));
        keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错
        if (ifSetSelect){
            for (KeyValue<Integer, List<String>> keyValue : keyValues) {
                /*起始行、终止行、起始列、终止列  起始行为1即表示表头不设置**/
                CellRangeAddressList addressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW, keyValue.getKey(), keyValue.getKey());
                /*设置下拉框数据**/
                DataValidationConstraint constraint = helper.createExplicitListConstraint(keyValue.getValue().toArray(new String[0]));
                DataValidation dataValidation = helper.createValidation(constraint, addressList);
                if (dataValidation instanceof HSSFDataValidation) {
                    dataValidation.setSuppressDropDownArrow(false);
                } else {
                    dataValidation.setSuppressDropDownArrow(true);
                    dataValidation.setShowErrorBox(true);
                }
                // 2.2 阻止输入非下拉框的值
                dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
                dataValidation.createErrorBox("提示", "此值不存在于下拉选择中!");
                writeSheetHolder.getSheet().addValidationData(dataValidation);
            }
        }else{
            // 2. 创建数据字典的 sheet 页
            Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME);
            for (KeyValue<Integer, List<String>> keyValue : keyValues) {
                int rowLength = keyValue.getValue().size();
                // 2.1 设置字典 sheet 页的值,每一列一部字典项
                for (int i = 0; i < rowLength; i++) {
                    Row row = dictSheet.getRow(i);
                    if (row == null) {
                        row = dictSheet.createRow(i);
                    }
                    row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i));
                }
                // 2.2 设置单元格下拉选择
                setColumnSelect(writeSheetHolder, workbook, helper, keyValue);
            }
        }
    }
    /**
     * 设置单元格下拉选择
     */
    private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper,
                                        KeyValue<Integer, List<String>> keyValue) {
        // 1.1 创建可被其他单元格引用的名称
        Name name = workbook.createName();
        String excelColumn = ExcelUtil.indexToColName(keyValue.getKey());
        // 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2
        String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size();
        name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字
        name.setRefersToFormula(refers); // 设置公式
        // 2.1 设置约束
        DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束
        // 设置下拉单元格的首行、末行、首列、末列
        CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW,
                keyValue.getKey(), keyValue.getKey());
        DataValidation validation = helper.createValidation(constraint, rangeAddressList);
        if (validation instanceof HSSFDataValidation) {
            validation.setSuppressDropDownArrow(false);
        } else {
            validation.setSuppressDropDownArrow(true);
            validation.setShowErrorBox(true);
        }
        // 2.2 阻止输入非下拉框的值
        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
        validation.createErrorBox("提示", "此值不存在于下拉选择中!");
        // 2.3 添加下拉框约束
        writeSheetHolder.getSheet().addValidationData(validation);
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/core/util/ExcelUtils.java
对比新文件
@@ -0,0 +1,66 @@
package com.iailab.framework.excel.core.util;
import com.iailab.framework.excel.core.handler.SelectSheetWriteHandler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
 * Excel 工具类
 *
 * @author iailab
 */
public class ExcelUtils {
    /**
     * 将列表以 Excel 响应给前端
     *
     * @param response  响应
     * @param filename  文件名
     * @param sheetName Excel sheet 名
     * @param head      Excel head 头
     * @param data      数据列表哦
     * @param <T>       泛型,保证 head 和 data 类型的一致性
     * @throws IOException 写入失败的情况
     */
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) throws IOException {
        // 输出 Excel
        EasyExcel.write(response.getOutputStream(), head)
                .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
                .registerWriteHandler(new SelectSheetWriteHandler(head,false)) // 基于固定 sheet 实现下拉框
                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                .sheet(sheetName).doWrite(data);
        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        return EasyExcel.read(file.getInputStream(), head, null)
                .autoCloseStream(false)  // 不要自动关闭,交给 Servlet 自己处理
                .doReadAllSync();
    }
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data, boolean selectFlag) throws IOException {
        // 输出 Excel
        EasyExcel.write(response.getOutputStream(), head)
                .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
                .registerWriteHandler(new SelectSheetWriteHandler(head,selectFlag)) // 基于固定 sheet 实现下拉框
                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                .sheet(sheetName).doWrite(data);
        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }
}
iailab-framework/iailab-common-excel/src/main/java/com/iailab/framework/excel/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 基于 EasyExcel 实现 Excel 相关的操作
 */
package com.iailab.framework.excel;
iailab-framework/iailab-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,2 @@
com.iailab.framework.dict.config.IailabDictRpcAutoConfiguration
com.iailab.framework.dict.config.IailabDictAutoConfiguration
iailab-framework/iailab-common-excel/src/test/java/com/iailab/framework/dict/core/util/DictFrameworkUtilsTest.java
对比新文件
@@ -0,0 +1,51 @@
package com.iailab.framework.dict.core.util;
import com.iailab.framework.common.enums.CommonStatusEnum;
import com.iailab.framework.dict.core.DictFrameworkUtils;
import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
import com.iailab.module.system.api.dict.DictDataApi;
import com.iailab.module.system.api.dict.dto.DictDataRespDTO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import static com.iailab.framework.common.pojo.CommonResult.success;
import static com.iailab.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
/**
 * {@link DictFrameworkUtils} 的单元测试
 */
public class DictFrameworkUtilsTest extends BaseMockitoUnitTest {
    @Mock
    private DictDataApi dictDataApi;
    @BeforeEach
    public void setUp() {
        DictFrameworkUtils.init(dictDataApi);
    }
    @Test
    public void testGetDictDataLabel() {
        // mock 数据
        DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
        // mock 方法
        when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(success(dataRespDTO));
        // 断言返回值
        assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue()));
    }
    @Test
    public void testParseDictDataValue() {
        // mock 数据
        DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
        // mock 方法
        when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(success(resp));
        // 断言返回值
        assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel()));
    }
}
iailab-framework/iailab-common-job/pom.xml
对比新文件
@@ -0,0 +1,50 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-job</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>任务拓展,基于 XXL-Job 实现</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Job 相关 -->
        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
        </dependency>
        <!-- 工具类相关 -->
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabAsyncAutoConfiguration.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.quartz.config;
import com.alibaba.ttl.TtlRunnable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
 * 异步任务 Configuration
 */
@AutoConfiguration
@EnableAsync
public class IailabAsyncAutoConfiguration {
    @Bean
    public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if (!(bean instanceof ThreadPoolTaskExecutor)) {
                    return bean;
                }
                // 修改提交的任务,接入 TransmittableThreadLocal
                ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean;
                executor.setTaskDecorator(TtlRunnable::get);
                return executor;
            }
        };
    }
}
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/IailabXxlJobAutoConfiguration.java
对比新文件
@@ -0,0 +1,47 @@
package com.iailab.framework.quartz.config;
import com.xxl.job.core.executor.XxlJobExecutor;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
 * XXL-Job 自动配置类
 *
 * @author iailab
 */
@AutoConfiguration
@ConditionalOnClass(XxlJobSpringExecutor.class)
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties({XxlJobProperties.class})
@EnableScheduling // 开启 Spring 自带的定时任务
@Slf4j
public class IailabXxlJobAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public XxlJobExecutor xxlJobExecutor(XxlJobProperties properties) {
        log.info("[xxlJobExecutor][初始化 XXL-Job 执行器的配置]");
        XxlJobProperties.AdminProperties admin = properties.getAdmin();
        XxlJobProperties.ExecutorProperties executor = properties.getExecutor();
        // 初始化执行器
        XxlJobExecutor xxlJobExecutor = new XxlJobSpringExecutor();
        xxlJobExecutor.setIp(executor.getIp());
        xxlJobExecutor.setPort(executor.getPort());
        xxlJobExecutor.setAppname(executor.getAppName());
        xxlJobExecutor.setLogPath(executor.getLogPath());
        xxlJobExecutor.setLogRetentionDays(executor.getLogRetentionDays());
        xxlJobExecutor.setAdminAddresses(admin.getAddresses());
        xxlJobExecutor.setAccessToken(properties.getAccessToken());
        return xxlJobExecutor;
    }
}
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/config/XxlJobProperties.java
对比新文件
@@ -0,0 +1,99 @@
package com.iailab.framework.quartz.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
 * XXL-Job 配置类
 */
@ConfigurationProperties("xxl.job")
@Validated
@Data
public class XxlJobProperties {
    /**
     * 是否开启,默认为 true 关闭
     */
    private Boolean enabled = true;
    /**
     * 访问令牌
     */
    private String accessToken;
    /**
     * 控制器配置
     */
    @NotNull(message = "控制器配置不能为空")
    private AdminProperties admin;
    /**
     * 执行器配置
     */
    @NotNull(message = "执行器配置不能为空")
    private ExecutorProperties executor;
    /**
     * XXL-Job 调度器配置类
     */
    @Data
    @Valid
    public static class AdminProperties {
        /**
         * 调度器地址
         */
        @NotEmpty(message = "调度器地址不能为空")
        private String addresses;
    }
    /**
     * XXL-Job 执行器配置类
     */
    @Data
    @Valid
    public static class ExecutorProperties {
        /**
         * 默认端口
         *
         * 这里使用 -1 表示随机
         */
        private static final Integer PORT_DEFAULT = -1;
        /**
         * 默认日志保留天数
         *
         * 如果想永久保留,则设置为 -1
         */
        private static final Integer LOG_RETENTION_DAYS_DEFAULT = 30;
        /**
         * 应用名
         */
        @NotEmpty(message = "应用名不能为空")
        private String appName;
        /**
         * 执行器的 IP
         */
        private String ip;
        /**
         * 执行器的 Port
         */
        private Integer port = PORT_DEFAULT;
        /**
         * 日志地址
         */
        @NotEmpty(message = "日志地址不能为空")
        private String logPath;
        /**
         * 日志保留天数
         */
        private Integer logRetentionDays = LOG_RETENTION_DAYS_DEFAULT;
    }
}
iailab-framework/iailab-common-job/src/main/java/com/iailab/framework/quartz/package-info.java
对比新文件
@@ -0,0 +1,5 @@
/**
 * 1. 定时任务,基于 XXL-Job 实现。
 * 2. 异步任务,采用 Spring Async 异步执行。
 */
package com.iailab.framework.quartz;
iailab-framework/iailab-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,2 @@
com.iailab.framework.quartz.config.IailabXxlJobAutoConfiguration
com.iailab.framework.quartz.config.IailabAsyncAutoConfiguration
iailab-framework/iailab-common-monitor/pom.xml
对比新文件
@@ -0,0 +1,73 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-monitor</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>服务监控,提供链路追踪、日志服务、指标收集等等功能</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
        </dependency>
        <!-- 监控相关 -->
        <dependency>
            <groupId>io.opentracing</groupId>
            <artifactId>opentracing-util</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.skywalking</groupId>
            <artifactId>apm-toolkit-trace</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.skywalking</groupId>
            <artifactId>apm-toolkit-logback-1.x</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.skywalking</groupId>
            <artifactId>apm-toolkit-opentracing</artifactId>
        </dependency>
        <!-- Micrometer 对 Prometheus 的支持 -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabMetricsAutoConfiguration.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.tracer.config;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * Metrics 配置类
 *
 * @author iailab
 */
@AutoConfiguration
@ConditionalOnClass({MeterRegistryCustomizer.class})
@ConditionalOnProperty(prefix = "iailab.metrics", value = "enable", matchIfMissing = true) // 允许使用 iailab.metrics.enable=false 禁用 Metrics
public class IailabMetricsAutoConfiguration {
    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags(
            @Value("${spring.application.name}") String applicationName) {
        return registry -> registry.config().commonTags("application", applicationName);
    }
}
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/IailabTracerAutoConfiguration.java
对比新文件
@@ -0,0 +1,55 @@
package com.iailab.framework.tracer.config;
import com.iailab.framework.common.enums.WebFilterOrderEnum;
import com.iailab.framework.tracer.core.aop.BizTraceAspect;
import com.iailab.framework.tracer.core.filter.TraceFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
/**
 * Tracer 配置类
 *
 * @author mashu
 */
@AutoConfiguration
@ConditionalOnClass({BizTraceAspect.class})
@EnableConfigurationProperties(TracerProperties.class)
@ConditionalOnProperty(prefix = "iailab.tracer", value = "enable", matchIfMissing = true)
public class IailabTracerAutoConfiguration {
    // TODO @iailab:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk
//    @Bean
//    public TracerProperties bizTracerProperties() {
//        return new TracerProperties();
//    }
//
//    @Bean
//    public BizTraceAspect bizTracingAop() {
//        return new BizTraceAspect(tracer());
//    }
//
//    @Bean
//    public Tracer tracer() {
//        // 创建 SkywalkingTracer 对象
//        SkywalkingTracer tracer = new SkywalkingTracer();
//        // 设置为 GlobalTracer 的追踪器
//        GlobalTracer.register(tracer);
//        return tracer;
//    }
    /**
     * 创建 TraceFilter 过滤器,响应 header 设置 traceId
     */
    @Bean
    public FilterRegistrationBean<TraceFilter> traceFilter() {
        FilterRegistrationBean<TraceFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new TraceFilter());
        registrationBean.setOrder(WebFilterOrderEnum.TRACE_FILTER);
        return registrationBean;
    }
}
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/config/TracerProperties.java
对比新文件
@@ -0,0 +1,14 @@
package com.iailab.framework.tracer.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
 * BizTracer配置类
 *
 * @author 麻薯
 */
@ConfigurationProperties("iailab.tracer")
@Data
public class TracerProperties {
}
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/annotation/BizTrace.java
对比新文件
@@ -0,0 +1,42 @@
package com.iailab.framework.tracer.core.annotation;
import java.lang.annotation.*;
/**
 * 打印业务编号 / 业务类型注解
 *
 * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项,
 * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。
 *
 * @author 麻薯
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface BizTrace {
    /**
     * 业务编号 tag 名
     */
    String ID_TAG = "biz.id";
    /**
     * 业务类型 tag 名
     */
    String TYPE_TAG = "biz.type";
    /**
     * @return 操作名
     */
    String operationName() default "";
    /**
     * @return 业务编号
     */
    String id();
    /**
     * @return 业务类型
     */
    String type();
}
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/aop/BizTraceAspect.java
对比新文件
@@ -0,0 +1,77 @@
package com.iailab.framework.tracer.core.aop;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.tracer.core.annotation.BizTrace;
import com.iailab.framework.common.util.spring.SpringExpressionUtils;
import com.iailab.framework.tracer.core.util.TracerFrameworkUtils;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.tag.Tags;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import java.util.Map;
import static java.util.Arrays.asList;
/**
 * {@link BizTrace} 切面,记录业务链路
 *
 * @author mashu
 */
@Aspect
@AllArgsConstructor
@Slf4j
public class BizTraceAspect {
    private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/";
    private final Tracer tracer;
    @Around(value = "@annotation(trace)")
    public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable {
        // 创建 span
        String operationName = getOperationName(joinPoint, trace);
        Span span = tracer.buildSpan(operationName)
                .withTag(Tags.COMPONENT.getKey(), "biz")
                .start();
        try {
            // 执行原有方法
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            TracerFrameworkUtils.onError(throwable, span);
            throw throwable;
        } finally {
            // 设置 Span 的 biz 属性
            setBizTag(span, joinPoint, trace);
            // 完成 Span
            span.finish();
        }
    }
    private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) {
        // 自定义操作名
        if (StrUtil.isNotEmpty(trace.operationName())) {
            return BIZ_OPERATION_NAME_PREFIX + trace.operationName();
        }
        // 默认操作名,使用方法名
        return BIZ_OPERATION_NAME_PREFIX
                + joinPoint.getSignature().getDeclaringType().getSimpleName()
                + "/" + joinPoint.getSignature().getName();
    }
    private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) {
        try {
            Map<String, Object> result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id()));
            span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type()));
            span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id()));
        } catch (Exception ex) {
            log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex);
        }
    }
}
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/filter/TraceFilter.java
对比新文件
@@ -0,0 +1,33 @@
package com.iailab.framework.tracer.core.filter;
import com.iailab.framework.common.util.monitor.TracerUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * Trace 过滤器,打印 traceId 到 header 中返回
 *
 * @author iailab
 */
public class TraceFilter extends OncePerRequestFilter {
    /**
     * Header 名 - 链路追踪编号
     */
    private static final String HEADER_NAME_TRACE_ID = "trace-id";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 设置响应 traceId
        response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId());
        // 继续过滤
        chain.doFilter(request, response);
    }
}
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/core/util/TracerFrameworkUtils.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.tracer.core.util;
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
/**
 * 链路追踪 Util
 *
 * @author iailab
 */
public class TracerFrameworkUtils {
    /**
     * 将异常记录到 Span 中,参考自 com.aliyuncs.utils.TraceUtils
     *
     * @param throwable 异常
     * @param span Span
     */
    public static void onError(Throwable throwable, Span span) {
        Tags.ERROR.set(span, Boolean.TRUE);
        if (throwable != null) {
            span.log(errorLogs(throwable));
        }
    }
    private static Map<String, Object> errorLogs(Throwable throwable) {
        Map<String, Object> errorLogs = new HashMap<String, Object>(10);
        errorLogs.put("event", Tags.ERROR.getKey());
        errorLogs.put("error.object", throwable);
        errorLogs.put("error.kind", throwable.getClass().getName());
        String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage();
        if (message != null) {
            errorLogs.put("message", message);
        }
        StringWriter sw = new StringWriter();
        throwable.printStackTrace(new PrintWriter(sw));
        errorLogs.put("stack", sw.toString());
        return errorLogs;
    }
}
iailab-framework/iailab-common-monitor/src/main/java/com/iailab/framework/tracer/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * 使用 SkyWalking 组件,作为链路追踪、日志中心。
 *
 * @author iailab
 */
package com.iailab.framework.tracer;
iailab-framework/iailab-common-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,2 @@
com.iailab.framework.tracer.config.IailabTracerAutoConfiguration
com.iailab.framework.tracer.config.IailabMetricsAutoConfiguration
iailab-framework/iailab-common-mq/pom.xml
对比新文件
@@ -0,0 +1,43 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-mq</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <!-- DB 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-redis</artifactId>
        </dependency>
        <!-- 消息队列相关 -->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
<!--            <optional>true</optional>-->
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
<!--            <optional>true</optional>-->
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
<!--            <optional>true</optional>-->
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/common/RoutingConstant.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.mq.common;
/**
 *
 *
 * @author PanZhibao
 * @Description
 * @createTime 2024年11月05日
 */
public interface RoutingConstant {
    String Iailab_Data_PointCollectFinish = "Iailab.Data.PointCollectFinish";
    // 摄像头通配路由
    String Iailab_Data_Image = "Iailab.Data.Image.*";
    // 大华摄像头路由
    String Iailab_Data_Image_Dahua = "Iailab.Data.Image.Dahua";
    // 海康摄像头路由
    String Iailab_Data_Image_Hikvision = "Iailab.Data.Image.Hikvision";
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种
 */
package com.iailab.framework.mq;
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/config/IailabRabbitMQAutoConfiguration.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.mq.rabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
/**
 * RabbitMQ 消息队列配置类
 *
 * @author iailab
 */
@AutoConfiguration
@Slf4j
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
public class IailabRabbitMQAutoConfiguration {
    /**
     * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息
     */
    @Bean
    public MessageConverter createMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/core/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 占位符,无特殊逻辑
 */
package com.iailab.framework.mq.rabbitmq.core;
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/rabbitmq/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 消息队列,基于 RabbitMQ 提供
 */
package com.iailab.framework.mq.rabbitmq;
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQConsumerAutoConfiguration.java
对比新文件
@@ -0,0 +1,151 @@
package com.iailab.framework.mq.redis.config;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil;
import com.iailab.framework.common.enums.DocumentEnum;
import com.iailab.framework.mq.redis.core.RedisMQTemplate;
import com.iailab.framework.mq.redis.core.job.RedisPendingMessageResendJob;
import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.List;
import java.util.Properties;
/**
 * Redis 消息队列 Consumer 配置类
 *
 * @author iailab
 */
@Slf4j
@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
public class IailabRedisMQConsumerAutoConfiguration {
    /**
     * 创建 Redis Pub/Sub 广播消费的容器
     */
    @Bean
    @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) {
        // 创建 RedisMessageListenerContainer 对象
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        // 设置 RedisConnection 工厂。
        container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory());
        // 添加监听器
        listeners.forEach(listener -> {
            listener.setRedisMQTemplate(redisMQTemplate);
            container.addMessageListener(listener, new ChannelTopic(listener.getChannel()));
            log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]",
                    listener.getChannel(), listener.getClass().getName());
        });
        return container;
    }
    /**
     * 创建 Redis Stream 重新消费的任务
     */
    @Bean
    @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
    public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
                                                                     RedisMQTemplate redisTemplate,
                                                                     @Value("${spring.application.name}") String groupName,
                                                                     RedissonClient redissonClient) {
        return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
    }
    /**
     * 创建 Redis Stream 集群消费的容器
     *
     * 基础知识:<a href="https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html">Redis Stream 的 xreadgroup 命令</a>
     */
    @Bean(initMethod = "start", destroyMethod = "stop")
    @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
    public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
            RedisMQTemplate redisMQTemplate, List<AbstractRedisStreamMessageListener<?>> listeners) {
        RedisTemplate<String, ?> redisTemplate = redisMQTemplate.getRedisTemplate();
        checkRedisVersion(redisTemplate);
        // 第一步,创建 StreamMessageListenerContainer 容器
        // 创建 options 配置
        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
                StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
                        .batchSize(10) // 一次性最多拉取多少条消息
                        .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
                        .build();
        // 创建 container 对象
        StreamMessageListenerContainer<String, ObjectRecord<String, String>> container =
                StreamMessageListenerContainer.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions);
        // 第二步,注册监听器,消费对应的 Stream 主题
        String consumerName = buildConsumerName();
        listeners.parallelStream().forEach(listener -> {
            log.info("[redisStreamMessageListenerContainer][开始注册 StreamKey({}) 对应的监听器({})]",
                    listener.getStreamKey(), listener.getClass().getName());
            // 创建 listener 对应的消费者分组
            try {
                redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
            } catch (Exception ignore) {
            }
            // 设置 listener 对应的 redisTemplate
            listener.setRedisMQTemplate(redisMQTemplate);
            // 创建 Consumer 对象
            Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
            // 设置 Consumer 消费进度,以最小消费进度为准
            StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed());
            // 设置 Consumer 监听
            StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
                    .builder(streamOffset).consumer(consumer)
                    .autoAcknowledge(false) // 不自动 ack
                    .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
            container.register(builder.build(), listener);
            log.info("[redisStreamMessageListenerContainer][完成注册 StreamKey({}) 对应的监听器({})]",
                    listener.getStreamKey(), listener.getClass().getName());
        });
        return container;
    }
    /**
     * 构建消费者名字,使用本地 IP + 进程编号的方式。
     * 参考自 RocketMQ clientId 的实现
     *
     * @return 消费者名字
     */
    private static String buildConsumerName() {
        return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
    }
    /**
     * 校验 Redis 版本号,是否满足最低的版本号要求!
     */
    private static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
        // 获得 Redis 版本
        Properties info = redisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info);
        String version = MapUtil.getStr(info, "redis_version");
        // 校验最低版本必须大于等于 5.0.0
        int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false));
        if (majorVersion < 5) {
            throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" +
                    "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl()));
        }
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/config/IailabRedisMQProducerAutoConfiguration.java
对比新文件
@@ -0,0 +1,31 @@
package com.iailab.framework.mq.redis.config;
import com.iailab.framework.mq.redis.core.RedisMQTemplate;
import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
/**
 * Redis 消息队列 Producer 配置类
 *
 * @author iailab
 */
@Slf4j
@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
public class IailabRedisMQProducerAutoConfiguration {
    @Bean
    public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
                                           List<RedisMessageInterceptor> interceptors) {
        RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
        // 添加拦截器
        interceptors.forEach(redisMQTemplate::addInterceptor);
        return redisMQTemplate;
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/RedisMQTemplate.java
对比新文件
@@ -0,0 +1,87 @@
package com.iailab.framework.mq.redis.core;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessage;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.ArrayList;
import java.util.List;
/**
 * Redis MQ 操作模板类
 *
 * @author iailab
 */
@AllArgsConstructor
public class RedisMQTemplate {
    @Getter
    private final RedisTemplate<String, ?> redisTemplate;
    /**
     * 拦截器数组
     */
    @Getter
    private final List<RedisMessageInterceptor> interceptors = new ArrayList<>();
    /**
     * 发送 Redis 消息,基于 Redis pub/sub 实现
     *
     * @param message 消息
     */
    public <T extends AbstractRedisChannelMessage> void send(T message) {
        try {
            sendMessageBefore(message);
            // 发送消息
            redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
        } finally {
            sendMessageAfter(message);
        }
    }
    /**
     * 发送 Redis 消息,基于 Redis Stream 实现
     *
     * @param message 消息
     * @return 消息记录的编号对象
     */
    public <T extends AbstractRedisStreamMessage> RecordId send(T message) {
        try {
            sendMessageBefore(message);
            // 发送消息
            return redisTemplate.opsForStream().add(StreamRecords.newRecord()
                    .ofObject(JsonUtils.toJsonString(message)) // 设置内容
                    .withStreamKey(message.getStreamKey())); // 设置 stream key
        } finally {
            sendMessageAfter(message);
        }
    }
    /**
     * 添加拦截器
     *
     * @param interceptor 拦截器
     */
    public void addInterceptor(RedisMessageInterceptor interceptor) {
        interceptors.add(interceptor);
    }
    private void sendMessageBefore(AbstractRedisMessage message) {
        // 正序
        interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message));
    }
    private void sendMessageAfter(AbstractRedisMessage message) {
        // 倒序
        for (int i = interceptors.size() - 1; i >= 0; i--) {
            interceptors.get(i).sendMessageAfter(message);
        }
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java
对比新文件
@@ -0,0 +1,26 @@
package com.iailab.framework.mq.redis.core.interceptor;
import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
/**
 * {@link AbstractRedisMessage} 消息拦截器
 * 通过拦截器,作为插件机制,实现拓展。
 * 例如说,多租户场景下的 MQ 消息处理
 *
 * @author iailab
 */
public interface RedisMessageInterceptor {
    default void sendMessageBefore(AbstractRedisMessage message) {
    }
    default void sendMessageAfter(AbstractRedisMessage message) {
    }
    default void consumeMessageBefore(AbstractRedisMessage message) {
    }
    default void consumeMessageAfter(AbstractRedisMessage message) {
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
对比新文件
@@ -0,0 +1,100 @@
package com.iailab.framework.mq.redis.core.job;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.mq.redis.core.RedisMQTemplate;
import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
 * 这个任务用于处理,crash 之后的消费者未消费完的消息
 */
@Slf4j
@AllArgsConstructor
public class RedisPendingMessageResendJob {
    private static final String LOCK_KEY = "redis:pending:msg:lock";
    /**
     * 消息超时时间,默认 5 分钟
     *
     * 1. 超时的消息才会被重新投递
     * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到
     */
    private static final int EXPIRE_TIME = 5 * 60;
    private final List<AbstractRedisStreamMessageListener<?>> listeners;
    private final RedisMQTemplate redisTemplate;
    private final String groupName;
    private final RedissonClient redissonClient;
    /**
     * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
     */
    @Scheduled(cron = "35 * * * * ?")
    public void messageResend() {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        // 尝试加锁
        if (lock.tryLock()) {
            try {
                execute();
            } catch (Exception ex) {
                log.error("[messageResend][执行异常]", ex);
            } finally {
                lock.unlock();
            }
        }
    }
    /**
     * 执行清理逻辑
     *
     * @see <a href="https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/480/files">讨论</a>
     */
    private void execute() {
        StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
        listeners.forEach(listener -> {
            PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName));
            // 每个消费者的 pending 队列消息数量
            Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
            pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> {
                log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
                // 每个消费者的 pending消息的详情信息
                PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount);
                if (pendingMessages.isEmpty()) {
                    return;
                }
                pendingMessages.forEach(pendingMessage -> {
                    // 获取消息上一次传递到 consumer 的时间,
                    long lastDelivery = pendingMessage.getElapsedTimeSinceLastDelivery().getSeconds();
                    if (lastDelivery < EXPIRE_TIME){
                        return;
                    }
                    // 获取指定 id 的消息体
                    List<MapRecord<String, Object, Object>> records = ops.range(listener.getStreamKey(),
                            Range.of(Range.Bound.inclusive(pendingMessage.getIdAsString()), Range.Bound.inclusive(pendingMessage.getIdAsString())));
                    if (CollUtil.isEmpty(records)) {
                        return;
                    }
                    // 重新投递消息
                    redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord()
                            .ofObject(records.get(0).getValue()) // 设置内容
                            .withStreamKey(listener.getStreamKey()));
                    // ack 消息消费完成
                    redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0));
                    log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId());
                });
            });
        });
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/message/AbstractRedisMessage.java
对比新文件
@@ -0,0 +1,29 @@
package com.iailab.framework.mq.redis.core.message;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
 * Redis 消息抽象基类
 *
 * @author iailab
 */
@Data
public abstract class AbstractRedisMessage {
    /**
     * 头
     */
    private Map<String, String> headers = new HashMap<>();
    public String getHeader(String key) {
        return headers.get(key);
    }
    public void addHeader(String key, String value) {
        headers.put(key, value);
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java
对比新文件
@@ -0,0 +1,23 @@
package com.iailab.framework.mq.redis.core.pubsub;
import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
 * Redis Channel Message 抽象类
 *
 * @author iailab
 */
public abstract class AbstractRedisChannelMessage extends AbstractRedisMessage {
    /**
     * 获得 Redis Channel,默认使用类名
     *
     * @return Channel
     */
    @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。
    public String getChannel() {
        return getClass().getSimpleName();
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java
对比新文件
@@ -0,0 +1,103 @@
package com.iailab.framework.mq.redis.core.pubsub;
import cn.hutool.core.util.TypeUtil;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.mq.redis.core.RedisMQTemplate;
import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
import lombok.Setter;
import lombok.SneakyThrows;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import java.lang.reflect.Type;
import java.util.List;
/**
 * Redis Pub/Sub 监听器抽象类,用于实现广播消费
 *
 * @param <T> 消息类型。一定要填写噢,不然会报错
 *
 * @author iailab
 */
public abstract class AbstractRedisChannelMessageListener<T extends AbstractRedisChannelMessage> implements MessageListener {
    /**
     * 消息类型
     */
    private final Class<T> messageType;
    /**
     * Redis Channel
     */
    private final String channel;
    /**
     * RedisMQTemplate
     */
    @Setter
    private RedisMQTemplate redisMQTemplate;
    @SneakyThrows
    protected AbstractRedisChannelMessageListener() {
        this.messageType = getMessageClass();
        this.channel = messageType.getDeclaredConstructor().newInstance().getChannel();
    }
    /**
     * 获得 Sub 订阅的 Redis Channel 通道
     *
     * @return channel
     */
    public final String getChannel() {
        return channel;
    }
    @Override
    public final void onMessage(Message message, byte[] bytes) {
        T messageObj = JsonUtils.parseObject(message.getBody(), messageType);
        try {
            consumeMessageBefore(messageObj);
            // 消费消息
            this.onMessage(messageObj);
        } finally {
            consumeMessageAfter(messageObj);
        }
    }
    /**
     * 处理消息
     *
     * @param message 消息
     */
    public abstract void onMessage(T message);
    /**
     * 通过解析类上的泛型,获得消息类型
     *
     * @return 消息类型
     */
    @SuppressWarnings("unchecked")
    private Class<T> getMessageClass() {
        Type type = TypeUtil.getTypeArgument(getClass(), 0);
        if (type == null) {
            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
        }
        return (Class<T>) type;
    }
    private void consumeMessageBefore(AbstractRedisMessage message) {
        assert redisMQTemplate != null;
        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
        // 正序
        interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
    }
    private void consumeMessageAfter(AbstractRedisMessage message) {
        assert redisMQTemplate != null;
        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
        // 倒序
        for (int i = interceptors.size() - 1; i >= 0; i--) {
            interceptors.get(i).consumeMessageAfter(message);
        }
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java
对比新文件
@@ -0,0 +1,23 @@
package com.iailab.framework.mq.redis.core.stream;
import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
 * Redis Stream Message 抽象类
 *
 * @author iailab
 */
public abstract class AbstractRedisStreamMessage extends AbstractRedisMessage {
    /**
     * 获得 Redis Stream Key,默认使用类名
     *
     * @return Channel
     */
    @JsonIgnore // 避免序列化
    public String getStreamKey() {
        return getClass().getSimpleName();
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java
对比新文件
@@ -0,0 +1,113 @@
package com.iailab.framework.mq.redis.core.stream;
import cn.hutool.core.util.TypeUtil;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.mq.redis.core.RedisMQTemplate;
import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.stream.StreamListener;
import java.lang.reflect.Type;
import java.util.List;
/**
 * Redis Stream 监听器抽象类,用于实现集群消费
 *
 * @param <T> 消息类型。一定要填写噢,不然会报错
 *
 * @author iailab
 */
public abstract class AbstractRedisStreamMessageListener<T extends AbstractRedisStreamMessage>
        implements StreamListener<String, ObjectRecord<String, String>> {
    /**
     * 消息类型
     */
    private final Class<T> messageType;
    /**
     * Redis Channel
     */
    @Getter
    private final String streamKey;
    /**
     * Redis 消费者分组,默认使用 spring.application.name 名字
     */
    @Value("${spring.application.name}")
    @Getter
    private String group;
    /**
     * RedisMQTemplate
     */
    @Setter
    private RedisMQTemplate redisMQTemplate;
    @SneakyThrows
    protected AbstractRedisStreamMessageListener() {
        this.messageType = getMessageClass();
        this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey();
    }
    @Override
    public void onMessage(ObjectRecord<String, String> message) {
        // 消费消息
        T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
        try {
            consumeMessageBefore(messageObj);
            // 消费消息
            this.onMessage(messageObj);
            // ack 消息消费完成
            redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message);
            // TODO iailab:需要额外考虑以下几个点:
            // 1. 处理异常的情况
            // 2. 发送日志;以及事务的结合
            // 3. 消费日志;以及通用的幂等性
            // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
        } finally {
            consumeMessageAfter(messageObj);
        }
    }
    /**
     * 处理消息
     *
     * @param message 消息
     */
    public abstract void onMessage(T message);
    /**
     * 通过解析类上的泛型,获得消息类型
     *
     * @return 消息类型
     */
    @SuppressWarnings("unchecked")
    private Class<T> getMessageClass() {
        Type type = TypeUtil.getTypeArgument(getClass(), 0);
        if (type == null) {
            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
        }
        return (Class<T>) type;
    }
    private void consumeMessageBefore(AbstractRedisMessage message) {
        assert redisMQTemplate != null;
        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
        // 正序
        interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
    }
    private void consumeMessageAfter(AbstractRedisMessage message) {
        assert redisMQTemplate != null;
        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
        // 倒序
        for (int i = interceptors.size() - 1; i >= 0; i--) {
            interceptors.get(i).consumeMessageAfter(message);
        }
    }
}
iailab-framework/iailab-common-mq/src/main/java/com/iailab/framework/mq/redis/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * 消息队列,基于 Redis 提供:
 * 1. 基于 Pub/Sub 实现广播消费
 * 2. 基于 Stream 实现集群消费
 */
package com.iailab.framework.mq.redis;
iailab-framework/iailab-common-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,3 @@
com.iailab.framework.mq.redis.config.IailabRedisMQProducerAutoConfiguration
com.iailab.framework.mq.redis.config.IailabRedisMQConsumerAutoConfiguration
com.iailab.framework.mq.rabbitmq.config.IailabRabbitMQAutoConfiguration
iailab-framework/iailab-common-mybatis/pom.xml
对比新文件
@@ -0,0 +1,95 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-mybatis</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>数据库连接池、多数据源、事务、MyBatis 拓展</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-web</artifactId>
            <scope>provided</scope> <!-- 设置为 provided,只有 OncePerRequestFilter 使用到 -->
        </dependency>
        <!-- DB 相关 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
<!--            <optional>true</optional>-->
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.dameng</groupId>
            <artifactId>DmJdbcDriver18</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
        </dependency>
        <dependency>
            <groupId>com.github.yulichang</groupId>
            <artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
        </dependency>
        <dependency>
            <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
            <artifactId>easy-trans-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fhs-opensource</groupId>
            <artifactId>easy-trans-mybatis-plus-extend</artifactId>
        </dependency>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/dao/BaseDao.java
对比新文件
@@ -0,0 +1,21 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
 * 基础Dao
 *
 * @author Mark sunlightcs@gmail.com
 * @since 1.0.0
 */
public interface BaseDao<T> extends BaseMapper<T> {
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/entity/BaseEntity.java
对比新文件
@@ -0,0 +1,41 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
 * 基础实体类,所有实体都需要继承
 *
 * @author Mark sunlightcs@gmail.com
 */
@Data
public abstract class BaseEntity implements Serializable {
    /**
     * id
     */
    @TableId
    private Long id;
    /**
     * 创建者
     */
    @TableField(fill = FieldFill.INSERT)
    private Long  creator;
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createDate;
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/package-info.java
对比新文件
@@ -0,0 +1 @@
package com.iailab.framework.common;
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/page/PageData.java
对比新文件
@@ -0,0 +1,43 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.page;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
 * 分页工具类
 *
 * @author Mark sunlightcs@gmail.com
 */
@Data
@Tag(name = "分页数据")
public class PageData<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "总记录数")
    private int total;
    @Schema(description = "列表数据")
    private List<T> list;
    /**
     * 分页
     * @param list   列表数据
     * @param total  总记录数
     */
    public PageData(List<T> list, long total) {
        this.list = list;
        this.total = (int)total;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/BaseService.java
对比新文件
@@ -0,0 +1,116 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import java.io.Serializable;
import java.util.Collection;
/**
 * 基础服务接口,所有Service接口都要继承
 *
 * @author Mark sunlightcs@gmail.com
 */
public interface BaseService<T> {
    Class<T> currentModelClass();
    /**
     * <p>
     * 插入一条记录(选择字段,策略插入)
     * </p>
     *
     * @param entity 实体对象
     */
    boolean insert(T entity);
    /**
     * <p>
     * 插入(批量),该方法不支持 Oracle、SQL Server
     * </p>
     *
     * @param entityList 实体对象集合
     */
    boolean insertBatch(Collection<T> entityList);
    /**
     * <p>
     * 插入(批量),该方法不支持 Oracle、SQL Server
     * </p>
     *
     * @param entityList 实体对象集合
     * @param batchSize  插入批次数量
     */
    boolean insertBatch(Collection<T> entityList, int batchSize);
    /**
     * <p>
     * 根据 ID 选择修改
     * </p>
     *
     * @param entity 实体对象
     */
    boolean updateById(T entity);
    /**
     * <p>
     * 根据 whereEntity 条件,更新记录
     * </p>
     *
     * @param entity        实体对象
     * @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
     */
    boolean update(T entity, Wrapper<T> updateWrapper);
    /**
     * <p>
     * 根据ID 批量更新
     * </p>
     *
     * @param entityList 实体对象集合
     */
    boolean updateBatchById(Collection<T> entityList);
    /**
     * <p>
     * 根据ID 批量更新
     * </p>
     *
     * @param entityList 实体对象集合
     * @param batchSize  更新批次数量
     */
    boolean updateBatchById(Collection<T> entityList, int batchSize);
    /**
     * <p>
     * 根据 ID 查询
     * </p>
     *
     * @param id 主键ID
     */
    T selectById(Serializable id);
    /**
     * <p>
     * 根据 ID 删除
     * </p>
     *
     * @param id 主键ID
     */
    boolean deleteById(Serializable id);
    /**
     * <p>
     * 删除(根据ID 批量删除)
     * </p>
     *
     * @param idList 主键ID列表
     */
    boolean deleteBatchIds(Collection<? extends Serializable> idList);
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/CrudService.java
对比新文件
@@ -0,0 +1,35 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 * <p>
 * https://www.renren.io
 * <p>
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.service;
import com.iailab.framework.common.page.PageData;
import java.util.List;
import java.util.Map;
/**
 *  CRUD基础服务接口
 *
 * @author Mark sunlightcs@gmail.com
 */
public interface CrudService<T, D> extends BaseService<T> {
    PageData<D> page(Map<String, Object> params);
    List<D> list(Map<String, Object> params);
    D get(Long id);
    void save(D dto);
    void update(D dto);
    void delete(Long[] ids);
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/BaseServiceImpl.java
对比新文件
@@ -0,0 +1,219 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.iailab.framework.common.constant.Constant;
import com.iailab.framework.common.page.PageData;
import com.iailab.framework.common.service.BaseService;
import com.iailab.framework.common.util.object.BeanUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
/**
 * 基础服务类,所有Service都要继承
 *
 * @author Mark sunlightcs@gmail.com
 */
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T>  implements BaseService<T> {
    @Autowired
    protected M baseDao;
    protected Log log = LogFactory.getLog(getClass());
    /**
     * 获取分页对象
     * @param params      分页查询参数
     * @param defaultOrderField  默认排序字段
     * @param isAsc              排序方式
     */
    protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
        //分页参数
        long curPage = 1;
        long limit = 10;
        if(params.get(Constant.PAGE) != null){
            curPage = Long.parseLong((String)params.get(Constant.PAGE));
        }
        if(params.get(Constant.LIMIT) != null){
            limit = Long.parseLong((String)params.get(Constant.LIMIT));
        }
        //分页对象
        Page<T> page = new Page<>(curPage, limit);
        //分页参数
        params.put(Constant.PAGE, page);
        //排序字段
        String orderField = (String)params.get(Constant.ORDER_FIELD);
        String order = (String)params.get(Constant.ORDER);
        //前端字段排序
        if(StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)){
            if(Constant.ASC.equalsIgnoreCase(order)) {
                return page.addOrder(OrderItem.asc(orderField));
            }else {
                return page.addOrder(OrderItem.desc(orderField));
            }
        }
        //没有排序字段,则不排序
        if(StringUtils.isBlank(defaultOrderField)){
            return page;
        }
        //默认排序
        if(isAsc) {
            page.addOrder(OrderItem.asc(defaultOrderField));
        }else {
            page.addOrder(OrderItem.desc(defaultOrderField));
        }
        return page;
    }
    protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target){
        List<T> targetList = BeanUtils.toBean(list, target);
        return new PageData<>(targetList, total);
    }
    protected <T> PageData<T> getPageData(IPage page, Class<T> target){
        return getPageData(page.getRecords(), page.getTotal(), target);
    }
    protected void paramsToLike(Map<String, Object> params, String... likes){
        for (String like : likes){
            String val = (String)params.get(like);
            if (StringUtils.isNotBlank(val)){
                params.put(like, "%" + val + "%");
            }else {
                params.put(like, null);
            }
        }
    }
    /**
     * <p>
     * 判断数据库操作是否成功
     * </p>
     * <p>
     * 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
     * </p>
     *
     * @param result 数据库操作返回影响条数
     * @return boolean
     */
    protected static boolean retBool(Integer result) {
        return SqlHelper.retBool(result);
    }
    protected Class<M> currentMapperClass() {
        return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
    }
    @Override
    public Class<T> currentModelClass() {
        return (Class<T>)ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
    }
    protected String getSqlStatement(SqlMethod sqlMethod) {
        return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
    }
    @Override
    public boolean insert(T entity) {
        return BaseServiceImpl.retBool(baseDao.insert(entity));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean insertBatch(Collection<T> entityList) {
        return insertBatch(entityList, 100);
    }
    /**
     * 批量插入
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean insertBatch(Collection<T> entityList, int batchSize) {
        String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
    }
    /**
     * 执行批量操作
     */
    protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
    }
    @Override
    public boolean updateById(T entity) {
        return BaseServiceImpl.retBool(baseDao.updateById(entity));
    }
    @Override
    public boolean update(T entity, Wrapper<T> updateWrapper) {
        return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateBatchById(Collection<T> entityList) {
        return updateBatchById(entityList, 30);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateBatchById(Collection<T> entityList, int batchSize) {
        String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
            param.put(Constants.ENTITY, entity);
            sqlSession.update(sqlStatement, param);
        });
    }
    @Override
    public T selectById(Serializable id) {
        return baseDao.selectById(id);
    }
    @Override
    public boolean deleteById(Serializable id) {
        return SqlHelper.retBool(baseDao.deleteById(id));
    }
    @Override
    public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
        return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/common/service/impl/CrudServiceImpl.java
对比新文件
@@ -0,0 +1,80 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 * <p>
 * https://www.renren.io
 * <p>
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.iailab.framework.common.page.PageData;
import com.iailab.framework.common.service.CrudService;
import com.iailab.framework.common.util.object.ConvertUtils;
import org.springframework.beans.BeanUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
 *  CRUD基础服务类
 *
 * @author Mark sunlightcs@gmail.com
 */
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> implements CrudService<T, D> {
    protected Class<D> currentDtoClass() {
        return (Class<D>)ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
    }
    @Override
    public PageData<D> page(Map<String, Object> params) {
        IPage<T> page = baseDao.selectPage(
            getPage(params, null, false),
            getWrapper(params)
        );
        return getPageData(page, currentDtoClass());
    }
    @Override
    public List<D> list(Map<String, Object> params) {
        List<T> entityList = baseDao.selectList(getWrapper(params));
        return ConvertUtils.sourceToTarget(entityList, currentDtoClass());
    }
    public abstract QueryWrapper<T> getWrapper(Map<String, Object> params);
    @Override
    public D get(Long id) {
        T entity = baseDao.selectById(id);
        return ConvertUtils.sourceToTarget(entity, currentDtoClass());
    }
    @Override
    public void save(D dto) {
        T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
        insert(entity);
        //copy主键值到dto
        BeanUtils.copyProperties(entity, dto);
    }
    @Override
    public void update(D dto) {
        T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
        updateById(entity);
    }
    @Override
    public void delete(Long[] ids) {
        baseDao.deleteBatchIds(Arrays.asList(ids));
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/config/IailabDataSourceAutoConfiguration.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.datasource.config;
import com.iailab.framework.datasource.core.filter.DruidAdRemoveFilter;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
 * 数据库配置类
 *
 * @author iailab
 */
@AutoConfiguration
@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理
@EnableConfigurationProperties(DruidStatProperties.class)
public class IailabDataSourceAutoConfiguration {
    /**
     * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告
     */
    @Bean
    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
    public FilterRegistrationBean<DruidAdRemoveFilter> druidAdRemoveFilterFilter(DruidStatProperties properties) {
        // 获取 druid web 监控页面的参数
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        // 提取 common.js 的配置路径
        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
        // 创建 DruidAdRemoveFilter Bean
        FilterRegistrationBean<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new DruidAdRemoveFilter());
        registrationBean.addUrlPatterns(commonJsPattern);
        return registrationBean;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/enums/DataSourceEnum.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.datasource.core.enums;
/**
 * 对应于多数据源中不同数据源配置
 *
 * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。
 * 注意,默认是 {@link #MASTER} 数据源
 *
 * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html
 */
public interface DataSourceEnum {
    /**
     * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解
     */
    String MASTER = "master";
    /**
     * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解
     */
    String SLAVE = "slave";
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/core/filter/DruidAdRemoveFilter.java
对比新文件
@@ -0,0 +1,38 @@
package com.iailab.framework.datasource.core.filter;
import com.alibaba.druid.util.Utils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * Druid 底部广告过滤器
 *
 * @author iailab
 */
public class DruidAdRemoveFilter extends OncePerRequestFilter {
    /**
     * common.js 的路径
     */
    private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        chain.doFilter(request, response);
        // 重置缓冲区,响应头不会被重置
        response.resetBuffer();
        // 获取 common.js
        String text = Utils.readFromResource(COMMON_JS_ILE_PATH);
        // 正则替换 banner, 除去底部的广告信息
        text = text.replaceAll("<a.*?banner\"></a><br/>", "");
        text = text.replaceAll("powered.*?shrek.wang</a>", "");
        response.getWriter().write(text);
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/datasource/package-info.java
对比新文件
@@ -0,0 +1,5 @@
/**
 * 数据库连接池,采用 Druid
 * 多数据源,采用爆米花
 */
package com.iailab.framework.datasource;
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IailabMybatisAutoConfiguration.java
对比新文件
@@ -0,0 +1,65 @@
package com.iailab.framework.mybatis.config;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.mybatis.core.enums.SqlConstants;
import com.iailab.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.incrementer.*;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment;
/**
 * MyBaits 配置类
 *
 * @author iailab
 */
@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志
@MapperScan(value = "${iailab.info.base-package}", annotationClass = Mapper.class,
        lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
public class IailabMybatisAutoConfiguration {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
        return mybatisPlusInterceptor;
    }
    @Bean
    public MetaObjectHandler defaultMetaObjectHandler(){
        return new DefaultDBFieldHandler(); // 自动填充参数类
    }
    @Bean
    @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT")
    public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) {
        DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment);
        if (dbType != null) {
            switch (dbType) {
                case POSTGRE_SQL:
                    return new PostgreKeyGenerator();
                case ORACLE:
                case ORACLE_12C:
                    return new OracleKeyGenerator();
                case H2:
                    return new H2KeyGenerator();
                case KINGBASE_ES:
                    return new KingbaseKeyGenerator();
                case DM:
                    return new DmKeyGenerator();
            }
        }
        // 找不到合适的 IKeyGenerator 实现类
        throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java
对比新文件
@@ -0,0 +1,106 @@
package com.iailab.framework.mybatis.config;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.util.collection.SetUtils;
import com.iailab.framework.mybatis.core.enums.SqlConstants;
import com.iailab.framework.mybatis.core.util.JdbcUtils;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.Set;
/**
 * 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置
 *
 * @author iailab
 */
@Slf4j
public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor {
    private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type";
    private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic";
    private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass";
    private static final Set<DbType> INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C,
            DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2);
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 如果获取不到 DbType,则不进行处理
        DbType dbType = getDbType(environment);
        if (dbType == null) {
            return;
        }
        // 设置 Quartz JobStore 对应的 Driver
        // TODO iailab:暂时没有找到特别合适的地方,先放在这里
        setJobStoreDriverIfPresent(environment, dbType);
        // 初始化 SQL 静态变量
        SqlConstants.init(dbType);
        // 如果非 NONE,则不进行处理
        IdType idType = getIdType(environment);
        if (idType != IdType.NONE) {
            return;
        }
        // 情况一,用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
        if (INPUT_ID_TYPES.contains(dbType)) {
            setIdType(environment, IdType.INPUT);
            return;
        }
        // 情况二,自增 ID,适合 MySQL 等直接自增的数据库
        setIdType(environment, IdType.AUTO);
    }
    public IdType getIdType(ConfigurableEnvironment environment) {
        return environment.getProperty(ID_TYPE_KEY, IdType.class);
    }
    public void setIdType(ConfigurableEnvironment environment, IdType idType) {
        environment.getSystemProperties().put(ID_TYPE_KEY, idType);
        log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType);
    }
    public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) {
        String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY);
        if (StrUtil.isNotEmpty(driverClass)) {
            return;
        }
        // 根据 dbType 类型,获取对应的 driverClass
        switch (dbType) {
            case POSTGRE_SQL:
                driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
                break;
            case ORACLE:
            case ORACLE_12C:
                driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
                break;
            case SQL_SERVER:
            case SQL_SERVER2005:
                driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
                break;
        }
        // 设置 driverClass 变量
        if (StrUtil.isNotEmpty(driverClass)) {
            environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass);
        }
    }
    public static DbType getDbType(ConfigurableEnvironment environment) {
        String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary");
        if (StrUtil.isEmpty(primary)) {
            return null;
        }
        String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url");
        if (StrUtil.isEmpty(url)) {
            return null;
        }
        return JdbcUtils.getDbType(url);
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/config/MyBatisConfiguration.java
对比新文件
@@ -0,0 +1,84 @@
//package com.iailab.framework.mybatis.config;
//
//import com.alibaba.fastjson.JSONArray;
//import com.alibaba.fastjson.JSONObject;
//import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
//import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
//import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
//import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
//import com.iailab.framework.mybatis.core.handler.MybatisHandler;
//import com.iailab.framework.mybatis.interceptor.DataFilterInterceptor;
//import org.apache.ibatis.session.SqlSessionFactory;
//import org.apache.ibatis.type.JdbcType;
//import org.mybatis.spring.SqlSessionFactoryBean;
//import org.mybatis.spring.SqlSessionTemplate;
//import org.springframework.beans.factory.annotation.Value;
//import org.springframework.boot.jdbc.DataSourceBuilder;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.context.annotation.Primary;
//import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
//import org.springframework.jdbc.datasource.DataSourceTransactionManager;
//
//
///**
// * @author houzhongjian
// * @Title: MyBatisConfiguration
// * @ProjectName design-parent
// * @Description: 解决独立启动某个服务时报错问题 No typehandler found for property sqlSessionTemplate
// * @date 2024/7/2 16:35
// */
//@Configuration
//public class MyBatisConfiguration {
//
//    //  配置mapper的扫描,找到所有的mapper.xml映射文件
////    @Value("${iailab.info.base-package}")
//    @Value("${mybatis-plus.mapper-locations}")
//    private String mapperLocations;
//
//    @Bean
//    @Primary
//    public SqlSessionFactory sqlSessionFactory() throws Exception {
//        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
//        sqlSessionFactoryBean.setDataSource(DataSourceBuilder.create().build());
//        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
//        sqlSessionFactoryBean.setConfiguration(buildConfiguration());
//        return sqlSessionFactoryBean.getObject();
//    }
//
//    @Bean
//    @Primary
//    public SqlSessionTemplate sqlSessionTemplate() throws Exception {
//        return  new SqlSessionTemplate(sqlSessionFactory());
//    }
//
//    @Bean
//    @Primary
//    public DataSourceTransactionManager transactionManager() {
//        return new DataSourceTransactionManager(DataSourceBuilder.create().build());
//    }
//
//    private org.apache.ibatis.session.Configuration buildConfiguration() {
//        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
//        configuration.getTypeHandlerRegistry().register(JSONObject.class, JdbcType.VARCHAR, MybatisHandler.class);
//        configuration.getTypeHandlerRegistry().register(JSONArray.class, JdbcType.VARCHAR, MybatisHandler.class);
//        return configuration;
//    }
//
//    @Bean
//    public MybatisPlusInterceptor mybatisPlusInterceptor() {
//        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//        // 数据权限
//        mybatisPlusInterceptor.addInnerInterceptor(new DataFilterInterceptor());
//        // 分页插件
//        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//        // 乐观锁
//        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
//        // 防止全表更新与删除
//        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
//
//        return mybatisPlusInterceptor;
//    }
//
//
//}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/dataobject/BaseDO.java
对比新文件
@@ -0,0 +1,56 @@
package com.iailab.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fhs.core.trans.vo.TransPojo;
import lombok.Data;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * 基础实体对象
 *
 * 为什么实现 {@link TransPojo} 接口?
 * 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询
 *
 * @author iailab
 */
@Data
@JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错
public abstract class BaseDO implements Serializable, TransPojo {
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * 最后更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * 创建者,目前使用 SysUser 的 id 编号
     *
     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
     */
    @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
    private String creator;
    /**
     * 更新者,目前使用 SysUser 的 id 编号
     *
     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
     */
    @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
    private String updater;
    /**
     * 是否删除
     */
    @TableLogic
    private Boolean deleted;
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/DbTypeEnum.java
对比新文件
@@ -0,0 +1,84 @@
package com.iailab.framework.mybatis.core.enums;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息
 */
@Getter
@AllArgsConstructor
public enum DbTypeEnum {
    /**
     * MySQL
     */
    MY_SQL( DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
    /**
     * Oracle
     */
    ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
    /**
     * PostgreSQL
     *
     * 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
     */
    POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
    /**
     * SQL Server
     */
    SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
    /**
     * 达梦
     */
    DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
    /**
     * 人大金仓
     */
    KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
    ;
    public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())
            .collect(Collectors.toMap(DbTypeEnum::getProductName, Function.identity()));
    public static final Map<DbType, DbTypeEnum> MAP_BY_MP = Arrays.stream(values())
            .collect(Collectors.toMap(DbTypeEnum::getMpDbType, Function.identity()));
    /**
     * MyBatis Plus 类型
     */
    private final DbType mpDbType;
    /**
     * 数据库产品名
     */
    private final String productName;
    /**
     * SQL FIND_IN_SET 模板
     */
    private final String findInSetTemplate;
    public static DbType find(String databaseProductName) {
        if (StrUtil.isBlank(databaseProductName)) {
            return null;
        }
        return MAP_BY_NAME.get(databaseProductName).getMpDbType();
    }
    public static String getFindInSetTemplate(DbType dbType) {
        return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
                .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/enums/SqlConstants.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.mybatis.core.enums;
import com.baomidou.mybatisplus.annotation.DbType;
/**
 * SQL相关常量类
 *
 * @author iailab
 */
public class SqlConstants {
    /**
     * 数据库的类型
     */
    // TODO 此处使用nacos配置文件的话,先初始化nacos配置才初始化dbType,导致读不到值。暂时先固定Mysql
    public static DbType DB_TYPE = DbType.MYSQL;
    public static void init(DbType dbType) {
        DB_TYPE = dbType;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/DefaultDBFieldHandler.java
对比新文件
@@ -0,0 +1,62 @@
package com.iailab.framework.mybatis.core.handler;
import com.iailab.framework.mybatis.core.dataobject.BaseDO;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import java.time.LocalDateTime;
import java.util.Objects;
/**
 * 通用参数填充实现类
 *
 * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
 *
 * @author hexiaowu
 */
public class DefaultDBFieldHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
            LocalDateTime current = LocalDateTime.now();
            // 创建时间为空,则以当前时间为插入时间
            if (Objects.isNull(baseDO.getCreateTime())) {
                baseDO.setCreateTime(current);
            }
            // 更新时间为空,则以当前时间为更新时间
            if (Objects.isNull(baseDO.getUpdateTime())) {
                baseDO.setUpdateTime(current);
            }
            Long userId = WebFrameworkUtils.getLoginUserId();
            // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
            if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
                baseDO.setCreator(userId.toString());
            }
            // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
            if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
                baseDO.setUpdater(userId.toString());
            }
        }
    }
    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新时间为空,则以当前时间为更新时间
        Object modifyTime = getFieldValByName("updateTime", metaObject);
        if (Objects.isNull(modifyTime)) {
            setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
        }
        // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
        Object modifier = getFieldValByName("updater", metaObject);
        Long userId = WebFrameworkUtils.getLoginUserId();
        if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
            setFieldValByName("updater", userId.toString(), metaObject);
        }
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/handler/MybatisHandler.java
对比新文件
@@ -0,0 +1,45 @@
package com.iailab.framework.mybatis.core.handler;
import com.alibaba.fastjson.JSON;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class MybatisHandler extends BaseTypeHandler<Object> {
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) throws SQLException {
        preparedStatement.setString(i, JSON.toJSONString(o));
    }
    @Override
    public Object getNullableResult(ResultSet resultSet, String s) throws SQLException {
        String sqlJson = resultSet.getString(s);
        if (null != sqlJson) {
            return JSON.parse(sqlJson);
        }
        return null;
    }
    @Override
    public Object getNullableResult(ResultSet resultSet, int i) throws SQLException {
        String sqlJson = resultSet.getString(i);
        if (null != sqlJson) {
            return JSON.parse(sqlJson);
        }
        return null;
    }
    @Override
    public Object getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        String sqlJson = callableStatement.getString(i);
        if (null != sqlJson) {
            return JSON.parse(sqlJson);
        }
        return null;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/mapper/BaseMapperX.java
对比新文件
@@ -0,0 +1,225 @@
package com.iailab.framework.mybatis.core.mapper;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.iailab.framework.common.pojo.PageParam;
import com.iailab.framework.common.pojo.PageResult;
import com.iailab.framework.common.pojo.SortablePageParam;
import com.iailab.framework.common.pojo.SortingField;
import com.iailab.framework.mybatis.core.enums.SqlConstants;
import com.iailab.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.github.yulichang.base.MPJBaseMapper;
import com.github.yulichang.interfaces.MPJBaseJoin;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
 * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力
 *
 * 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力
 * 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力
 */
public interface BaseMapperX<T> extends MPJBaseMapper<T> {
    /**
     * 获取分页对象
     * @param params      分页查询参数
     */
    default IPage<T> getPage(PageParam params) {
        //分页参数
        long curPage = 1;
        long limit = 10;
        if(params.getPageNo() != null){
            curPage = params.getPageNo();
        }
        if(params.getPageSize() != null){
            limit = params.getPageSize();
        }
        //分页对象
        return new Page<>(curPage, limit);
    }
    default PageResult<T> selectPage(SortablePageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
        return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper);
    }
    default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
        return selectPage(pageParam, null, queryWrapper);
    }
    default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
        // 特殊:不分页,直接查询全部
        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
            List<T> list = selectList(queryWrapper);
            return new PageResult<>(list, (long) list.size());
        }
        // MyBatis Plus 查询
        IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields);
        selectPage(mpPage, queryWrapper);
        // 转换返回
        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
    }
    default <D> PageResult<D> selectJoinPage(PageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) {
        // 特殊:不分页,直接查询全部
        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
            List<D> list = selectJoinList(clazz, lambdaWrapper);
            return new PageResult<>(list, (long) list.size());
        }
        // MyBatis Plus Join 查询
        IPage<D> mpPage = MyBatisUtils.buildPage(pageParam);
        mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper);
        // 转换返回
        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
    }
    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
        IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
        selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
        // 转换返回
        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
    }
    default T selectOne(String field, Object value) {
        return selectOne(new QueryWrapper<T>().eq(field, value));
    }
    default T selectOne(SFunction<T, ?> field, Object value) {
        return selectOne(new LambdaQueryWrapper<T>().eq(field, value));
    }
    default T selectOne(String field1, Object value1, String field2, Object value2) {
        return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2));
    }
    default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
        return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
    }
    default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
                        SFunction<T, ?> field3, Object value3) {
        return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)
                .eq(field3, value3));
    }
    default Long selectCount() {
        return selectCount(new QueryWrapper<>());
    }
    default Long selectCount(String field, Object value) {
        return selectCount(new QueryWrapper<T>().eq(field, value));
    }
    default Long selectCount(SFunction<T, ?> field, Object value) {
        return selectCount(new LambdaQueryWrapper<T>().eq(field, value));
    }
    default List<T> selectList() {
        return selectList(new QueryWrapper<>());
    }
    default List<T> selectList(String field, Object value) {
        return selectList(new QueryWrapper<T>().eq(field, value));
    }
    default List<T> selectList(SFunction<T, ?> field, Object value) {
        return selectList(new LambdaQueryWrapper<T>().eq(field, value));
    }
    default List<T> selectList(String field, Collection<?> values) {
        if (CollUtil.isEmpty(values)) {
            return CollUtil.newArrayList();
        }
        return selectList(new QueryWrapper<T>().in(field, values));
    }
    default List<T> selectList(SFunction<T, ?> field, Collection<?> values) {
        if (CollUtil.isEmpty(values)) {
            return CollUtil.newArrayList();
        }
        return selectList(new LambdaQueryWrapper<T>().in(field, values));
    }
    @Deprecated
    default List<T> selectList(SFunction<T, ?> leField, SFunction<T, ?> geField, Object value) {
        return selectList(new LambdaQueryWrapper<T>().le(leField, value).ge(geField, value));
    }
    default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
        return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
    }
    /**
     * 批量插入,适合大量数据插入
     *
     * @param entities 实体们
     */
    default Boolean insertBatch(Collection<T> entities) {
        // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
        if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) {
            entities.forEach(this::insert);
            return CollUtil.isNotEmpty(entities);
        }
        return Db.saveBatch(entities);
    }
    /**
     * 批量插入,适合大量数据插入
     *
     * @param entities 实体们
     * @param size     插入数量 Db.saveBatch 默认为 1000
     */
    default Boolean insertBatch(Collection<T> entities, int size) {
        // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
        if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) {
            entities.forEach(this::insert);
            return CollUtil.isNotEmpty(entities);
        }
        return Db.saveBatch(entities, size);
    }
    default int updateBatch(T update) {
        return update(update, new QueryWrapper<>());
    }
    default Boolean updateBatch(Collection<T> entities) {
        return Db.updateBatchById(entities);
    }
    default Boolean updateBatch(Collection<T> entities, int size) {
        return Db.updateBatchById(entities, size);
    }
    default boolean insertOrUpdate(T entity) {
        return  Db.saveOrUpdate(entity);
    }
    default Boolean insertOrUpdateBatch(Collection<T> collection) {
        return Db.saveOrUpdateBatch(collection);
    }
    default int delete(String field, String value) {
        return delete(new QueryWrapper<T>().eq(field, value));
    }
    default int delete(SFunction<T, ?> field, Object value) {
        return delete(new LambdaQueryWrapper<T>().eq(field, value));
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/LambdaQueryWrapperX.java
对比新文件
@@ -0,0 +1,135 @@
package com.iailab.framework.mybatis.core.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.iailab.framework.common.util.collection.ArrayUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
 * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
 * <p>
 * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
 *
 * @param <T> 数据类型
 */
public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
    public LambdaQueryWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
        if (StringUtils.hasText(val)) {
            return (LambdaQueryWrapperX<T>) super.like(column, val);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
            return (LambdaQueryWrapperX<T>) super.in(column, values);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
            return (LambdaQueryWrapperX<T>) super.in(column, values);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
        if (ObjectUtil.isNotEmpty(val)) {
            return (LambdaQueryWrapperX<T>) super.eq(column, val);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
        if (ObjectUtil.isNotEmpty(val)) {
            return (LambdaQueryWrapperX<T>) super.ne(column, val);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (LambdaQueryWrapperX<T>) super.gt(column, val);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (LambdaQueryWrapperX<T>) super.ge(column, val);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (LambdaQueryWrapperX<T>) super.lt(column, val);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (LambdaQueryWrapperX<T>) super.le(column, val);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
        if (val1 != null && val2 != null) {
            return (LambdaQueryWrapperX<T>) super.between(column, val1, val2);
        }
        if (val1 != null) {
            return (LambdaQueryWrapperX<T>) ge(column, val1);
        }
        if (val2 != null) {
            return (LambdaQueryWrapperX<T>) le(column, val2);
        }
        return this;
    }
    public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
        Object val1 = ArrayUtils.get(values, 0);
        Object val2 = ArrayUtils.get(values, 1);
        return betweenIfPresent(column, val1, val2);
    }
    // ========== 重写父类方法,方便链式调用 ==========
    @Override
    public LambdaQueryWrapperX<T> eq(boolean condition, SFunction<T, ?> column, Object val) {
        super.eq(condition, column, val);
        return this;
    }
    @Override
    public LambdaQueryWrapperX<T> eq(SFunction<T, ?> column, Object val) {
        super.eq(column, val);
        return this;
    }
    @Override
    public LambdaQueryWrapperX<T> orderByDesc(SFunction<T, ?> column) {
        super.orderByDesc(true, column);
        return this;
    }
    @Override
    public LambdaQueryWrapperX<T> last(String lastSql) {
        super.last(lastSql);
        return this;
    }
    @Override
    public LambdaQueryWrapperX<T> in(SFunction<T, ?> column, Collection<?> coll) {
        super.in(column, coll);
        return this;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/MPJLambdaWrapperX.java
对比新文件
@@ -0,0 +1,313 @@
package com.iailab.framework.mybatis.core.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.iailab.framework.common.util.collection.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.github.yulichang.toolkit.MPJWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.function.Consumer;
/**
 * 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能:
 * <p>
 * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
 *
 * @param <T> 数据类型
 */
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
    public MPJLambdaWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
        MPJWrappers.lambdaJoin().like(column, val);
        if (StringUtils.hasText(val)) {
            return (MPJLambdaWrapperX<T>) super.like(column, val);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
            return (MPJLambdaWrapperX<T>) super.in(column, values);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
        if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
            return (MPJLambdaWrapperX<T>) super.in(column, values);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
        if (ObjectUtil.isNotEmpty(val)) {
            return (MPJLambdaWrapperX<T>) super.eq(column, val);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
        if (ObjectUtil.isNotEmpty(val)) {
            return (MPJLambdaWrapperX<T>) super.ne(column, val);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (MPJLambdaWrapperX<T>) super.gt(column, val);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (MPJLambdaWrapperX<T>) super.ge(column, val);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (MPJLambdaWrapperX<T>) super.lt(column, val);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
        if (val != null) {
            return (MPJLambdaWrapperX<T>) super.le(column, val);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
        if (val1 != null && val2 != null) {
            return (MPJLambdaWrapperX<T>) super.between(column, val1, val2);
        }
        if (val1 != null) {
            return (MPJLambdaWrapperX<T>) ge(column, val1);
        }
        if (val2 != null) {
            return (MPJLambdaWrapperX<T>) le(column, val2);
        }
        return this;
    }
    public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
        Object val1 = ArrayUtils.get(values, 0);
        Object val2 = ArrayUtils.get(values, 1);
        return betweenIfPresent(column, val1, val2);
    }
    // ========== 重写父类方法,方便链式调用 ==========
    @Override
    public <X> MPJLambdaWrapperX<T> eq(boolean condition, SFunction<X, ?> column, Object val) {
        super.eq(condition, column, val);
        return this;
    }
    @Override
    public <X> MPJLambdaWrapperX<T> eq(SFunction<X, ?> column, Object val) {
        super.eq(column, val);
        return this;
    }
    @Override
    public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) {
        //noinspection unchecked
        super.orderByDesc(true, column);
        return this;
    }
    @Override
    public MPJLambdaWrapperX<T> last(String lastSql) {
        super.last(lastSql);
        return this;
    }
    @Override
    public <X> MPJLambdaWrapperX<T> in(SFunction<X, ?> column, Collection<?> coll) {
        super.in(column, coll);
        return this;
    }
    @Override
    public MPJLambdaWrapperX<T> selectAll(Class<?> clazz) {
        super.selectAll(clazz);
        return this;
    }
    @Override
    public MPJLambdaWrapperX<T> selectAll(Class<?> clazz, String prefix) {
        super.selectAll(clazz, prefix);
        return this;
    }
    @Override
    public <S> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, String alias) {
        super.selectAs(column, alias);
        return this;
    }
    @Override
    public <E> MPJLambdaWrapperX<T> selectAs(String column, SFunction<E, ?> alias) {
        super.selectAs(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, SFunction<X, ?> alias) {
        super.selectAs(column, alias);
        return this;
    }
    @Override
    public <E, X> MPJLambdaWrapperX<T> selectAs(String index, SFunction<E, ?> column, SFunction<X, ?> alias) {
        super.selectAs(index, column, alias);
        return this;
    }
    @Override
    public <E> MPJLambdaWrapperX<T> selectAsClass(Class<E> source, Class<?> tag) {
        super.selectAsClass(source, tag);
        return this;
    }
    @Override
    public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
        super.selectSub(clazz, consumer, alias);
        return this;
    }
    @Override
    public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, String st, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
        super.selectSub(clazz, st, consumer, alias);
        return this;
    }
    @Override
    public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column) {
        super.selectCount(column);
        return this;
    }
    @Override
    public MPJLambdaWrapperX<T> selectCount(Object column, String alias) {
        super.selectCount(column, alias);
        return this;
    }
    @Override
    public <X> MPJLambdaWrapperX<T> selectCount(Object column, SFunction<X, ?> alias) {
        super.selectCount(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) {
        super.selectCount(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, SFunction<X, ?> alias) {
        super.selectCount(column, alias);
        return this;
    }
    @Override
    public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column) {
        super.selectSum(column);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) {
        super.selectSum(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, SFunction<X, ?> alias) {
        super.selectSum(column, alias);
        return this;
    }
    @Override
    public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column) {
        super.selectMax(column);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) {
        super.selectMax(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, SFunction<X, ?> alias) {
        super.selectMax(column, alias);
        return this;
    }
    @Override
    public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column) {
        super.selectMin(column);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) {
        super.selectMin(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, SFunction<X, ?> alias) {
        super.selectMin(column, alias);
        return this;
    }
    @Override
    public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column) {
        super.selectAvg(column);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) {
        super.selectAvg(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, SFunction<X, ?> alias) {
        super.selectAvg(column, alias);
        return this;
    }
    @Override
    public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column) {
        super.selectLen(column);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) {
        super.selectLen(column, alias);
        return this;
    }
    @Override
    public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, SFunction<X, ?> alias) {
        super.selectLen(column, alias);
        return this;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/query/QueryWrapperX.java
对比新文件
@@ -0,0 +1,166 @@
package com.iailab.framework.mybatis.core.query;
import cn.hutool.core.lang.Assert;
import com.iailab.framework.mybatis.core.enums.SqlConstants;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
 * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
 *
 * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
 *
 * @param <T> 数据类型
 */
public class QueryWrapperX<T> extends QueryWrapper<T> {
    public QueryWrapperX<T> likeIfPresent(String column, String val) {
        if (StringUtils.hasText(val)) {
            return (QueryWrapperX<T>) super.like(column, val);
        }
        return this;
    }
    public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
        if (!CollectionUtils.isEmpty(values)) {
            return (QueryWrapperX<T>) super.in(column, values);
        }
        return this;
    }
    public QueryWrapperX<T> inIfPresent(String column, Object... values) {
        if (!ArrayUtils.isEmpty(values)) {
            return (QueryWrapperX<T>) super.in(column, values);
        }
        return this;
    }
    public QueryWrapperX<T> eqIfPresent(String column, Object val) {
        if (val != null) {
            return (QueryWrapperX<T>) super.eq(column, val);
        }
        return this;
    }
    public QueryWrapperX<T> neIfPresent(String column, Object val) {
        if (val != null) {
            return (QueryWrapperX<T>) super.ne(column, val);
        }
        return this;
    }
    public QueryWrapperX<T> gtIfPresent(String column, Object val) {
        if (val != null) {
            return (QueryWrapperX<T>) super.gt(column, val);
        }
        return this;
    }
    public QueryWrapperX<T> geIfPresent(String column, Object val) {
        if (val != null) {
            return (QueryWrapperX<T>) super.ge(column, val);
        }
        return this;
    }
    public QueryWrapperX<T> ltIfPresent(String column, Object val) {
        if (val != null) {
            return (QueryWrapperX<T>) super.lt(column, val);
        }
        return this;
    }
    public QueryWrapperX<T> leIfPresent(String column, Object val) {
        if (val != null) {
            return (QueryWrapperX<T>) super.le(column, val);
        }
        return this;
    }
    public QueryWrapperX<T> betweenIfPresent(String column, Object val1, Object val2) {
        if (val1 != null && val2 != null) {
            return (QueryWrapperX<T>) super.between(column, val1, val2);
        }
        if (val1 != null) {
            return (QueryWrapperX<T>) ge(column, val1);
        }
        if (val2 != null) {
            return (QueryWrapperX<T>) le(column, val2);
        }
        return this;
    }
    public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
        if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
            return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
        }
        if (values!= null && values.length != 0 && values[0] != null) {
            return (QueryWrapperX<T>) ge(column, values[0]);
        }
        if (values!= null && values.length != 0 && values[1] != null) {
            return (QueryWrapperX<T>) le(column, values[1]);
        }
        return this;
    }
    // ========== 重写父类方法,方便链式调用 ==========
    @Override
    public QueryWrapperX<T> eq(boolean condition, String column, Object val) {
        super.eq(condition, column, val);
        return this;
    }
    @Override
    public QueryWrapperX<T> eq(String column, Object val) {
        super.eq(column, val);
        return this;
    }
    @Override
    public QueryWrapperX<T> orderByDesc(String column) {
        super.orderByDesc(true, column);
        return this;
    }
    @Override
    public QueryWrapperX<T> last(String lastSql) {
        super.last(lastSql);
        return this;
    }
    @Override
    public QueryWrapperX<T> in(String column, Collection<?> coll) {
        super.in(column, coll);
        return this;
    }
    /**
     * 设置只返回最后一条
     *
     * TODO iailab:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同
     *
     * @return this
     */
    public QueryWrapperX<T> limitN(int n) {
        Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型");
        switch (SqlConstants.DB_TYPE) {
            case ORACLE:
            case ORACLE_12C:
                super.le("ROWNUM", n);
                break;
            case SQL_SERVER:
            case SQL_SERVER2005:
                super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段
                break;
            default:
                super.last("LIMIT " + n);
        }
        return this;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/EncryptTypeHandler.java
对比新文件
@@ -0,0 +1,75 @@
package com.iailab.framework.mybatis.core.type;
import cn.hutool.core.lang.Assert;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.extra.spring.SpringUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
 * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现
 * 可通过 jasypt.encryptor.password 配置项,设置密钥
 *
 * @author iailab
 */
public class EncryptTypeHandler extends BaseTypeHandler<String> {
    private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password";
    private static AES aes;
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, encrypt(parameter));
    }
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return decrypt(value);
    }
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return decrypt(value);
    }
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return decrypt(value);
    }
    private static String decrypt(String value) {
        if (value == null) {
            return null;
        }
        return getEncryptor().decryptStr(value);
    }
    public static String encrypt(String rawValue) {
        if (rawValue == null) {
            return null;
        }
        return getEncryptor().encryptBase64(rawValue);
    }
    private static AES getEncryptor() {
        if (aes != null) {
            return aes;
        }
        // 构建 AES
        String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME);
        Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME);
        aes = SecureUtil.aes(password.getBytes());
        return aes;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/IntegerListTypeHandler.java
对比新文件
@@ -0,0 +1,56 @@
package com.iailab.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
 * List<Integer> 的类型转换器实现类,对应数据库的 varchar 类型
 *
 * @author jason
 */
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class IntegerListTypeHandler implements TypeHandler<List<Integer>> {
    private static final String COMMA = ",";
    @Override
    public void setParameter(PreparedStatement ps, int i, List<Integer> strings, JdbcType jdbcType) throws SQLException {
        ps.setString(i, CollUtil.join(strings, COMMA));
    }
    @Override
    public List<Integer> getResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return getResult(value);
    }
    @Override
    public List<Integer> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return getResult(value);
    }
    @Override
    public List<Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return getResult(value);
    }
    private List<Integer> getResult(String value) {
        if (value == null) {
            return null;
        }
        return StrUtils.splitToInteger(value, COMMA);
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/JsonLongSetTypeHandler.java
对比新文件
@@ -0,0 +1,31 @@
//package com.iailab.framework.mybatis.core.type;
//
//import com.iailab.framework.common.util.json.JsonUtils;
//import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
//import com.fasterxml.jackson.core.type.TypeReference;
//
//import java.util.Set;
//
///**
// * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现
// * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。
// *
// * 例如说哦,SysUserDO 的 postIds 属性
// *
// * @author iailab
// */
//public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler<Object> {
//
//    private static final TypeReference<Set<Long>> TYPE_REFERENCE = new TypeReference<Set<Long>>(){};
//
//    @Override
//    protected Object parse(String json) {
//        return JsonUtils.parseObject(json, TYPE_REFERENCE);
//    }
//
//    @Override
//    protected String toJson(Object obj) {
//        return JsonUtils.toJsonString(obj);
//    }
//
//}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/LongListTypeHandler.java
对比新文件
@@ -0,0 +1,57 @@
package com.iailab.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
 * List<Long> 的类型转换器实现类,对应数据库的 varchar 类型
 *
 * @author iailab
 */
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class LongListTypeHandler implements TypeHandler<List<Long>> {
    private static final String COMMA = ",";
    @Override
    public void setParameter(PreparedStatement ps, int i, List<Long> strings, JdbcType jdbcType) throws SQLException {
        // 设置占位符
        ps.setString(i, CollUtil.join(strings, COMMA));
    }
    @Override
    public List<Long> getResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return getResult(value);
    }
    @Override
    public List<Long> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return getResult(value);
    }
    @Override
    public List<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return getResult(value);
    }
    private List<Long> getResult(String value) {
        if (value == null) {
            return null;
        }
        return StrUtils.splitToLong(value, COMMA);
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/type/StringListTypeHandler.java
对比新文件
@@ -0,0 +1,58 @@
package com.iailab.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
 * List<String> 的类型转换器实现类,对应数据库的 varchar 类型
 *
 * @author 永不言败
 * @since 2022 3/23 12:50:15
 */
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class StringListTypeHandler implements TypeHandler<List<String>> {
    private static final String COMMA = ",";
    @Override
    public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
        // 设置占位符
        ps.setString(i, CollUtil.join(strings, COMMA));
    }
    @Override
    public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return getResult(value);
    }
    @Override
    public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return getResult(value);
    }
    @Override
    public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return getResult(value);
    }
    private List<String> getResult(String value) {
        if (value == null) {
            return null;
        }
        return StrUtil.splitTrim(value, COMMA);
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/JdbcUtils.java
对比新文件
@@ -0,0 +1,61 @@
package com.iailab.framework.mybatis.core.util;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.annotation.DbType;
import com.iailab.framework.common.util.spring.SpringUtils;
import com.iailab.framework.mybatis.core.enums.DbTypeEnum;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
 * JDBC 工具类
 *
 * @author iailab
 */
public class JdbcUtils {
    /**
     * 判断连接是否正确
     *
     * @param url      数据源连接
     * @param username 账号
     * @param password 密码
     * @return 是否正确
     */
    public static boolean isConnectionOK(String url, String username, String password) {
        try (Connection ignored = DriverManager.getConnection(url, username, password)) {
            return true;
        } catch (Exception ex) {
            return false;
        }
    }
    /**
     * 获得 URL 对应的 DB 类型
     *
     * @param url URL
     * @return DB 类型
     */
    public static DbType getDbType(String url) {
        return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url);
    }
    /**
     * 通过当前数据库连接获得对应的 DB 类型
     *
     * @return DB 类型
     */
    public static DbType getDbType() {
        DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
        DataSource dataSource = dynamicRoutingDataSource.determineDataSource();
        try (Connection conn = dataSource.getConnection()) {
            return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName());
        } catch (SQLException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/core/util/MyBatisUtils.java
对比新文件
@@ -0,0 +1,106 @@
package com.iailab.framework.mybatis.core.util;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.iailab.framework.common.pojo.PageParam;
import com.iailab.framework.common.pojo.SortingField;
import com.iailab.framework.mybatis.core.enums.DbTypeEnum;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
 * MyBatis 工具类
 */
public class MyBatisUtils {
    private static final String MYSQL_ESCAPE_CHARACTER = "`";
    public static <T> Page<T> buildPage(PageParam pageParam) {
        return buildPage(pageParam, null);
    }
    public static <T> Page<T> buildPage(PageParam pageParam, Collection<SortingField> sortingFields) {
        // 页码 + 数量
        Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
        // 排序字段
        if (!CollectionUtil.isEmpty(sortingFields)) {
            page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ?
                            OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField()))
                    .collect(Collectors.toList()));
        }
        return page;
    }
    /**
     * 将拦截器添加到链中
     * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
     *
     * @param interceptor 链
     * @param inner       拦截器
     * @param index       位置
     */
    public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) {
        List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
        inners.add(index, inner);
        interceptor.setInterceptors(inners);
    }
    /**
     * 获得 Table 对应的表名
     * <p>
     * 兼容 MySQL 转义表名 `t_xxx`
     *
     * @param table 表
     * @return 去除转移字符后的表名
     */
    public static String getTableName(Table table) {
        String tableName = table.getName();
        if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
            tableName = tableName.substring(1, tableName.length() - 1);
        }
        return tableName;
    }
    /**
     * 构建 Column 对象
     *
     * @param tableName  表名
     * @param tableAlias 别名
     * @param column     字段名
     * @return Column 对象
     */
    public static Column buildColumn(String tableName, Alias tableAlias, String column) {
        if (tableAlias != null) {
            tableName = tableAlias.getName();
        }
        return new Column(tableName + StringPool.DOT + column);
    }
    /**
     * 跨数据库的 find_in_set 实现
     *
     * @param column 字段名称
     * @param value  查询值(不带单引号)
     * @return sql
     */
    public static String findInSet(String column, Object value) {
        // 这里不用SqlConstants.DB_TYPE,因为它是使用 primary 数据源的 url 推断出来的类型
        DbType dbType = JdbcUtils.getDbType();
        return DbTypeEnum.getFindInSetTemplate(dbType)
                .replace("#{column}", column)
                .replace("#{value}", StrUtil.toString(value));
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataFilterInterceptor.java
对比新文件
@@ -0,0 +1,89 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.mybatis.interceptor;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Map;
/**
 * 数据过滤
 *
 * @author Mark sunlightcs@gmail.com
 */
public class DataFilterInterceptor implements InnerInterceptor {
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        DataScope scope = getDataScope(parameter);
        // 不进行数据过滤
        if(scope == null || StrUtil.isBlank(scope.getSqlFilter())){
            return;
        }
        // 拼接新SQL
        String buildSql = getSelect(boundSql.getSql(), scope);
        // 重写SQL
        PluginUtils.mpBoundSql(boundSql).sql(buildSql);
    }
    private DataScope getDataScope(Object parameter){
        if (parameter == null){
            return null;
        }
        // 判断参数里是否有DataScope对象
        if (parameter instanceof Map) {
            Map<?, ?> parameterMap = (Map<?, ?>) parameter;
            for (Map.Entry entry : parameterMap.entrySet()) {
                if (entry.getValue() != null && entry.getValue() instanceof DataScope) {
                    return (DataScope) entry.getValue();
                }
            }
        } else if (parameter instanceof DataScope) {
            return (DataScope) parameter;
        }
        return null;
    }
    private String getSelect(String buildSql, DataScope scope){
        try {
            Select select = (Select) CCJSqlParserUtil.parse(buildSql);
            PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
            Expression expression = plainSelect.getWhere();
            if(expression == null){
                plainSelect.setWhere(new StringValue(scope.getSqlFilter()));
            }else{
                AndExpression andExpression =  new AndExpression(expression, new StringValue(scope.getSqlFilter()));
                plainSelect.setWhere(andExpression);
            }
            return select.toString().replaceAll("'", "");
        }catch (JSQLParserException e){
            return buildSql;
        }
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/interceptor/DataScope.java
对比新文件
@@ -0,0 +1,36 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.mybatis.interceptor;
/**
 * 数据范围
 *
 * @author Mark sunlightcs@gmail.com
 * @since 1.0.0
 */
public class DataScope {
    private String sqlFilter;
    public DataScope(String sqlFilter) {
        this.sqlFilter = sqlFilter;
    }
    public String getSqlFilter() {
        return sqlFilter;
    }
    public void setSqlFilter(String sqlFilter) {
        this.sqlFilter = sqlFilter;
    }
    @Override
    public String toString() {
        return this.sqlFilter;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/mybatis/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率
 */
package com.iailab.framework.mybatis;
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/config/IailabTranslateAutoConfiguration.java
对比新文件
@@ -0,0 +1,18 @@
package com.iailab.framework.translate.config;
import com.iailab.framework.translate.core.TranslateUtils;
import com.fhs.trans.service.impl.TransService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class IailabTranslateAutoConfiguration {
    @Bean
    @SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"})
    public TranslateUtils translateUtils(TransService transService) {
        TranslateUtils.init(transService);
        return new TranslateUtils();
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/core/TranslateUtils.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.translate.core;
import cn.hutool.core.collection.CollUtil;
import com.fhs.core.trans.vo.VO;
import com.fhs.trans.service.impl.TransService;
import java.util.List;
/**
 * VO 数据翻译 Utils
 *
 * @author iailab
 */
public class TranslateUtils {
    private static TransService transService;
    public static void init(TransService transService) {
        TranslateUtils.transService = transService;
    }
    /**
     * 数据翻译
     *
     * 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译
     *
     * @param data 数据
     * @return 翻译结果
     */
    public static <T extends VO> List<T> translate(List<T> data) {
        if (CollUtil.isNotEmpty((data))) {
            transService.transBatch(data);
        }
        return data;
    }
}
iailab-framework/iailab-common-mybatis/src/main/java/com/iailab/framework/translate/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 使用 Easy-Trans 提升使用 VO 数据翻译的开发效率
 */
package com.iailab.framework.translate;
iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring.factories
对比新文件
@@ -0,0 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.iailab.framework.mybatis.config.IdTypeEnvironmentPostProcessor
iailab-framework/iailab-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,3 @@
com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration
com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration
com.iailab.framework.translate.config.IailabTranslateAutoConfiguration
iailab-framework/iailab-common-protection/pom.xml
对比新文件
@@ -0,0 +1,47 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-protection</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>服务保证,提供分布式锁、幂等、限流、熔断等等功能</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <!-- Web 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-web</artifactId>
            <scope>provided</scope> <!-- 设置为 provided,只有限流、幂等使用到 -->
        </dependency>
        <!-- DB 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-redis</artifactId>
        </dependency>
        <!-- 服务保障相关 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/config/IailabIdempotentConfiguration.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.idempotent.config;
import com.iailab.framework.idempotent.core.aop.IdempotentAspect;
import com.iailab.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.iailab.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver;
import com.iailab.framework.idempotent.core.redis.IdempotentRedisDAO;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
public class IailabIdempotentConfiguration {
    @Bean
    public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
        return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
    }
    @Bean
    public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
        return new IdempotentRedisDAO(stringRedisTemplate);
    }
    // ========== 各种 IdempotentKeyResolver Bean ==========
    @Bean
    public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
        return new DefaultIdempotentKeyResolver();
    }
    @Bean
    public UserIdempotentKeyResolver userIdempotentKeyResolver() {
        return new UserIdempotentKeyResolver();
    }
    @Bean
    public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
        return new ExpressionIdempotentKeyResolver();
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/annotation/Idempotent.java
对比新文件
@@ -0,0 +1,63 @@
package com.iailab.framework.idempotent.core.annotation;
import com.iailab.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import com.iailab.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
 * 幂等注解
 *
 * @author iailab
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    /**
     * 幂等的超时时间,默认为 1 秒
     *
     * 注意,如果执行时间超过它,请求还是会进来
     */
    int timeout() default 1;
    /**
     * 时间单位,默认为 SECONDS 秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     * 提示信息,正在执行中的提示
     */
    String message() default "重复请求,请稍后重试";
    /**
     * 使用的 Key 解析器
     *
     * @see DefaultIdempotentKeyResolver 全局级别
     * @see UserIdempotentKeyResolver 用户级别
     * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
     */
    Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
    /**
     * 使用的 Key 参数
     */
    String keyArg() default "";
    /**
     * 删除 Key,当发生异常时候
     *
     * 问题:为什么发生异常时,需要删除 Key 呢?
     * 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。
     *
     * 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢?
     * 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解
     */
    boolean deleteKeyWhenException() default true;
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/aop/IdempotentAspect.java
对比新文件
@@ -0,0 +1,68 @@
package com.iailab.framework.idempotent.core.aop;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.common.util.collection.CollectionUtils;
import com.iailab.framework.idempotent.core.annotation.Idempotent;
import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.iailab.framework.idempotent.core.redis.IdempotentRedisDAO;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Map;
/**
 * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作
 *
 * @author iailab
 */
@Aspect
@Slf4j
public class IdempotentAspect {
    /**
     * IdempotentKeyResolver 集合
     */
    private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
    private final IdempotentRedisDAO idempotentRedisDAO;
    public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
        this.idempotentRedisDAO = idempotentRedisDAO;
    }
    @Around(value = "@annotation(idempotent)")
    public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        // 获得 IdempotentKeyResolver
        IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
        Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
        // 解析 Key
        String key = keyResolver.resolver(joinPoint, idempotent);
        // 1. 锁定 Key
        boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
        // 锁定失败,抛出异常
        if (!success) {
            log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
            throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
        }
        // 2. 执行逻辑
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            // 3. 异常时,删除 Key
            // 参考美团 GTIS 思路:https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html
            if (idempotent.deleteKeyWhenException()) {
                idempotentRedisDAO.delete(key);
            }
            throw throwable;
        }
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.idempotent.core.keyresolver;
import com.iailab.framework.idempotent.core.annotation.Idempotent;
import org.aspectj.lang.JoinPoint;
/**
 * 幂等 Key 解析器接口
 *
 * @author iailab
 */
public interface IdempotentKeyResolver {
    /**
     * 解析一个 Key
     *
     * @param idempotent 幂等注解
     * @param joinPoint  AOP 切面
     * @return Key
     */
    String resolver(JoinPoint joinPoint, Idempotent idempotent);
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.iailab.framework.idempotent.core.annotation.Idempotent;
import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
 * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author iailab
 */
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        return SecureUtil.md5(methodName + argsStr);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java
对比新文件
@@ -0,0 +1,63 @@
package com.iailab.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.ArrayUtil;
import com.iailab.framework.idempotent.core.annotation.Idempotent;
import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
/**
 * 基于 Spring EL 表达式,
 *
 * @author iailab
 */
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
    private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
    private final ExpressionParser expressionParser = new SpelExpressionParser();
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        // 获得被拦截方法参数名列表
        Method method = getMethod(joinPoint);
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
        // 准备 Spring EL 表达式解析的上下文
        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
        if (ArrayUtil.isNotEmpty(parameterNames)) {
            for (int i = 0; i < parameterNames.length; i++) {
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
        }
        // 解析参数
        Expression expression = expressionParser.parseExpression(idempotent.keyArg());
        return expression.getValue(evaluationContext, String.class);
    }
    private static Method getMethod(JoinPoint point) {
        // 处理,声明在类上的情况
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (!method.getDeclaringClass().isInterface()) {
            return method;
        }
        // 处理,声明在接口上的情况
        try {
            return point.getTarget().getClass().getDeclaredMethod(
                    point.getSignature().getName(), method.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.iailab.framework.idempotent.core.annotation.Idempotent;
import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import org.aspectj.lang.JoinPoint;
/**
 * 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author iailab
 */
public class UserIdempotentKeyResolver implements IdempotentKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        Long userId = WebFrameworkUtils.getLoginUserId();
        Integer userType = WebFrameworkUtils.getLoginUserType();
        return SecureUtil.md5(methodName + argsStr + userId + userType);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/core/redis/IdempotentRedisDAO.java
对比新文件
@@ -0,0 +1,41 @@
package com.iailab.framework.idempotent.core.redis;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
 * 幂等 Redis DAO
 *
 * @author iailab
 */
@AllArgsConstructor
public class IdempotentRedisDAO {
    /**
     * 幂等操作
     *
     * KEY 格式:idempotent:%s // 参数为 uuid
     * VALUE 格式:String
     * 过期时间:不固定
     */
    private static final String IDEMPOTENT = "idempotent:%s";
    private final StringRedisTemplate redisTemplate;
    public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
        String redisKey = formatKey(key);
        return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
    }
    public void delete(String key) {
        String redisKey = formatKey(key);
        redisTemplate.delete(redisKey);
    }
    private static String formatKey(String key) {
        return String.format(IDEMPOTENT, key);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/idempotent/package-info.java
对比新文件
@@ -0,0 +1,12 @@
/**
 * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现
 * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。
 *
 * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。
 *
 * 和 it4alla/idempotent 组件的差异点,主要体现在两点:
 *  1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力
 * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。
 *  2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。
 */
package com.iailab.framework.idempotent;
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/config/IailabLock4jConfiguration.java
对比新文件
@@ -0,0 +1,18 @@
package com.iailab.framework.lock4j.config;
import com.iailab.framework.lock4j.core.DefaultLockFailureStrategy;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
@AutoConfiguration(before = LockAutoConfiguration.class)
@ConditionalOnClass(name = "com.baomidou.lock.annotation.Lock4j")
public class IailabLock4jConfiguration {
    @Bean
    public DefaultLockFailureStrategy lockFailureStrategy() {
        return new DefaultLockFailureStrategy();
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/DefaultLockFailureStrategy.java
对比新文件
@@ -0,0 +1,21 @@
package com.iailab.framework.lock4j.core;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.baomidou.lock.LockFailureStrategy;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method;
/**
 * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常
 */
@Slf4j
public class DefaultLockFailureStrategy implements LockFailureStrategy {
    @Override
    public void onLockFailure(String key, Method method, Object[] arguments) {
        log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments);
        throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/core/Lock4jRedisKeyConstants.java
对比新文件
@@ -0,0 +1,19 @@
package com.iailab.framework.lock4j.core;
/**
 * Lock4j Redis Key 枚举类
 *
 * @author iailab
 */
public interface Lock4jRedisKeyConstants {
    /**
     * 分布式锁
     *
     * KEY 格式:lock4j:%s // 参数来自 DefaultLockKeyBuilder 类
     * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构
     * 过期时间:不固定
     */
    String LOCK4J = "lock4j:%s";
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/lock4j/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目
 */
package com.iailab.framework.lock4j;
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/config/IailabRateLimiterConfiguration.java
对比新文件
@@ -0,0 +1,55 @@
package com.iailab.framework.ratelimiter.config;
import com.iailab.framework.ratelimiter.core.aop.RateLimiterAspect;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import com.iailab.framework.ratelimiter.core.keyresolver.impl.*;
import com.iailab.framework.ratelimiter.core.redis.RateLimiterRedisDAO;
import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.List;
@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
public class IailabRateLimiterConfiguration {
    @Bean
    public RateLimiterAspect rateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
        return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO);
    }
    @Bean
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) {
        return new RateLimiterRedisDAO(redissonClient);
    }
    // ========== 各种 RateLimiterRedisDAO Bean ==========
    @Bean
    public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() {
        return new DefaultRateLimiterKeyResolver();
    }
    @Bean
    public UserRateLimiterKeyResolver userRateLimiterKeyResolver() {
        return new UserRateLimiterKeyResolver();
    }
    @Bean
    public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() {
        return new ClientIpRateLimiterKeyResolver();
    }
    @Bean
    public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() {
        return new ServerNodeRateLimiterKeyResolver();
    }
    @Bean
    public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() {
        return new ExpressionRateLimiterKeyResolver();
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/annotation/RateLimiter.java
对比新文件
@@ -0,0 +1,62 @@
package com.iailab.framework.ratelimiter.core.annotation;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import com.iailab.framework.ratelimiter.core.keyresolver.impl.ClientIpRateLimiterKeyResolver;
import com.iailab.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver;
import com.iailab.framework.ratelimiter.core.keyresolver.impl.ServerNodeRateLimiterKeyResolver;
import com.iailab.framework.ratelimiter.core.keyresolver.impl.UserRateLimiterKeyResolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
 * 限流注解
 *
 * @author iailab
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
    /**
     * 限流的时间,默认为 1 秒
     */
    int time() default 1;
    /**
     * 时间单位,默认为 SECONDS 秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     * 限流次数
     */
    int count() default 100;
    /**
     * 提示信息,请求过快的提示
     *
     * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS
     */
    String message() default ""; // 为空时,使用 TOO_MANY_REQUESTS 错误提示
    /**
     * 使用的 Key 解析器
     *
     * @see DefaultRateLimiterKeyResolver 全局级别
     * @see UserRateLimiterKeyResolver 用户 ID 级别
     * @see ClientIpRateLimiterKeyResolver 用户 IP 级别
     * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别
     * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
     */
    Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;
    /**
     * 使用的 Key 参数
     */
    String keyArg() default "";
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/aop/RateLimiterAspect.java
对比新文件
@@ -0,0 +1,60 @@
package com.iailab.framework.ratelimiter.core.aop;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.common.util.collection.CollectionUtils;
import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import com.iailab.framework.ratelimiter.core.redis.RateLimiterRedisDAO;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Map;
/**
 * 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作
 *
 * @author iailab
 */
@Aspect
@Slf4j
public class RateLimiterAspect {
    /**
     * RateLimiterKeyResolver 集合
     */
    private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;
    private final RateLimiterRedisDAO rateLimiterRedisDAO;
    public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
        this.rateLimiterRedisDAO = rateLimiterRedisDAO;
    }
    @Before("@annotation(rateLimiter)")
    public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
        // 获得 IdempotentKeyResolver 对象
        RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
        Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
        // 解析 Key
        String key = keyResolver.resolver(joinPoint, rateLimiter);
        // 获取 1 次限流
        boolean success = rateLimiterRedisDAO.tryAcquire(key,
                rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit());
        if (!success) {
            log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs());
            String message = StrUtil.blankToDefault(rateLimiter.message(),
                    GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());
            throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message);
        }
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.ratelimiter.core.keyresolver;
import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
import org.aspectj.lang.JoinPoint;
/**
 * 限流 Key 解析器接口
 *
 * @author iailab
 */
public interface RateLimiterKeyResolver {
    /**
     * 解析一个 Key
     *
     * @param rateLimiter 限流注解
     * @param joinPoint  AOP 切面
     * @return Key
     */
    String resolver(JoinPoint joinPoint, RateLimiter rateLimiter);
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
 * IP 级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author iailab
 */
public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        String clientIp = ServletUtils.getClientIP();
        return SecureUtil.md5(methodName + argsStr + clientIp);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
 * 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author iailab
 */
public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        return SecureUtil.md5(methodName + argsStr);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java
对比新文件
@@ -0,0 +1,64 @@
package com.iailab.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.ArrayUtil;
import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
/**
 * 基于 Spring EL 表达式的 {@link RateLimiterKeyResolver} 实现类
 *
 * @author iailab
 */
public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver {
    private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    private final ExpressionParser expressionParser = new SpelExpressionParser();
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        // 获得被拦截方法参数名列表
        Method method = getMethod(joinPoint);
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
        // 准备 Spring EL 表达式解析的上下文
        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
        if (ArrayUtil.isNotEmpty(parameterNames)) {
            for (int i = 0; i < parameterNames.length; i++) {
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
        }
        // 解析参数
        Expression expression = expressionParser.parseExpression(rateLimiter.keyArg());
        return expression.getValue(evaluationContext, String.class);
    }
    private static Method getMethod(JoinPoint point) {
        // 处理,声明在类上的情况
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (!method.getDeclaringClass().isInterface()) {
            return method;
        }
        // 处理,声明在接口上的情况
        try {
            return point.getTarget().getClass().getDeclaredMethod(
                    point.getSignature().getName(), method.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.system.SystemUtil;
import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
 * Server 节点级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author iailab
 */
public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
        return SecureUtil.md5(methodName + argsStr + serverNode);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.iailab.framework.ratelimiter.core.annotation.RateLimiter;
import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import org.aspectj.lang.JoinPoint;
/**
 * 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author iailab
 */
public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {
    @Override
    public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
        String methodName = joinPoint.getSignature().toString();
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        Long userId = WebFrameworkUtils.getLoginUserId();
        Integer userType = WebFrameworkUtils.getLoginUserType();
        return SecureUtil.md5(methodName + argsStr + userId + userType);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java
对比新文件
@@ -0,0 +1,60 @@
package com.iailab.framework.ratelimiter.core.redis;
import lombok.AllArgsConstructor;
import org.redisson.api.*;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
 * 限流 Redis DAO
 *
 * @author iailab
 */
@AllArgsConstructor
public class RateLimiterRedisDAO {
    /**
     * 限流操作
     *
     * KEY 格式:rate_limiter:%s // 参数为 uuid
     * VALUE 格式:String
     * 过期时间:不固定
     */
    private static final String RATE_LIMITER = "rate_limiter:%s";
    private final RedissonClient redissonClient;
    public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
        // 1. 获得 RRateLimiter,并设置 rate 速率
        RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
        // 2. 尝试获取 1 个
        return rateLimiter.tryAcquire();
    }
    private static String formatKey(String key) {
        return String.format(RATE_LIMITER, key);
    }
    private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
        String redisKey = formatKey(key);
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
        long rateInterval = timeUnit.toSeconds(time);
        // 1. 如果不存在,设置 rate 速率
        RateLimiterConfig config = rateLimiter.getConfig();
        if (config == null) {
            rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
            return rateLimiter;
        }
        // 2. 如果存在,并且配置相同,则直接返回
        if (config.getRateType() == RateType.OVERALL
                && Objects.equals(config.getRate(), count)
                && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
            return rateLimiter;
        }
        // 3. 如果存在,并且配置不同,则进行新建
        rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
        return rateLimiter;
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/ratelimiter/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 限流组件,基于 Redisson {@link org.redisson.api.RRateLimiter} 限流实现
 */
package com.iailab.framework.ratelimiter;
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/config/IailabApiSignatureAutoConfiguration.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.signature.config;
import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
import com.iailab.framework.signature.core.aop.ApiSignatureAspect;
import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
 * HTTP API 签名的自动配置类
 *
 * @author Zhougang
 */
@AutoConfiguration(after = IailabRedisAutoConfiguration.class)
public class IailabApiSignatureAutoConfiguration {
    @Bean
    public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
        return new ApiSignatureAspect(signatureRedisDAO);
    }
    @Bean
    public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
        return new ApiSignatureRedisDAO(stringRedisTemplate);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/annotation/ApiSignature.java
对比新文件
@@ -0,0 +1,59 @@
package com.iailab.framework.signature.core.annotation;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
 * HTTP API 签名注解
 *
 * @author Zhougang
 */
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiSignature {
    /**
     * 同一个请求多长时间内有效 默认 60 秒
     */
    int timeout() default 60;
    /**
     * 时间单位,默认为 SECONDS 秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    // ========================== 签名参数 ==========================
    /**
     * 提示信息,签名失败的提示
     *
     * @see GlobalErrorCodeConstants#BAD_REQUEST
     */
    String message() default "签名不正确"; // 为空时,使用 BAD_REQUEST 错误提示
    /**
     * 签名字段:appId 应用ID
     */
    String appId() default "appId";
    /**
     * 签名字段:timestamp 时间戳
     */
    String timestamp() default "timestamp";
    /**
     * 签名字段:nonce 随机数,10 位以上
     */
    String nonce() default "nonce";
    /**
     * sign 客户端签名
     */
    String sign() default "sign";
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/aop/ApiSignatureAspect.java
对比新文件
@@ -0,0 +1,169 @@
package com.iailab.framework.signature.core.aop;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.signature.core.annotation.ApiSignature;
import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
/**
 * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名
 *
 * @author Zhougang
 */
@Aspect
@Slf4j
@AllArgsConstructor
public class ApiSignatureAspect {
    private final ApiSignatureRedisDAO signatureRedisDAO;
    @Before("@annotation(signature)")
    public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
        // 1. 验证通过,直接结束
        if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
            return;
        }
        // 2. 验证不通过,抛出异常
        log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
                joinPoint.getArgs());
        throw new ServiceException(BAD_REQUEST.getCode(),
                StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg()));
    }
    public boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
        // 1.1 校验 Header
        if (!verifyHeaders(signature, request)) {
            return false;
        }
        // 1.2 校验 appId 是否能获取到对应的 appSecret
        String appId = request.getHeader(signature.appId());
        String appSecret = signatureRedisDAO.getAppSecret(appId);
        Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
        // 2. 校验签名【重要!】
        String clientSignature = request.getHeader(signature.sign()); // 客户端签名
        String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
        String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
        if (ObjUtil.notEqual(clientSignature, serverSignature)) {
            return false;
        }
        // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
        String nonce = request.getHeader(signature.nonce());
        signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
        return true;
    }
    /**
     * 校验请求头加签参数
     *
     * 1. appId 是否为空
     * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
     * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
     * 4. sign 是否为空
     *
     * @param signature signature
     * @param request   request
     * @return 是否校验 Header 通过
     */
    private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
        // 1. 非空校验
        String appId = request.getHeader(signature.appId());
        if (StrUtil.isBlank(appId)) {
            return false;
        }
        String timestamp = request.getHeader(signature.timestamp());
        if (StrUtil.isBlank(timestamp)) {
            return false;
        }
        String nonce = request.getHeader(signature.nonce());
        if (StrUtil.length(nonce) < 10) {
            return false;
        }
        String sign = request.getHeader(signature.sign());
        if (StrUtil.isBlank(sign)) {
            return false;
        }
        // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)
        long expireTime = signature.timeUnit().toMillis(signature.timeout());
        long requestTimestamp = Long.parseLong(timestamp);
        long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
        if (timestampDisparity > expireTime) {
            return false;
        }
        // 3. 检查 nonce 是否存在,有且仅能使用一次
        return signatureRedisDAO.getNonce(nonce) == null;
    }
    /**
     * 构建签名字符串
     *
     * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
     *
     * @param signature signature
     * @param request   request
     * @param appSecret appSecret
     * @return 签名字符串
     */
    private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
        SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头
        SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request); // 请求参数
        String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体
        return MapUtil.join(parameterMap, "&", "=")
                + requestBody
                + MapUtil.join(headerMap, "&", "=")
                + appSecret;
    }
    /**
     * 获取请求头加签参数 Map
     *
     * @param request 请求
     * @param signature 签名注解
     * @return signature params
     */
    private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
        sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
        sortedMap.put(signature.nonce(), request.getHeader(signature.nonce()));
        return sortedMap;
    }
    /**
     * 获取请求参数 Map
     *
     * @param request 请求
     * @return queryParams
     */
    private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
        SortedMap<String, String> sortedMap = new TreeMap<>();
        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
            sortedMap.put(entry.getKey(), entry.getValue()[0]);
        }
        return sortedMap;
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/core/redis/ApiSignatureRedisDAO.java
对比新文件
@@ -0,0 +1,57 @@
package com.iailab.framework.signature.core.redis;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
 * HTTP API 签名 Redis DAO
 *
 * @author Zhougang
 */
@AllArgsConstructor
public class ApiSignatureRedisDAO {
    private final StringRedisTemplate stringRedisTemplate;
    /**
     * 验签随机数
     *
     * KEY 格式:signature_nonce:%s // 参数为 随机数
     * VALUE 格式:String
     * 过期时间:不固定
     */
    private static final String SIGNATURE_NONCE = "api_signature_nonce:%s";
    /**
     * 签名密钥
     *
     * HASH 结构
     * KEY 格式:%s // 参数为 appid
     * VALUE 格式:String
     * 过期时间:永不过期(预加载到 Redis)
     */
    private static final String SIGNATURE_APPID = "api_signature_app";
    // ========== 验签随机数 ==========
    public String getNonce(String nonce) {
        return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
    }
    public void setNonce(String nonce, int time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
    }
    private static String formatNonceKey(String key) {
        return String.format(SIGNATURE_NONCE, key);
    }
    // ========== 签名密钥 ==========
    public String getAppSecret(String appId) {
        return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
    }
}
iailab-framework/iailab-common-protection/src/main/java/com/iailab/framework/signature/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * HTTP API 签名,校验安全性
 *
 * @see <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3>微信支付 —— 安全规范</a>
 */
package com.iailab.framework.signature;
iailab-framework/iailab-common-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,3 @@
com.iailab.framework.idempotent.config.IailabIdempotentConfiguration
com.iailab.framework.lock4j.config.IailabLock4jConfiguration
com.iailab.framework.ratelimiter.config.IailabRateLimiterConfiguration
iailab-framework/iailab-common-protection/src/test/java/com/iailab/framework/signature/core/ApiSignatureTest.java
对比新文件
@@ -0,0 +1,75 @@
package com.iailab.framework.signature.core;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.iailab.framework.signature.core.annotation.ApiSignature;
import com.iailab.framework.signature.core.aop.ApiSignatureAspect;
import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
 * {@link ApiSignatureTest} 的单元测试
 */
@ExtendWith(MockitoExtension.class)
public class ApiSignatureTest {
    @InjectMocks
    private ApiSignatureAspect apiSignatureAspect;
    @Mock
    private ApiSignatureRedisDAO signatureRedisDAO;
    @Test
    public void testSignatureGet() throws IOException {
        // 搞一个签名
        Long timestamp = System.currentTimeMillis();
        String nonce = IdUtil.randomUUID();
        String appId = "xxxxxx";
        String appSecret = "yyyyyy";
        String signString = "k1=v1&v1=k1testappId=xxxxxx&nonce=" + nonce + "&timestamp=" + timestamp + "yyyyyy";
        String sign = DigestUtil.sha256Hex(signString);
        // 准备参数
        ApiSignature apiSignature = mock(ApiSignature.class);
        when(apiSignature.appId()).thenReturn("appId");
        when(apiSignature.timestamp()).thenReturn("timestamp");
        when(apiSignature.nonce()).thenReturn("nonce");
        when(apiSignature.sign()).thenReturn("sign");
        when(apiSignature.timeout()).thenReturn(60);
        when(apiSignature.timeUnit()).thenReturn(TimeUnit.SECONDS);
        HttpServletRequest request = mock(HttpServletRequest.class);
        when(request.getHeader(eq("appId"))).thenReturn(appId);
        when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp));
        when(request.getHeader(eq("nonce"))).thenReturn(nonce);
        when(request.getHeader(eq("sign"))).thenReturn(sign);
        when(request.getParameterMap()).thenReturn(MapUtil.<String, String[]>builder()
                .put("v1", new String[]{"k1"}).put("k1", new String[]{"v1"}).build());
        when(request.getContentType()).thenReturn("application/json");
        when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
        // mock 方法
        when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
        // 调用
        boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
        // 断言结果
        assertTrue(result);
        // 断言调用
        verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS));
    }
}
iailab-framework/iailab-common-redis/pom.xml
对比新文件
@@ -0,0 +1,41 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-redis</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>Redis 封装拓展</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- DB 相关 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 -->
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheAutoConfiguration.java
对比新文件
@@ -0,0 +1,82 @@
package com.iailab.framework.redis.config;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.redis.core.TimeoutRedisCacheManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.StringUtils;
import java.util.Objects;
import static com.iailab.framework.redis.config.IailabRedisAutoConfiguration.buildRedisSerializer;
/**
 * Cache 配置类,基于 Redis 实现
 */
@AutoConfiguration
@EnableConfigurationProperties({CacheProperties.class, IailabCacheProperties.class})
@EnableCaching
public class IailabCacheAutoConfiguration {
    /**
     * RedisCacheConfiguration Bean
     * <p>
     * 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法
     */
    @Bean
    @Primary
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置使用 : 单冒号,而不是双 :: 冒号,避免 Redis Desktop Manager 多余空格
        // 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客
        // 再次修复单冒号,而不是双 :: 冒号问题,Issues 详情:https://gitee.com/zhijiantianya/iailab-cloud/issues/I86VY2
        config = config.computePrefixWith(cacheName -> {
            String keyPrefix = cacheProperties.getRedis().getKeyPrefix();
            if (StringUtils.hasText(keyPrefix)) {
                keyPrefix = keyPrefix.lastIndexOf(StrUtil.COLON) == -1 ? keyPrefix + StrUtil.COLON : keyPrefix;
                return keyPrefix + cacheName + StrUtil.COLON;
            }
            return cacheName + StrUtil.COLON;
        });
        // 设置使用 JSON 序列化方式
        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer()));
        // 设置 CacheProperties.Redis 的属性
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
                                               RedisCacheConfiguration redisCacheConfiguration,
                                               IailabCacheProperties iailabCacheProperties) {
        // 创建 RedisCacheWriter 对象
        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
                BatchStrategies.scan(iailabCacheProperties.getRedisScanBatchSize()));
        // 创建 TenantRedisCacheManager 对象
        return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
    }
}
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabCacheProperties.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.redis.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
 * Cache 配置项
 *
 * @author Wanwan
 */
@ConfigurationProperties("iailab.cache")
@Data
@Validated
public class IailabCacheProperties {
    /**
     * {@link #redisScanBatchSize} 默认值
     */
    private static final Integer REDIS_SCAN_BATCH_SIZE_DEFAULT = 30;
    /**
     * redis scan 一次返回数量
     */
    private Integer redisScanBatchSize = REDIS_SCAN_BATCH_SIZE_DEFAULT;
}
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/config/IailabRedisAutoConfiguration.java
对比新文件
@@ -0,0 +1,45 @@
package com.iailab.framework.redis.config;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
 * Redis 配置类
 */
@AutoConfiguration(before = RedissonAutoConfiguration.class) // 目的:使用自己定义的 RedisTemplate Bean
public class IailabRedisAutoConfiguration {
    /**
     * 创建 RedisTemplate Bean,使用 JSON 序列化方式
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。
        template.setConnectionFactory(factory);
        // 使用 String 序列化方式,序列化 KEY 。
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
        template.setValueSerializer(buildRedisSerializer());
        template.setHashValueSerializer(buildRedisSerializer());
        return template;
    }
    public static RedisSerializer<?> buildRedisSerializer() {
        RedisSerializer<Object> json = RedisSerializer.json();
        // 解决 LocalDateTime 的序列化
        ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
        objectMapper.registerModules(new JavaTimeModule());
        return json;
    }
}
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/core/TimeoutRedisCacheManager.java
对比新文件
@@ -0,0 +1,86 @@
package com.iailab.framework.redis.core;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.time.Duration;
/**
 * 支持自定义过期时间的 {@link RedisCacheManager} 实现类
 *
 * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间。
 * 单位为最后一个字母(支持的单位有:d 天,h 小时,m 分钟,s 秒),默认单位为 s 秒
 *
 * @author iailab
 */
public class TimeoutRedisCacheManager extends RedisCacheManager {
    private static final String SPLIT = "#";
    public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }
    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        if (StrUtil.isEmpty(name)) {
            return super.createRedisCache(name, cacheConfig);
        }
        // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间
        String[] names = StrUtil.splitToArray(name, SPLIT);
        if (names.length != 2) {
            return super.createRedisCache(name, cacheConfig);
        }
        // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间
        if (cacheConfig != null) {
            // 移除 # 后面的 : 以及后面的内容,避免影响解析
            String ttlStr = StrUtil.subBefore(names[1], StrUtil.COLON, false); // 获得 ttlStr 时间部分
            names[1] = StrUtil.subAfter(names[1], ttlStr, false); // 移除掉 ttlStr 时间部分
            // 解析时间
            Duration duration = parseDuration(ttlStr);
            cacheConfig = cacheConfig.entryTtl(duration);
        }
        // 创建 RedisCache 对象,需要忽略掉 ttlStr
        return super.createRedisCache(names[0] + names[1], cacheConfig);
    }
    /**
     * 解析过期时间 Duration
     *
     * @param ttlStr 过期时间字符串
     * @return 过期时间 Duration
     */
    private Duration parseDuration(String ttlStr) {
        String timeUnit = StrUtil.subSuf(ttlStr, -1);
        switch (timeUnit) {
            case "d":
                return Duration.ofDays(removeDurationSuffix(ttlStr));
            case "h":
                return Duration.ofHours(removeDurationSuffix(ttlStr));
            case "m":
                return Duration.ofMinutes(removeDurationSuffix(ttlStr));
            case "s":
                return Duration.ofSeconds(removeDurationSuffix(ttlStr));
            default:
                return Duration.ofSeconds(Long.parseLong(ttlStr));
        }
    }
    /**
     * 移除多余的后缀,返回具体的时间
     *
     * @param ttlStr 过期时间字符串
     * @return 时间
     */
    private Long removeDurationSuffix(String ttlStr) {
        return NumberUtil.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1));
    }
}
iailab-framework/iailab-common-redis/src/main/java/com/iailab/framework/redis/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端
 */
package com.iailab.framework.redis;
iailab-framework/iailab-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,2 @@
com.iailab.framework.redis.config.IailabRedisAutoConfiguration
com.iailab.framework.redis.config.IailabCacheAutoConfiguration
iailab-framework/iailab-common-rpc/pom.xml
对比新文件
@@ -0,0 +1,47 @@
<?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">
    <parent>
        <artifactId>iailab-framework</artifactId>
        <groupId>com.iailab</groupId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-rpc</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        OpenFeign:提供 RESTful API 的调用
    </description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- RPC 远程调用相关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>
        <!-- 工具相关 -->
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/config/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 占坑 TODO
 */
package com.iailab.framework.rpc.config;
iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/core/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 占坑 TODO
 */
package com.iailab.framework.rpc.core;
iailab-framework/iailab-common-rpc/src/main/java/com/iailab/framework/rpc/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * OpenFeign:提供 RESTful API 的调用
 *
 * @author iailab
 */
package com.iailab.framework.rpc;
iailab-framework/iailab-common-security/pom.xml
对比新文件
@@ -0,0 +1,78 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-security</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        1. security:用户的认证、权限的校验,实现「谁」可以做「什么事」
        2. operatelog:操作日志,实现「谁」在「什么时间」对「什么」做了「什么事」
    </description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-web</artifactId>
        </dependency>
        <!-- spring boot 配置所需依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- RPC 远程调用相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-rpc</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 业务组件 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
            <version>${revision}</version>
        </dependency>
        <!-- 工具类相关 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
        <dependency>
            <!-- Spring Boot 通用操作日志组件,基于注解实现 -->
            <!-- 此组件解决的问题是:「谁」在「什么时间」对「什么」做了「什么事」 -->
            <groupId>io.github.mouzt</groupId>
            <artifactId>bizlog-sdk</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogConfiguration.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.operatelog.config;
import com.iailab.framework.operatelog.core.service.LogRecordServiceImpl;
import com.mzt.logapi.service.ILogRecordService;
import com.mzt.logapi.starter.annotation.EnableLogRecord;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
/**
 * 操作日志配置类
 *
 * @author HUIHUI
 */
@EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦
@AutoConfiguration
@Slf4j
public class IailabOperateLogConfiguration {
    @Bean
    @Primary
    public ILogRecordService iLogRecordServiceImpl() {
        return new LogRecordServiceImpl();
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/config/IailabOperateLogRpcAutoConfiguration.java
对比新文件
@@ -0,0 +1,15 @@
package com.iailab.framework.operatelog.config;
import com.iailab.module.system.api.logger.OperateLogApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
 * OperateLog 使用到 Feign 的配置项
 *
 * @author iailab
 */
@AutoConfiguration
@EnableFeignClients(clients = {OperateLogApi.class}) // 主要是引入相关的 API 服务
public class IailabOperateLogRpcAutoConfiguration {
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 占位,无特殊作用
 */
package com.iailab.framework.operatelog.core;
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/core/service/LogRecordServiceImpl.java
对比新文件
@@ -0,0 +1,87 @@
package com.iailab.framework.operatelog.core.service;
import com.iailab.framework.common.util.monitor.TracerUtils;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.module.system.api.logger.OperateLogApi;
import com.iailab.module.system.api.logger.dto.OperateLogCreateReqDTO;
import com.mzt.logapi.beans.LogRecord;
import com.mzt.logapi.service.ILogRecordService;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
 * 操作日志 ILogRecordService 实现类
 *
 * 基于 {@link OperateLogApi} 实现,记录操作日志
 *
 * @author HUIHUI
 */
@Slf4j
public class LogRecordServiceImpl implements ILogRecordService {
    @Resource
    private OperateLogApi operateLogApi;
    @Override
    public void record(LogRecord logRecord) {
        // 1. 补全通用字段
        OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO();
        reqDTO.setTraceId(TracerUtils.getTraceId());
        // 补充用户信息
        fillUserFields(reqDTO);
        // 补全模块信息
        fillModuleFields(reqDTO, logRecord);
        // 补全请求信息
        fillRequestFields(reqDTO);
        // 2. 异步记录日志
        operateLogApi.createOperateLog(reqDTO);
    }
    private static void fillUserFields(OperateLogCreateReqDTO reqDTO) {
        // 使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web;
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return;
        }
        reqDTO.setUserId(loginUser.getId());
        reqDTO.setUserType(loginUser.getUserType());
    }
    public static void fillModuleFields(OperateLogCreateReqDTO reqDTO, LogRecord logRecord) {
        reqDTO.setType(logRecord.getType()); // 大模块类型,例如:CRM 客户
        reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户
        reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号
        reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从平台改成源码。
        reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"}
    }
    private static void fillRequestFields(OperateLogCreateReqDTO reqDTO) {
        // 获得 Request 对象
        HttpServletRequest request = ServletUtils.getRequest();
        if (request == null) {
            return;
        }
        // 补全请求信息
        reqDTO.setRequestMethod(request.getMethod());
        reqDTO.setRequestUrl(request.getRequestURI());
        reqDTO.setUserIp(ServletUtils.getClientIP(request));
        reqDTO.setUserAgent(ServletUtils.getUserAgent(request));
    }
    @Override
    public List<LogRecord> queryLog(String bizNo, String type) {
        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
    }
    @Override
    public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {
        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/operatelog/package-info.java
对比新文件
@@ -0,0 +1,7 @@
/**
 * 基于 mzt-log 框架
 * 实现操作日志功能
 *
 * @author HUIHUI
 */
package com.iailab.framework.operatelog;
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/AuthorizeRequestsCustomizer.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.security.config;
import com.iailab.framework.web.config.WebProperties;
import org.springframework.core.Ordered;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import javax.annotation.Resource;
/**
 * 自定义的 URL 的安全配置
 * 目的:每个 Maven Module 可以自定义规则!
 *
 * @author iailab
 */
public abstract class AuthorizeRequestsCustomizer
        implements Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>, Ordered {
    @Resource
    private WebProperties webProperties;
    protected String buildAdminApi(String url) {
        return webProperties.getAdminApi().getPrefix() + url;
    }
    protected String buildAppApi(String url) {
        return webProperties.getAppApi().getPrefix() + url;
    }
    @Override
    public int getOrder() {
        return 0;
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityAutoConfiguration.java
对比新文件
@@ -0,0 +1,118 @@
package com.iailab.framework.security.config;
import cn.hutool.extra.spring.SpringUtil;
import com.iailab.framework.security.core.aop.PreAuthenticatedAspect;
import com.iailab.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
import com.iailab.framework.security.core.filter.TokenAuthenticationFilter;
import com.iailab.framework.security.core.handler.AccessDeniedHandlerImpl;
import com.iailab.framework.security.core.handler.AuthenticationEntryPointImpl;
import com.iailab.framework.security.core.service.SecurityFrameworkService;
import com.iailab.framework.security.core.service.SecurityFrameworkServiceImpl;
import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
import com.iailab.module.system.api.oauth2.OAuth2TokenApi;
import com.iailab.module.system.api.permission.PermissionApi;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.annotation.Resource;
/**
 * Spring Security 自动配置类,主要用于相关组件的配置
 *
 * 注意,不能和 {@link IailabWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。
 * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。
 *
 * @author iailab
 */
@AutoConfiguration
@AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效
@EnableConfigurationProperties(SecurityProperties.class)
public class IailabSecurityAutoConfiguration {
    @Resource
    private SecurityProperties securityProperties;
    /**
     * 处理用户未登录拦截的切面的 Bean
     */
    @Bean
    public PreAuthenticatedAspect preAuthenticatedAspect() {
        return new PreAuthenticatedAspect();
    }
    /**
     * 认证失败处理类 Bean
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new AuthenticationEntryPointImpl();
    }
    /**
     * 权限不够处理器 Bean
     */
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new AccessDeniedHandlerImpl();
    }
    /**
     * Spring Security 加密器
     * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器
     *
     * @see <a href="http://stackabuse.com/password-encoding-with-spring-security/">Password Encoding with Spring Security</a>
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength());
    }
    /**
     * Token 认证过滤器 Bean
     */
    @Bean
    public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler,
                                                               OAuth2TokenApi oauth2TokenApi) {
        try {
            OAuth2TokenApi oAuth2TokenApi = SpringUtil.getBean("aAuth2TokenApiImpl", OAuth2TokenApi.class);
            if (oAuth2TokenApi != null) {
                oauth2TokenApi = oAuth2TokenApi;
            }
        } catch (Exception ignored) {}
        return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
    }
    @Bean("ss") // 使用 Spring Security 的缩写,方便使用
    public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) {
        try {
            PermissionApi permissionApiImpl = SpringUtil.getBean("permissionApiImpl", PermissionApi.class);
            if (permissionApiImpl != null) {
                permissionApi = permissionApiImpl;
            }
        } catch (Exception ignored) {}
        return new SecurityFrameworkServiceImpl(permissionApi);
    }
    /**
     * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法,
     * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略
     */
    @Bean
    public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() {
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
        methodInvokingFactoryBean.setTargetMethod("setStrategyName");
        methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName());
        return methodInvokingFactoryBean;
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabSecurityRpcAutoConfiguration.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.security.config;
import com.iailab.framework.security.core.rpc.LoginUserRequestInterceptor;
import com.iailab.module.system.api.oauth2.OAuth2TokenApi;
import com.iailab.module.system.api.permission.PermissionApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
/**
 * Security 使用到 Feign 的配置项
 *
 * @author iailab
 */
@AutoConfiguration
@EnableFeignClients(clients = {OAuth2TokenApi.class, // 主要是引入相关的 API 服务
        PermissionApi.class})
public class IailabSecurityRpcAutoConfiguration {
    @Bean
    public LoginUserRequestInterceptor loginUserRequestInterceptor() {
        return new LoginUserRequestInterceptor();
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/IailabWebSecurityConfigurerAdapter.java
对比新文件
@@ -0,0 +1,217 @@
package com.iailab.framework.security.config;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.security.core.filter.TokenAuthenticationFilter;
import com.iailab.framework.web.config.WebProperties;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
/**
 * 自定义的 Spring Security 配置适配器实现
 *
 * @author iailab
 */
@AutoConfiguration
@AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效
@EnableMethodSecurity(securedEnabled = true)
public class IailabWebSecurityConfigurerAdapter {
    @Resource
    private WebProperties webProperties;
    @Resource
    private SecurityProperties securityProperties;
    /**
     * 认证失败处理类 Bean
     */
    @Resource
    private AuthenticationEntryPoint authenticationEntryPoint;
    /**
     * 权限不够处理器 Bean
     */
    @Resource
    private AccessDeniedHandler accessDeniedHandler;
    /**
     * Token 认证过滤器 Bean
     */
    @Resource
    private TokenAuthenticationFilter authenticationTokenFilter;
    /**
     * 自定义的权限映射 Bean 们
     *
     * @see #filterChain(HttpSecurity)
     */
    @Resource
    private List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers;
    @Resource
    private ApplicationContext applicationContext;
    /**
     * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
     * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
     */
    @Bean
    public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    /**
     * 配置 URL 的安全配置
     *
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // 登出
        httpSecurity
                // 开启跨域
                .cors(Customizer.withDefaults())
                // CSRF 禁用,因为不使用 Session
                .csrf(AbstractHttpConfigurer::disable)
                // 基于 token 机制,所以不需要 Session
                .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                // 一堆自定义的 Spring Security 处理器
                .exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler));
        // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高
        // 获得 @PermitAll 带来的 URL 列表,免登录
        Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
        // 设置每个请求的权限
        httpSecurity
                // ①:全局共享规则
                .authorizeHttpRequests(c -> c
                        // 1.1 静态资源,可匿名访问
                        .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
                        // 1.2 设置 @PermitAll 无需认证
                        .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll()
                        // 1.3 基于 yudao.security.permit-all-urls 无需认证
                        .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
                )
                // ②:每个项目的自定义规则
                .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c)))
                // ③:兜底规则,必须认证
                .authorizeHttpRequests(c -> c.anyRequest().authenticated());
        // 添加 Token Filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
    private String buildAppApi(String url) {
        return webProperties.getAppApi().getPrefix() + url;
    }
    private Multimap<HttpMethod, String> getPermitAllUrlsFromAnnotations() {
        Multimap<HttpMethod, String> result = HashMultimap.create();
        // 获得接口对应的 HandlerMethod 集合
        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
                applicationContext.getBean("requestMappingHandlerMapping");
        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
        // 获得有 @PermitAll 注解的接口
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
            HandlerMethod handlerMethod = entry.getValue();
            if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) {
                continue;
            }
            Set<String> urls = new HashSet<>();
            if (entry.getKey().getPatternsCondition() != null) {
                urls.addAll(entry.getKey().getPatternsCondition().getPatterns());
            }
            if (entry.getKey().getPathPatternsCondition() != null) {
                urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
            }
            if (urls.isEmpty()) {
                continue;
            }
            // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录
            Set<RequestMethod> methods = entry.getKey().getMethodsCondition().getMethods();
            if (CollUtil.isEmpty(methods)) {
                result.putAll(HttpMethod.GET, urls);
                result.putAll(HttpMethod.POST, urls);
                result.putAll(HttpMethod.PUT, urls);
                result.putAll(HttpMethod.DELETE, urls);
                result.putAll(HttpMethod.HEAD, urls);
                result.putAll(HttpMethod.PATCH, urls);
                continue;
            }
            // 根据请求方法,添加到 result 结果
            entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> {
                switch (requestMethod) {
                    case GET:
                        result.putAll(HttpMethod.GET, urls);
                        break;
                    case POST:
                        result.putAll(HttpMethod.POST, urls);
                        break;
                    case PUT:
                        result.putAll(HttpMethod.PUT, urls);
                        break;
                    case DELETE:
                        result.putAll(HttpMethod.DELETE, urls);
                        break;
                    case HEAD:
                        result.putAll(HttpMethod.HEAD, urls);
                        break;
                    case PATCH:
                        result.putAll(HttpMethod.PATCH, urls);
                        break;
                }
            });
        }
        return result;
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/config/SecurityProperties.java
对比新文件
@@ -0,0 +1,51 @@
package com.iailab.framework.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
@ConfigurationProperties(prefix = "iailab.security")
@Validated
@Data
public class SecurityProperties {
    /**
     * HTTP 请求时,访问令牌的请求 Header
     */
    @NotEmpty(message = "Token Header 不能为空")
    private String tokenHeader = "Authorization";
    /**
     * HTTP 请求时,访问令牌的请求参数
     *
     * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
     */
    @NotEmpty(message = "Token Parameter 不能为空")
    private String tokenParameter = "token";
    /**
     * mock 模式的开关
     */
    @NotNull(message = "mock 模式的开关不能为空")
    private Boolean mockEnable = false;
    /**
     * mock 模式的密钥
     * 一定要配置密钥,保证安全性
     */
    @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
    private String mockSecret = "test";
    /**
     * 免登录的 URL 列表
     */
    private List<String> permitAllUrls = Collections.emptyList();
    /**
     * PasswordEncoder 加密复杂度,越高开销越大
     */
    private Integer passwordEncoderLength = 4;
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java
对比新文件
@@ -0,0 +1,75 @@
package com.iailab.framework.security.core;
import cn.hutool.core.map.MapUtil;
import com.iailab.framework.common.enums.UserTypeEnum;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * 登录用户信息
 *
 * @author iailab
 */
@Data
public class LoginUser {
    public static final String INFO_KEY_NICKNAME = "nickname";
    public static final String INFO_KEY_DEPT_ID = "deptId";
    /**
     * 用户编号
     */
    private Long id;
    /**
     * 用户类型
     *
     * 关联 {@link UserTypeEnum}
     */
    private Integer userType;
    /**
     * 额外的用户信息
     */
    private Map<String, String> info;
    /**
     * 租户编号
     */
    private Long tenantId;
    /**
     * 授权范围
     */
    private List<String> scopes;
    /**
     * 过期时间
     */
    private LocalDateTime expiresTime;
    /**
     * 访问令牌
     */
    private String accessToken;
    // ========== 上下文 ==========
    /**
     * 上下文字段,不进行持久化
     *
     * 1. 用于基于 LoginUser 维度的临时缓存
     */
    @JsonIgnore
    private Map<String, Object> context;
    public void setContext(String key, Object value) {
        if (context == null) {
            context = new HashMap<>();
        }
        context.put(key, value);
    }
    public <T> T getContext(String key, Class<T> type) {
        return MapUtil.get(context, key, type);
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/annotations/PreAuthenticated.java
对比新文件
@@ -0,0 +1,17 @@
package com.iailab.framework.security.core.annotations;
import java.lang.annotation.*;
/**
 * 声明用户需要登录
 *
 * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录
 *
 * @author iailab
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthenticated {
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/aop/PreAuthenticatedAspect.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.security.core.aop;
import com.iailab.framework.security.core.annotations.PreAuthenticated;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception;
@Aspect
@Slf4j
public class PreAuthenticatedAspect {
    @Around("@annotation(preAuthenticated)")
    public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable {
        if (SecurityFrameworkUtils.getLoginUser() == null) {
            throw exception(UNAUTHORIZED);
        }
        return joinPoint.proceed();
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java
对比新文件
@@ -0,0 +1,48 @@
package com.iailab.framework.security.core.context;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.util.Assert;
/**
 * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略
 * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题
 *
 * @author iailab
 */
public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    /**
     * 使用 TransmittableThreadLocal 作为上下文
     */
    private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>();
    @Override
    public void clearContext() {
        CONTEXT_HOLDER.remove();
    }
    @Override
    public SecurityContext getContext() {
        SecurityContext ctx = CONTEXT_HOLDER.get();
        if (ctx == null) {
            ctx = createEmptyContext();
            CONTEXT_HOLDER.set(ctx);
        }
        return ctx;
    }
    @Override
    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        CONTEXT_HOLDER.set(context);
    }
    @Override
    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/filter/TokenAuthenticationFilter.java
对比新文件
@@ -0,0 +1,147 @@
package com.iailab.framework.security.core.filter;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.security.config.SecurityProperties;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import com.iailab.module.system.api.oauth2.OAuth2TokenApi;
import com.iailab.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
/**
 * Token 过滤器,验证 token 的有效性
 * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
 *
 * @author iailab
 */
@RequiredArgsConstructor
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final SecurityProperties securityProperties;
    private final GlobalExceptionHandler globalExceptionHandler;
    private final OAuth2TokenApi oauth2TokenApi;
    @Override
    @SuppressWarnings("NullableProblems")
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        // 情况一,基于 header[login-user] 获得用户,例如说来自 Gateway 或者其它服务透传
        LoginUser loginUser = buildLoginUserByHeader(request);
        // 情况二,基于 Token 获得用户
        // 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。
        if (loginUser == null) {
            String token = SecurityFrameworkUtils.obtainAuthorization(request,
                    securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
            if (StrUtil.isNotEmpty(token)) {
                Integer userType = WebFrameworkUtils.getLoginUserType(request);
                try {
                    // 1.1 基于 token 构建登录用户
                    loginUser = buildLoginUserByToken(token, userType);
                    // 1.2 模拟 Login 功能,方便日常开发调试
                    if (loginUser == null) {
                        loginUser = mockLoginUser(request, token, userType);
                    }
                } catch (Throwable ex) {
                    CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
                    ServletUtils.writeJSON(response, result);
                    return;
                }
            }
        }
        // 设置当前用户
        if (loginUser != null) {
            SecurityFrameworkUtils.setLoginUser(loginUser, request);
        }
        // 继续过滤链
        chain.doFilter(request, response);
    }
    private LoginUser buildLoginUserByToken(String token, Integer userType) {
        try {
            // 校验访问令牌
            OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token).getCheckedData();
            if (accessToken == null) {
                return null;
            }
            // 用户类型不匹配,无权限
            // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
            // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
            if (userType != null
                    && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
                throw new AccessDeniedException("错误的用户类型");
            }
            // 构建登录用户
            return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
                    .setInfo(accessToken.getUserInfo()) // 额外的用户信息
                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes())
                    .setExpiresTime(accessToken.getExpiresTime());
        } catch (ServiceException serviceException) {
            // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
            return null;
        }
    }
    /**
     * 模拟登录用户,方便日常开发调试
     *
     * 注意,在线上环境下,一定要关闭该功能!!!
     *
     * @param request 请求
     * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号
     * @param userType 用户类型
     * @return 模拟的 LoginUser
     */
    private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) {
        if (!securityProperties.getMockEnable()) {
            return null;
        }
        // 必须以 mockSecret 开头
        if (!token.startsWith(securityProperties.getMockSecret())) {
            return null;
        }
        // 构建模拟用户
        Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
        return new LoginUser().setId(userId).setUserType(userType)
                .setTenantId(WebFrameworkUtils.getTenantId(request));
    }
    @SneakyThrows
    private LoginUser buildLoginUserByHeader(HttpServletRequest request) {
        String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER);
        if (StrUtil.isEmpty(loginUserStr)) {
            return null;
        }
        try {
            loginUserStr = URLDecoder.decode(loginUserStr, StandardCharsets.UTF_8.name()); // 解码,解决中文乱码问题
            return JsonUtils.parseObject(loginUserStr, LoginUser.class);
        } catch (Exception ex) {
            log.error("[buildLoginUserByHeader][解析 LoginUser({}) 发生异常]", loginUserStr, ex);  ;
            throw ex;
        }
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AccessDeniedHandlerImpl.java
对比新文件
@@ -0,0 +1,43 @@
package com.iailab.framework.security.core.handler;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.framework.common.util.servlet.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
/**
 * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
 *
 * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
 *
 * @author iailab
 */
@Slf4j
@SuppressWarnings("JavadocReference")
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
            throws IOException, ServletException {
        // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
        log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
                SecurityFrameworkUtils.getLoginUserId(), e);
        // 返回 403
        ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN));
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/handler/AuthenticationEntryPointImpl.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.security.core.handler;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.servlet.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
/**
 * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
 *
 * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
 *
 * @author ruoyi
 */
@Slf4j
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
        log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
        // 返回 401
        ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED));
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/rpc/LoginUserRequestInterceptor.java
对比新文件
@@ -0,0 +1,39 @@
package com.iailab.framework.security.core.rpc;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
 * LoginUser 的 RequestInterceptor 实现类:Feign 请求时,将 {@link LoginUser} 设置到 header 中,继续透传给被调用的服务
 *
 * @author iailab
 */
@Slf4j
public class LoginUserRequestInterceptor implements RequestInterceptor {
    @Override
    @SneakyThrows
    public void apply(RequestTemplate requestTemplate) {
        LoginUser user = SecurityFrameworkUtils.getLoginUser();
        if (user == null) {
            return;
        }
        try {
            String userStr = JsonUtils.toJsonString(user);
            userStr = URLEncoder.encode(userStr, StandardCharsets.UTF_8.name()); // 编码,避免中文乱码
            requestTemplate.header(SecurityFrameworkUtils.LOGIN_USER_HEADER, userStr);
        } catch (Exception ex) {
            log.error("[apply][序列化 LoginUser({}) 发生异常]", user, ex);
            throw ex;
        }
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkService.java
对比新文件
@@ -0,0 +1,59 @@
package com.iailab.framework.security.core.service;
/**
 * Security 框架 Service 接口,定义权限相关的校验操作
 *
 * @author iailab
 */
public interface SecurityFrameworkService {
    /**
     * 判断是否有权限
     *
     * @param permission 权限
     * @return 是否
     */
    boolean hasPermission(String permission);
    /**
     * 判断是否有权限,任一一个即可
     *
     * @param permissions 权限
     * @return 是否
     */
    boolean hasAnyPermissions(String... permissions);
    /**
     * 判断是否有角色
     *
     * 注意,角色使用的是 SysRoleDO 的 code 标识
     *
     * @param role 角色
     * @return 是否
     */
    boolean hasRole(String role);
    /**
     * 判断是否有角色,任一一个即可
     *
     * @param roles 角色数组
     * @return 是否
     */
    boolean hasAnyRoles(String... roles);
    /**
     * 判断是否有授权
     *
     * @param scope 授权
     * @return 是否
     */
    boolean hasScope(String scope);
    /**
     * 判断是否有授权范围,任一一个即可
     *
     * @param scope 授权范围数组
     * @return 是否
     */
    boolean hasAnyScopes(String... scope);
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/service/SecurityFrameworkServiceImpl.java
对比新文件
@@ -0,0 +1,103 @@
package com.iailab.framework.security.core.service;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.common.core.KeyValue;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.module.system.api.permission.PermissionApi;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
import static com.iailab.framework.common.util.cache.CacheUtils.buildCache;
import static com.iailab.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
 * 默认的 {@link SecurityFrameworkService} 实现类
 *
 * @author iailab
 */
@AllArgsConstructor
public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
    private final PermissionApi permissionApi;
    /**
     * 针对 {@link #hasAnyRoles(String...)} 的缓存
     */
    private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyRolesCache = buildCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
                @Override
                public Boolean load(KeyValue<Long, List<String>> key) {
                    return permissionApi.hasAnyRoles(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
                }
            });
    /**
     * 针对 {@link #hasAnyPermissions(String...)} 的缓存
     */
    private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyPermissionsCache = buildCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
                @Override
                public Boolean load(KeyValue<Long, List<String>> key) {
                    return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
                }
            });
    @Override
    public boolean hasPermission(String permission) {
        return hasAnyPermissions(permission);
    }
    @Override
    @SneakyThrows
    public boolean hasAnyPermissions(String... permissions) {
        Long userId = getLoginUserId();
        if (userId == null) {
            return false;
        }
        return hasAnyPermissionsCache.get(new KeyValue<>(userId, Arrays.asList(permissions)));
    }
    @Override
    public boolean hasRole(String role) {
        return hasAnyRoles(role);
    }
    @Override
    @SneakyThrows
    public boolean hasAnyRoles(String... roles) {
        Long userId = getLoginUserId();
        if (userId == null) {
            return false;
        }
        return hasAnyRolesCache.get(new KeyValue<>(userId, Arrays.asList(roles)));
    }
    @Override
    public boolean hasScope(String scope) {
        return hasAnyScopes(scope);
    }
    @Override
    public boolean hasAnyScopes(String... scope) {
        LoginUser user = SecurityFrameworkUtils.getLoginUser();
        if (user == null) {
            return false;
        }
        return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope));
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/util/SecurityFrameworkUtils.java
对比新文件
@@ -0,0 +1,142 @@
package com.iailab.framework.security.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
/**
 * 安全服务工具类
 *
 * @author iailab
 */
public class SecurityFrameworkUtils {
    /**
     * HEADER 认证头 value 的前缀
     */
    public static final String AUTHORIZATION_BEARER = "Bearer";
    public static final String LOGIN_USER_HEADER = "login-user";
    private SecurityFrameworkUtils() {}
    /**
     * 从请求中,获得认证 Token
     *
     * @param request 请求
     * @param headerName 认证 Token 对应的 Header 名字
     * @param parameterName 认证 Token 对应的 Parameter 名字
     * @return 认证 Token
     */
    public static String obtainAuthorization(HttpServletRequest request,
                                             String headerName, String parameterName) {
        // 1. 获得 Token。优先级:Header > Parameter
        String token = request.getHeader(headerName);
        if (StrUtil.isEmpty(token)) {
            token = request.getParameter(parameterName);
        }
        if (!StringUtils.hasText(token)) {
            return null;
        }
        // 2. 去除 Token 中带的 Bearer
        int index = token.indexOf(AUTHORIZATION_BEARER + " ");
        return index >= 0 ? token.substring(index + 7).trim() : token;
    }
    /**
     * 获得当前认证信息
     *
     * @return 认证信息
     */
    public static Authentication getAuthentication() {
        SecurityContext context = SecurityContextHolder.getContext();
        if (context == null) {
            return null;
        }
        return context.getAuthentication();
    }
    /**
     * 获取当前用户
     *
     * @return 当前用户
     */
    @Nullable
    public static LoginUser getLoginUser() {
        Authentication authentication = getAuthentication();
        if (authentication == null) {
            return null;
        }
        return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
    }
    /**
     * 获得当前用户的编号,从上下文中
     *
     * @return 用户编号
     */
    @Nullable
    public static Long getLoginUserId() {
        LoginUser loginUser = getLoginUser();
        return loginUser != null ? loginUser.getId() : null;
    }
    /**
     * 获得当前用户的昵称,从上下文中
     *
     * @return 昵称
     */
    @Nullable
    public static String getLoginUserNickname() {
        LoginUser loginUser = getLoginUser();
        return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null;
    }
    /**
     * 获得当前用户的部门编号,从上下文中
     *
     * @return 部门编号
     */
    @Nullable
    public static Long getLoginUserDeptId() {
        LoginUser loginUser = getLoginUser();
        return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null;
    }
    /**
     * 设置当前用户
     *
     * @param loginUser 登录用户
     * @param request 请求
     */
    public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
        // 创建 Authentication,并设置到上下文
        Authentication authentication = buildAuthentication(loginUser, request);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
        // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
        WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
        WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
    }
    private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
        // 创建 UsernamePasswordAuthenticationToken 对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                loginUser, null, Collections.emptyList());
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        return authenticationToken;
    }
}
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/package-info.java
对比新文件
@@ -0,0 +1,7 @@
/**
 * 基于 Spring Security 框架
 * 实现安全认证功能
 *
 * @author iailab
 */
package com.iailab.framework.security;
iailab-framework/iailab-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,5 @@
com.iailab.framework.security.config.IailabSecurityRpcAutoConfiguration
com.iailab.framework.security.config.IailabSecurityAutoConfiguration
com.iailab.framework.security.config.IailabWebSecurityConfigurerAdapter
com.iailab.framework.operatelog.config.IailabOperateLogConfiguration
com.iailab.framework.operatelog.config.IailabOperateLogRpcAutoConfiguration
iailab-framework/iailab-common-security/src/main/resources/assembly.xml
对比新文件
@@ -0,0 +1,50 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<!--    <id>jar-with-dependencies</id>-->
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>true</includeBaseDirectory>
    <!--依赖jar包以及项目打包文件存储文件-->
    <dependencySets>
        <dependencySet>
            <!--存储在projectName-assembly-version/lib下-->
            <outputDirectory>lib</outputDirectory>
        </dependencySet>
    </dependencySets>
    <files>
        <file>
            <fileMode>775</fileMode>
            <source>target/${project.build.finalName}.jar</source>
            <destName>demo-maven-assembly.jar</destName>
            <outputDirectory>./target</outputDirectory>
        </file>
    </files>
    <fileSets>
        <fileSet>
            <directory>src/main/resources</directory>
            <outputDirectory>./conf</outputDirectory>
            <includes>
                <include>*.yml</include>
                <include>*.properties</include>
            </includes>
        </fileSet>
        <fileSet>
            <fileMode>775</fileMode>
            <directory>deploy/bin</directory>
            <outputDirectory>./bin</outputDirectory>
        </fileSet>
        <fileSet>
            <directory>${basedir}</directory>
            <includes>
                <include>*.md</include>
            </includes>
        </fileSet>
    </fileSets>
</assembly>
iailab-framework/iailab-common-test/pom.xml
对比新文件
@@ -0,0 +1,60 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-test</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>测试组件,用于单元测试、集成测试</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- DB 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-mybatis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-redis</artifactId>
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId> <!-- 单元测试,我们采用 H2 作为数据库 -->
            <artifactId>h2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
            <artifactId>jedis-mock</artifactId>
        </dependency>
        <dependency>
            <groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
            <artifactId>podam</artifactId>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/RedisTestConfiguration.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.test.config;
import com.github.fppt.jedismock.RedisServer;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import java.io.IOException;
/**
 * Redis 测试 Configuration,主要实现内嵌 Redis 的启动
 *
 * @author iailab
 */
@Configuration(proxyBeanMethods = false)
@Lazy(false) // 禁止延迟加载
@EnableConfigurationProperties(RedisProperties.class)
public class RedisTestConfiguration {
    /**
     * 创建模拟的 Redis Server 服务器
     */
    @Bean
    public RedisServer redisServer(RedisProperties properties) throws IOException {
        RedisServer redisServer = new RedisServer(properties.getPort());
        // 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。
        try {
            redisServer.start();
        } catch (Exception ignore) {}
        return redisServer;
    }
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/config/SqlInitializationTestConfiguration.java
对比新文件
@@ -0,0 +1,52 @@
package com.iailab.framework.test.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import javax.sql.DataSource;
/**
 * SQL 初始化的测试 Configuration
 *
 * 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢?
 * 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化
 * 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈!
 *
 * @author iailab
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator")
@Lazy(value = false) // 禁止延迟加载
@EnableConfigurationProperties(SqlInitializationProperties.class)
public class SqlInitializationTestConfiguration {
    @Bean
    public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
                                                                                   SqlInitializationProperties initializationProperties) {
        DatabaseInitializationSettings settings = createFrom(initializationProperties);
        return new DataSourceScriptDatabaseInitializer(dataSource, settings);
    }
    static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
        DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
        settings.setSchemaLocations(properties.getSchemaLocations());
        settings.setDataLocations(properties.getDataLocations());
        settings.setContinueOnError(properties.isContinueOnError());
        settings.setSeparator(properties.getSeparator());
        settings.setEncoding(properties.getEncoding());
        settings.setMode(properties.getMode());
        return settings;
    }
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbAndRedisUnitTest.java
对比新文件
@@ -0,0 +1,51 @@
package com.iailab.framework.test.core.ut;
import com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration;
import com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration;
import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
import com.iailab.framework.test.config.RedisTestConfiguration;
import com.iailab.framework.test.config.SqlInitializationTestConfiguration;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
/**
 * 依赖内存 DB + Redis 的单元测试
 *
 * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
 *
 * @author iailab
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
public class BaseDbAndRedisUnitTest {
    @Import({
            // DB 配置类
            IailabDataSourceAutoConfiguration.class, // 自己的 DB 配置类
            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
            DruidDataSourceAutoConfigure.class, // Druid 自动配置类
            SqlInitializationTestConfiguration.class, // SQL 初始化
            // MyBatis 配置类
            IailabMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
            // Redis 配置类
            RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
            IailabRedisAutoConfiguration.class, // 自己的 Redis 配置类
            RedisAutoConfiguration.class, // Spring Redis 自动配置类
            RedissonAutoConfiguration.class, // Redisson 自动配置类
    })
    public static class Application {
    }
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseDbUnitTest.java
对比新文件
@@ -0,0 +1,43 @@
package com.iailab.framework.test.core.ut;
import com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration;
import com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration;
import com.iailab.framework.test.config.SqlInitializationTestConfiguration;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
/**
 * 依赖内存 DB 的单元测试
 *
 * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
 *
 * @author iailab
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class)
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
public class BaseDbUnitTest {
    @Import({
            // DB 配置类
            IailabDataSourceAutoConfiguration.class, // 自己的 DB 配置类
            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
            DruidDataSourceAutoConfigure.class, // Druid 自动配置类
            SqlInitializationTestConfiguration.class, // SQL 初始化
            // MyBatis 配置类
            IailabMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
            MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类
    })
    public static class Application {
    }
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseMockitoUnitTest.java
对比新文件
@@ -0,0 +1,13 @@
package com.iailab.framework.test.core.ut;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
/**
 * 纯 Mockito 的单元测试
 *
 * @author iailab
 */
@ExtendWith(MockitoExtension.class)
public class BaseMockitoUnitTest {
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/BaseRedisUnitTest.java
对比新文件
@@ -0,0 +1,32 @@
package com.iailab.framework.test.core.ut;
import com.iailab.framework.redis.config.IailabRedisAutoConfiguration;
import com.iailab.framework.test.config.RedisTestConfiguration;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
/**
 * 依赖内存 Redis 的单元测试
 *
 * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis
 *
 * @author iailab
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class)
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
public class BaseRedisUnitTest {
    @Import({
            // Redis 配置类
            RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
            RedisAutoConfiguration.class, // Spring Redis 自动配置类
            IailabRedisAutoConfiguration.class, // 自己的 Redis 配置类
            RedissonAutoConfiguration.class, // Redisson 自动配置类
    })
    public static class Application {
    }
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/ut/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 提供单元测试 Unit Test 的基类
 */
package com.iailab.framework.test.core.ut;
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/AssertUtils.java
对比新文件
@@ -0,0 +1,101 @@
package com.iailab.framework.test.core.util;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import com.iailab.framework.common.exception.ErrorCode;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.exception.util.ServiceExceptionUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.function.Executable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
 * 单元测试,assert 断言工具类
 *
 * @author iailab
 */
public class AssertUtils {
    /**
     * 比对两个对象的属性是否一致
     *
     * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略
     *
     * @param expected 期望对象
     * @param actual 实际对象
     * @param ignoreFields 忽略的属性数组
     */
    public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) {
        Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
        Arrays.stream(expectedFields).forEach(expectedField -> {
            // 忽略 jacoco 自动生成的 $jacocoData 属性的情况
            if (expectedField.isSynthetic()) {
                return;
            }
            // 如果是忽略的属性,则不进行比对
            if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
                return;
            }
            // 忽略不存在的属性
            Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName());
            if (actualField == null) {
                return;
            }
            // 比对
            Assertions.assertEquals(
                    ReflectUtil.getFieldValue(expected, expectedField),
                    ReflectUtil.getFieldValue(actual, actualField),
                    String.format("Field(%s) 不匹配", expectedField.getName())
            );
        });
    }
    /**
     * 比对两个对象的属性是否一致
     *
     * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略
     *
     * @param expected 期望对象
     * @param actual 实际对象
     * @param ignoreFields 忽略的属性数组
     * @return 是否一致
     */
    public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) {
        Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
        return Arrays.stream(expectedFields).allMatch(expectedField -> {
            // 如果是忽略的属性,则不进行比对
            if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
                return true;
            }
            // 忽略不存在的属性
            Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName());
            if (actualField == null) {
                return true;
            }
            return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField),
                    ReflectUtil.getFieldValue(actual, actualField));
        });
    }
    /**
     * 执行方法,校验抛出的 Service 是否符合条件
     *
     * @param executable 业务异常
     * @param errorCode 错误码对象
     * @param messageParams 消息参数
     */
    public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) {
        // 调用方法
        ServiceException serviceException = assertThrows(ServiceException.class, executable);
        // 校验错误码
        Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配");
        String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams);
        Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配");
    }
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/core/util/RandomUtils.java
对比新文件
@@ -0,0 +1,137 @@
package com.iailab.framework.test.core.util;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.enums.CommonStatusEnum;
import uk.co.jemos.podam.api.PodamFactory;
import uk.co.jemos.podam.api.PodamFactoryImpl;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
 * 随机工具类
 *
 * @author iailab
 */
public class RandomUtils {
    private static final int RANDOM_STRING_LENGTH = 10;
    private static final int TINYINT_MAX = 127;
    private static final int RANDOM_DATE_MAX = 30;
    private static final int RANDOM_COLLECTION_LENGTH = 5;
    private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl();
    static {
        // 字符串
        PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class,
                (dataProviderStrategy, attributeMetadata, map) -> randomString());
        // Integer
        PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> {
            // 如果是 status 的字段,返回 0 或 1
            if ("status".equals(attributeMetadata.getAttributeName())) {
                return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
            }
            // 如果是 type、status 结尾的字段,返回 tinyint 范围
            if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(),
                    "type", "status", "category", "scope", "result")) {
                return RandomUtil.randomInt(0, TINYINT_MAX + 1);
            }
            return RandomUtil.randomInt();
        });
        // LocalDateTime
        PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class,
                (dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime());
        // Boolean
        PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> {
            // 如果是 deleted 的字段,返回非删除
            if ("deleted".equals(attributeMetadata.getAttributeName())) {
                return false;
            }
            return RandomUtil.randomBoolean();
        });
    }
    public static String randomString() {
        return RandomUtil.randomString(RANDOM_STRING_LENGTH);
    }
    public static Long randomLongId() {
        return RandomUtil.randomLong(0, Long.MAX_VALUE);
    }
    public static Integer randomInteger() {
        return RandomUtil.randomInt(0, Integer.MAX_VALUE);
    }
    public static Date randomDate() {
        return RandomUtil.randomDay(0, RANDOM_DATE_MAX);
    }
    public static LocalDateTime randomLocalDateTime() {
        // 设置 Nano 为零的原因,避免 MySQL、H2 存储不到时间戳
        return LocalDateTimeUtil.of(randomDate()).withNano(0);
    }
    public static Short randomShort() {
        return (short) RandomUtil.randomInt(0, Short.MAX_VALUE);
    }
    public static <T> Set<T> randomSet(Class<T> clazz) {
        return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH))
                .map(i -> randomPojo(clazz)).collect(Collectors.toSet());
    }
    public static Integer randomCommonStatus() {
        return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
    }
    public static String randomEmail() {
        return randomString() + "@qq.com";
    }
    public static String randomURL() {
        return "https://www.baidu.com/" + randomString();
    }
    @SafeVarargs
    public static <T> T randomPojo(Class<T> clazz, Consumer<T>... consumers) {
        T pojo = PODAM_FACTORY.manufacturePojo(clazz);
        // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理
        if (ArrayUtil.isNotEmpty(consumers)) {
            Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo));
        }
        return pojo;
    }
    @SafeVarargs
    public static <T> T randomPojo(Class<T> clazz, Type type, Consumer<T>... consumers) {
        T pojo = PODAM_FACTORY.manufacturePojo(clazz, type);
        // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理
        if (ArrayUtil.isNotEmpty(consumers)) {
            Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo));
        }
        return pojo;
    }
    @SafeVarargs
    public static <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) {
        int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH);
        return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers))
                .collect(Collectors.toList());
    }
}
iailab-framework/iailab-common-test/src/main/java/com/iailab/framework/test/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 测试组件,用于单元测试、集成测试等等
 */
package com.iailab.framework.test;
iailab-framework/iailab-common-web/pom.xml
对比新文件
@@ -0,0 +1,99 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-web</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>Web 框架,全局异常、API 日志、脱敏、错误码等</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId> <!-- 捕获mybatis全局异常 -->
        </dependency>
        <!-- Spring Boot 配置所需依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,主要是 GlobalExceptionHandler 使用 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
            <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>  <!-- 接口文档 -->
            <artifactId>springdoc-openapi-ui</artifactId>
        </dependency>
        <!-- RPC 远程调用相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-rpc</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 业务组件 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行错误码的记录 -->
            <version>${revision}</version>
        </dependency>
        <!-- xss -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogAutoConfiguration.java
对比新文件
@@ -0,0 +1,62 @@
package com.iailab.framework.apilog.config;
import com.iailab.framework.apilog.core.filter.ApiAccessLogFilter;
import com.iailab.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkService;
import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService;
import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
import com.iailab.framework.common.enums.WebFilterOrderEnum;
import com.iailab.framework.web.config.WebProperties;
import com.iailab.framework.web.config.IailabWebAutoConfiguration;
import com.iailab.module.infra.api.logger.ApiAccessLogApi;
import com.iailab.module.infra.api.logger.ApiErrorLogApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Filter;
@AutoConfiguration(after = IailabWebAutoConfiguration.class)
public class IailabApiLogAutoConfiguration implements WebMvcConfigurer {
    @Bean
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
        return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
    }
    @Bean
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
        return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
    }
    /**
     * 创建 ApiAccessLogFilter Bean,记录 API 请求日志
     */
    @Bean
    @ConditionalOnProperty(prefix = "iailab.access-log", value = "enable", matchIfMissing = true) // 允许使用 iailab.access-log.enable=false 禁用访问日志
    public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
                                                                         @Value("${spring.application.name}") String applicationName,
                                                                         ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
        return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
    }
    private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
        bean.setOrder(order);
        return bean;
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ApiAccessLogInterceptor());
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/config/IailabApiLogRpcAutoConfiguration.java
对比新文件
@@ -0,0 +1,18 @@
package com.iailab.framework.apilog.config;
import com.iailab.module.infra.api.logger.ApiAccessLogApi;
import com.iailab.module.infra.api.logger.ApiErrorLogApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
/**
 * API 日志使用到 Feign 的配置项
 *
 * @author iailab
 */
@AutoConfiguration
@EnableFeignClients(clients = {ApiAccessLogApi.class, // 主要是引入相关的 API 服务
        ApiErrorLogApi.class})
public class IailabApiLogRpcAutoConfiguration {
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/annotation/ApiAccessLog.java
对比新文件
@@ -0,0 +1,65 @@
package com.iailab.framework.apilog.core.annotation;
import com.iailab.framework.apilog.core.enums.OperateTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 访问日志注解
 *
 * @author iailab
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiAccessLog {
    // ========== 开关字段 ==========
    /**
     * 是否记录访问日志
     */
    boolean enable() default true;
    /**
     * 是否记录请求参数
     *
     * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭
     */
    boolean requestEnable() default true;
    /**
     * 是否记录响应结果
     *
     * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开
     */
    boolean responseEnable() default false;
    /**
     * 敏感参数数组
     *
     * 添加后,请求参数、响应结果不会记录该参数
     */
    String[] sanitizeKeys() default {};
    // ========== 模块字段 ==========
    /**
     * 操作模块
     *
     * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性
     */
    String operateModule() default "";
    /**
     * 操作名
     *
     * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性
     */
    String operateName() default "";
    /**
     * 操作分类
     *
     * 实际并不是数组,因为枚举不能设置 null 作为默认值
     */
    OperateTypeEnum[] operateType() default {};
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/enums/OperateTypeEnum.java
对比新文件
@@ -0,0 +1,51 @@
package com.iailab.framework.apilog.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * 操作日志的操作类型
 *
 * @author ruoyi
 */
@Getter
@AllArgsConstructor
public enum OperateTypeEnum {
    /**
     * 查询
     */
    GET(1),
    /**
     * 新增
     */
    CREATE(2),
    /**
     * 修改
     */
    UPDATE(3),
    /**
     * 删除
     */
    DELETE(4),
    /**
     * 导出
     */
    EXPORT(5),
    /**
     * 导入
     */
    IMPORT(6),
    /**
     * 其它
     *
     * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识
     */
    OTHER(0);
    /**
     * 类型
     */
    private final Integer type;
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/filter/ApiAccessLogFilter.java
对比新文件
@@ -0,0 +1,251 @@
package com.iailab.framework.apilog.core.filter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.apilog.core.annotation.ApiAccessLog;
import com.iailab.framework.apilog.core.enums.OperateTypeEnum;
import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkService;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.common.util.monitor.TracerUtils;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.web.config.WebProperties;
import com.iailab.framework.web.core.filter.ApiRequestFilter;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.Map;
import static com.iailab.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*;
import static com.iailab.framework.common.util.json.JsonUtils.toJsonString;
/**
 * API 访问日志 Filter
 *
 * 目的:记录 API 访问日志到数据库中
 *
 * @author iailab
 */
@Slf4j
public class ApiAccessLogFilter extends ApiRequestFilter {
    private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"};
    private final String applicationName;
    private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
        super(webProperties);
        this.applicationName = applicationName;
        this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
    }
    @Override
    @SuppressWarnings("NullableProblems")
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 获得开始时间
        LocalDateTime beginTime = LocalDateTime.now();
        // 提前获得参数,避免 XssFilter 过滤处理
        Map<String, String> queryString = ServletUtils.getParamMap(request);
        String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
        try {
            // 继续过滤器
            filterChain.doFilter(request, response);
            // 正常执行,记录日志
            createApiAccessLog(request, beginTime, queryString, requestBody, null);
        } catch (Exception ex) {
            // 异常执行,记录日志
            createApiAccessLog(request, beginTime, queryString, requestBody, ex);
            throw ex;
        }
    }
    private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime,
                                    Map<String, String> queryString, String requestBody, Exception ex) {
        ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
        try {
            boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex);
            if (!enable) {
                return;
            }
            apiAccessLogFrameworkService.createApiAccessLog(accessLog);
        } catch (Throwable th) {
            log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
        }
    }
    private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime,
                                      Map<String, String> queryString, String requestBody, Exception ex) {
        // 判断:是否要记录操作日志
        HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD);
        ApiAccessLog accessLogAnnotation = null;
        if (handlerMethod != null) {
            accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class);
            if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) {
                return false;
            }
        }
        // 处理用户信息
        accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request))
                .setUserType(WebFrameworkUtils.getLoginUserType(request));
        // 设置访问结果
        CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
        if (result != null) {
            accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg());
        } else if (ex != null) {
            accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode())
                    .setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
        } else {
            accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg("");
        }
        // 设置请求字段
        accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName)
                .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod())
                .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request));
        String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null;
        Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE;
        if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false
            Map<String, Object> requestParams = MapUtil.<String, Object>builder()
                    .put("query", sanitizeMap(queryString, sanitizeKeys))
                    .put("body", sanitizeJson(requestBody, sanitizeKeys)).build();
            accessLog.setRequestParams(toJsonString(requestParams));
        }
        Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE;
        if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true
            accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys));
        }
        // 持续时间
        accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now())
                .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
        // 操作模块
        if (handlerMethod != null) {
            Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class);
            Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class);
            String operateModule = accessLogAnnotation != null ? accessLogAnnotation.operateModule() :
                    tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null;
            String operateName = accessLogAnnotation != null ? accessLogAnnotation.operateName() :
                    operationAnnotation != null ? operationAnnotation.summary() : null;
            OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ?
                    accessLogAnnotation.operateType()[0] : parseOperateLogType(request);
            accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType());
        }
        return true;
    }
    // ========== 解析 @ApiAccessLog、@Swagger 注解  ==========
    private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
        RequestMethod requestMethod = ArrayUtil.firstMatch(method ->
                StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values());
        if (requestMethod == null) {
            return OperateTypeEnum.OTHER;
        }
        switch (requestMethod) {
            case GET:
                return OperateTypeEnum.GET;
            case POST:
                return OperateTypeEnum.CREATE;
            case PUT:
                return OperateTypeEnum.UPDATE;
            case DELETE:
                return OperateTypeEnum.DELETE;
            default:
                return OperateTypeEnum.OTHER;
        }
    }
    // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========
    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
        if (CollUtil.isNotEmpty(map)) {
            return null;
        }
        if (sanitizeKeys != null) {
            MapUtil.removeAny(map, sanitizeKeys);
        }
        MapUtil.removeAny(map, SANITIZE_KEYS);
        return JsonUtils.toJsonString(map);
    }
    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
        if (StrUtil.isEmpty(jsonString)) {
            return null;
        }
        try {
            JsonNode rootNode = JsonUtils.parseTree(jsonString);
            sanitizeJson(rootNode, sanitizeKeys);
            return JsonUtils.toJsonString(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }
    private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
        if (commonResult == null) {
            return null;
        }
        String jsonString = toJsonString(commonResult);
        try {
            JsonNode rootNode = JsonUtils.parseTree(jsonString);
            sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
            return JsonUtils.toJsonString(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }
    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
        // 情况一:数组,遍历处理
        if (node.isArray()) {
            for (JsonNode childNode : node) {
                sanitizeJson(childNode, sanitizeKeys);
            }
            return;
        }
        // 情况二:非 Object,只是某个值,直接返回
        if (!node.isObject()) {
            return;
        }
        //  情况三:Object,遍历处理
        Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
        while (iterator.hasNext()) {
            Map.Entry<String, JsonNode> entry = iterator.next();
            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
                    || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
                iterator.remove();
                continue;
            }
            sanitizeJson(entry.getValue(), sanitizeKeys);
        }
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java
对比新文件
@@ -0,0 +1,103 @@
package com.iailab.framework.apilog.core.interceptor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.common.util.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.IntStream;
/**
 * API 访问日志 Interceptor
 *
 * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。
 *
 * @author iailab
 */
@Slf4j
public class ApiAccessLogInterceptor implements HandlerInterceptor {
    public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD";
    private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用
        HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null;
        if (handlerMethod != null) {
            request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod);
        }
        // 打印 request 日志
        if (!SpringUtils.isProd()) {
            Map<String, String> queryString = ServletUtils.getParamMap(request);
            String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
            if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
                log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
            } else {
                log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(),
                        StrUtil.blankToDefault(requestBody, queryString.toString()));
            }
            // 计时
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch);
            // 打印 Controller 路径
            printHandlerMethodPosition(handlerMethod);
        }
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 打印 response 日志
        if (!SpringUtils.isProd()) {
            StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH);
            stopWatch.stop();
            log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]",
                    request.getRequestURI(), stopWatch.getTotalTimeMillis());
        }
    }
    /**
     * 打印 Controller 方法路径
     */
    private void printHandlerMethodPosition(HandlerMethod handlerMethod) {
        if (handlerMethod == null) {
            return;
        }
        Method method = handlerMethod.getMethod();
        Class<?> clazz = method.getDeclaringClass();
        try {
            // 获取 method 的 lineNumber
            List<String> clazzContents = FileUtil.readUtf8Lines(
                    ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/")
                            + clazz.getSimpleName() + ".java");
            Optional<Integer> lineNumber = IntStream.range(0, clazzContents.size())
                    .filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名
                    .mapToObj(i -> i + 1) // 行号从 1 开始
                    .findFirst();
            if (!lineNumber.isPresent()) {
                return;
            }
            // 打印结果
            System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get());
        } catch (Exception ignore) {
            // 忽略异常。原因:仅仅打印,非重要逻辑
        }
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkService.java
对比新文件
@@ -0,0 +1,19 @@
package com.iailab.framework.apilog.core.service;
import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
/**
 * API 访问日志 Framework Service 接口
 *
 * @author iailab
 */
public interface ApiAccessLogFrameworkService {
    /**
     * 创建 API 访问日志
     *
     * @param reqDTO API 访问日志
     */
    void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO);
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
对比新文件
@@ -0,0 +1,33 @@
package com.iailab.framework.apilog.core.service;
import com.iailab.module.infra.api.logger.ApiAccessLogApi;
import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
/**
 * API 访问日志 Framework Service 实现类
 *
 * 基于 {@link ApiAccessLogApi} 服务,记录访问日志
 *
 * @author iailab
 */
@RequiredArgsConstructor
@Slf4j
public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
    private final ApiAccessLogApi apiAccessLogApi;
    @Override
    @Async
    public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) {
        try {
            apiAccessLogApi.createApiAccessLog(reqDTO);
        } catch (Throwable ex) {
            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
            log.error("[createApiAccessLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
        }
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkService.java
对比新文件
@@ -0,0 +1,19 @@
package com.iailab.framework.apilog.core.service;
import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
/**
 * API 错误日志 Framework Service 接口
 *
 * @author iailab
 */
public interface ApiErrorLogFrameworkService {
    /**
     * 创建 API 错误日志
     *
     * @param reqDTO API 错误日志
     */
    void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO);
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
对比新文件
@@ -0,0 +1,33 @@
package com.iailab.framework.apilog.core.service;
import com.iailab.module.infra.api.logger.ApiErrorLogApi;
import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
/**
 * API 错误日志 Framework Service 实现类
 *
 * 基于 {@link ApiErrorLogApi} 服务,记录错误日志
 *
 * @author iailab
 */
@RequiredArgsConstructor
@Slf4j
public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
    private final ApiErrorLogApi apiErrorLogApi;
    @Override
    @Async
    public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) {
        try {
            apiErrorLogApi.createApiErrorLog(reqDTO);
        } catch (Throwable ex) {
            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
            log.error("[createApiErrorLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
        }
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/apilog/package-info.java
对比新文件
@@ -0,0 +1,8 @@
/**
 * API 日志:包含两类
 * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
 * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
 *
 * @author iailab
 */
package com.iailab.framework.apilog;
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/config/IailabBannerAutoConfiguration.java
对比新文件
@@ -0,0 +1,20 @@
package com.iailab.framework.banner.config;
import com.iailab.framework.banner.core.BannerApplicationRunner;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
 * Banner 的自动配置类
 *
 * @author iailab
 */
@AutoConfiguration
public class IailabBannerAutoConfiguration {
    @Bean
    public BannerApplicationRunner bannerApplicationRunner() {
        return new BannerApplicationRunner();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/core/BannerApplicationRunner.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.banner.core;
import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import java.util.concurrent.TimeUnit;
/**
 * 项目启动成功后,提供文档相关的地址
 *
 * @author iailab
 */
@Slf4j
public class BannerApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        ThreadUtil.execute(() -> {
            ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾
            log.info("\n----------------------------------------------------------\n\t" +
                            "项目启动成功!\n\t" +
                            "----------------------------------------------------------");
        });
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/banner/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * Banner 用于在 console 控制台,打印开发文档、接口文档等
 *
 * @author iailab
 */
package com.iailab.framework.banner;
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/annotation/DesensitizeBy.java
对比新文件
@@ -0,0 +1,32 @@
package com.iailab.framework.desensitize.core.base.annotation;
import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
import com.iailab.framework.desensitize.core.base.serializer.StringDesensitizeSerializer;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 顶级脱敏注解,自定义注解需要使用此注解
 *
 * @author gaibu
 */
@Documented
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分
@JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器
public @interface DesensitizeBy {
    /**
     * 脱敏处理器
     */
    @SuppressWarnings("rawtypes")
    Class<? extends DesensitizationHandler> handler();
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/handler/DesensitizationHandler.java
对比新文件
@@ -0,0 +1,21 @@
package com.iailab.framework.desensitize.core.base.handler;
import java.lang.annotation.Annotation;
/**
 * 脱敏处理器接口
 *
 * @author gaibu
 */
public interface DesensitizationHandler<T extends Annotation> {
    /**
     * 脱敏
     *
     * @param origin     原始字符串
     * @param annotation 注解信息
     * @return 脱敏后的字符串
     */
    String desensitize(String origin, T annotation);
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java
对比新文件
@@ -0,0 +1,92 @@
package com.iailab.framework.desensitize.core.base.serializer;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import lombok.Getter;
import lombok.Setter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
/**
 * 脱敏序列化器
 *
 * 实现 JSON 返回数据时,使用 {@link DesensitizationHandler} 对声明脱敏注解的字段,进行脱敏处理。
 *
 * @author gaibu
 */
@SuppressWarnings("rawtypes")
public class StringDesensitizeSerializer extends StdSerializer<String> implements ContextualSerializer {
    @Getter
    @Setter
    private DesensitizationHandler desensitizationHandler;
    protected StringDesensitizeSerializer() {
        super(String.class);
    }
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) {
        DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class);
        if (annotation == null) {
            return this;
        }
        // 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器
        StringDesensitizeSerializer serializer = new StringDesensitizeSerializer();
        serializer.setDesensitizationHandler(Singleton.get(annotation.handler()));
        return serializer;
    }
    @Override
    @SuppressWarnings("unchecked")
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
        if (StrUtil.isBlank(value)) {
            gen.writeNull();
            return;
        }
        // 获取序列化字段
        Field field = getField(gen);
        // 自定义处理器
        DesensitizeBy[] annotations = AnnotationUtil.getCombinationAnnotations(field, DesensitizeBy.class);
        if (ArrayUtil.isEmpty(annotations)) {
            gen.writeString(value);
            return;
        }
        for (Annotation annotation : field.getAnnotations()) {
            if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) {
                value = this.desensitizationHandler.desensitize(value, annotation);
                gen.writeString(value);
                return;
            }
        }
        gen.writeString(value);
    }
    /**
     * 获取字段
     *
     * @param generator JsonGenerator
     * @return 字段
     */
    private Field getField(JsonGenerator generator) {
        String currentName = generator.getOutputContext().getCurrentName();
        Object currentValue = generator.getCurrentValue();
        Class<?> currentValueClass = currentValue.getClass();
        return ReflectUtil.getField(currentValueClass, currentName);
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/EmailDesensitize.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.desensitize.core.regex.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.regex.handler.EmailDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 邮箱脱敏注解
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = EmailDesensitizationHandler.class)
public @interface EmailDesensitize {
    /**
     * 匹配的正则表达式
     */
    String regex() default "(^.)[^@]*(@.*$)";
    /**
     * 替换规则,邮箱;
     *
     * 比如:example@gmail.com 脱敏之后为 e****@gmail.com
     */
    String replacer() default "$1****$2";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/annotation/RegexDesensitize.java
对比新文件
@@ -0,0 +1,38 @@
package com.iailab.framework.desensitize.core.regex.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 正则脱敏注解
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = DefaultRegexDesensitizationHandler.class)
public @interface RegexDesensitize {
    /**
     * 匹配的正则表达式(默认匹配所有)
     */
    String regex() default "^[\\s\\S]*$";
    /**
     * 替换规则,会将匹配到的字符串全部替换成 replacer
     *
     * 例如:regex=123; replacer=******
     * 原始字符串 123456789
     * 脱敏后字符串 ******456789
     */
    String replacer() default "******";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java
对比新文件
@@ -0,0 +1,38 @@
package com.iailab.framework.desensitize.core.regex.handler;
import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
import java.lang.annotation.Annotation;
/**
 * 正则表达式脱敏处理器抽象类,已实现通用的方法
 *
 * @author gaibu
 */
public abstract class AbstractRegexDesensitizationHandler<T extends Annotation>
        implements DesensitizationHandler<T> {
    @Override
    public String desensitize(String origin, T annotation) {
        String regex = getRegex(annotation);
        String replacer = getReplacer(annotation);
        return origin.replaceAll(regex, replacer);
    }
    /**
     * 获取注解上的 regex 参数
     *
     * @param annotation 注解信息
     * @return 正则表达式
     */
    abstract String getRegex(T annotation);
    /**
     * 获取注解上的 replacer 参数
     *
     * @param annotation 注解信息
     * @return 待替换的字符串
     */
    abstract String getReplacer(T annotation);
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java
对比新文件
@@ -0,0 +1,21 @@
package com.iailab.framework.desensitize.core.regex.handler;
import com.iailab.framework.desensitize.core.regex.annotation.RegexDesensitize;
/**
 * {@link RegexDesensitize} 的正则脱敏处理器
 *
 * @author gaibu
 */
public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitizationHandler<RegexDesensitize> {
    @Override
    String getRegex(RegexDesensitize annotation) {
        return annotation.regex();
    }
    @Override
    String getReplacer(RegexDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.desensitize.core.regex.handler;
import com.iailab.framework.desensitize.core.regex.annotation.EmailDesensitize;
/**
 * {@link EmailDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class EmailDesensitizationHandler extends AbstractRegexDesensitizationHandler<EmailDesensitize> {
    @Override
    String getRegex(EmailDesensitize annotation) {
        return annotation.regex();
    }
    @Override
    String getReplacer(EmailDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/BankCardDesensitize.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.BankCardDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 银行卡号
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = BankCardDesensitization.class)
public @interface BankCardDesensitize {
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 6;
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 2;
    /**
     * 替换规则,银行卡号; 比如:9988002866797031 脱敏之后为 998800********31
     */
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.CarLicenseDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 车牌号
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = CarLicenseDesensitization.class)
public @interface CarLicenseDesensitize {
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 3;
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 1;
    /**
     * 替换规则,车牌号;比如:粤A66666 脱敏之后为粤A6***6
     */
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.ChineseNameDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 中文名
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = ChineseNameDesensitization.class)
public @interface ChineseNameDesensitize {
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 1;
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 0;
    /**
     * 替换规则,中文名;比如:刘子豪脱敏之后为刘**
     */
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.FixedPhoneDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 固定电话
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = FixedPhoneDesensitization.class)
public @interface FixedPhoneDesensitize {
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 4;
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 2;
    /**
     * 替换规则,固定电话;比如:01086551122 脱敏之后为 0108*****22
     */
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/IdCardDesensitize.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.IdCardDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 身份证
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = IdCardDesensitization.class)
public @interface IdCardDesensitize {
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 6;
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 2;
    /**
     * 替换规则,身份证号码;比如:530321199204074611 脱敏之后为 530321**********11
     */
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/MobileDesensitize.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.MobileDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 手机号
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = MobileDesensitization.class)
public @interface MobileDesensitize {
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 3;
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 4;
    /**
     * 替换规则,手机号;比如:13248765917 脱敏之后为 132****5917
     */
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/PasswordDesensitize.java
对比新文件
@@ -0,0 +1,42 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.PasswordDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 密码
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = PasswordDesensitization.class)
public @interface PasswordDesensitize {
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 0;
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 0;
    /**
     * 替换规则,密码;
     *
     * 比如:123456 脱敏之后为 ******
     */
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/annotation/SliderDesensitize.java
对比新文件
@@ -0,0 +1,43 @@
package com.iailab.framework.desensitize.core.slider.annotation;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 滑动脱敏注解
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = DefaultDesensitizationHandler.class)
public @interface SliderDesensitize {
    /**
     * 后缀保留长度
     */
    int suffixKeep() default 0;
    /**
     * 替换规则,会将前缀后缀保留后,全部替换成 replacer
     *
     * 例如:prefixKeep = 1; suffixKeep = 2; replacer = "*";
     * 原始字符串  123456
     * 脱敏后     1***56
     */
    String replacer() default "*";
    /**
     * 前缀保留长度
     */
    int prefixKeep() default 0;
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java
对比新文件
@@ -0,0 +1,78 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
import java.lang.annotation.Annotation;
/**
 * 滑动脱敏处理器抽象类,已实现通用的方法
 *
 * @author gaibu
 */
public abstract class AbstractSliderDesensitizationHandler<T extends Annotation>
        implements DesensitizationHandler<T> {
    @Override
    public String desensitize(String origin, T annotation) {
        int prefixKeep = getPrefixKeep(annotation);
        int suffixKeep = getSuffixKeep(annotation);
        String replacer = getReplacer(annotation);
        int length = origin.length();
        // 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换
        if (prefixKeep >= length || suffixKeep >= length) {
            return buildReplacerByLength(replacer, length);
        }
        // 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换
        if ((prefixKeep + suffixKeep) >= length) {
            return buildReplacerByLength(replacer, length);
        }
        // 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串
        int interval = length - prefixKeep - suffixKeep;
        return origin.substring(0, prefixKeep) +
                buildReplacerByLength(replacer, interval) +
                origin.substring(prefixKeep + interval);
    }
    /**
     * 根据长度循环构建替换符
     *
     * @param replacer 替换符
     * @param length   长度
     * @return 构建后的替换符
     */
    private String buildReplacerByLength(String replacer, int length) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(replacer);
        }
        return builder.toString();
    }
    /**
     * 前缀保留长度
     *
     * @param annotation 注解信息
     * @return 前缀保留长度
     */
    abstract Integer getPrefixKeep(T annotation);
    /**
     * 后缀保留长度
     *
     * @param annotation 注解信息
     * @return 后缀保留长度
     */
    abstract Integer getSuffixKeep(T annotation);
    /**
     * 替换符
     *
     * @param annotation 注解信息
     * @return 替换符
     */
    abstract String getReplacer(T annotation);
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/BankCardDesensitization.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.BankCardDesensitize;
/**
 * {@link BankCardDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class BankCardDesensitization extends AbstractSliderDesensitizationHandler<BankCardDesensitize> {
    @Override
    Integer getPrefixKeep(BankCardDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(BankCardDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(BankCardDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.CarLicenseDesensitize;
/**
 * {@link CarLicenseDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler<CarLicenseDesensitize> {
    @Override
    Integer getPrefixKeep(CarLicenseDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(CarLicenseDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(CarLicenseDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.ChineseNameDesensitize;
/**
 * {@link ChineseNameDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler<ChineseNameDesensitize> {
    @Override
    Integer getPrefixKeep(ChineseNameDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(ChineseNameDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(ChineseNameDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.SliderDesensitize;
/**
 * {@link SliderDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler<SliderDesensitize> {
    @Override
    Integer getPrefixKeep(SliderDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(SliderDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(SliderDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize;
/**
 * {@link FixedPhoneDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler<FixedPhoneDesensitize> {
    @Override
    Integer getPrefixKeep(FixedPhoneDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(FixedPhoneDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(FixedPhoneDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/IdCardDesensitization.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.IdCardDesensitize;
/**
 * {@link IdCardDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class IdCardDesensitization extends AbstractSliderDesensitizationHandler<IdCardDesensitize> {
    @Override
    Integer getPrefixKeep(IdCardDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(IdCardDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(IdCardDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/MobileDesensitization.java
对比新文件
@@ -0,0 +1,26 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.MobileDesensitize;
/**
 * {@link MobileDesensitize} 的脱敏处理器
 *
 * @author gaibu
 */
public class MobileDesensitization extends AbstractSliderDesensitizationHandler<MobileDesensitize> {
    @Override
    Integer getPrefixKeep(MobileDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(MobileDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(MobileDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/core/slider/handler/PasswordDesensitization.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.desensitize.core.slider.handler;
import com.iailab.framework.desensitize.core.slider.annotation.PasswordDesensitize;
/**
 * {@link PasswordDesensitize} 的码脱敏处理器
 *
 * @author gaibu
 */
public class PasswordDesensitization extends AbstractSliderDesensitizationHandler<PasswordDesensitize> {
    @Override
    Integer getPrefixKeep(PasswordDesensitize annotation) {
        return annotation.prefixKeep();
    }
    @Override
    Integer getSuffixKeep(PasswordDesensitize annotation) {
        return annotation.suffixKeep();
    }
    @Override
    String getReplacer(PasswordDesensitize annotation) {
        return annotation.replacer();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/desensitize/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏
 */
package com.iailab.framework.desensitize;
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/config/IailabJacksonAutoConfiguration.java
对比新文件
@@ -0,0 +1,52 @@
package com.iailab.framework.jackson.config;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.common.util.json.JsonUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.iailab.framework.common.util.json.databind.NumberSerializer;
import com.iailab.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
import com.iailab.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
@AutoConfiguration
@Slf4j
public class IailabJacksonAutoConfiguration {
    @Bean
    @SuppressWarnings("InstantiationOfUtilityClass")
    public JsonUtils jsonUtils(List<ObjectMapper> objectMappers) {
        // 1.1 创建 SimpleModule 对象
        SimpleModule simpleModule = new SimpleModule();
        simpleModule
                // 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型
                .addSerializer(Long.class, NumberSerializer.INSTANCE)
                .addSerializer(Long.TYPE, NumberSerializer.INSTANCE)
                .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE)
                .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
                .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE)
                .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
                // 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳
                .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
                .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
        // 1.2 注册到 objectMapper
        objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule));
        // 2. 设置 objectMapper 到 JsonUtils
        JsonUtils.init(CollUtil.getFirst(objectMappers));
        log.info("[init][初始化 JsonUtils 成功]");
        return new JsonUtils();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/NumberSerializer.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.jackson.core.databind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import java.io.IOException;
/**
 * Long 序列化规则
 *
 * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
 *
 * @author 星语
 */
@JacksonStdImpl
public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
    private static final long MAX_SAFE_INTEGER = 9007199254740991L;
    private static final long MIN_SAFE_INTEGER = -9007199254740991L;
    public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
    public NumberSerializer(Class<? extends Number> rawType) {
        super(rawType);
    }
    @Override
    public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        // 超出范围 序列化位字符串
        if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
            super.serialize(value, gen, serializers);
        } else {
            gen.writeString(value.toString());
        }
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.jackson.core.databind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
 * 基于时间戳的 LocalDateTime 反序列化器
 *
 * @author 老五
 */
public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
    public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
    @Override
    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        // 将 Long 时间戳,转换为 LocalDateTime 对象
        return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java
对比新文件
@@ -0,0 +1,26 @@
package com.iailab.framework.jackson.core.databind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
 * 基于时间戳的 LocalDateTime 序列化器
 *
 * @author 老五
 */
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
    public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
    @Override
    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        // 将 LocalDateTime 对象,转换为 Long 时间戳
        gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/jackson/core/package-info.java
对比新文件
@@ -0,0 +1 @@
package com.iailab.framework.jackson.core;
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * Web 框架,全局异常、API 日志等
 */
package com.iailab.framework;
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/IailabSwaggerAutoConfiguration.java
对比新文件
@@ -0,0 +1,157 @@
package com.iailab.framework.swagger.config;
import io.swagger.v3.oas.models.Components;
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 io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.*;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
 * Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。
 *
 * 友情提示:
 * 1. Springdoc 文档地址:<a href="https://github.com/springdoc/springdoc-openapi">仓库</a>
 * 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西
 *
 * @author iailab
 */
@AutoConfiguration
@ConditionalOnClass({OpenAPI.class})
@EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
public class IailabSwaggerAutoConfiguration {
    // ========== 全局 OpenAPI 配置 ==========
    @Bean
    public OpenAPI createApi(SwaggerProperties properties) {
        Map<String, SecurityScheme> securitySchemas = buildSecuritySchemes();
        OpenAPI openAPI = new OpenAPI()
                // 接口信息
                .info(buildInfo(properties))
                // 接口安全配置
                .components(new Components().securitySchemes(securitySchemas))
                .addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION));
        securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key)));
        return openAPI;
    }
    /**
     * API 摘要信息
     */
    private Info buildInfo(SwaggerProperties properties) {
        return new Info()
                .title(properties.getTitle())
                .description(properties.getDescription())
                .version(properties.getVersion())
                .contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail()))
                .license(new License().name(properties.getLicense()).url(properties.getLicenseUrl()));
    }
    /**
     * 安全模式,这里配置通过请求头 Authorization 传递 token 参数
     */
    private Map<String, SecurityScheme> buildSecuritySchemes() {
        Map<String, SecurityScheme> securitySchemes = new HashMap<>();
        SecurityScheme securityScheme = new SecurityScheme()
                .type(SecurityScheme.Type.APIKEY) // 类型
                .name(HttpHeaders.AUTHORIZATION) // 请求头的 name
                .in(SecurityScheme.In.HEADER); // token 所在位置
        securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme);
        return securitySchemes;
    }
    /**
     * 自定义 OpenAPI 处理器
     */
    @Bean
    @Primary // 目的:以我们创建的 OpenAPIService Bean 为主,避免一键改包后,启动报错!
    public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
                                         SecurityService securityParser,
                                         SpringDocConfigProperties springDocConfigProperties,
                                         PropertyResolverUtils propertyResolverUtils,
                                         Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
                                         Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
                                         Optional<JavadocProvider> javadocProvider) {
        return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
                propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
    }
    // ========== 分组 OpenAPI 配置 ==========
    /**
     * 所有模块的 API 分组
     */
    @Bean
    public GroupedOpenApi allGroupedOpenApi() {
        return buildGroupedOpenApi("all", "");
    }
    public static GroupedOpenApi buildGroupedOpenApi(String group) {
        return buildGroupedOpenApi(group, group);
    }
    public static GroupedOpenApi buildGroupedOpenApi(String group, String path) {
        return GroupedOpenApi.builder()
                .group(group)
                .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**")
                .addOperationCustomizer((operation, handlerMethod) -> operation
                        .addParametersItem(buildTenantHeaderParameter())
                        .addParametersItem(buildSecurityHeaderParameter()))
                .build();
    }
    /**
     * 构建 Tenant 租户编号请求头参数
     *
     * @return 多租户参数
     */
    private static Parameter buildTenantHeaderParameter() {
        return new Parameter()
                .name(HEADER_TENANT_ID) // header 名
                .description("租户编号") // 描述
                .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
                .schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_ID).description("租户编号")); // 默认:使用租户编号为 1
    }
    /**
     * 构建 Authorization 认证请求头参数
     *
     * 解决 Knife4j <a href="https://gitee.com/xiaoym/knife4j/issues/I69QBU">Authorize 未生效,请求header里未包含参数</a>
     *
     * @return 认证参数
     */
    private static Parameter buildSecurityHeaderParameter() {
        return new Parameter()
                .name(HttpHeaders.AUTHORIZATION) // header 名
                .description("认证 Token") // 描述
                .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
                .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/config/SwaggerProperties.java
对比新文件
@@ -0,0 +1,60 @@
package com.iailab.framework.swagger.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.validation.constraints.NotEmpty;
/**
 * Swagger 配置属性
 *
 * @author iailab
 */
@ConfigurationProperties("iailab.swagger")
@Data
public class SwaggerProperties {
    /**
     * 标题
     */
    @NotEmpty(message = "标题不能为空")
    private String title;
    /**
     * 描述
     */
    @NotEmpty(message = "描述不能为空")
    private String description;
    /**
     * 作者
     */
    @NotEmpty(message = "作者不能为空")
    private String author;
    /**
     * 版本
     */
    @NotEmpty(message = "版本不能为空")
    private String version;
    /**
     * url
     */
    @NotEmpty(message = "扫描的 package 不能为空")
    private String url;
    /**
     * email
     */
    @NotEmpty(message = "扫描的 email 不能为空")
    private String email;
    /**
     * license
     */
    @NotEmpty(message = "扫描的 license 不能为空")
    private String license;
    /**
     * license-url
     */
    @NotEmpty(message = "扫描的 license-url 不能为空")
    private String licenseUrl;
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/swagger/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * 基于 Swagger + Knife4j 实现 API 接口文档
 *
 * @author iailab
 */
package com.iailab.framework.swagger;
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/IailabWebAutoConfiguration.java
对比新文件
@@ -0,0 +1,131 @@
package com.iailab.framework.web.config;
import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService;
import com.iailab.framework.common.enums.WebFilterOrderEnum;
import com.iailab.framework.web.core.filter.CacheRequestBodyFilter;
import com.iailab.framework.web.core.filter.DemoFilter;
import com.iailab.framework.web.core.handler.GlobalExceptionHandler;
import com.iailab.framework.web.core.handler.GlobalResponseBodyHandler;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import javax.servlet.Filter;
@AutoConfiguration
@EnableConfigurationProperties(WebProperties.class)
public class IailabWebAutoConfiguration implements WebMvcConfigurer {
    @Resource
    private WebProperties webProperties;
    /**
     * 应用名
     */
    @Value("${spring.application.name}")
    private String applicationName;
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurePathMatch(configurer, webProperties.getAdminApi());
        configurePathMatch(configurer, webProperties.getAppApi());
    }
    /**
     * 设置 API 前缀,仅仅匹配 controller 包下的
     *
     * @param configurer 配置
     * @param api        API 配置
     */
    private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
        AntPathMatcher antPathMatcher = new AntPathMatcher(".");
        configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)
                && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包
    }
    @Bean
    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) {
        return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService);
    }
    @Bean
    public GlobalResponseBodyHandler globalResponseBodyHandler() {
        return new GlobalResponseBodyHandler();
    }
    @Bean
    @SuppressWarnings("InstantiationOfUtilityClass")
    public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) {
        // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean
        return new WebFrameworkUtils(webProperties);
    }
    // ========== Filter 相关 ==========
    /**
     * 创建 CorsFilter Bean,解决跨域问题
     */
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilterBean() {
        // 创建 CorsConfiguration 对象
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*"); // 设置访问源地址
        config.addAllowedHeader("*"); // 设置访问源请求头
        config.addAllowedMethod("*"); // 设置访问源请求方法
        // 创建 UrlBasedCorsConfigurationSource 对象
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
        return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER);
    }
    /**
     * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
     */
    @Bean
    public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() {
        return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
    }
    /**
     * 创建 DemoFilter Bean,演示模式
     */
    @Bean
    @ConditionalOnProperty(value = "iailab.demo", havingValue = "true")
    public FilterRegistrationBean<DemoFilter> demoFilter() {
        return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
    }
    public static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
        bean.setOrder(order);
        return bean;
    }
    /**
     * 创建 RestTemplate 实例
     *
     * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
     */
    @Bean
    @ConditionalOnMissingBean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.build();
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/config/WebProperties.java
对比新文件
@@ -0,0 +1,66 @@
package com.iailab.framework.web.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ConfigurationProperties(prefix = "iailab.web")
@Validated
@Data
public class WebProperties {
    @NotNull(message = "APP API 不能为空")
    private Api appApi = new Api("/app-api", "**.controller.app.**");
    @NotNull(message = "Admin API 不能为空")
    private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
    @NotNull(message = "Admin UI 不能为空")
    private Ui adminUi;
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Valid
    public static class Api {
        /**
         * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
         *
         *
         * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
         *      这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
         *
         * @see IailabWebAutoConfiguration#configurePathMatch(PathMatchConfigurer)
         */
        @NotEmpty(message = "API 前缀不能为空")
        private String prefix;
        /**
         * Controller 所在包的 Ant 路径规则
         *
         * 主要目的是,给该 Controller 设置指定的 {@link #prefix}
         */
        @NotEmpty(message = "Controller 所在包不能为空")
        private String controller;
    }
    @Data
    @Valid
    public static class Ui {
        /**
         * 访问地址
         */
        private String url;
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/ApiRequestFilter.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.web.core.filter;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.web.config.WebProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.http.HttpServletRequest;
/**
 * 过滤 /admin-api、/app-api 等 API 请求的过滤器
 *
 * @author iailab
 */
@RequiredArgsConstructor
public abstract class ApiRequestFilter extends OncePerRequestFilter {
    protected final WebProperties webProperties;
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        // 只过滤 API 请求的地址
        return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(),
                webProperties.getAppApi().getPrefix());
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyFilter.java
对比新文件
@@ -0,0 +1,31 @@
package com.iailab.framework.web.core.filter;
import com.iailab.framework.common.util.servlet.ServletUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * Request Body 缓存 Filter,实现它的可重复读取
 *
 * @author iailab
 */
public class CacheRequestBodyFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        filterChain.doFilter(new CacheRequestBodyWrapper(request), response);
    }
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        // 只处理 json 请求内容
        return !ServletUtils.isJsonRequest(request);
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/CacheRequestBodyWrapper.java
对比新文件
@@ -0,0 +1,68 @@
package com.iailab.framework.web.core.filter;
import com.iailab.framework.common.util.servlet.ServletUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
 *  Request Body 缓存 Wrapper
 *
 * @author iailab
 */
public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
    /**
     * 缓存的内容
     */
    private final byte[] body;
    public CacheRequestBodyWrapper(HttpServletRequest request) {
        super(request);
        body = ServletUtils.getBodyBytes(request);
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
        // 返回 ServletInputStream
        return new ServletInputStream() {
            @Override
            public int read() {
                return inputStream.read();
            }
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {}
            @Override
            public int available() {
                return body.length;
            }
        };
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/filter/DemoFilter.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.web.core.filter;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY;
/**
 * 演示 Filter,禁止用户发起写操作,避免影响测试数据
 *
 * @author iailab
 */
public class DemoFilter extends OncePerRequestFilter {
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String method = request.getMethod();
        return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE")  // 写操作时,不进行过滤率
                || WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        // 直接返回 DEMO_DENY 的结果。即,请求不继续
        ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY));
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalExceptionHandler.java
对比新文件
@@ -0,0 +1,356 @@
package com.iailab.framework.web.core.handler;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.collection.SetUtils;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.common.util.monitor.TracerUtils;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
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.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.dao.DuplicateKeyException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Set;
import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.*;
/**
 * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
 *
 * @author iailab
 */
@RestControllerAdvice
@AllArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 忽略的 ServiceException 错误提示,避免打印过多 logger
     */
    public static final Set<String> IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌");
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    private final String applicationName;
    private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
    /**
     * 处理所有异常,主要是提供给 Filter 使用
     * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
     *
     * @param request 请求
     * @param ex 异常
     * @return 通用返回
     */
    public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
        if (ex instanceof MissingServletRequestParameterException) {
            return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
        }
        if (ex instanceof MethodArgumentTypeMismatchException) {
            return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
        }
        if (ex instanceof MethodArgumentNotValidException) {
            return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
        }
        if (ex instanceof BindException) {
            return bindExceptionHandler((BindException) ex);
        }
        if (ex instanceof ConstraintViolationException) {
            return constraintViolationExceptionHandler((ConstraintViolationException) ex);
        }
        if (ex instanceof ValidationException) {
            return validationException((ValidationException) ex);
        }
        if (ex instanceof NoHandlerFoundException) {
            return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
        }
        if (ex instanceof HttpRequestMethodNotSupportedException) {
            return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
        }
        if (ex instanceof ServiceException) {
            return serviceExceptionHandler((ServiceException) ex);
        }
        if (ex instanceof AccessDeniedException) {
            return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
        }
        if (ex instanceof DuplicateKeyException) {
            return duplicateKeyExceptionHandler((DuplicateKeyException) ex);
        }
        return defaultExceptionHandler(request, ex);
    }
    /**
     * 处理 SpringMVC 请求参数缺失
     *
     * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数
     */
    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
    }
    /**
     * 处理 SpringMVC 请求参数类型错误
     *
     * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
    }
    /**
     * 处理 SpringMVC 参数校验不正确
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
        log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
        FieldError fieldError = ex.getBindingResult().getFieldError();
        assert fieldError != null; // 断言,避免告警
        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
    }
    /**
     * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
     */
    @ExceptionHandler(BindException.class)
    public CommonResult<?> bindExceptionHandler(BindException ex) {
        log.warn("[handleBindException]", ex);
        FieldError fieldError = ex.getFieldError();
        assert fieldError != null; // 断言,避免告警
        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
    }
    /**
     * 处理 Validator 校验不通过产生的异常
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
        log.warn("[constraintViolationExceptionHandler]", ex);
        ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
    }
    /**
     * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
     */
    @ExceptionHandler(value = ValidationException.class)
    public CommonResult<?> validationException(ValidationException ex) {
        log.warn("[constraintViolationExceptionHandler]", ex);
        // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
        return CommonResult.error(BAD_REQUEST);
    }
    /**
     * 处理 SpringMVC 请求地址不存在
     *
     * 注意,它需要设置如下两个配置项:
     * 1. spring.mvc.throw-exception-if-no-handler-found 为 true
     * 2. spring.mvc.static-path-pattern 为 /statics/**
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
        log.warn("[noHandlerFoundExceptionHandler]", ex);
        return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
    }
    /**
     * 处理 SpringMVC 请求方法不正确
     *
     * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
        log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
        return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
    }
    /**
     * 处理 Spring Security 权限不足的异常
     *
     * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截
     */
    @ExceptionHandler(value = AccessDeniedException.class)
    public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
        log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req),
                req.getRequestURL(), ex);
        return CommonResult.error(FORBIDDEN);
    }
    /**
     * 处理业务异常 SQLIntegrityConstraintViolationException
     *
     * 数据库存在重复数据
     */
    @ExceptionHandler(value = DuplicateKeyException.class)
    public CommonResult<?> duplicateKeyExceptionHandler(DuplicateKeyException ex) {
        log.warn("[duplicateKeyExceptionHandler]", ex);
        return CommonResult.error(DATA_REPETITION.getCode(), DATA_REPETITION.getMsg());
    }
    /**
     * 处理业务异常 ServiceException
     *
     * 例如说,商品库存不足,用户手机号已存在。
     */
    @ExceptionHandler(value = ServiceException.class)
    public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
        // 不包含的时候,才进行打印,避免 ex 堆栈过多
        if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) {
            // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到
            StackTraceElement[] stackTrace = ex.getStackTrace();
            log.warn("[serviceExceptionHandler]\n\t{}", stackTrace[0]);
        }
        return CommonResult.error(ex.getCode(), ex.getMessage());
    }
    /**
     * 处理系统异常,兜底处理所有的一切
     */
    @ExceptionHandler(value = Exception.class)
    public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
        // 情况一:处理表不存在的异常
        CommonResult<?> tableNotExistsResult = handleTableNotExists(ex);
        if (tableNotExistsResult != null) {
            return tableNotExistsResult;
        }
        // 情况二:处理异常
        log.error("[defaultExceptionHandler]", ex);
        // 插入异常日志
        createExceptionLog(req, ex);
        // 返回 ERROR CommonResult
        return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
    }
    private void createExceptionLog(HttpServletRequest req, Throwable e) {
        // 插入错误日志
        ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
        try {
            // 初始化 errorLog
            buildExceptionLog(errorLog, req, e);
            // 执行插入 errorLog
            apiErrorLogFrameworkService.createApiErrorLog(errorLog);
        } catch (Throwable th) {
            log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(),  JsonUtils.toJsonString(errorLog), th);
        }
    }
    private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
        // 处理用户信息
        errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
        errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
        // 设置异常字段
        errorLog.setExceptionName(e.getClass().getName());
        errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
        errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
        errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
        StackTraceElement[] stackTraceElements = e.getStackTrace();
        Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
        StackTraceElement stackTraceElement = stackTraceElements[0];
        errorLog.setExceptionClassName(stackTraceElement.getClassName());
        errorLog.setExceptionFileName(stackTraceElement.getFileName());
        errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
        errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
        // 设置其它字段
        errorLog.setTraceId(TracerUtils.getTraceId());
        errorLog.setApplicationName(applicationName);
        errorLog.setRequestUrl(request.getRequestURI());
        Map<String, Object> requestParams = MapUtil.<String, Object>builder()
                .put("query", ServletUtils.getParamMap(request))
                .put("body", ServletUtils.getBody(request)).build();
        errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
        errorLog.setRequestMethod(request.getMethod());
        errorLog.setUserAgent(ServletUtils.getUserAgent(request));
        errorLog.setUserIp(ServletUtils.getClientIP(request));
        errorLog.setExceptionTime(LocalDateTime.now());
    }
    /**
     * 处理 Table 不存在的异常情况
     *
     * @param ex 异常
     * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult
     */
    private CommonResult<?> handleTableNotExists(Throwable ex) {
        String message = ExceptionUtil.getRootCauseMessage(ex);
        if (!message.contains("doesn't exist")) {
            return null;
        }
        // 1. 数据报表
        if (message.contains("report_")) {
            log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
        }
        // 2. 工作流
        if (message.contains("bpm_")) {
            log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
        }
        // 3. 微信公众号
        if (message.contains("mp_")) {
            log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
        }
        // 4. 商城系统
        if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
            log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
        }
        // 5. ERP 系统
        if (message.contains("erp_")) {
            log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
        }
        // 6. CRM 系统
        if (message.contains("crm_")) {
            log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
        }
        // 7. 支付平台
        if (message.contains("pay_")) {
            log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
        }
        // 8. AI 大模型
        if (message.contains("ai_")) {
            log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                    "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
        }
        return null;
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java
对比新文件
@@ -0,0 +1,45 @@
package com.iailab.framework.web.core.handler;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.web.core.util.WebFrameworkUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
 * 全局响应结果(ResponseBody)处理器
 *
 * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult},
 * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。
 * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构
 *
 * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果,
 * 方便 {@link com.iailab.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志
 */
@ControllerAdvice
public class GlobalResponseBodyHandler implements ResponseBodyAdvice {
    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public boolean supports(MethodParameter returnType, Class converterType) {
        if (returnType.getMethod() == null) {
            return false;
        }
        // 只拦截返回结果为 CommonResult 类型
        return returnType.getMethod().getReturnType() == CommonResult.class;
    }
    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        // 记录 Controller 结果
        WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult<?>) body);
        return body;
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java
对比新文件
@@ -0,0 +1,169 @@
package com.iailab.framework.web.core.util;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.iailab.framework.common.enums.RpcConstants;
import com.iailab.framework.common.enums.TerminalEnum;
import com.iailab.framework.common.enums.UserTypeEnum;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.web.config.WebProperties;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
/**
 * 专属于 web 包的工具类
 *
 * @author iailab
 */
public class WebFrameworkUtils {
    private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id";
    private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";
    private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
    public static final String HEADER_TENANT_ID = "tenant-id";
    /**
     * 终端的 Header
     *
     * @see com.iailab.framework.common.enums.TerminalEnum
     */
    public static final String HEADER_TERMINAL = "terminal";
    private static WebProperties properties;
    public WebFrameworkUtils(WebProperties webProperties) {
        WebFrameworkUtils.properties = webProperties;
    }
    /**
     * 获得租户编号,从 header 中
     * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
     *
     * @param request 请求
     * @return 租户编号
     */
    public static Long getTenantId(HttpServletRequest request) {
        String tenantId = request.getHeader(HEADER_TENANT_ID);
        return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
    }
    public static void setLoginUserId(ServletRequest request, Long userId) {
        request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
    }
    /**
     * 设置用户类型
     *
     * @param request 请求
     * @param userType 用户类型
     */
    public static void setLoginUserType(ServletRequest request, Integer userType) {
        request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType);
    }
    /**
     * 获得当前用户的编号,从请求中
     * 注意:该方法仅限于 framework 框架使用!!!
     *
     * @param request 请求
     * @return 用户编号
     */
    public static Long getLoginUserId(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID);
    }
    /**
     * 获得当前用户的类型
     * 注意:该方法仅限于 web 相关的 framework 组件使用!!!
     *
     * @param request 请求
     * @return 用户编号
     */
    public static Integer getLoginUserType(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        // 1. 优先,从 Attribute 中获取
        Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
        if (userType != null) {
            return userType;
        }
        // 2. 其次,基于 URL 前缀的约定
        if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) {
            return UserTypeEnum.ADMIN.getValue();
        }
        if (request.getServletPath().startsWith(properties.getAppApi().getPrefix())) {
            return UserTypeEnum.MEMBER.getValue();
        }
        return null;
    }
    public static Integer getLoginUserType() {
        HttpServletRequest request = getRequest();
        return getLoginUserType(request);
    }
    public static Long getLoginUserId() {
        HttpServletRequest request = getRequest();
        return getLoginUserId(request);
    }
    public static Integer getTerminal() {
        HttpServletRequest request = getRequest();
        if (request == null) {
            return TerminalEnum.UNKNOWN.getTerminal();
        }
        String terminalValue = request.getHeader(HEADER_TERMINAL);
        return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal());
    }
    public static void setCommonResult(ServletRequest request, CommonResult<?> result) {
        request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result);
    }
    public static CommonResult<?> getCommonResult(ServletRequest request) {
        return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
    }
    public static HttpServletRequest getRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (!(requestAttributes instanceof ServletRequestAttributes)) {
            return null;
        }
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        return servletRequestAttributes.getRequest();
    }
    /**
     * 判断是否为 RPC 请求
     *
     * @param request 请求
     * @return 是否为 RPC 请求
     */
    public static boolean isRpcRequest(HttpServletRequest request) {
        return request.getRequestURI().startsWith(RpcConstants.RPC_API_PREFIX);
    }
    /**
     * 判断是否为 RPC 请求
     *
     * 约定大于配置,只要以 Api 结尾,都认为是 RPC 接口
     *
     * @param className 类名
     * @return 是否为 RPC 请求
     */
    public static boolean isRpcRequest(String className) {
        return className.endsWith("Api");
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 针对 SpringMVC 的基础封装
 */
package com.iailab.framework.web;
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java
对比新文件
@@ -0,0 +1,63 @@
package com.iailab.framework.xss.config;
import com.iailab.framework.common.enums.WebFilterOrderEnum;
import com.iailab.framework.xss.core.clean.JsoupXssCleaner;
import com.iailab.framework.xss.core.clean.XssCleaner;
import com.iailab.framework.xss.core.filter.XssFilter;
import com.iailab.framework.xss.core.json.XssStringJsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import static com.iailab.framework.web.config.IailabWebAutoConfiguration.createFilterBean;
@AutoConfiguration
@EnableConfigurationProperties(XssProperties.class)
@ConditionalOnProperty(prefix = "iailab.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
public class IailabXssAutoConfiguration implements WebMvcConfigurer {
    /**
     * Xss 清理者
     *
     * @return XssCleaner
     */
    @Bean
    @ConditionalOnMissingBean(XssCleaner.class)
    public XssCleaner xssCleaner() {
        return new JsoupXssCleaner();
    }
    /**
     * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
     *
     * @return Jackson2ObjectMapperBuilderCustomizer
     */
    @Bean
    @ConditionalOnMissingBean(name = "xssJacksonCustomizer")
    @ConditionalOnBean(ObjectMapper.class)
    @ConditionalOnProperty(value = "iailab.xss.enable", havingValue = "true")
    public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
                                                                      PathMatcher pathMatcher,
                                                                      XssCleaner xssCleaner) {
        // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
        return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner));
    }
    /**
     * 创建 XssFilter Bean,解决 Xss 安全问题
     */
    @Bean
    @ConditionalOnBean(XssCleaner.class)
    public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
        return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java
对比新文件
@@ -0,0 +1,29 @@
package com.iailab.framework.xss.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.util.Collections;
import java.util.List;
/**
 * Xss 配置属性
 *
 * @author iailab
 */
@ConfigurationProperties(prefix = "iailab.xss")
@Validated
@Data
public class XssProperties {
    /**
     * 是否开启,默认为 true
     */
    private boolean enable = true;
    /**
     * 需要排除的 URL,默认为空
     */
    private List<String> excludeUrls = Collections.emptyList();
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java
对比新文件
@@ -0,0 +1,64 @@
package com.iailab.framework.xss.core.clean;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
/**
 * 基于 JSONP 实现 XSS 过滤字符串
 */
public class JsoupXssCleaner implements XssCleaner {
    private final Safelist safelist;
    /**
     * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
     */
    private final String baseUri;
    /**
     * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
     */
    public JsoupXssCleaner() {
        this.safelist = buildSafelist();
        this.baseUri = "";
    }
    /**
     * 构建一个 Xss 清理的 Safelist 规则。
     * 基于 Safelist#relaxed() 的基础上:
     * 1. 扩展支持了 style 和 class 属性
     * 2. a 标签额外支持了 target 属性
     * 3. img 标签额外支持了 data 协议,便于支持 base64
     *
     * @return Safelist
     */
    private Safelist buildSafelist() {
        // 使用 jsoup 提供的默认的
        Safelist relaxedSafelist = Safelist.relaxed();
        // 富文本编辑时一些样式是使用 style 来进行实现的
        // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
        // 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
        relaxedSafelist.addAttributes(":all", "style", "class");
        // 保留 a 标签的 target 属性
        relaxedSafelist.addAttributes("a", "target");
        // 支持img 为base64
        relaxedSafelist.addProtocols("img", "src", "data");
        // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
        // WHITELIST.preserveRelativeLinks(false);
        // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
        // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
        // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
        // WHITELIST.removeProtocols("img", "src", "http", "https");
        return relaxedSafelist;
    }
    @Override
    public String clean(String html) {
        return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java
对比新文件
@@ -0,0 +1,16 @@
package com.iailab.framework.xss.core.clean;
/**
 * 对 html 文本中的有 Xss 风险的数据进行清理
 */
public interface XssCleaner {
    /**
     * 清理有 Xss 风险的文本
     *
     * @param html 原 html
     * @return 清理后的 html
     */
    String clean(String html);
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java
对比新文件
@@ -0,0 +1,52 @@
package com.iailab.framework.xss.core.filter;
import com.iailab.framework.xss.config.XssProperties;
import com.iailab.framework.xss.core.clean.XssCleaner;
import lombok.AllArgsConstructor;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * Xss 过滤器
 *
 * @author iailab
 */
@AllArgsConstructor
public class XssFilter extends OncePerRequestFilter {
    /**
     * 属性
     */
    private final XssProperties properties;
    /**
     * 路径匹配器
     */
    private final PathMatcher pathMatcher;
    private final XssCleaner xssCleaner;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
    }
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        // 如果关闭,则不过滤
        if (!properties.isEnable()) {
            return true;
        }
        // 如果匹配到无需过滤,则不过滤
        String uri = request.getRequestURI();
        return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri));
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java
对比新文件
@@ -0,0 +1,92 @@
package com.iailab.framework.xss.core.filter;
import com.iailab.framework.xss.core.clean.XssCleaner;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.LinkedHashMap;
import java.util.Map;
/**
 * Xss 请求 Wrapper
 *
 * @author iailab
 */
public class XssRequestWrapper extends HttpServletRequestWrapper {
    private final XssCleaner xssCleaner;
    public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
        super(request);
        this.xssCleaner = xssCleaner;
    }
    // ============================ parameter ============================
    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> map = new LinkedHashMap<>();
        Map<String, String[]> parameters = super.getParameterMap();
        for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
            String[] values = entry.getValue();
            for (int i = 0; i < values.length; i++) {
                values[i] = xssCleaner.clean(values[i]);
            }
            map.put(entry.getKey(), values);
        }
        return map;
    }
    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) {
            return null;
        }
        int count = values.length;
        String[] encodedValues = new String[count];
        for (int i = 0; i < count; i++) {
            encodedValues[i] = xssCleaner.clean(values[i]);
        }
        return encodedValues;
    }
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (value == null) {
            return null;
        }
        return xssCleaner.clean(value);
    }
    // ============================ attribute ============================
    @Override
    public Object getAttribute(String name) {
        Object value = super.getAttribute(name);
        if (value instanceof String) {
            return xssCleaner.clean((String) value);
        }
        return value;
    }
    // ============================ header ============================
    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        if (value == null) {
            return null;
        }
        return xssCleaner.clean(value);
    }
    // ============================ queryString ============================
    @Override
    public String getQueryString() {
        String value = super.getQueryString();
        if (value == null) {
            return null;
        }
        return xssCleaner.clean(value);
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java
对比新文件
@@ -0,0 +1,82 @@
package com.iailab.framework.xss.core.json;
import com.iailab.framework.common.util.servlet.ServletUtils;
import com.iailab.framework.xss.config.XssProperties;
import com.iailab.framework.xss.core.clean.XssCleaner;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PathMatcher;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
 * XSS 过滤 jackson 反序列化器。
 * 在反序列化的过程中,会对字符串进行 XSS 过滤。
 *
 * @author Hccake
 */
@Slf4j
@AllArgsConstructor
public class XssStringJsonDeserializer extends StringDeserializer {
    /**
     * 属性
     */
    private final XssProperties properties;
    /**
     * 路径匹配器
     */
    private final PathMatcher pathMatcher;
    private final XssCleaner xssCleaner;
    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        // 1. 白名单 URL 的处理
        HttpServletRequest request = ServletUtils.getRequest();
        if (request != null) {
            String uri = ServletUtils.getRequest().getRequestURI();
            if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) {
                return p.getText();
            }
        }
        // 2. 真正使用 xssCleaner 进行过滤
        if (p.hasToken(JsonToken.VALUE_STRING)) {
            return xssCleaner.clean(p.getText());
        }
        JsonToken t = p.currentToken();
        // [databind#381]
        if (t == JsonToken.START_ARRAY) {
            return _deserializeFromArray(p, ctxt);
        }
        // need to gracefully handle byte[] data, as base64
        if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
            Object ob = p.getEmbeddedObject();
            if (ob == null) {
                return null;
            }
            if (ob instanceof byte[]) {
                return ctxt.getBase64Variant().encode((byte[]) ob, false);
            }
            // otherwise, try conversion using toString()...
            return ob.toString();
        }
        // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
        if (t == JsonToken.START_OBJECT) {
            return ctxt.extractScalarFromObject(p, this, _valueClass);
        }
        if (t.isScalarValue()) {
            String text = p.getValueAsString();
            return xssCleaner.clean(text);
        }
        return (String) ctxt.handleUnexpectedToken(_valueClass, p);
    }
}
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * 针对 XSS 的基础封装
 *
 * XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html
 */
package com.iailab.framework.xss;
iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,6 @@
com.iailab.framework.apilog.config.IailabApiLogAutoConfiguration
com.iailab.framework.jackson.config.IailabJacksonAutoConfiguration
com.iailab.framework.swagger.config.IailabSwaggerAutoConfiguration
com.iailab.framework.web.config.IailabWebAutoConfiguration
com.iailab.framework.apilog.config.IailabApiLogRpcAutoConfiguration
com.iailab.framework.banner.config.IailabBannerAutoConfiguration
iailab-framework/iailab-common-web/src/main/resources/banner.txt
对比新文件
@@ -0,0 +1,12 @@
iailab
Application Version: ${iailab.info.version}
Spring Boot Version: ${spring-boot.version}
 ██           ██  ██           ██                 ██             ██     ████
░░           ░░  ░██          ░██        ██████  ░██            ░██    ░██░
 ██  ██████   ██ ░██  ██████  ░██       ░██░░░██ ░██  ██████   ██████ ██████  ██████  ██████ ██████████
░██ ░░░░░░██ ░██ ░██ ░░░░░░██ ░██████   ░██  ░██ ░██ ░░░░░░██ ░░░██░ ░░░██░  ██░░░░██░░██░░█░░██░░██░░██
░██  ███████ ░██ ░██  ███████ ░██░░░██  ░██████  ░██  ███████   ░██    ░██  ░██   ░██ ░██ ░  ░██ ░██ ░██
░██ ██░░░░██ ░██ ░██ ██░░░░██ ░██  ░██  ░██░░░   ░██ ██░░░░██   ░██    ░██  ░██   ░██ ░██    ░██ ░██ ░██
░██░░████████░██ ███░░████████░██████   ░██      ███░░████████  ░░██   ░██  ░░██████ ░███    ███ ░██ ░██
░░  ░░░░░░░░ ░░ ░░░  ░░░░░░░░ ░░░░░     ░░      ░░░  ░░░░░░░░    ░░    ░░    ░░░░░░  ░░░    ░░░  ░░  ░░
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java
对比新文件
@@ -0,0 +1,100 @@
package com.iailab.framework.desensitize.core;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.desensitize.core.regex.annotation.EmailDesensitize;
import com.iailab.framework.desensitize.core.regex.annotation.RegexDesensitize;
import com.iailab.framework.desensitize.core.annotation.Address;
import com.iailab.framework.desensitize.core.slider.annotation.BankCardDesensitize;
import com.iailab.framework.desensitize.core.slider.annotation.CarLicenseDesensitize;
import com.iailab.framework.desensitize.core.slider.annotation.ChineseNameDesensitize;
import com.iailab.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize;
import com.iailab.framework.desensitize.core.slider.annotation.IdCardDesensitize;
import com.iailab.framework.desensitize.core.slider.annotation.PasswordDesensitize;
import com.iailab.framework.desensitize.core.slider.annotation.MobileDesensitize;
import com.iailab.framework.desensitize.core.slider.annotation.SliderDesensitize;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
/**
 * {@link DesensitizeTest} 的单元测试
 */
@ExtendWith(MockitoExtension.class)
public class DesensitizeTest {
    @Test
    public void test() {
        // 准备参数
        DesensitizeDemo desensitizeDemo = new DesensitizeDemo();
        desensitizeDemo.setNickname("iailab");
        desensitizeDemo.setBankCard("9988002866797031");
        desensitizeDemo.setCarLicense("粤A66666");
        desensitizeDemo.setFixedPhone("01086551122");
        desensitizeDemo.setIdCard("530321199204074611");
        desensitizeDemo.setPassword("123456");
        desensitizeDemo.setPhoneNumber("13248765917");
        desensitizeDemo.setSlider1("ABCDEFG");
        desensitizeDemo.setSlider2("ABCDEFG");
        desensitizeDemo.setSlider3("ABCDEFG");
        desensitizeDemo.setEmail("1@email.com");
        desensitizeDemo.setRegex("你好,我是iailab");
        desensitizeDemo.setAddress("北京市海淀区上地十街10号");
        desensitizeDemo.setOrigin("iailab");
        // 调用
        DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class);
        // 断言
        assertNotNull(d);
        assertEquals("芋***", d.getNickname());
        assertEquals("998800********31", d.getBankCard());
        assertEquals("粤A6***6", d.getCarLicense());
        assertEquals("0108*****22", d.getFixedPhone());
        assertEquals("530321**********11", d.getIdCard());
        assertEquals("******", d.getPassword());
        assertEquals("132****5917", d.getPhoneNumber());
        assertEquals("#######", d.getSlider1());
        assertEquals("ABC*EFG", d.getSlider2());
        assertEquals("*******", d.getSlider3());
        assertEquals("1****@email.com", d.getEmail());
        assertEquals("你好,我是*", d.getRegex());
        assertEquals("北京市海淀区上地十街10号*", d.getAddress());
        assertEquals("iailab", d.getOrigin());
    }
    @Data
    public static class DesensitizeDemo {
        @ChineseNameDesensitize
        private String nickname;
        @BankCardDesensitize
        private String bankCard;
        @CarLicenseDesensitize
        private String carLicense;
        @FixedPhoneDesensitize
        private String fixedPhone;
        @IdCardDesensitize
        private String idCard;
        @PasswordDesensitize
        private String password;
        @MobileDesensitize
        private String phoneNumber;
        @SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#")
        private String slider1;
        @SliderDesensitize(prefixKeep = 3, suffixKeep = 3)
        private String slider2;
        @SliderDesensitize(prefixKeep = 10)
        private String slider3;
        @EmailDesensitize
        private String email;
        @RegexDesensitize(regex = "iailab", replacer = "*")
        private String regex;
        @Address
        private String address;
        private String origin;
    }
}
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java
对比新文件
@@ -0,0 +1,30 @@
package com.iailab.framework.desensitize.core.annotation;
import com.iailab.framework.desensitize.core.DesensitizeTest;
import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.iailab.framework.desensitize.core.handler.AddressHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 地址
 *
 * 用于 {@link DesensitizeTest} 测试使用
 *
 * @author gaibu
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@DesensitizeBy(handler = AddressHandler.class)
public @interface Address {
    String replacer() default "*";
}
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java
对比新文件
@@ -0,0 +1,19 @@
package com.iailab.framework.desensitize.core.handler;
import com.iailab.framework.desensitize.core.DesensitizeTest;
import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler;
import com.iailab.framework.desensitize.core.annotation.Address;
/**
 * {@link Address} 的脱敏处理器
 *
 * 用于 {@link DesensitizeTest} 测试使用
 */
public class AddressHandler implements DesensitizationHandler<Address> {
    @Override
    public String desensitize(String origin, Address annotation) {
        return origin + annotation.replacer();
    }
}
iailab-framework/iailab-common-websocket/pom.xml
对比新文件
@@ -0,0 +1,73 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common-websocket</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>WebSocket 框架,支持多节点的广播</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
                 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
                 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
            -->
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-security</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!-- 消息队列相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-mq</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework.kafka</groupId>-->
<!--            <artifactId>spring-kafka</artifactId>-->
<!--            <optional>true</optional>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>org.springframework.amqp</groupId>-->
<!--            <artifactId>spring-rabbit</artifactId>-->
<!--            <optional>true</optional>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>org.apache.rocketmq</groupId>-->
<!--            <artifactId>rocketmq-spring-boot-starter</artifactId>-->
<!--            <optional>true</optional>-->
<!--        </dependency>-->
        <!-- 业务组件 -->
        <dependency>
            <!-- 为什么要依赖 tenant 组件?
                因为广播某个类型的用户时候,需要根据租户过滤下,避免广播到别的租户!
            -->
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-biz-tenant</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java
对比新文件
@@ -0,0 +1,177 @@
package com.iailab.framework.websocket.config;
import com.iailab.framework.mq.redis.config.IailabRedisMQConsumerAutoConfiguration;
import com.iailab.framework.mq.redis.core.RedisMQTemplate;
import com.iailab.framework.websocket.core.handler.JsonWebSocketMessageHandler;
import com.iailab.framework.websocket.core.listener.WebSocketMessageListener;
import com.iailab.framework.websocket.core.security.LoginUserHandshakeInterceptor;
import com.iailab.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
import com.iailab.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer;
import com.iailab.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer;
import com.iailab.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
import com.iailab.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
import com.iailab.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
import com.iailab.framework.websocket.core.session.WebSocketSessionManagerImpl;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.List;
/**
 * WebSocket 自动配置
 *
 * @author xingyu4j
 */
@AutoConfiguration(before = IailabRedisMQConsumerAutoConfiguration.class) // before IailabRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer
@EnableWebSocket // 开启 websocket
@ConditionalOnProperty(prefix = "iailab.websocket", value = "enable", matchIfMissing = true) // 允许使用 iailab.websocket.enable=false 禁用 websocket
@EnableConfigurationProperties(WebSocketProperties.class)
public class IailabWebSocketAutoConfiguration {
    @Bean
    public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
                                                   WebSocketHandler webSocketHandler,
                                                   WebSocketProperties webSocketProperties) {
        return registry -> registry
                // 添加 WebSocketHandler
                .addHandler(webSocketHandler, webSocketProperties.getPath())
                .addInterceptors(handshakeInterceptors)
                // 允许跨域,否则前端连接会直接断开
                .setAllowedOriginPatterns("*");
    }
    @Bean
    public HandshakeInterceptor handshakeInterceptor() {
        return new LoginUserHandshakeInterceptor();
    }
    @Bean
    public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
                                             List<? extends WebSocketMessageListener<?>> messageListeners) {
        // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
        JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
        // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
        return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
    }
    @Bean
    public WebSocketSessionManager webSocketSessionManager() {
        return new WebSocketSessionManagerImpl();
    }
    // ==================== Sender 相关 ====================
    @Configuration
    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
    public class LocalWebSocketMessageSenderConfiguration {
        @Bean
        public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
            return new LocalWebSocketMessageSender(sessionManager);
        }
    }
    @Configuration
    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
    public class RedisWebSocketMessageSenderConfiguration {
        @Bean
        public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
                                                                       RedisMQTemplate redisMQTemplate) {
            return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
        }
        @Bean
        public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
                RedisWebSocketMessageSender redisWebSocketMessageSender) {
            return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
        }
    }
    @Configuration
    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
    public class RocketMQWebSocketMessageSenderConfiguration {
        @Bean
        public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
                WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
                @Value("${iailab.websocket.sender-rocketmq.topic}") String topic) {
            return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
        }
        @Bean
        public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
                RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
            return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
        }
    }
    @Configuration
    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
    public class RabbitMQWebSocketMessageSenderConfiguration {
        @Bean
        public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
                WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
                TopicExchange websocketTopicExchange) {
            return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
        }
        @Bean
        public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
                RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
            return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
        }
        /**
         * 创建 Topic Exchange
         */
        @Bean
        public TopicExchange websocketTopicExchange(@Value("${iailab.websocket.sender-rabbitmq.exchange}") String exchange) {
            return new TopicExchange(exchange,
                    true,  // durable: 是否持久化
                    false);  // exclusive: 是否排它
        }
    }
    @Configuration
    @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
    public class KafkaWebSocketMessageSenderConfiguration {
        @Bean
        public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
                WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate,
                @Value("${iailab.websocket.sender-kafka.topic}") String topic) {
            return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
        }
        @Bean
        public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
                KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
            return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
        }
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java
对比新文件
@@ -0,0 +1,34 @@
package com.iailab.framework.websocket.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
 * WebSocket 配置项
 *
 * @author xingyu4j
 */
@ConfigurationProperties("iailab.websocket")
@Data
@Validated
public class WebSocketProperties {
    /**
     * WebSocket 的连接路径
     */
    @NotEmpty(message = "WebSocket 的连接路径不能为空")
    private String path = "/ws";
    /**
     * 消息发送器的类型
     *
     * 可选值:local、redis、rocketmq、kafka、rabbitmq
     */
    @NotNull(message = "WebSocket 的消息发送者不能为空")
    private String senderType = "local";
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
对比新文件
@@ -0,0 +1,83 @@
package com.iailab.framework.websocket.core.handler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.tenant.core.util.TenantUtils;
import com.iailab.framework.websocket.core.listener.WebSocketMessageListener;
import com.iailab.framework.websocket.core.message.JsonWebSocketMessage;
import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
/**
 * JSON 格式 {@link WebSocketHandler} 实现类
 *
 * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
 *
 * @author iailab
 */
@Slf4j
public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
    /**
     * type 与 WebSocketMessageListener 的映射
     */
    private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
    @SuppressWarnings({"rawtypes", "unchecked"})
    public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
        listenersList.forEach((Consumer<WebSocketMessageListener>)
                listener -> listeners.put(listener.getType(), listener));
    }
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1.1 空消息,跳过
        if (message.getPayloadLength() == 0) {
            return;
        }
        // 1.2 ping 心跳消息,直接返回 pong 消息。
        if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
            session.sendMessage(new TextMessage("pong"));
            return;
        }
        // 2.1 解析消息
        try {
            JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
            if (jsonMessage == null) {
                log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
                return;
            }
            if (StrUtil.isEmpty(jsonMessage.getType())) {
                log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
                return;
            }
            // 2.2 获得对应的 WebSocketMessageListener
            WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
            if (messageListener == null) {
                log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
                return;
            }
            // 2.3 处理消息
            Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
            Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
            Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
            TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
        } catch (Throwable ex) {
            log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
        }
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java
对比新文件
@@ -0,0 +1,31 @@
package com.iailab.framework.websocket.core.listener;
import com.iailab.framework.websocket.core.message.JsonWebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
/**
 * WebSocket 消息监听器接口
 *
 * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
 *
 * @param <T> 泛型,消息类型
 */
public interface WebSocketMessageListener<T> {
    /**
     * 处理消息
     *
     * @param session Session
     * @param message 消息
     */
    void onMessage(WebSocketSession session, T message);
    /**
     * 获得消息类型
     *
     * @see JsonWebSocketMessage#getType()
     * @return 消息类型
     */
    String getType();
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java
对比新文件
@@ -0,0 +1,29 @@
package com.iailab.framework.websocket.core.message;
import com.iailab.framework.websocket.core.listener.WebSocketMessageListener;
import lombok.Data;
import java.io.Serializable;
/**
 * JSON 格式的 WebSocket 消息帧
 *
 * @author iailab
 */
@Data
public class JsonWebSocketMessage implements Serializable {
    /**
     * 消息类型
     *
     * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
     */
    private String type;
    /**
     * 消息内容
     *
     * 要求 JSON 对象
     */
    private String content;
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
对比新文件
@@ -0,0 +1,42 @@
package com.iailab.framework.websocket.core.security;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.security.core.filter.TokenAuthenticationFilter;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
 * 登录用户的 {@link HandshakeInterceptor} 实现类
 *
 * 流程如下:
 * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
 * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
 *
 * @author iailab
 */
public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) {
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser != null) {
            WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
        }
        return true;
    }
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler wsHandler, Exception exception) {
        // do nothing
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
对比新文件
@@ -0,0 +1,24 @@
package com.iailab.framework.websocket.core.security;
import com.iailab.framework.security.config.AuthorizeRequestsCustomizer;
import com.iailab.framework.websocket.config.WebSocketProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
/**
 * WebSocket 的权限自定义
 *
 * @author iailab
 */
@RequiredArgsConstructor
public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
    private final WebSocketProperties webSocketProperties;
    @Override
    public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
        registry.requestMatchers(webSocketProperties.getPath()).permitAll();
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
对比新文件
@@ -0,0 +1,104 @@
package com.iailab.framework.websocket.core.sender;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.websocket.core.message.JsonWebSocketMessage;
import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
 * WebSocketMessageSender 实现类
 *
 * @author iailab
 */
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender {
    private final WebSocketSessionManager sessionManager;
    @Override
    public void send(Integer userType, Long userId, String messageType, String messageContent) {
        send(null, userType, userId, messageType, messageContent);
    }
    @Override
    public void send(Integer userType, String messageType, String messageContent) {
        send(null, userType, null, messageType, messageContent);
    }
    @Override
    public void send(String sessionId, String messageType, String messageContent) {
        send(sessionId, null, null, messageType, messageContent);
    }
    /**
     * 发送消息
     *
     * @param sessionId Session 编号
     * @param userType 用户类型
     * @param userId 用户编号
     * @param messageType 消息类型
     * @param messageContent 消息内容
     */
    public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) {
        // 1. 获得 Session 列表
        List<WebSocketSession> sessions = Collections.emptyList();
        if (StrUtil.isNotEmpty(sessionId)) {
            WebSocketSession session = sessionManager.getSession(sessionId);
            if (session != null) {
                sessions = Collections.singletonList(session);
            }
        } else if (userType != null && userId != null) {
            sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId);
        } else if (userType != null) {
            sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
        }
        if (CollUtil.isEmpty(sessions)) {
            log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
                    sessionId, userType, userId, messageType, messageContent);
        }
        // 2. 执行发送
        doSend(sessions, messageType, messageContent);
    }
    /**
     * 发送消息的具体实现
     *
     * @param sessions Session 列表
     * @param messageType 消息类型
     * @param messageContent 消息内容
     */
    public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) {
        JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
        String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化
        sessions.forEach(session -> {
            // 1. 各种校验,保证 Session 可以被发送
            if (session == null) {
                log.error("[doSend][session 为空, message({})]", message);
                return;
            }
            if (!session.isOpen()) {
                log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message);
                return;
            }
            // 2. 执行发送
            try {
                session.sendMessage(new TextMessage(payload));
                log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message);
            } catch (IOException ex) {
                log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex);
            }
        });
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java
对比新文件
@@ -0,0 +1,52 @@
package com.iailab.framework.websocket.core.sender;
import com.iailab.framework.common.util.json.JsonUtils;
/**
 * WebSocket 消息的发送器接口
 *
 * @author iailab
 */
public interface WebSocketMessageSender {
    /**
     * 发送消息给指定用户
     *
     * @param userType 用户类型
     * @param userId 用户编号
     * @param messageType 消息类型
     * @param messageContent 消息内容,JSON 格式
     */
    void send(Integer userType, Long userId, String messageType, String messageContent);
    /**
     * 发送消息给指定用户类型
     *
     * @param userType 用户类型
     * @param messageType 消息类型
     * @param messageContent 消息内容,JSON 格式
     */
    void send(Integer userType, String messageType, String messageContent);
    /**
     * 发送消息给指定 Session
     *
     * @param sessionId Session 编号
     * @param messageType 消息类型
     * @param messageContent 消息内容,JSON 格式
     */
    void send(String sessionId, String messageType, String messageContent);
    default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
        send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
    }
    default void sendObject(Integer userType, String messageType, Object messageContent) {
        send(userType, messageType, JsonUtils.toJsonString(messageContent));
    }
    default void sendObject(String sessionId, String messageType, Object messageContent) {
        send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.websocket.core.sender.kafka;
import lombok.Data;
/**
 * Kafka 广播 WebSocket 的消息
 *
 * @author iailab
 */
@Data
public class KafkaWebSocketMessage {
    /**
     * Session 编号
     */
    private String sessionId;
    /**
     * 用户类型
     */
    private Integer userType;
    /**
     * 用户编号
     */
    private Long userId;
    /**
     * 消息类型
     */
    private String messageType;
    /**
     * 消息内容
     */
    private String messageContent;
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.websocket.core.sender.kafka;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.kafka.annotation.KafkaListener;
/**
 * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去
 *
 * @author iailab
 */
@RequiredArgsConstructor
public class KafkaWebSocketMessageConsumer {
    private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender;
    @RabbitHandler
    @KafkaListener(
            topics = "${iailab.websocket.sender-kafka.topic}",
            // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的
            groupId = "${iailab.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}")
    public void onMessage(KafkaWebSocketMessage message) {
        rabbitMQWebSocketMessageSender.send(message.getSessionId(),
                message.getUserType(), message.getUserId(),
                message.getMessageType(), message.getMessageContent());
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
对比新文件
@@ -0,0 +1,67 @@
package com.iailab.framework.websocket.core.sender.kafka;
import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import java.util.concurrent.ExecutionException;
/**
 * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类
 *
 * @author iailab
 */
@Slf4j
public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender {
    private final KafkaTemplate<Object, Object> kafkaTemplate;
    private final String topic;
    public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager,
                                       KafkaTemplate<Object, Object> kafkaTemplate,
                                       String topic) {
        super(sessionManager);
        this.kafkaTemplate = kafkaTemplate;
        this.topic = topic;
    }
    @Override
    public void send(Integer userType, Long userId, String messageType, String messageContent) {
        sendKafkaMessage(null, userId, userType, messageType, messageContent);
    }
    @Override
    public void send(Integer userType, String messageType, String messageContent) {
        sendKafkaMessage(null, null, userType, messageType, messageContent);
    }
    @Override
    public void send(String sessionId, String messageType, String messageContent) {
        sendKafkaMessage(sessionId, null, null, messageType, messageContent);
    }
    /**
     * 通过 Kafka 广播消息
     *
     * @param sessionId Session 编号
     * @param userId 用户编号
     * @param userType 用户类型
     * @param messageType 消息类型
     * @param messageContent 消息内容
     */
    private void sendKafkaMessage(String sessionId, Long userId, Integer userType,
                                  String messageType, String messageContent) {
        KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage()
                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
                .setMessageType(messageType).setMessageContent(messageContent);
        try {
            kafkaTemplate.send(topic, mqMessage).get();
        } catch (InterruptedException | ExecutionException e) {
            log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e);
        }
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
对比新文件
@@ -0,0 +1,20 @@
package com.iailab.framework.websocket.core.sender.local;
import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
/**
 * 本地的 {@link WebSocketMessageSender} 实现类
 *
 * 注意:仅仅适合单机场景!!!
 *
 * @author iailab
 */
public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender {
    public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) {
        super(sessionManager);
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.websocket.core.sender.rabbitmq;
import lombok.Data;
import java.io.Serializable;
/**
 * RabbitMQ 广播 WebSocket 的消息
 *
 * @author iailab
 */
@Data
public class RabbitMQWebSocketMessage implements Serializable {
    /**
     * Session 编号
     */
    private String sessionId;
    /**
     * 用户类型
     */
    private Integer userType;
    /**
     * 用户编号
     */
    private Long userId;
    /**
     * 消息类型
     */
    private String messageType;
    /**
     * 消息内容
     */
    private String messageContent;
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
对比新文件
@@ -0,0 +1,39 @@
package com.iailab.framework.websocket.core.sender.rabbitmq;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
/**
 * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
 *
 * @author iailab
 */
@RabbitListener(
        bindings = @QueueBinding(
                value = @Queue(
                        // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的
                        name = "${iailab.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}",
                        // Consumer 关闭时,该队列就可以被自动删除了
                        autoDelete = "true"
                ),
                exchange = @Exchange(
                        name = "${iailab.websocket.sender-rabbitmq.exchange}",
                        type = ExchangeTypes.TOPIC,
                        declare = "false"
                )
        )
)
@RequiredArgsConstructor
public class RabbitMQWebSocketMessageConsumer {
    private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender;
    @RabbitHandler
    public void onMessage(RabbitMQWebSocketMessage message) {
        rabbitMQWebSocketMessageSender.send(message.getSessionId(),
                message.getUserType(), message.getUserId(),
                message.getMessageType(), message.getMessageContent());
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
对比新文件
@@ -0,0 +1,62 @@
package com.iailab.framework.websocket.core.sender.rabbitmq;
import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
 * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类
 *
 * @author iailab
 */
@Slf4j
public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
    private final RabbitTemplate rabbitTemplate;
    private final TopicExchange topicExchange;
    public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
                                          RabbitTemplate rabbitTemplate,
                                          TopicExchange topicExchange) {
        super(sessionManager);
        this.rabbitTemplate = rabbitTemplate;
        this.topicExchange = topicExchange;
    }
    @Override
    public void send(Integer userType, Long userId, String messageType, String messageContent) {
        sendRabbitMQMessage(null, userId, userType, messageType, messageContent);
    }
    @Override
    public void send(Integer userType, String messageType, String messageContent) {
        sendRabbitMQMessage(null, null, userType, messageType, messageContent);
    }
    @Override
    public void send(String sessionId, String messageType, String messageContent) {
        sendRabbitMQMessage(sessionId, null, null, messageType, messageContent);
    }
    /**
     * 通过 RabbitMQ 广播消息
     *
     * @param sessionId Session 编号
     * @param userId 用户编号
     * @param userType 用户类型
     * @param messageType 消息类型
     * @param messageContent 消息内容
     */
    private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType,
                                     String messageType, String messageContent) {
        RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage()
                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
                .setMessageType(messageType).setMessageContent(messageContent);
        rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage);
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
对比新文件
@@ -0,0 +1,34 @@
package com.iailab.framework.websocket.core.sender.redis;
import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
import lombok.Data;
/**
 * Redis 广播 WebSocket 的消息
 */
@Data
public class RedisWebSocketMessage extends AbstractRedisChannelMessage {
    /**
     * Session 编号
     */
    private String sessionId;
    /**
     * 用户类型
     */
    private Integer userType;
    /**
     * 用户编号
     */
    private Long userId;
    /**
     * 消息类型
     */
    private String messageType;
    /**
     * 消息内容
     */
    private String messageContent;
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
对比新文件
@@ -0,0 +1,23 @@
package com.iailab.framework.websocket.core.sender.redis;
import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
import lombok.RequiredArgsConstructor;
/**
 * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去
 *
 * @author iailab
 */
@RequiredArgsConstructor
public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener<RedisWebSocketMessage> {
    private final RedisWebSocketMessageSender redisWebSocketMessageSender;
    @Override
    public void onMessage(RedisWebSocketMessage message) {
        redisWebSocketMessageSender.send(message.getSessionId(),
                message.getUserType(), message.getUserId(),
                message.getMessageType(), message.getMessageContent());
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
对比新文件
@@ -0,0 +1,57 @@
package com.iailab.framework.websocket.core.sender.redis;
import com.iailab.framework.mq.redis.core.RedisMQTemplate;
import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
/**
 * 基于 Redis 的 {@link WebSocketMessageSender} 实现类
 *
 * @author iailab
 */
@Slf4j
public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender {
    private final RedisMQTemplate redisMQTemplate;
    public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager,
                                       RedisMQTemplate redisMQTemplate) {
        super(sessionManager);
        this.redisMQTemplate = redisMQTemplate;
    }
    @Override
    public void send(Integer userType, Long userId, String messageType, String messageContent) {
        sendRedisMessage(null, userId, userType, messageType, messageContent);
    }
    @Override
    public void send(Integer userType, String messageType, String messageContent) {
        sendRedisMessage(null, null, userType, messageType, messageContent);
    }
    @Override
    public void send(String sessionId, String messageType, String messageContent) {
        sendRedisMessage(sessionId, null, null, messageType, messageContent);
    }
    /**
     * 通过 Redis 广播消息
     *
     * @param sessionId Session 编号
     * @param userId 用户编号
     * @param userType 用户类型
     * @param messageType 消息类型
     * @param messageContent 消息内容
     */
    private void sendRedisMessage(String sessionId, Long userId, Integer userType,
                                  String messageType, String messageContent) {
        RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
                .setMessageType(messageType).setMessageContent(messageContent);
        redisMQTemplate.send(mqMessage);
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.websocket.core.sender.rocketmq;
import lombok.Data;
/**
 * RocketMQ 广播 WebSocket 的消息
 *
 * @author iailab
 */
@Data
public class RocketMQWebSocketMessage {
    /**
     * Session 编号
     */
    private String sessionId;
    /**
     * 用户类型
     */
    private Integer userType;
    /**
     * 用户编号
     */
    private Long userId;
    /**
     * 消息类型
     */
    private String messageType;
    /**
     * 消息内容
     */
    private String messageContent;
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
对比新文件
@@ -0,0 +1,30 @@
package com.iailab.framework.websocket.core.sender.rocketmq;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
/**
 * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
 *
 * @author iailab
 */
@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic
        topic = "${iailab.websocket.sender-rocketmq.topic}",
        consumerGroup = "${iailab.websocket.sender-rocketmq.consumer-group}",
        messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息
)
@RequiredArgsConstructor
public class RocketMQWebSocketMessageConsumer implements RocketMQListener<RocketMQWebSocketMessage> {
    private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender;
    @Override
    public void onMessage(RocketMQWebSocketMessage message) {
        rocketMQWebSocketMessageSender.send(message.getSessionId(),
                message.getUserType(), message.getUserId(),
                message.getMessageType(), message.getMessageContent());
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
对比新文件
@@ -0,0 +1,61 @@
package com.iailab.framework.websocket.core.sender.rocketmq;
import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender;
import com.iailab.framework.websocket.core.sender.WebSocketMessageSender;
import com.iailab.framework.websocket.core.session.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
/**
 * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类
 *
 * @author iailab
 */
@Slf4j
public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
    private final RocketMQTemplate rocketMQTemplate;
    private final String topic;
    public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
                                          RocketMQTemplate rocketMQTemplate,
                                          String topic) {
        super(sessionManager);
        this.rocketMQTemplate = rocketMQTemplate;
        this.topic = topic;
    }
    @Override
    public void send(Integer userType, Long userId, String messageType, String messageContent) {
        sendRocketMQMessage(null, userId, userType, messageType, messageContent);
    }
    @Override
    public void send(Integer userType, String messageType, String messageContent) {
        sendRocketMQMessage(null, null, userType, messageType, messageContent);
    }
    @Override
    public void send(String sessionId, String messageType, String messageContent) {
        sendRocketMQMessage(sessionId, null, null, messageType, messageContent);
    }
    /**
     * 通过 RocketMQ 广播消息
     *
     * @param sessionId Session 编号
     * @param userId 用户编号
     * @param userType 用户类型
     * @param messageType 消息类型
     * @param messageContent 消息内容
     */
    private void sendRocketMQMessage(String sessionId, Long userId, Integer userType,
                                     String messageType, String messageContent) {
        RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage()
                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
                .setMessageType(messageType).setMessageContent(messageContent);
        rocketMQTemplate.syncSend(topic, mqMessage);
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
对比新文件
@@ -0,0 +1,49 @@
package com.iailab.framework.websocket.core.session;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
/**
 * {@link WebSocketHandler} 的装饰类,实现了以下功能:
 *
 * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
 * 2. 封装 {@link WebSocketSession} 支持并发操作
 *
 * @author iailab
 */
public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {
    /**
     * 发送时间的限制,单位:毫秒
     */
    private static final Integer SEND_TIME_LIMIT = 1000 * 5;
    /**
     * 发送消息缓冲上线,单位:bytes
     */
    private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;
    private final WebSocketSessionManager sessionManager;
    public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
                                            WebSocketSessionManager sessionManager) {
        super(delegate);
        this.sessionManager = sessionManager;
    }
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
        session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
        // 添加到 WebSocketSessionManager 中
        sessionManager.addSession(session);
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
        sessionManager.removeSession(session);
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java
对比新文件
@@ -0,0 +1,53 @@
package com.iailab.framework.websocket.core.session;
import org.springframework.web.socket.WebSocketSession;
import java.util.Collection;
/**
 * {@link WebSocketSession} 管理器的接口
 *
 * @author iailab
 */
public interface WebSocketSessionManager {
    /**
     * 添加 Session
     *
     * @param session Session
     */
    void addSession(WebSocketSession session);
    /**
     * 移除 Session
     *
     * @param session Session
     */
    void removeSession(WebSocketSession session);
    /**
     * 获得指定编号的 Session
     *
     * @param id Session 编号
     * @return Session
     */
    WebSocketSession getSession(String id);
    /**
     * 获得指定用户类型的 Session 列表
     *
     * @param userType 用户类型
     * @return Session 列表
     */
    Collection<WebSocketSession> getSessionList(Integer userType);
    /**
     * 获得指定用户编号的 Session 列表
     *
     * @param userType 用户类型
     * @param userId 用户编号
     * @return Session 列表
     */
    Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java
对比新文件
@@ -0,0 +1,125 @@
package com.iailab.framework.websocket.core.session;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
 * 默认的 {@link WebSocketSessionManager} 实现类
 *
 * @author iailab
 */
public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
    /**
     * id 与 WebSocketSession 映射
     *
     * key:Session 编号
     */
    private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
    /**
     * user 与 WebSocketSession 映射
     *
     * key1:用户类型
     * key2:用户编号
     */
    private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions
            = new ConcurrentHashMap<>();
    @Override
    public void addSession(WebSocketSession session) {
        // 添加到 idSessions 中
        idSessions.put(session.getId(), session);
        // 添加到 userSessions 中
        LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
        if (user == null) {
            return;
        }
        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
        if (userSessionsMap == null) {
            userSessionsMap = new ConcurrentHashMap<>();
            if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
                userSessionsMap = userSessions.get(user.getUserType());
            }
        }
        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
        if (sessions == null) {
            sessions = new CopyOnWriteArrayList<>();
            if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
                sessions = userSessionsMap.get(user.getId());
            }
        }
        sessions.add(session);
    }
    @Override
    public void removeSession(WebSocketSession session) {
        // 移除从 idSessions 中
        idSessions.remove(session.getId());
        // 移除从 idSessions 中
        LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
        if (user == null) {
            return;
        }
        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
        if (userSessionsMap == null) {
            return;
        }
        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
        sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
        if (CollUtil.isEmpty(sessions)) {
            userSessionsMap.remove(user.getId(), sessions);
        }
    }
    @Override
    public WebSocketSession getSession(String id) {
        return idSessions.get(id);
    }
    @Override
    public Collection<WebSocketSession> getSessionList(Integer userType) {
        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
        if (CollUtil.isEmpty(userSessionsMap)) {
            return new ArrayList<>();
        }
        LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
        Long contextTenantId = TenantContextHolder.getTenantId();
        for (List<WebSocketSession> sessions : userSessionsMap.values()) {
            if (CollUtil.isEmpty(sessions)) {
                continue;
            }
            // 特殊:如果租户不匹配,则直接排除
            if (contextTenantId != null) {
                Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
                if (!contextTenantId.equals(userTenantId)) {
                    continue;
                }
            }
            result.addAll(sessions);
        }
        return result;
    }
    @Override
    public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
        if (CollUtil.isEmpty(userSessionsMap)) {
            return new ArrayList<>();
        }
        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);
        return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java
对比新文件
@@ -0,0 +1,67 @@
package com.iailab.framework.websocket.core.util;
import com.iailab.framework.security.core.LoginUser;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
 * 专属于 web 包的工具类
 *
 * @author iailab
 */
public class WebSocketFrameworkUtils {
    public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
    /**
     * 设置当前用户
     *
     * @param loginUser 登录用户
     * @param attributes Session
     */
    public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
        attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
    }
    /**
     * 获取当前用户
     *
     * @return 当前用户
     */
    public static LoginUser getLoginUser(WebSocketSession session) {
        return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
    }
    /**
     * 获得当前用户的编号
     *
     * @return 用户编号
     */
    public static Long getLoginUserId(WebSocketSession session) {
        LoginUser loginUser = getLoginUser(session);
        return loginUser != null ? loginUser.getId() : null;
    }
    /**
     * 获得当前用户的类型
     *
     * @return 用户编号
     */
    public static Integer getLoginUserType(WebSocketSession session) {
        LoginUser loginUser = getLoginUser(session);
        return loginUser != null ? loginUser.getUserType() : null;
    }
    /**
     * 获得当前用户的租户编号
     *
     * @param session Session
     * @return 租户编号
     */
    public static Long getTenantId(WebSocketSession session) {
        LoginUser loginUser = getLoginUser(session);
        return loginUser != null ? loginUser.getTenantId() : null;
    }
}
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * WebSocket 框架,支持多节点的广播
 */
package com.iailab.framework.websocket;
iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1 @@
com.iailab.framework.websocket.config.IailabWebSocketAutoConfiguration
iailab-framework/iailab-common/pom.xml
对比新文件
@@ -0,0 +1,159 @@
<?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">
    <parent>
        <groupId>com.iailab</groupId>
        <artifactId>iailab-framework</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iailab-common</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>定义基础 pojo 类、枚举、工具类等等</description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Web 相关 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- 监控相关 -->
        <dependency>
            <groupId>org.apache.skywalking</groupId>
            <artifactId>apm-toolkit-trace</artifactId>
        </dependency>
        <!-- 工具类相关 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,只有工具类需要使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
<!--            <scope>provided</scope> &lt;!&ndash; 设置为 provided,主要是 PageParam 使用到 &ndash;&gt;-->
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
            <artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java
对比新文件
@@ -0,0 +1,59 @@
package com.fhs.trans.service;
import com.fhs.core.trans.vo.VO;
import java.util.ArrayList;
import java.util.List;
/**
 * 只有实现了这个接口的才能自动翻译
 *
 * 为什么要赋值粘贴到 iailab-common 包下?
 * 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 iailab-module-xxx-api 模块下使用
 *
 * @author jackwang
 * @since  2020-05-19 10:26:15
 */
public interface AutoTransable<V extends VO> {
    /**
     * 根据 ids 查询数据列表
     *
     * 改方法已过期啦,请使用 selectByIds
     *
     * @param ids 编号数组
     * @return 数据列表
     */
    @Deprecated
    default List<V> findByIds(List<? extends Object> ids){
        return new ArrayList<>();
    }
    /**
     * 根据 ids 查询
     *
     * @param ids 编号数组
     * @return 数据列表
     */
    default List<V> selectByIds(List<? extends Object> ids){
        return this.findByIds(ids);
    }
    /**
     * 获取 db 中所有的数据
     *
     * @return db 中所有的数据
     */
    default List<V> select(){
        return new ArrayList<>();
    }
    /**
     * 根据 id 获取 vo
     *
     * @param primaryValue id
     * @return vo
     */
    V selectById(Object primaryValue);
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java
对比新文件
@@ -0,0 +1,16 @@
package com.iailab.framework.common.annotation;
import java.lang.annotation.*;
/**
 * 业务流程
 *
 * @author PanZhibao
 * @Description
 * @createTime 2022年12月20日 16:10:00
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoBpm {
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java
对比新文件
@@ -0,0 +1,20 @@
package com.iailab.framework.common.annotation;
import java.lang.annotation.*;
/**
 * @author PanZhibao
 * @Description
 * @createTime 2022年05月21日 10:59:00
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoDict {
    /**
     * 暂时无用
     * @return
     */
    String value() default "";
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java
对比新文件
@@ -0,0 +1,20 @@
package com.iailab.framework.common.annotation;
import java.lang.annotation.*;
/**
 * @author PanZhibao
 * @Description
 * @createTime 2023年06月07日 11:35:00
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoUser {
    /**
     * 暂时无用
     * @return
     */
    String value() default "";
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 业务流程ID
 *
 * @author PanZhibao
 * @Description
 * @createTime 2022年12月20日 16:17:00
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BpmProcess {
    /**
     * 业务ID
     *
     * @return
     */
    String businessKey();
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java
对比新文件
@@ -0,0 +1,25 @@
package com.iailab.framework.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 业务流程状态
 *
 * @author PanZhibao
 * @Description
 * @createTime 2022年12月20日 16:14:00
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BpmStatus {
    /**
     * 业务ID
     *
     * @return
     */
    String businessKey();
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java
对比新文件
@@ -0,0 +1,32 @@
package com.iailab.framework.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 字典注解
 *
 * @author PanZhibao
 * @Description
 * @createTime 2022年05月20日 17:36:00
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {
    /**
     * 数据code
     *
     * @return
     */
    String dicCode();
    /**
     * 数据itemValue
     *
     * @return
     */
    String itemValue();
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java
对比新文件
@@ -0,0 +1,30 @@
package com.iailab.framework.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * @author PanZhibao
 * @Description
 * @createTime 2023年06月07日 11:37:00
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserRealName {
    /**
     * 用户ID
     *
     * @return
     */
    String userid() default "";
    /**
     * 用户账号
     *
     * @return
     */
    String username() default "";
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java
对比新文件
@@ -0,0 +1,108 @@
package com.iailab.framework.common.constant;
/**
 * @author: huangxutao
 * @date: 2019-06-14
 * @description: 缓存常量
 */
public interface CacheConstant {
    /**
     * 字典信息缓存(含禁用的字典项)
     */
    public static final String SYS_DICT_CACHE = "sys:cache:dict";
    /**
     * 字典信息缓存 status为有效的
     */
    public static final String SYS_ENABLE_DICT_CACHE = "sys:cache:dictEnable";
    /**
     * 表字典信息缓存
     */
    public static final String SYS_DICT_TABLE_CACHE = "sys:cache:dictTable";
    public static final String SYS_DICT_TABLE_BY_KEYS_CACHE = SYS_DICT_TABLE_CACHE + "ByKeys";
    /**
     * 数据权限配置缓存
     */
    public static final String SYS_DATA_PERMISSIONS_CACHE = "sys:cache:permission:datarules";
    /**
     * 缓存用户信息
     */
    public static final String SYS_USERS_CACHE = "sys:cache:user";
    /**
     * 全部部门信息缓存
     */
    public static final String SYS_DEPARTS_CACHE = "sys:cache:depart:alldata";
    /**
     * 全部部门ids缓存
     */
    public static final String SYS_DEPART_IDS_CACHE = "sys:cache:depart:allids";
    /**
     * 测试缓存key
     */
    public static final String TEST_DEMO_CACHE = "test:demo";
    /**
     * 字典信息缓存
     */
    public static final String SYS_DYNAMICDB_CACHE = "sys:cache:dbconnect:dynamic:";
    /**
     * gateway路由缓存
     */
    public static final String GATEWAY_ROUTES = "sys:cache:cloud:gateway_routes";
    /**
     * gatewayAPI缓存
     */
    public static final String GATEWAY_APIS = "sys:cache:cloud:gateway_apis";
    /**
     * gateway路由 reload key
     */
    public static final String ROUTE_JVM_RELOAD_TOPIC = "gateway_jvm_route_reload_topic";
    /**
     * TODO 冗余代码 待删除
     *插件商城排行榜
     */
    public static final String PLUGIN_MALL_RANKING = "pluginMall::rankingList";
    /**
     * TODO 冗余代码 待删除
     *插件商城排行榜
     */
    public static final String PLUGIN_MALL_PAGE_LIST = "pluginMall::queryPageList";
    /**
     * online列表页配置信息缓存key
     */
    public static final String ONLINE_LIST = "sys:cache:online:list";
    /**
     * online表单页配置信息缓存key
     */
    public static final String ONLINE_FORM = "sys:cache:online:form";
    /**
     * online报表
     */
    public static final String ONLINE_RP = "sys:cache:online:rp";
    /**
     * online图表
     */
    public static final String ONLINE_GRAPH = "sys:cache:online:graph";
    /**
     * 拖拽页面信息缓存
     */
    public static final String DRAG_PAGE_CACHE = "drag:cache:param";
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java
对比新文件
@@ -0,0 +1,406 @@
package com.iailab.framework.common.constant;
import java.math.BigDecimal;
/**
 * @Description: 通用常量
 */
public interface CommonConstant {
    BigDecimal BAD_VALUE = new BigDecimal("-2");
    BigDecimal ZERO_VALUE = new BigDecimal("0");
    /**
     * 正常状态
     */
    public static final Integer STATUS_NORMAL = 0;
    /**
     * 禁用状态
     */
    public static final Integer STATUS_DISABLE = -1;
    /**
     * 删除标志
     */
    public static final Integer DEL_FLAG_1 = 1;
    /**
     * 未删除
     */
    public static final Integer DEL_FLAG_0 = 0;
    /**
     * 未提交
     */
    public static final Integer SUBMINT_STATUS_0 = 0;
    /**
     * 系统日志类型: 登录
     */
    public static final int LOG_TYPE_1 = 1;
    /**
     * 系统日志类型: 操作
     */
    public static final int LOG_TYPE_2 = 2;
    /**
     * 操作日志类型: 查询
     */
    public static final int OPERATE_TYPE_1 = 1;
    /**
     * 操作日志类型: 添加
     */
    public static final int OPERATE_TYPE_2 = 2;
    /**
     * 操作日志类型: 更新
     */
    public static final int OPERATE_TYPE_3 = 3;
    /**
     * 操作日志类型: 删除
     */
    public static final int OPERATE_TYPE_4 = 4;
    /**
     * 操作日志类型: 倒入
     */
    public static final int OPERATE_TYPE_5 = 5;
    /**
     * 操作日志类型: 导出
     */
    public static final int OPERATE_TYPE_6 = 6;
    /**
     * 提交
     */
    public static final int SUBMIT_FLAG_1 = 1;
    /**
     * 提交
     */
    public static final int SUBMIT_FLAG_0 = 0;
    /**
     * 启用
     */
    public static final int IS_ENABLE = 1;
    /**
     * 常量点类型
     */
    public static final String POINT_TYPE_NAME_CONSTANT = "CONSTANT";
    /** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */
    public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500;
    /** {@code 200 OK} (HTTP/1.0 - RFC 1945) */
    public static final Integer SC_OK_200 = 200;
    /**访问权限认证未通过 510*/
    public static final Integer SC_JEECG_NO_AUTHZ=510;
    /** 登录用户Shiro权限缓存KEY前缀 */
    public static String PREFIX_USER_SHIRO_CACHE  = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
    /** 登录用户Token令牌缓存KEY前缀 */
    public static final String PREFIX_USER_TOKEN  = "prefix_user_token_";
//    /** Token缓存时间:3600秒即一小时 */
//    public static final int  TOKEN_EXPIRE_TIME  = 3600;
    /** 登录二维码 */
    public static final String  LOGIN_QRCODE_PRE  = "QRCODELOGIN:";
    public static final String  LOGIN_QRCODE  = "LQ:";
    /** 登录二维码token */
    public static final String  LOGIN_QRCODE_TOKEN  = "LQT:";
    /**
     *  0:一级菜单
     */
    public static final Integer MENU_TYPE_0  = 0;
   /**
    *  1:子菜单
    */
    public static final Integer MENU_TYPE_1  = 1;
    /**
     *  2:按钮权限
     */
    public static final Integer MENU_TYPE_2  = 2;
    /**通告对象类型(USER:指定用户,ALL:全体用户)*/
    public static final String MSG_TYPE_UESR  = "USER";
    public static final String MSG_TYPE_ALL  = "ALL";
    /**发布状态(0未发布,1已发布,2已撤销)*/
    public static final String NO_SEND  = "0";
    public static final String HAS_SEND  = "1";
    public static final String HAS_CANCLE  = "2";
    /**阅读状态(0未读,1已读)*/
    public static final String HAS_READ_FLAG  = "1";
    public static final String NO_READ_FLAG  = "0";
    /**优先级(L低,M中,H高)*/
    public static final String PRIORITY_L  = "L";
    public static final String PRIORITY_M  = "M";
    public static final String PRIORITY_H  = "H";
    /**
     * 短信模板方式  0 .登录模板、1.注册模板、2.忘记密码模板
     */
    public static final String SMS_TPL_TYPE_0  = "0";
    public static final String SMS_TPL_TYPE_1  = "1";
    public static final String SMS_TPL_TYPE_2  = "2";
    /**
     * 状态(0无效1有效)
     */
    public static final String STATUS_0 = "0";
    public static final String STATUS_1 = "1";
    /**
     * 同步工作流引擎1同步0不同步
     */
    public static final Integer ACT_SYNC_1 = 1;
    public static final Integer ACT_SYNC_0 = 0;
    /**
     * 消息类型1:通知公告2:系统消息
     */
    public static final String MSG_CATEGORY_1 = "1";
    public static final String MSG_CATEGORY_2 = "2";
    /**
     * 是否配置菜单的数据权限 1是0否
     */
    public static final Integer RULE_FLAG_0 = 0;
    public static final Integer RULE_FLAG_1 = 1;
    /**
     * 是否用户已被冻结 1正常(解冻) 2冻结
     */
    public static final Integer USER_UNFREEZE = 1;
    public static final Integer USER_FREEZE = 2;
    /**字典翻译文本后缀*/
    public static final String DICT_TEXT_SUFFIX = "_dictText";
    /**
     * 表单设计器主表类型
     */
    public static final Integer DESIGN_FORM_TYPE_MAIN = 1;
    /**
     * 表单设计器子表表类型
     */
    public static final Integer DESIGN_FORM_TYPE_SUB = 2;
    /**
     * 表单设计器URL授权通过
     */
    public static final Integer DESIGN_FORM_URL_STATUS_PASSED = 1;
    /**
     * 表单设计器URL授权未通过
     */
    public static final Integer DESIGN_FORM_URL_STATUS_NOT_PASSED = 2;
    /**
     * 表单设计器新增 Flag
     */
    public static final String DESIGN_FORM_URL_TYPE_ADD = "add";
    /**
     * 表单设计器修改 Flag
     */
    public static final String DESIGN_FORM_URL_TYPE_EDIT = "edit";
    /**
     * 表单设计器详情 Flag
     */
    public static final String DESIGN_FORM_URL_TYPE_DETAIL = "detail";
    /**
     * 表单设计器复用数据 Flag
     */
    public static final String DESIGN_FORM_URL_TYPE_REUSE = "reuse";
    /**
     * 表单设计器编辑 Flag (已弃用)
     */
    public static final String DESIGN_FORM_URL_TYPE_VIEW = "view";
    /**
     * online参数值设置(是:Y, 否:N)
     */
    public static final String ONLINE_PARAM_VAL_IS_TURE = "Y";
    public static final String ONLINE_PARAM_VAL_IS_FALSE = "N";
    /**
     * 文件上传类型(本地:local,Minio:minio,阿里云:alioss)
     */
    public static final String UPLOAD_TYPE_LOCAL = "local";
    public static final String UPLOAD_TYPE_MINIO = "minio";
    public static final String UPLOAD_TYPE_OSS = "alioss";
    /**
     * 文档上传自定义桶名称
     */
    public static final String UPLOAD_CUSTOM_BUCKET = "eoafile";
    /**
     * 文档上传自定义路径
     */
    public static final String UPLOAD_CUSTOM_PATH = "eoafile";
    /**
     * 文件外链接有效天数
     */
    public static final Integer UPLOAD_EFFECTIVE_DAYS = 1;
    /**
     * 员工身份 (1:普通员工  2:上级)
     */
    public static final Integer USER_IDENTITY_1 = 1;
    public static final Integer USER_IDENTITY_2 = 2;
    /** sys_user 表 username 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_SYS_USER_USERNAME = "uniq_sys_user_username";
    /** sys_user 表 work_no 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_SYS_USER_WORK_NO = "uniq_sys_user_work_no";
    /** sys_user 表 phone 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_SYS_USER_PHONE = "uniq_sys_user_phone";
    /** 达梦数据库升提示。违反表[SYS_USER]唯一性约束 */
    public static final String SQL_INDEX_UNIQ_SYS_USER = "唯一性约束";
    /** sys_user 表 email 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_SYS_USER_EMAIL = "uniq_sys_user_email";
    /** sys_quartz_job 表 job_class_name 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_JOB_CLASS_NAME = "uniq_job_class_name";
    /** sys_position 表 code 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_CODE = "uniq_code";
    /** sys_role 表 code 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_SYS_ROLE_CODE = "uniq_sys_role_role_code";
    /** sys_depart 表 code 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_DEPART_ORG_CODE = "uniq_depart_org_code";
    /** sys_category 表 code 唯一键索引 */
    public static final String SQL_INDEX_UNIQ_CATEGORY_CODE = "idx_sc_code";
    /**
     * 在线聊天 是否为默认分组
     */
    public static final String IM_DEFAULT_GROUP = "1";
    /**
     * 在线聊天 图片文件保存路径
     */
    public static final String IM_UPLOAD_CUSTOM_PATH = "imfile";
    /**
     * 在线聊天 用户状态
     */
    public static final String IM_STATUS_ONLINE = "online";
    /**
     * 在线聊天 SOCKET消息类型
     */
    public static final String IM_SOCKET_TYPE = "chatMessage";
    /**
     * 在线聊天 是否开启默认添加好友 1是 0否
     */
    public static final String IM_DEFAULT_ADD_FRIEND = "1";
    /**
     * 在线聊天 用户好友缓存前缀
     */
    public static final String IM_PREFIX_USER_FRIEND_CACHE = "sys:cache:im:im_prefix_user_friend_";
    /**
     * 考勤补卡业务状态 (1:同意  2:不同意)
     */
    public static final String SIGN_PATCH_BIZ_STATUS_1 = "1";
    public static final String SIGN_PATCH_BIZ_STATUS_2 = "2";
    /**
     * 公文文档上传自定义路径
     */
    public static final String UPLOAD_CUSTOM_PATH_OFFICIAL = "officialdoc";
     /**
     * 公文文档下载自定义路径
     */
    public static final String DOWNLOAD_CUSTOM_PATH_OFFICIAL = "officaldown";
    /**
     * WPS存储值类别(1 code文号 2 text(WPS模板还是公文发文模板))
     */
    public static final String WPS_TYPE_1="1";
    public static final String WPS_TYPE_2="2";
    public final static String X_ACCESS_TOKEN = "X-Access-Token";
    public final static String X_SIGN = "X-Sign";
    public final static String X_TIMESTAMP = "X-TIMESTAMP";
    public final static String TOKEN_IS_INVALID_MSG = "Token失效,请重新登录!";
    /**
     * 多租户 请求头
     */
    public final static String TENANT_ID = "tenant-id";
    /**
     * 微服务读取配置文件属性 服务地址
     */
    public final static String CLOUD_SERVER_KEY = "spring.cloud.nacos.discovery.server-addr";
    /**
     * 第三方登录 验证密码/创建用户 都需要设置一个操作码 防止被恶意调用
     */
    public final static String THIRD_LOGIN_CODE = "third_login_code";
    /**
     * 第三方APP同步方向:本地 --> 第三方APP
     */
    String THIRD_SYNC_TO_APP = "SYNC_TO_APP";
    /**
     * 第三方APP同步方向:第三方APP --> 本地
     */
    String THIRD_SYNC_TO_LOCAL = "SYNC_TO_LOCAL";
    /** 系统通告消息状态:0=未发布 */
    String ANNOUNCEMENT_SEND_STATUS_0 = "0";
    /** 系统通告消息状态:1=已发布 */
    String ANNOUNCEMENT_SEND_STATUS_1 = "1";
    /** 系统通告消息状态:2=已撤销 */
    String ANNOUNCEMENT_SEND_STATUS_2 = "2";
    /**ONLINE 报表权限用 从request中获取地址栏后的参数*/
    String ONL_REP_URL_PARAM_STR="onlRepUrlParamStr";
    /**POST请求*/
    String HTTP_POST = "POST";
    /**PUT请求*/
    String HTTP_PUT = "PUT";
    /**PATCH请求*/
    String HTTP_PATCH = "PATCH";
    /**未知的*/
    String UNKNOWN = "unknown";
    /**字符串http*/
    String STR_HTTP = "http";
    /**String 类型的空值*/
    String STRING_NULL = "null";
    /**java.util.Date 包*/
    String JAVA_UTIL_DATE = "java.util.Date";
    /**.do*/
    String SPOT_DO = ".do";
    /**前端vue版本标识*/
    String VERSION="X-Version";
    /**前端vue版本*/
    String VERSION_VUE3="vue3";
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java
对比新文件
@@ -0,0 +1,161 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.constant;
/**
 * 常量
 *
 * @author Mark sunlightcs@gmail.com
 */
public interface Constant {
    /**
     * 成功
     */
    int SUCCESS = 1;
    /**
     * 失败
     */
    int FAIL = 0;
    /**
     * 菜单根节点标识
     */
    Long MENU_ROOT = 0L;
    /**
     * 部门根节点标识
     */
    Long DEPT_ROOT = 0L;
    /**
     *  升序
     */
    String ASC = "asc";
    /**
     * 降序
     */
    String DESC = "desc";
    /**
     * 创建时间字段名
     */
    String CREATE_DATE = "create_date";
    String CREATE_TIME = "create_time";
    /**
     * 数据权限过滤
     */
    String SQL_FILTER = "sqlFilter";
    /**
     * 当前页码
     */
    String PAGE = "page";
    /**
     * 每页显示记录数
     */
    String LIMIT = "limit";
    /**
     * 排序字段
     */
    String ORDER_FIELD = "orderField";
    /**
     * 排序方式
     */
    String ORDER = "order";
    /**
     * token header
     */
    String TOKEN_HEADER = "authorization";
    /**
     * tenantCode
     */
    String TENANT_CODE = "tenantCode";
    /**
     * tenantId
     */
    String TENANT_ID = "tenantId";
    /**
     * tenantId
     */
    String HEAD_TENANT_ID = "tenant-id";
    /**
     * 云存储配置KEY
     */
    String CLOUD_STORAGE_CONFIG_KEY = "CLOUD_STORAGE_CONFIG_KEY";
    Integer DEL_FLAG_0 = 0;
    enum EnableStatus {
        DISABLED(0),
        NORMAL(1);
        private int value;
        private EnableStatus(int value) {
            this.value = value;
        }
        public int getValue() {
            return this.value;
        }
    }
    /**
     * 定时任务状态
     */
    enum ScheduleStatus {
        /**
         * 暂停
         */
        PAUSE(0),
        /**
         * 正常
         */
        NORMAL(1);
        private int value;
        ScheduleStatus(int value) {
            this.value = value;
        }
        public int getValue() {
            return value;
        }
    }
    /**
     * 云服务商
     */
    enum CloudService {
        /**
         * 七牛云
         */
        QINIU(1),
        /**
         * 阿里云
         */
        ALIYUN(2),
        /**
         * 腾讯云
         */
        QCLOUD(3);
        private int value;
        CloudService(int value) {
            this.value = value;
        }
        public int getValue() {
            return value;
        }
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java
对比新文件
@@ -0,0 +1,29 @@
package com.iailab.framework.common.constant;
/**
* @Description: GlobalConstants
* @author: scott
* @date: 2020/01/01 16:01
*/
public class GlobalConstants {
    /**
     * 业务处理器beanName传递参数
     */
    public static final String HANDLER_NAME = "handlerName";
    /**
     * 路由刷新触发器
     */
    public static final String LODER_ROUDER_HANDLER = "loderRouderHandler";
    /**
     * API刷新触发器
     */
    public static final String LODER_API_HANDLER = "loderApiHandler";
    /**
     * redis消息通道名称
     */
    public static final String REDIS_TOPIC_NAME="redis_topic";
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java
对比新文件
@@ -0,0 +1,15 @@
package com.iailab.framework.common.core;
/**
 * 可生成 Int 数组的接口
 *
 * @author iailab
 */
public interface IntArrayValuable {
    /**
     * @return int 数组
     */
    int[] array();
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java
对比新文件
@@ -0,0 +1,22 @@
package com.iailab.framework.common.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
 * Key Value 的键值对
 *
 * @author iailab
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyValue<K, V> implements Serializable {
    private K key;
    private V value;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java
对比新文件
@@ -0,0 +1,20 @@
package com.iailab.framework.common.dto;
import lombok.Data;
import java.util.List;
/**
 * @author PanZhibao
 * @Description
 * @createTime 2024年09月23日
 */
@Data
public class TreeLabelDTO {
    private String value;
    private String label;
    private List<TreeLabelDTO> children;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.common.enums;
import cn.hutool.core.util.ObjUtil;
import com.iailab.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * 通用状态枚举
 *
 * @author iailab
 */
@Getter
@AllArgsConstructor
public enum CommonStatusEnum implements IntArrayValuable {
    ENABLE(0, "开启"),
    DISABLE(1, "关闭");
    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray();
    /**
     * 状态值
     */
    private final Integer status;
    /**
     * 状态名
     */
    private final String name;
    @Override
    public int[] array() {
        return ARRAYS;
    }
    public static boolean isEnable(Integer status) {
        return ObjUtil.equal(ENABLE.status, status);
    }
    public static boolean isDisable(Integer status) {
        return ObjUtil.equal(DISABLE.status, status);
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.common.enums;
import cn.hutool.core.util.ArrayUtil;
import com.iailab.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * 时间间隔的枚举
 *
 * @author dhb52
 */
@Getter
@AllArgsConstructor
public enum DateIntervalEnum implements IntArrayValuable {
    DAY(1, "天"),
    WEEK(2, "周"),
    MONTH(3, "月"),
    QUARTER(4, "季度"),
    YEAR(5, "年")
    ;
    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DateIntervalEnum::getInterval).toArray();
    /**
     * 类型
     */
    private final Integer interval;
    /**
     * 名称
     */
    private final String name;
    @Override
    public int[] array() {
        return ARRAYS;
    }
    public static DateIntervalEnum valueOf(Integer interval) {
        return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values());
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java
对比新文件
@@ -0,0 +1,21 @@
package com.iailab.framework.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * 文档地址
 *
 * @author iailab
 */
@Getter
@AllArgsConstructor
public enum DocumentEnum {
    REDIS_INSTALL("https://iailab.cn", "Redis 安装文档"),
    TENANT("https://iailab.cn", "SaaS 多租户文档");
    private final String url;
    private final String memo;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java
对比新文件
@@ -0,0 +1,46 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.enums;
/**
 * 错误编码,由5位数字组成,前2位为模块编码,后3位为业务编码
 * <p>
 * 如:10001(10代表系统模块,001代表业务代码)
 * </p>
 *
 * @author Mark sunlightcs@gmail.com
 * @since 1.0.0
 */
public interface ErrorCode {
    int INTERNAL_SERVER_ERROR = 500;
    int UNAUTHORIZED = 401;
    int NOT_NULL = 10001;
    int DB_RECORD_EXISTS = 10002;
    int PARAMS_GET_ERROR = 10003;
    int ACCOUNT_PASSWORD_ERROR = 10004;
    int ACCOUNT_DISABLE = 10005;
    int IDENTIFIER_NOT_NULL = 10006;
    int CAPTCHA_ERROR = 10007;
    int SUB_MENU_EXIST = 10008;
    int PASSWORD_ERROR = 10009;
    int SUPERIOR_DEPT_ERROR = 10011;
    int SUPERIOR_MENU_ERROR = 10012;
    int DATA_SCOPE_PARAMS_ERROR = 10013;
    int DEPT_SUB_DELETE_ERROR = 10014;
    int DEPT_USER_DELETE_ERROR = 10015;
    int UPLOAD_FILE_EMPTY = 10019;
    int TOKEN_NOT_EMPTY = 10020;
    int TOKEN_INVALID = 10021;
    int ACCOUNT_LOCK = 10022;
    int OSS_UPLOAD_FILE_ERROR = 10024;
    int REDIS_ERROR = 10027;
    int JOB_ERROR = 10028;
    int INVALID_SYMBOL = 10029;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java
对比新文件
@@ -0,0 +1,17 @@
package com.iailab.framework.common.enums;
/**
 * RPC 相关的枚举
 *
 * 虽然放在 iailab-common-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处
 *
 * @author iailab
 */
public class RpcConstants {
    /**
     * RPC API 的前缀
     */
    public static final String RPC_API_PREFIX = "/rpc-api";
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java
对比新文件
@@ -0,0 +1,40 @@
package com.iailab.framework.common.enums;
import com.iailab.framework.common.core.IntArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * 终端的枚举
 *
 * @author iailab
 */
@RequiredArgsConstructor
@Getter
public enum TerminalEnum implements IntArrayValuable {
    UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
    WECHAT_MINI_PROGRAM(10, "微信小程序"),
    WECHAT_WAP(11, "微信公众号"),
    H5(20, "H5 网页"),
    APP(31, "手机 App"),
    ;
    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray();
    /**
     * 终端
     */
    private final Integer terminal;
    /**
     * 终端名
     */
    private final String name;
    @Override
    public int[] array() {
        return ARRAYS;
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java
对比新文件
@@ -0,0 +1,39 @@
package com.iailab.framework.common.enums;
import cn.hutool.core.util.ArrayUtil;
import com.iailab.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * 全局用户类型枚举
 */
@AllArgsConstructor
@Getter
public enum UserTypeEnum implements IntArrayValuable {
    MEMBER(1, "会员"), // 面向 c 端,普通用户
    ADMIN(2, "管理员"); // 面向 b 端,管理后台
    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray();
    /**
     * 类型
     */
    private final Integer value;
    /**
     * 类型名
     */
    private final String name;
    public static UserTypeEnum valueOf(Integer value) {
        return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
    }
    @Override
    public int[] array() {
        return ARRAYS;
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.common.enums;
/**
 * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
 *
 *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
 *
 * @author iailab
 */
public interface WebFilterOrderEnum {
    int CORS_FILTER = Integer.MIN_VALUE;
    int TRACE_FILTER = CORS_FILTER + 1;
    int ENV_TAG_FILTER = TRACE_FILTER + 1;
    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
    // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
    int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
    int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
    int XSS_FILTER = -102;  // 需要保证在 RequestBodyCacheFilter 后面
    // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
    int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
    int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
    int DEMO_FILTER = Integer.MAX_VALUE;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java
对比新文件
@@ -0,0 +1,32 @@
package com.iailab.framework.common.exception;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.iailab.framework.common.exception.enums.ServiceErrorCodeRange;
import lombok.Data;
/**
 * 错误码对象
 *
 * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
 * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
 *
 * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
 */
@Data
public class ErrorCode {
    /**
     * 错误码
     */
    private final Integer code;
    /**
     * 错误提示
     */
    private final String msg;
    public ErrorCode(Integer code, String message) {
        this.code = code;
        this.msg = message;
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java
对比新文件
@@ -0,0 +1,53 @@
/**
 * Copyright (c) 2018 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */
package com.iailab.framework.common.exception;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
 * Exception工具类
 *
 * @author Mark sunlightcs@gmail.com
 */
public class ExceptionUtils {
    /**
     * 获取异常信息
     * @param ex  异常
     * @return    返回异常信息
     */
    public static String getErrorStackTrace(Exception ex){
        StringWriter sw = null;
        PrintWriter pw = null;
        try {
            sw = new StringWriter();
            pw = new PrintWriter(sw, true);
            ex.printStackTrace(pw);
        }finally {
            try {
                if(pw != null) {
                    pw.close();
                }
            } catch (Exception e) {
            }
            try {
                if(sw != null) {
                    sw.close();
                }
            } catch (IOException e) {
            }
        }
        return sw.toString();
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java
对比新文件
@@ -0,0 +1,60 @@
package com.iailab.framework.common.exception;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 服务器异常 Exception
 */
@Data
@EqualsAndHashCode(callSuper = true)
public final class ServerException extends RuntimeException {
    /**
     * 全局错误码
     *
     * @see GlobalErrorCodeConstants
     */
    private Integer code;
    /**
     * 错误提示
     */
    private String message;
    /**
     * 空构造方法,避免反序列化问题
     */
    public ServerException() {
    }
    public ServerException(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMsg();
    }
    public ServerException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public ServerException setCode(Integer code) {
        this.code = code;
        return this;
    }
    @Override
    public String getMessage() {
        return message;
    }
    public ServerException setMessage(String message) {
        this.message = message;
        return this;
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java
对比新文件
@@ -0,0 +1,60 @@
package com.iailab.framework.common.exception;
import com.iailab.framework.common.exception.enums.ServiceErrorCodeRange;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 业务逻辑异常 Exception
 */
@Data
@EqualsAndHashCode(callSuper = true)
public final class ServiceException extends RuntimeException {
    /**
     * 业务错误码
     *
     * @see ServiceErrorCodeRange
     */
    private Integer code;
    /**
     * 错误提示
     */
    private String message;
    /**
     * 空构造方法,避免反序列化问题
     */
    public ServiceException() {
    }
    public ServiceException(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMsg();
    }
    public ServiceException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public ServiceException setCode(Integer code) {
        this.code = code;
        return this;
    }
    @Override
    public String getMessage() {
        return message;
    }
    public ServiceException setMessage(String message) {
        this.message = message;
        return this;
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java
对比新文件
@@ -0,0 +1,42 @@
package com.iailab.framework.common.exception.enums;
import com.iailab.framework.common.exception.ErrorCode;
/**
 * 全局错误码枚举
 * 0-999 系统异常编码保留
 *
 * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
 * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
 * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
 *
 * @author iailab
 */
public interface GlobalErrorCodeConstants {
    ErrorCode SUCCESS = new ErrorCode(0, "成功");
    // ========== 客户端错误段 ==========
    ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
    ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
    ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
    ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
    ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
    ErrorCode DATA_REPETITION = new ErrorCode(406, "数据库存在重复数据");
    ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
    ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
    // ========== 服务端错误段 ==========
    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
    ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
    ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项");
    // ========== 自定义错误段 ==========
    ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
    ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java
对比新文件
@@ -0,0 +1,46 @@
package com.iailab.framework.common.exception.enums;
/**
 * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
 *
 * 一共 10 位,分成四段
 *
 * 第一段,1 位,类型
 *      1 - 业务级别异常
 *      x - 预留
 * 第二段,3 位,系统类型
 *      001 - 用户系统
 *      002 - 商品系统
 *      003 - 订单系统
 *      004 - 支付系统
 *      005 - 优惠劵系统
 *      ... - ...
 * 第三段,3 位,模块
 *      不限制规则。
 *      一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
 *          001 - OAuth2 模块
 *          002 - User 模块
 *          003 - MobileCode 模块
 * 第四段,3 位,错误码
 *       不限制规则。
 *       一般建议,每个模块自增。
 *
 * @author iailab
 */
public class ServiceErrorCodeRange {
    // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
    // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
    // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000)
    // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
    // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
    // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
    // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
    // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
    // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
    // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java
对比新文件
@@ -0,0 +1,77 @@
package com.iailab.framework.common.exception.util;
import com.iailab.framework.common.exception.ErrorCode;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
/**
 * {@link ServiceException} 工具类
 *
 * 目的在于,格式化异常信息提示。
 * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
 *
 */
@Slf4j
public class ServiceExceptionUtil {
    // ========== 和 ServiceException 的集成 ==========
    public static ServiceException exception(ErrorCode errorCode) {
        return exception0(errorCode.getCode(), errorCode.getMsg());
    }
    public static ServiceException exception(ErrorCode errorCode, Object... params) {
        return exception0(errorCode.getCode(), errorCode.getMsg(), params);
    }
    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
        String message = doFormat(code, messagePattern, params);
        return new ServiceException(code, message);
    }
    public static ServiceException invalidParamException(String messagePattern, Object... params) {
        return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
    }
    // ========== 格式化方法 ==========
    /**
     * 将错误编号对应的消息使用 params 进行格式化。
     *
     * @param code           错误编号
     * @param messagePattern 消息模版
     * @param params         参数
     * @return 格式化后的提示
     */
    @VisibleForTesting
    public static String doFormat(int code, String messagePattern, Object... params) {
        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
        int i = 0;
        int j;
        int l;
        for (l = 0; l < params.length; l++) {
            j = messagePattern.indexOf("{}", i);
            if (j == -1) {
                log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
                if (i == 0) {
                    return messagePattern;
                } else {
                    sbuf.append(messagePattern.substring(i));
                    return sbuf.toString();
                }
            } else {
                sbuf.append(messagePattern, i, j);
                sbuf.append(params[l]);
                i = j + 2;
            }
        }
        if (messagePattern.indexOf("{}", i) != -1) {
            log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
        }
        sbuf.append(messagePattern.substring(i));
        return sbuf.toString();
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * 基础的通用类,和框架无关
 *
 * 例如说,CommonResult 为通用返回
 */
package com.iailab.framework.common;
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java
对比新文件
@@ -0,0 +1,128 @@
package com.iailab.framework.common.pojo;
import cn.hutool.core.lang.Assert;
import com.iailab.framework.common.exception.ErrorCode;
import com.iailab.framework.common.exception.ServiceException;
import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.iailab.framework.common.exception.util.ServiceExceptionUtil;
import lombok.Data;
import java.io.Serializable;
import java.util.Objects;
/**
 * 通用返回
 *
 * @param <T> 数据泛型
 */
@Data
public class CommonResult<T> implements Serializable {
    /**
     * 错误码
     *
     * @see ErrorCode#getCode()
     */
    private Integer code;
    /**
     * 返回数据
     */
    private T data;
    /**
     * 错误提示,用户可阅读
     *
     * @see ErrorCode#getMsg() ()
     */
    private String msg;
    /**
     * 将传入的 result 对象,转换成另外一个泛型结果的对象
     *
     * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
     *
     * @param result 传入的 result 对象
     * @param <T> 返回的泛型
     * @return 新的 CommonResult 对象
     */
    public static <T> CommonResult<T> error(CommonResult<?> result) {
        return error(result.getCode(), result.getMsg());
    }
    public static <T> CommonResult<T> error(Integer code, String message) {
        cn.hutool.core.lang.Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!");
        CommonResult<T> result = new CommonResult<>();
        result.code = code;
        result.msg = message;
        return result;
    }
    public static <T> CommonResult<T> error(ErrorCode errorCode, Object... params) {
        Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!");
        CommonResult<T> result = new CommonResult<>();
        result.code = errorCode.getCode();
        result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params);
        return result;
    }
    public static <T> CommonResult<T> error(ErrorCode errorCode) {
        return error(errorCode.getCode(), errorCode.getMsg());
    }
    public static <T> CommonResult<T> success(T data) {
        CommonResult<T> result = new CommonResult<>();
        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
        result.data = data;
        result.msg = "";
        return result;
    }
    public static CommonResult<String> success() {
        CommonResult<String> result = new CommonResult<>();
        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
        result.msg = "success";
        return result;
    }
    public static boolean isSuccess(Integer code) {
        return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
    }
    @JsonIgnore // 避免 jackson 序列化
    public boolean isSuccess() {
        return isSuccess(code);
    }
    @JsonIgnore // 避免 jackson 序列化
    public boolean isError() {
        return !isSuccess();
    }
    // ========= 和 Exception 异常体系集成 =========
    /**
     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
     */
    public void checkError() throws ServiceException {
        if (isSuccess()) {
            return;
        }
        // 业务异常
        throw new ServiceException(code, msg);
    }
    /**
     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
     * 如果没有,则返回 {@link #data} 数据
     */
    @JsonIgnore // 避免 jackson 序列化
    public T getCheckedData() {
        checkError();
        return data;
    }
    public static <T> CommonResult<T> error(ServiceException serviceException) {
        return error(serviceException.getCode(), serviceException.getMessage());
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Schema(description="分页参数")
@Data
public class PageParam implements Serializable {
    private static final Integer PAGE_NO = 1;
    private static final Integer PAGE_SIZE = 10;
    /**
     * 每页条数 - 不分页
     *
     * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
     */
    public static final Integer PAGE_SIZE_NONE = -1;
    @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
    @NotNull(message = "页码不能为空")
    @Min(value = 1, message = "页码最小值为 1")
    private Integer pageNo = PAGE_NO;
    @Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "每页条数不能为空")
    @Min(value = 1, message = "每页条数最小值为 1")
    @Max(value = 100, message = "每页条数最大值为 100")
    private Integer pageSize = PAGE_SIZE;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java
对比新文件
@@ -0,0 +1,41 @@
package com.iailab.framework.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Schema(description = "分页结果")
@Data
public final class PageResult<T> implements Serializable {
    @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<T> list;
    @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
    private Long total;
    public PageResult() {
    }
    public PageResult(List<T> list, Long total) {
        this.list = list;
        this.total = total;
    }
    public PageResult(Long total) {
        this.list = new ArrayList<>();
        this.total = total;
    }
    public static <T> PageResult<T> empty() {
        return new PageResult<>(0L);
    }
    public static <T> PageResult<T> empty(Long total) {
        return new PageResult<>(total);
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java
对比新文件
@@ -0,0 +1,19 @@
package com.iailab.framework.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.List;
@Schema(description = "可排序的分页参数")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SortablePageParam extends PageParam {
    @Schema(description = "排序字段")
    private List<SortingField> sortingFields;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.common.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
 * 排序字段 DTO
 *
 * 类名加了 ing 的原因是,避免和 ES SortField 重名。
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SortingField implements Serializable {
    /**
     * 顺序 - 升序
     */
    public static final String ORDER_ASC = "asc";
    /**
     * 顺序 - 降序
     */
    public static final String ORDER_DESC = "desc";
    /**
     * 字段
     */
    private String field;
    /**
     * 顺序
     */
    private String order;
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java
对比新文件
@@ -0,0 +1,49 @@
package com.iailab.framework.common.util.cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.time.Duration;
import java.util.concurrent.Executors;
/**
 * Cache 工具类
 *
 * @author iailab
 */
public class CacheUtils {
    /**
     * 构建异步刷新的 LoadingCache 对象
     *
     * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法
     *
     * 或者简单理解:
     * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法
     * 2、和“全局”、“系统”相关的,使用当前缓存方法
     *
     * @param duration 过期时间
     * @param loader  CacheLoader 对象
     * @return LoadingCache 对象
     */
    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
        return CacheBuilder.newBuilder()
                // 只阻塞当前数据加载线程,其他线程返回旧值
                .refreshAfterWrite(duration)
                // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO iailab:可能要思考下,未来要不要做成可配置
    }
    /**
     * 构建同步刷新的 LoadingCache 对象
     *
     * @param duration 过期时间
     * @param loader  CacheLoader 对象
     * @return LoadingCache 对象
     */
    public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) {
        return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader);
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java
对比新文件
@@ -0,0 +1,58 @@
package com.iailab.framework.common.util.collection;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.util.ArrayUtil;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Function;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
/**
 * Array 工具类
 *
 * @author iailab
 */
public class ArrayUtils {
    /**
     * 将 object 和 newElements 合并成一个数组
     *
     * @param object 对象
     * @param newElements 数组
     * @param <T> 泛型
     * @return 结果数组
     */
    @SafeVarargs
    public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
        if (object == null) {
            return newElements;
        }
        Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
        result[0] = object;
        System.arraycopy(newElements, 0, result, 1, newElements.length);
        return result;
    }
    public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) {
        return toArray(convertList(from, mapper));
    }
    @SuppressWarnings("unchecked")
    public static <T> T[] toArray(Collection<T> from) {
        if (CollectionUtil.isEmpty(from)) {
            return (T[]) (new Object[0]);
        }
        return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
    }
    public static <T> T get(T[] array, int index) {
        if (null == array || index >= array.length) {
            return null;
        }
        return array[index];
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java
对比新文件
@@ -0,0 +1,338 @@
package com.iailab.framework.common.util.collection;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import com.google.common.collect.ImmutableMap;
import com.iailab.framework.common.pojo.PageResult;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Arrays.asList;
/**
 * Collection 工具类
 *
 * @author iailab
 */
public class CollectionUtils {
    public static boolean containsAny(Object source, Object... targets) {
        return asList(targets).contains(source);
    }
    public static boolean isAnyEmpty(Collection<?>... collections) {
        return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
    }
    public static <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) {
        return from.stream().anyMatch(predicate);
    }
    public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().filter(predicate).collect(Collectors.toList());
    }
    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return distinct(from, keyMapper, (t1, t2) -> t1);
    }
    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
    }
    public static <T, U> List<U> convertList(T[] from, Function<T, U> func) {
        if (ArrayUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return convertList(Arrays.asList(from), func);
    }
    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
    }
    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
    }
    public static <T, U> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) {
        if (ArrayUtil.isEmpty(from)) {
            return new PageResult<>(from.getTotal());
        }
        return new PageResult<>(convertList(from.getList(), func), from.getTotal());
    }
    public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
                                                      Function<T, ? extends Stream<? extends U>> func) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
    }
    public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
                                                         Function<? super T, ? extends U> mapper,
                                                         Function<U, ? extends Stream<? extends R>> func) {
        if (CollUtil.isEmpty(from)) {
            return new ArrayList<>();
        }
        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
    }
    public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
        return map.values()
                .stream()
                .flatMap(List::stream)
                .collect(Collectors.toList());
    }
    public static <T> Set<T> convertSet(Collection<T> from) {
        return convertSet(from, v -> v);
    }
    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
        if (CollUtil.isEmpty(from)) {
            return new HashSet<>();
        }
        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
    }
    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
        if (CollUtil.isEmpty(from)) {
            return new HashSet<>();
        }
        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
    }
    public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v));
    }
    public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
                                                    Function<T, ? extends Stream<? extends U>> func) {
        if (CollUtil.isEmpty(from)) {
            return new HashSet<>();
        }
        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
    }
    public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
                                                       Function<? super T, ? extends U> mapper,
                                                       Function<U, ? extends Stream<? extends R>> func) {
        if (CollUtil.isEmpty(from)) {
            return new HashSet<>();
        }
        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
    }
    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return convertMap(from, keyFunc, Function.identity());
    }
    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
        if (CollUtil.isEmpty(from)) {
            return supplier.get();
        }
        return convertMap(from, keyFunc, Function.identity(), supplier);
    }
    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
    }
    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
    }
    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
        if (CollUtil.isEmpty(from)) {
            return supplier.get();
        }
        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
    }
    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
    }
    public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
    }
    public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream()
                .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
    }
    // 暂时没想好名字,先以 2 结尾噶
    public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return new HashMap<>();
        }
        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
    }
    public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
        if (CollUtil.isEmpty(from)) {
            return Collections.emptyMap();
        }
        ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
        from.forEach(item -> builder.put(keyFunc.apply(item), item));
        return builder.build();
    }
    /**
     * 对比老、新两个列表,找出新增、修改、删除的数据
     *
     * @param oldList  老列表
     * @param newList  新列表
     * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同
     *                 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据
     * @return [新增列表、修改列表、删除列表]
     */
    public static <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList,
                                             BiFunction<T, T, Boolean> sameFunc) {
        List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
        List<T> updateList = new ArrayList<>();
        List<T> deleteList = new ArrayList<>();
        // 通过以 oldList 为主遍历,找出 updateList 和 deleteList
        for (T oldObj : oldList) {
            // 1. 寻找是否有匹配的
            T foundObj = null;
            for (Iterator<T> iterator = createList.iterator(); iterator.hasNext(); ) {
                T newObj = iterator.next();
                // 1.1 不匹配,则直接跳过
                if (!sameFunc.apply(oldObj, newObj)) {
                    continue;
                }
                // 1.2 匹配,则移除,并结束寻找
                iterator.remove();
                foundObj = newObj;
                break;
            }
            // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中
            if (foundObj != null) {
                updateList.add(foundObj);
            } else {
                deleteList.add(oldObj);
            }
        }
        return asList(createList, updateList, deleteList);
    }
    public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
        return org.springframework.util.CollectionUtils.containsAny(source, candidates);
    }
    public static <T> T getFirst(List<T> from) {
        return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
    }
    public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) {
        return findFirst(from, predicate, Function.identity());
    }
    public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        return from.stream().filter(predicate).findFirst().map(func).orElse(null);
    }
    public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        assert !from.isEmpty(); // 断言,避免告警
        T t = from.stream().max(Comparator.comparing(valueFunc)).get();
        return valueFunc.apply(t);
    }
    public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        assert from.size() > 0; // 断言,避免告警
        T t = from.stream().min(Comparator.comparing(valueFunc)).get();
        return valueFunc.apply(t);
    }
    public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) {
        if (CollUtil.isEmpty(from)) {
            return null;
        }
        assert from.size() > 0; // 断言,避免告警
        return from.stream().min(Comparator.comparing(valueFunc)).get();
    }
    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
                                                                     BinaryOperator<V> accumulator) {
        return getSumValue(from, valueFunc, accumulator, null);
    }
    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
                                                                     BinaryOperator<V> accumulator, V defaultValue) {
        if (CollUtil.isEmpty(from)) {
            return defaultValue;
        }
        assert !from.isEmpty(); // 断言,避免告警
        return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue);
    }
    public static <T> void addIfNotNull(Collection<T> coll, T item) {
        if (item == null) {
            return;
        }
        coll.add(item);
    }
    public static <T> Collection<T> singleton(T obj) {
        return obj == null ? Collections.emptyList() : Collections.singleton(obj);
    }
    public static <T> List<T> newArrayList(List<List<T>> list) {
        return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java
对比新文件
@@ -0,0 +1,68 @@
package com.iailab.framework.common.util.collection;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjUtil;
import com.iailab.framework.common.core.KeyValue;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
 * Map 工具类
 *
 * @author iailab
 */
public class MapUtils {
    /**
     * 从哈希表表中,获得 keys 对应的所有 value 数组
     *
     * @param multimap 哈希表
     * @param keys keys
     * @return value 数组
     */
    public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) {
        List<V> result = new ArrayList<>();
        keys.forEach(k -> {
            Collection<V> values = multimap.get(k);
            if (CollectionUtil.isEmpty(values)) {
                return;
            }
            result.addAll(values);
        });
        return result;
    }
    /**
     * 从哈希表查找到 key 对应的 value,然后进一步处理
     * key 为 null 时, 不处理
     * 注意,如果查找到的 value 为 null 时,不进行处理
     *
     * @param map 哈希表
     * @param key key
     * @param consumer 进一步处理的逻辑
     */
    public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) {
        if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) {
            return;
        }
        V value = map.get(key);
        if (value == null) {
            return;
        }
        consumer.accept(value);
    }
    public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) {
        Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size());
        keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue()));
        return map;
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java
对比新文件
@@ -0,0 +1,19 @@
package com.iailab.framework.common.util.collection;
import cn.hutool.core.collection.CollUtil;
import java.util.Set;
/**
 * Set 工具类
 *
 * @author iailab
 */
public class SetUtils {
    @SafeVarargs
    public static <T> Set<T> asSet(T... objs) {
        return CollUtil.newHashSet(objs);
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java
对比新文件
@@ -0,0 +1,304 @@
package com.iailab.framework.common.util.date;
import cn.hutool.core.date.LocalDateTimeUtil;
import org.joda.time.DateTime;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
 * 时间工具类
 *
 * @author iailab
 */
public class DateUtils {
    /**
     * 时区 - 默认
     */
    public static final String TIME_ZONE_DEFAULT = "GMT+8";
    /**
     * 秒转换成毫秒
     */
    public static final long SECOND_MILLIS = 1000;
    public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd";
    /** 时间格式(yyyy.MM.dd) */
    public final static String DATE_PATTERN_POINT = "yyyy.MM.dd";
    public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
    public final static String DATE_TIME_PATTERN_STRING = "yyyyMMddHHmmss";
    public static final String FORMAT_SIMPLE_TIME = "HH:mm";
    /**
     * 日期格式化 日期格式为:yyyy-MM-dd
     * @param date  日期
     * @return  返回yyyy-MM-dd格式日期
     */
    public static String format(Date date) {
        return format(date, FORMAT_YEAR_MONTH_DAY);
    }
    /**
     * 日期格式化 日期格式为:yyyy-MM-dd
     * @param date  日期
     * @param pattern  格式,如:DateUtils.DATE_TIME_PATTERN
     * @return  返回yyyy-MM-dd格式日期
     */
    public static String format(Date date, String pattern) {
        if(date != null){
            SimpleDateFormat df = new SimpleDateFormat(pattern);
            return df.format(date);
        }
        return null;
    }
    /**
     * 日期解析
     * @param date  日期
     * @param pattern  格式,如:DateUtils.DATE_TIME_PATTERN
     * @return  返回Date
     */
    public static Date parse(String date, String pattern) {
        try {
            return new SimpleDateFormat(pattern).parse(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 将 LocalDateTime 转换成 Date
     *
     * @param date LocalDateTime
     * @return LocalDateTime
     */
    public static Date of(LocalDateTime date) {
        if (date == null) {
            return null;
        }
        // 将此日期时间与时区相结合以创建 ZonedDateTime
        ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault());
        // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳
        Instant instant = zonedDateTime.toInstant();
        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
        return Date.from(instant);
    }
    /**
     * 将 Date 转换成 LocalDateTime
     *
     * @param date Date
     * @return LocalDateTime
     */
    public static LocalDateTime of(Date date) {
        if (date == null) {
            return null;
        }
        // 转为时间戳
        Instant instant = date.toInstant();
        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    }
    public static Date addTime(Duration duration) {
        return new Date(System.currentTimeMillis() + duration.toMillis());
    }
    public static boolean isExpired(LocalDateTime time) {
        LocalDateTime now = LocalDateTime.now();
        return now.isAfter(time);
    }
    /**
     * 创建指定时间
     *
     * @param year  年
     * @param mouth 月
     * @param day   日
     * @return 指定时间
     */
    public static Date buildTime(int year, int mouth, int day) {
        return buildTime(year, mouth, day, 0, 0, 0);
    }
    /**
     * 创建指定时间
     *
     * @param year   年
     * @param mouth  月
     * @param day    日
     * @param hour   小时
     * @param minute 分钟
     * @param second 秒
     * @return 指定时间
     */
    public static Date buildTime(int year, int mouth, int day,
                                 int hour, int minute, int second) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, mouth - 1);
        calendar.set(Calendar.DAY_OF_MONTH, day);
        calendar.set(Calendar.HOUR_OF_DAY, hour);
        calendar.set(Calendar.MINUTE, minute);
        calendar.set(Calendar.SECOND, second);
        calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒
        return calendar.getTime();
    }
    public static Date max(Date a, Date b) {
        if (a == null) {
            return b;
        }
        if (b == null) {
            return a;
        }
        return a.compareTo(b) > 0 ? a : b;
    }
    public static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
        if (a == null) {
            return b;
        }
        if (b == null) {
            return a;
        }
        return a.isAfter(b) ? a : b;
    }
    /**
     * 是否今天
     *
     * @param date 日期
     * @return 是否
     */
    public static boolean isToday(LocalDateTime date) {
        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now());
    }
    /**
     * 是否昨天
     *
     * @param date 日期
     * @return 是否
     */
    public static boolean isYesterday(LocalDateTime date) {
        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1));
    }
    public static List<String> getTimeScale(Date startDate, Date endDate, int seconds) {
        List<String> days = new ArrayList<String>();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(startDate);
        while (calendar.getTime().compareTo(endDate) <= 0) {
            days.add(DateUtils.format(calendar.getTime(), FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND));
            calendar.add(Calendar.SECOND, seconds);
        }
        return days;
    }
    public static List<String> getTimeScale(Date startDate, Date endDate, int seconds, String timeFormat) {
        List<String> days = new ArrayList<String>();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(startDate);
        while (calendar.getTime().compareTo(endDate) <= 0) {
            days.add(DateUtils.format(calendar.getTime(), timeFormat));
            calendar.add(Calendar.SECOND, seconds);
        }
        return days;
    }
    /**
     * 对日期的【秒】进行加/减
     *
     * @param date 日期
     * @param seconds 秒数,负数为减
     * @return 加/减几秒后的日期
     */
    public static Date addDateSeconds(Date date, int seconds) {
        DateTime dateTime = new DateTime(date);
        return dateTime.plusSeconds(seconds).toDate();
    }
    /**
     * 对日期的【分钟】进行加/减
     *
     * @param date 日期
     * @param minutes 分钟数,负数为减
     * @return 加/减几分钟后的日期
     */
    public static Date addDateMinutes(Date date, int minutes) {
        DateTime dateTime = new DateTime(date);
        return dateTime.plusMinutes(minutes).toDate();
    }
    /**
     * 对日期的【小时】进行加/减
     *
     * @param date 日期
     * @param hours 小时数,负数为减
     * @return 加/减几小时后的日期
     */
    public static Date addDateHours(Date date, int hours) {
        DateTime dateTime = new DateTime(date);
        return dateTime.plusHours(hours).toDate();
    }
    /**
     * 对日期的【天】进行加/减
     *
     * @param date 日期
     * @param days 天数,负数为减
     * @return 加/减几天后的日期
     */
    public static Date addDateDays(Date date, int days) {
        DateTime dateTime = new DateTime(date);
        return dateTime.plusDays(days).toDate();
    }
    /**
     * 对日期的【周】进行加/减
     *
     * @param date 日期
     * @param weeks 周数,负数为减
     * @return 加/减几周后的日期
     */
    public static Date addDateWeeks(Date date, int weeks) {
        DateTime dateTime = new DateTime(date);
        return dateTime.plusWeeks(weeks).toDate();
    }
    /**
     * 对日期的【月】进行加/减
     *
     * @param date 日期
     * @param months 月数,负数为减
     * @return 加/减几月后的日期
     */
    public static Date addDateMonths(Date date, int months) {
        DateTime dateTime = new DateTime(date);
        return dateTime.plusMonths(months).toDate();
    }
    /**
     * 对日期的【年】进行加/减
     *
     * @param date 日期
     * @param years 年数,负数为减
     * @return 加/减几年后的日期
     */
    public static Date addDateYears(Date date, int years) {
        DateTime dateTime = new DateTime(date);
        return dateTime.plusYears(years).toDate();
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java
对比新文件
@@ -0,0 +1,309 @@
package com.iailab.framework.common.util.date;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.enums.DateIntervalEnum;
import java.time.*;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
/**
 * 时间工具类,用于 {@link java.time.LocalDateTime}
 *
 * @author iailab
 */
public class LocalDateTimeUtils {
    /**
     * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
     */
    public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
    /**
     * 解析时间
     *
     * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功
     *
     * @param time 时间
     * @return 时间字符串
     */
    public static LocalDateTime parse(String time) {
        try {
            return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN);
        } catch (DateTimeParseException e) {
            return LocalDateTimeUtil.parse(time);
        }
    }
    public static LocalDateTime addTime(Duration duration) {
        return LocalDateTime.now().plus(duration);
    }
    public static LocalDateTime minusTime(Duration duration) {
        return LocalDateTime.now().minus(duration);
    }
    public static boolean beforeNow(LocalDateTime date) {
        return date.isBefore(LocalDateTime.now());
    }
    public static boolean afterNow(LocalDateTime date) {
        return date.isAfter(LocalDateTime.now());
    }
    /**
     * 创建指定时间
     *
     * @param year  年
     * @param mouth 月
     * @param day   日
     * @return 指定时间
     */
    public static LocalDateTime buildTime(int year, int mouth, int day) {
        return LocalDateTime.of(year, mouth, day, 0, 0, 0);
    }
    public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1,
                                                   int year2, int mouth2, int day2) {
        return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)};
    }
    /**
     * 判指定断时间,是否在该时间范围内
     *
     * @param startTime 开始时间
     * @param endTime 结束时间
     * @param time 指定时间
     * @return 是否
     */
    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) {
        if (startTime == null || endTime == null || time == null) {
            return false;
        }
        return LocalDateTimeUtil.isIn(parse(time), startTime, endTime);
    }
    /**
     * 判断当前时间是否在该时间范围内
     *
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @return 是否
     */
    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) {
        if (startTime == null || endTime == null) {
            return false;
        }
        return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime);
    }
    /**
     * 判断当前时间是否在该时间范围内
     *
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @return 是否
     */
    public static boolean isBetween(String startTime, String endTime) {
        if (startTime == null || endTime == null) {
            return false;
        }
        LocalDate nowDate = LocalDate.now();
        return LocalDateTimeUtil.isIn(LocalDateTime.now(),
                LocalDateTime.of(nowDate, LocalTime.parse(startTime)),
                LocalDateTime.of(nowDate, LocalTime.parse(endTime)));
    }
    /**
     * 判断时间段是否重叠
     *
     * @param startTime1 开始 time1
     * @param endTime1   结束 time1
     * @param startTime2 开始 time2
     * @param endTime2   结束 time2
     * @return 重叠:true 不重叠:false
     */
    public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) {
        LocalDate nowDate = LocalDate.now();
        return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1),
                LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
    }
    /**
     * 获取指定日期所在的月份的开始时间
     * 例如:2023-09-30 00:00:00,000
     *
     * @param date 日期
     * @return 月份的开始时间
     */
    public static LocalDateTime beginOfMonth(LocalDateTime date) {
        return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
    }
    /**
     * 获取指定日期所在的月份的最后时间
     * 例如:2023-09-30 23:59:59,999
     *
     * @param date 日期
     * @return 月份的结束时间
     */
    public static LocalDateTime endOfMonth(LocalDateTime date) {
        return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
    }
    /**
     * 获得指定日期所在季度
     *
     * @param date 日期
     * @return 所在季度
     */
    public static int getQuarterOfYear(LocalDateTime date) {
        return (date.getMonthValue() - 1) / 3 + 1;
    }
    /**
     * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负
     *
     * @param dateTime 日期
     * @return 相差天数
     */
    public static Long between(LocalDateTime dateTime) {
        return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS);
    }
    /**
     * 获取今天的开始时间
     *
     * @return 今天
     */
    public static LocalDateTime getToday() {
        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
    }
    /**
     * 获取昨天的开始时间
     *
     * @return 昨天
     */
    public static LocalDateTime getYesterday() {
        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1));
    }
    /**
     * 获取本月的开始时间
     *
     * @return 本月
     */
    public static LocalDateTime getMonth() {
        return beginOfMonth(LocalDateTime.now());
    }
    /**
     * 获取本年的开始时间
     *
     * @return 本年
     */
    public static LocalDateTime getYear() {
        return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
    }
    public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
                                                         LocalDateTime endTime,
                                                         Integer interval) {
        // 1.1 找到枚举
        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
        // 1.2 将时间对齐
        startTime = LocalDateTimeUtil.beginOfDay(startTime);
        endTime = LocalDateTimeUtil.endOfDay(endTime);
        // 2. 循环,生成时间范围
        List<LocalDateTime[]> timeRanges = new ArrayList<>();
        switch (intervalEnum) {
            case DAY:
                while (startTime.isBefore(endTime)) {
                    timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
                    startTime = startTime.plusDays(1);
                }
                break;
            case WEEK:
                while (startTime.isBefore(endTime)) {
                    LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1);
                    timeRanges.add(new LocalDateTime[]{startTime, endOfWeek});
                    startTime = endOfWeek.plusNanos(1);
                }
                break;
            case MONTH:
                while (startTime.isBefore(endTime)) {
                    LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1);
                    timeRanges.add(new LocalDateTime[]{startTime, endOfMonth});
                    startTime = endOfMonth.plusNanos(1);
                }
                break;
            case QUARTER:
                while (startTime.isBefore(endTime)) {
                    int quarterOfYear = getQuarterOfYear(startTime);
                    LocalDateTime quarterEnd = quarterOfYear == 4
                            ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1)
                            : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1);
                    timeRanges.add(new LocalDateTime[]{startTime, quarterEnd});
                    startTime = quarterEnd.plusNanos(1);
                }
                break;
            case YEAR:
                while (startTime.isBefore(endTime)) {
                    LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1);
                    timeRanges.add(new LocalDateTime[]{startTime, endOfYear});
                    startTime = endOfYear.plusNanos(1);
                }
                break;
            default:
                throw new IllegalArgumentException("Invalid interval: " + interval);
        }
        // 3. 兜底,最后一个时间,需要保持在 endTime 之前
        LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges);
        if (lastTimeRange != null) {
            lastTimeRange[1] = endTime;
        }
        return timeRanges;
    }
    /**
     * 格式化时间范围
     *
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @param interval  时间间隔
     * @return 时间范围
     */
    public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) {
        // 1. 找到枚举
        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
        // 2. 循环,生成时间范围
        switch (intervalEnum) {
            case DAY:
                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
            case WEEK:
                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN)
                        + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime));
            case MONTH:
                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN);
            case QUARTER:
                return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime));
            case YEAR:
                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN);
            default:
                throw new IllegalArgumentException("Invalid interval: " + interval);
        }
    }
}
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java
对比新文件
@@ -0,0 +1,385 @@
package com.iailab.framework.common.util.http;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
/**
 * HTTP 工具类
 *
 * @author iailab
 */
public class HttpUtils {
    @SuppressWarnings("unchecked")
    public static String replaceUrlQuery(String url, String key, String value) {
        UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
        // 先移除
        TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
                ReflectUtil.getFieldValue(builder.getQuery(), "query");
        query.remove(key);
        // 后添加
        builder.addQuery(key, value);
        return builder.build();
    }
    private String append(String base, Map<String, ?> query, boolean fragment) {
        return append(base, query, null, fragment);
    }
    /**
     * 拼接 URL
     *
     * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
     *
     * @param base 基础 URL
     * @param query 查询参数
     * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
     * @param fragment URL 的 fragment,即拼接到 # 中
     * @return 拼接后的 URL
     */
    public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
        UriComponentsBuilder template = UriComponentsBuilder.newInstance();
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
        URI redirectUri;
        try {
            // assume it's encoded to start with (if it came in over the wire)
            redirectUri = builder.build(true).toUri();
        } catch (Exception e) {
            // ... but allow client registrations to contain hard-coded non-encoded values
            redirectUri = builder.build().toUri();
            builder = UriComponentsBuilder.fromUri(redirectUri);
        }
        template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
                .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
        if (fragment) {
            StringBuilder values = new StringBuilder();
            if (redirectUri.getFragment() != null) {
                String append = redirectUri.getFragment();
                values.append(append);
            }
            for (String key : query.keySet()) {
                if (values.length() > 0) {
                    values.append("&");
                }
                String name = key;
                if (keys != null && keys.containsKey(key)) {
                    name = keys.get(key);
                }
                values.append(name).append("={").append(key).append("}");
            }
            if (values.length() > 0) {
                template.fragment(values.toString());
            }
            UriComponents encoded = template.build().expand(query).encode();
            builder.fragment(encoded.getFragment());
        } else {
            for (String key : query.keySet()) {
                String name = key;
                if (keys != null && keys.containsKey(key)) {
                    name = keys.get(key);
                }
                template.queryParam(name, "{" + key + "}");
            }
            template.fragment(redirectUri.getFragment());
            UriComponents encoded = template.build().expand(query).encode();
            builder.query(encoded.getQuery());
        }
        return builder.build().toUriString();
    }
    public static String[] obtainBasicAuthorization(HttpServletRequest request) {
        String clientId;
        String clientSecret;
        // 先从 Header 中获取
        String authorization = request.getHeader("Authorization");
        authorization = StrUtil.subAfter(authorization, "Basic ", true);
        if (StringUtils.hasText(authorization)) {
            authorization = Base64.decodeStr(authorization);
            clientId = StrUtil.subBefore(authorization, ":", false);
            clientSecret = StrUtil.subAfter(authorization, ":", false);
        // 再从 Param 中获取
        } else {
            clientId = request.getParameter("client_id");
            clientSecret = request.getParameter("client_secret");
        }
        // 如果两者非空,则返回
        if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
            return new String[]{clientId, clientSecret};
        }
        return null;
    }
    /**
     * 向指定URL发送GET方法的请求
     *
     * @param url   发送请求的URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return URL 所代表远程资源的响应结果
     */
    public static String sendGet(String url, String param) {
        String result = "";
        BufferedReader in = null;
        try {
            String urlNameString = url + "?" + param;
            URL realUrl = new URL(urlNameString);
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 获取所有响应头字段
            Map<String, List<String>> map = connection.getHeaderFields();
            // 遍历所有的响应头字段
            for (String key : map.keySet()) {
                System.out.println(key + "--->" + map.get(key));
            }
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }
    public static String sendGet(String url, Map<String, String> params, String authorization) {
        String result = "";
        BufferedReader in = null;
        try {
            StringBuilder sb = new StringBuilder();
            sb.append(url);
            if (!CollectionUtils.isEmpty(params)) {
                sb.append("?");
                params.forEach((k, v) -> {
                    sb.append(k + "=" + v + "&");
                });
                sb.append("t=" + System.currentTimeMillis());
            }
            String urlNameString = sb.toString();
            URL realUrl = new URL(urlNameString);
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("Authorization", authorization);
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 获取所有响应头字段
            Map<String, List<String>> map = connection.getHeaderFields();
            // 遍历所有的响应头字段
            for (String key : map.keySet()) {
                System.out.println(key + "--->" + map.get(key));
            }
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }
    /**
     * 向指定 URL 发送POST方法的请求
     *
     * @param url
     * @param json
     * @return
     */
    public static String sendPost(String url, String json) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try {
            URL realUrl = new URL(url);
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("content-type", "application/json");
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            // 发送请求参数
            out.print(json);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送 POST 请求出现异常!" + e);
            e.printStackTrace();
        }
        //使用finally块来关闭输出流、输入流
        finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return result;
    }
    /**
     * 向指定 URL 发送POST方法的请求
     *
     * @param url
     * @param json
     * @param authorization
     * @return
     */
    public static String sendPost(String url, String json, String authorization) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try {
            URL realUrl = new URL(url);
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("Authorization", authorization);
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            // 发送请求参数
            out.print(json);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送 POST 请求出现异常!" + e);
            e.printStackTrace();
        }
        //使用finally块来关闭输出流、输入流
        finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return result;
    }
    public static String sendPostToken(String url, String json, String authorization) {
        String result = "";
        try {
            URL realUrl = new URL(url);
            HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Authorization", authorization);
            connection.setDoOutput(true);
            // 发送POST请求的数据
            try (OutputStream os = connection.getOutputStream()) {
                os.write(json.getBytes());
                os.flush();
            }
            // 获取响应码和响应体
            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) { // 200 OK
                try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
                    StringBuilder response = new StringBuilder();
                    String responseLine;
                    while ((responseLine = br.readLine()) != null) {
                        response.append(responseLine.trim());
                    }
                    result = response.toString();
                }
            } else {
                System.out.println("POST request not worked");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}
在上述文件截断后对比
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/FileUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/IoUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/JsonUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/NumberSerializer.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/monitor/TracerUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/MoneyUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/NumberUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/BeanUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ConvertUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ObjectUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/PageUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/package-info.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/servlet/ServletUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringContextUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringExpressionUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/string/StrUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/validation/ValidationUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnum.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumCollectionValidator.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumValidator.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Mobile.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/MobileValidator.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Telephone.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/TelephoneValidator.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/AddGroup.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/DefaultGroup.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/Group.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/UpdateGroup.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/package-info.java iailab-framework/iailab-common/src/test/java/com/iailab/framework/common/util/collection/CollectionUtilsTest.java iailab-framework/pom.xml