houzhongjian
5 天以前 e9bfa1396ff47d171b3052a606e0931e6f93cc9c
还原framework代码
已添加436个文件
已修改1个文件
28422 ■■■■■ 文件已修改
iailab-framework/iailab-common-biz-data-permission/pom.xml 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/config/IailabDataPermissionAutoConfiguration.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/config/IailabDataPermissionRpcAutoConfiguration.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/config/IailabDeptDataPermissionAutoConfiguration.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/annotation/DataPermission.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/aop/DataPermissionContextHolder.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/db/DataPermissionRuleHandler.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rpc/DataPermissionRequestInterceptor.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rpc/DataPermissionRpcWebFilter.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/DataPermissionRule.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/DataPermissionRuleFactory.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/dept/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/util/DataPermissionUtils.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/aop/DataPermissionContextHolderTest.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/db/DataPermissionRuleHandlerTest.java 540 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/util/DataPermissionUtilsTest.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/pom.xml 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/Area.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/enums/AreaTypeEnum.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/utils/AreaUtils.java 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/utils/IPUtils.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/package-info.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/main/resources/area.csv 3662 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/main/resources/ip2region.xdb 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/test/java/com/iailab/framework/ip/core/utils/AreaUtilsTest.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-ip/src/test/java/com/iailab/framework/ip/core/utils/IPUtilsTest.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 | 历史
pom.xml 655 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-biz-data-permission/pom.xml
对比新文件
@@ -0,0 +1,63 @@
<?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-data-permission</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>
            <optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
        </dependency>
        <!-- DB 相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-mybatis</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>
        <!-- 业务组件 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 -->
            <version>${revision}</version>
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/config/IailabDataPermissionAutoConfiguration.java
对比新文件
@@ -0,0 +1,47 @@
package com.iailab.framework.datapermission.config;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.iailab.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
import com.iailab.framework.datapermission.core.db.DataPermissionRuleHandler;
import com.iailab.framework.datapermission.core.rule.DataPermissionRule;
import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactory;
import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
import com.iailab.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
 * 数据权限的自动配置类
 *
 * @author iailab
 */
@AutoConfiguration
public class IailabDataPermissionAutoConfiguration {
    @Bean
    public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
        return new DataPermissionRuleFactoryImpl(rules);
    }
    @Bean
    public DataPermissionRuleHandler dataPermissionRuleHandler(MybatisPlusInterceptor interceptor,
                                                               DataPermissionRuleFactory ruleFactory) {
        // 创建 DataPermissionInterceptor 拦截器
        DataPermissionRuleHandler handler = new DataPermissionRuleHandler(ruleFactory);
        DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
        // 添加到 interceptor 中
        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
        MyBatisUtils.addInterceptor(interceptor, inner, 0);
        return handler;
    }
    @Bean
    public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
        return new DataPermissionAnnotationAdvisor();
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/config/IailabDataPermissionRpcAutoConfiguration.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.datapermission.config;
import com.iailab.framework.datapermission.core.rpc.DataPermissionRequestInterceptor;
import com.iailab.framework.datapermission.core.rpc.DataPermissionRpcWebFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import static com.iailab.framework.common.enums.WebFilterOrderEnum.TENANT_CONTEXT_FILTER;
/**
 * 数据权限针对 RPC 的自动配置类
 *
 * @author iailab
 */
@AutoConfiguration
@ConditionalOnClass(name = "feign.RequestInterceptor")
public class IailabDataPermissionRpcAutoConfiguration {
    @Bean
    public DataPermissionRequestInterceptor dataPermissionRequestInterceptor() {
        return new DataPermissionRequestInterceptor();
    }
    @Bean
    public FilterRegistrationBean<DataPermissionRpcWebFilter> dataPermissionRpcFilter() {
        FilterRegistrationBean<DataPermissionRpcWebFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new DataPermissionRpcWebFilter());
        registrationBean.setOrder(TENANT_CONTEXT_FILTER - 1); // 顺序没有绝对的要求,在租户 Filter 前面稳妥点
        return registrationBean;
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/config/IailabDeptDataPermissionAutoConfiguration.java
对比新文件
@@ -0,0 +1,44 @@
package com.iailab.framework.datapermission.config;
import cn.hutool.extra.spring.SpringUtil;
import com.iailab.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
import com.iailab.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.module.system.api.permission.PermissionApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
 * 基于部门的数据权限 AutoConfiguration
 *
 * @author iailab
 */
@AutoConfiguration
@ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = DeptDataPermissionRuleCustomizer.class)
public class IailabDeptDataPermissionAutoConfiguration {
    @Bean
    public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
                                                         List<DeptDataPermissionRuleCustomizer> customizers) {
        // Cloud 专属逻辑:优先使用本地的 PermissionApi 实现类,而不是 Feign 调用
        // 原因:在创建租户时,租户还没创建好,导致 Feign 调用获取数据权限时,报“租户不存在”的错误
        try {
            PermissionApi permissionApiImpl = SpringUtil.getBean("permissionApiImpl", PermissionApi.class);
            if (permissionApiImpl != null) {
                permissionApi = permissionApiImpl;
            }
        } catch (Exception ignored) {}
        // 创建 DeptDataPermissionRule 对象
        DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
        // 补全表配置
        customizers.forEach(customizer -> customizer.customize(rule));
        return rule;
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/annotation/DataPermission.java
对比新文件
@@ -0,0 +1,35 @@
package com.iailab.framework.datapermission.core.annotation;
import com.iailab.framework.datapermission.core.rule.DataPermissionRule;
import java.lang.annotation.*;
/**
 * 数据权限注解
 * 可声明在类或者方法上,标识使用的数据权限规则
 *
 * @author iailab
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
    /**
     * 当前类或方法是否开启数据权限
     * 即使不添加 @DataPermission 注解,默认是开启状态
     * 可通过设置 enable 为 false 禁用
     */
    boolean enable() default true;
    /**
     * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()}
     */
    Class<? extends DataPermissionRule>[] includeRules() default {};
    /**
     * 排除的数据权限规则数组,优先级最低
     */
    Class<? extends DataPermissionRule>[] excludeRules() default {};
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.datapermission.core.aop;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
/**
 * {@link com.iailab.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类
 *
 * @author iailab
 */
@Getter
@EqualsAndHashCode(callSuper = true)
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
    private final Advice advice;
    private final Pointcut pointcut;
    public DataPermissionAnnotationAdvisor() {
        this.advice = new DataPermissionAnnotationInterceptor();
        this.pointcut = this.buildPointcut();
    }
    protected Pointcut buildPointcut() {
        Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
        Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
        return new ComposablePointcut(classPointcut).union(methodPointcut);
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java
对比新文件
@@ -0,0 +1,72 @@
package com.iailab.framework.datapermission.core.aop;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import lombok.Getter;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.MethodClassKey;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * {@link DataPermission} 注解的拦截器
 * 1. 在执行方法前,将 @DataPermission 注解入栈
 * 2. 在执行方法后,将 @DataPermission 注解出栈
 *
 * @author iailab
 */
@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
    /**
     * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位
     */
    static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
    @Getter
    private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        // 入栈
        DataPermission dataPermission = this.findAnnotation(methodInvocation);
        if (dataPermission != null) {
            DataPermissionContextHolder.add(dataPermission);
        }
        try {
            // 执行逻辑
            return methodInvocation.proceed();
        } finally {
            // 出栈
            if (dataPermission != null) {
                DataPermissionContextHolder.remove();
            }
        }
    }
    private DataPermission findAnnotation(MethodInvocation methodInvocation) {
        // 1. 从缓存中获取
        Method method = methodInvocation.getMethod();
        Object targetObject = methodInvocation.getThis();
        Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
        MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
        DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
        if (dataPermission != null) {
            return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
        }
        // 2.1 从方法中获取
        dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
        // 2.2 从类上获取
        if (dataPermission == null) {
            dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
        }
        // 2.3 添加到缓存中
        dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
        return dataPermission;
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/aop/DataPermissionContextHolder.java
对比新文件
@@ -0,0 +1,72 @@
package com.iailab.framework.datapermission.core.aop;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.LinkedList;
import java.util.List;
/**
 * {@link DataPermission} 注解的 Context 上下文
 *
 * @author iailab
 */
public class DataPermissionContextHolder {
    /**
     * 使用 List 的原因,可能存在方法的嵌套调用
     */
    private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
            TransmittableThreadLocal.withInitial(LinkedList::new);
    /**
     * 获得当前的 DataPermission 注解
     *
     * @return DataPermission 注解
     */
    public static DataPermission get() {
        return DATA_PERMISSIONS.get().peekLast();
    }
    /**
     * 入栈 DataPermission 注解
     *
     * @param dataPermission DataPermission 注解
     */
    public static void add(DataPermission dataPermission) {
        DATA_PERMISSIONS.get().addLast(dataPermission);
    }
    /**
     * 出栈 DataPermission 注解
     *
     * @return DataPermission 注解
     */
    public static DataPermission remove() {
        DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
        // 无元素时,清空 ThreadLocal
        if (DATA_PERMISSIONS.get().isEmpty()) {
            DATA_PERMISSIONS.remove();
        }
        return dataPermission;
    }
    /**
     * 获得所有 DataPermission
     *
     * @return DataPermission 队列
     */
    public static List<DataPermission> getAll() {
        return DATA_PERMISSIONS.get();
    }
    /**
     * 清空上下文
     *
     * 目前仅仅用于单测
     */
    public static void clear() {
        DATA_PERMISSIONS.remove();
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/db/DataPermissionRuleHandler.java
对比新文件
@@ -0,0 +1,57 @@
package com.iailab.framework.datapermission.core.db;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
import com.iailab.framework.datapermission.core.rule.DataPermissionRule;
import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactory;
import com.iailab.framework.mybatis.core.util.MyBatisUtils;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.schema.Table;
import java.util.List;
/**
 * 基于 {@link DataPermissionRule} 的数据权限处理器
 *
 * 它的底层,是基于 MyBatis Plus 的 <a href="https://baomidou.com/plugins/data-permission/">数据权限插件</a>
 * 核心原理:它会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来
 *
 * @author iailab
 */
@RequiredArgsConstructor
public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
    private final DataPermissionRuleFactory ruleFactory;
    @Override
    public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
        // 获得 Mapper 对应的数据权限的规则
        List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
        if (CollUtil.isEmpty(rules)) {
            return null;
        }
        // 生成条件
        Expression allExpression = null;
        for (DataPermissionRule rule : rules) {
            // 判断表名是否匹配
            String tableName = MyBatisUtils.getTableName(table);
            if (!rule.getTableNames().contains(tableName)) {
                continue;
            }
            // 单条规则的条件
            Expression oneExpress = rule.getExpression(tableName, table.getAlias());
            if (oneExpress == null) {
                continue;
            }
            // 拼接到 allExpression 中
            allExpression = allExpression == null ? oneExpress
                    : new AndExpression(allExpression, oneExpress);
        }
        return allExpression;
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rpc/DataPermissionRequestInterceptor.java
对比新文件
@@ -0,0 +1,27 @@
package com.iailab.framework.datapermission.core.rpc;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
 * DataPermission 的 RequestInterceptor 实现类:Feign 请求时,将 {@link DataPermission} 设置到 header 中,继续透传给被调用的服务
 *
 * 注意:由于 {@link DataPermission} 不支持序列化和反序列化,所以暂时只能传递它的 enable 属性
 *
 * @author iailab
 */
public class DataPermissionRequestInterceptor implements RequestInterceptor {
    public static final String ENABLE_HEADER_NAME = "data-permission-enable";
    @Override
    public void apply(RequestTemplate requestTemplate) {
        DataPermission dataPermission = DataPermissionContextHolder.get();
        if (dataPermission != null && Boolean.FALSE.equals(dataPermission.enable())) {
            requestTemplate.header(ENABLE_HEADER_NAME, "false");
        }
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rpc/DataPermissionRpcWebFilter.java
对比新文件
@@ -0,0 +1,37 @@
package com.iailab.framework.datapermission.core.rpc;
import com.iailab.framework.datapermission.core.util.DataPermissionUtils;
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.util.Objects;
/**
 * 针对 {@link DataPermissionRequestInterceptor} 的 RPC 调用,设置 {@link com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder} 的上下文
 *
 * @author iailab
 */
public class DataPermissionRpcWebFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String enable = request.getHeader(DataPermissionRequestInterceptor.ENABLE_HEADER_NAME);
        if (Objects.equals(enable, Boolean.FALSE.toString())) {
            DataPermissionUtils.executeIgnore(() -> {
                try {
                    chain.doFilter(request, response);
                } catch (IOException | ServletException e) {
                    throw new RuntimeException(e);
                }
            });
        } else {
            chain.doFilter(request, response);
        }
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/DataPermissionRule.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.datapermission.core.rule;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import java.util.Set;
/**
 * 数据权限规则接口
 * 通过实现接口,自定义数据规则。例如说,
 *
 * @author iailab
 */
public interface DataPermissionRule {
    /**
     * 返回需要生效的表名数组
     * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
     *
     * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得
     *
     * @return 表名数组
     */
    Set<String> getTableNames();
    /**
     * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
     *
     * @param tableName 表名
     * @param tableAlias 别名,可能为空
     * @return 过滤条件 Expression 表达式
     */
    Expression getExpression(String tableName, Alias tableAlias);
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/DataPermissionRuleFactory.java
对比新文件
@@ -0,0 +1,28 @@
package com.iailab.framework.datapermission.core.rule;
import java.util.List;
/**
 * {@link DataPermissionRule} 工厂接口
 * 作为 {@link DataPermissionRule} 的容器,提供管理能力
 *
 * @author iailab
 */
public interface DataPermissionRuleFactory {
    /**
     * 获得所有数据权限规则数组
     *
     * @return 数据权限规则数组
     */
    List<DataPermissionRule> getDataPermissionRules();
    /**
     * 获得指定 Mapper 的数据权限规则数组
     *
     * @param mappedStatementId 指定 Mapper 的编号
     * @return 数据权限规则数组
     */
    List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java
对比新文件
@@ -0,0 +1,62 @@
package com.iailab.framework.datapermission.core.rule;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder;
import lombok.RequiredArgsConstructor;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
 * 默认的 DataPermissionRuleFactoryImpl 实现类
 * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限
 *
 * @author iailab
 */
@RequiredArgsConstructor
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {
    /**
     * 数据权限规则数组
     */
    private final List<DataPermissionRule> rules;
    @Override
    public List<DataPermissionRule> getDataPermissionRules() {
        return rules;
    }
    @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
    public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
        // 1. 无数据权限
        if (CollUtil.isEmpty(rules)) {
            return Collections.emptyList();
        }
        // 2. 未配置,则默认开启
        DataPermission dataPermission = DataPermissionContextHolder.get();
        if (dataPermission == null) {
            return rules;
        }
        // 3. 已配置,但禁用
        if (!dataPermission.enable()) {
            return Collections.emptyList();
        }
        // 4. 已配置,只选择部分规则
        if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
            return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
                    .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
        }
        // 5. 已配置,只排除部分规则
        if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
            return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
                    .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
        }
        // 6. 已配置,全部规则
        return rules;
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
对比新文件
@@ -0,0 +1,207 @@
package com.iailab.framework.datapermission.core.rule.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.iailab.framework.common.enums.UserTypeEnum;
import com.iailab.framework.common.util.collection.CollectionUtils;
import com.iailab.framework.common.util.json.JsonUtils;
import com.iailab.framework.datapermission.core.rule.DataPermissionRule;
import com.iailab.framework.mybatis.core.dataobject.BaseDO;
import com.iailab.framework.mybatis.core.util.MyBatisUtils;
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.iailab.module.system.api.permission.dto.DeptDataPermissionRespDTO;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
 * 基于部门的 {@link DataPermissionRule} 数据权限规则实现
 *
 * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
 *
 * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
 * 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【iailab-server 采用该方案】
 * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
 *  1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】
 *      最终过滤条件是 WHERE dept_id = ?
 *  2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号;
 *      最终过滤条件是 WHERE user_id IN (?, ?, ? ...)
 *  3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤;
 *      最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...)
 *
 * @author iailab
 */
@AllArgsConstructor
@Slf4j
public class DeptDataPermissionRule implements DataPermissionRule {
    /**
     * LoginUser 的 Context 缓存 Key
     */
    protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName();
    private static final String DEPT_COLUMN_NAME = "dept_id";
    private static final String USER_COLUMN_NAME = "user_id";
    static final Expression EXPRESSION_NULL = new NullValue();
    private final PermissionApi permissionApi;
    /**
     * 基于部门的表字段配置
     * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
     *
     * key:表名
     * value:字段名
     */
    private final Map<String, String> deptColumns = new HashMap<>();
    /**
     * 基于用户的表字段配置
     * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
     *
     * key:表名
     * value:字段名
     */
    private final Map<String, String> userColumns = new HashMap<>();
    /**
     * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集
     */
    private final Set<String> TABLE_NAMES = new HashSet<>();
    @Override
    public Set<String> getTableNames() {
        return TABLE_NAMES;
    }
    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        // 只有有登陆用户的情况下,才进行数据权限的处理
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return null;
        }
        // 只有管理员类型的用户,才进行数据权限的处理
        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
            return null;
        }
        // 获得数据权限
        DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
        // 从上下文中拿不到,则调用逻辑进行获取
        if (deptDataPermission == null) {
            deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()).getCheckedData();
            if (deptDataPermission == null) {
                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
                        loginUser.getId(), tableName, tableAlias.getName()));
            }
            // 添加到上下文中,避免重复计算
            loginUser.setContext(CONTEXT_KEY, deptDataPermission);
        }
        // 情况一,如果是 ALL 可查看全部,则无需拼接条件
        if (deptDataPermission.getAll()) {
            return null;
        }
        // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
        if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
                && Boolean.FALSE.equals(deptDataPermission.getSelf())) {
            return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
        }
        // 情况三,拼接 Dept 和 User 的条件,最后组合
        Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
        Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
        if (deptExpression == null && userExpression == null) {
            // TODO iailab:获得不到条件的时候,暂时不抛出异常,而是不返回数据
            log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
                    JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
//            throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
//                    loginUser.getId(), tableName, tableAlias.getName()));
            return EXPRESSION_NULL;
        }
        if (deptExpression == null) {
            return userExpression;
        }
        if (userExpression == null) {
            return deptExpression;
        }
        // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
        return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression));
    }
    private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
        // 如果不存在配置,则无需作为条件
        String columnName = deptColumns.get(tableName);
        if (StrUtil.isEmpty(columnName)) {
            return null;
        }
        // 如果为空,则无条件
        if (CollUtil.isEmpty(deptIds)) {
            return null;
        }
        // 拼接条件
        return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
                // Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号
                new ParenthesedExpressionList(new ExpressionList<LongValue>(CollectionUtils.convertList(deptIds, LongValue::new))));
    }
    private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {
        // 如果不查看自己,则无需作为条件
        if (Boolean.FALSE.equals(self)) {
            return null;
        }
        String columnName = userColumns.get(tableName);
        if (StrUtil.isEmpty(columnName)) {
            return null;
        }
        // 拼接条件
        return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));
    }
    // ==================== 添加配置 ====================
    public void addDeptColumn(Class<? extends BaseDO> entityClass) {
        addDeptColumn(entityClass, DEPT_COLUMN_NAME);
    }
    public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
        addDeptColumn(tableName, columnName);
    }
    public void addDeptColumn(String tableName, String columnName) {
        deptColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }
    public void addUserColumn(Class<? extends BaseDO> entityClass) {
        addUserColumn(entityClass, USER_COLUMN_NAME);
    }
    public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
        addUserColumn(tableName, columnName);
    }
    public void addUserColumn(String tableName, String columnName) {
        userColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java
对比新文件
@@ -0,0 +1,20 @@
package com.iailab.framework.datapermission.core.rule.dept;
/**
 * {@link DeptDataPermissionRule} 的自定义配置接口
 *
 * @author iailab
 */
@FunctionalInterface
public interface DeptDataPermissionRuleCustomizer {
    /**
     * 自定义该权限规则
     * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则
     * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则
     *
     * @param rule 权限规则
     */
    void customize(DeptDataPermissionRule rule);
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/rule/dept/package-info.java
对比新文件
@@ -0,0 +1,6 @@
/**
 * 基于部门的数据权限规则
 *
 * @author iailab
 */
package com.iailab.framework.datapermission.core.rule.dept;
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/core/util/DataPermissionUtils.java
对比新文件
@@ -0,0 +1,63 @@
package com.iailab.framework.datapermission.core.util;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder;
import lombok.SneakyThrows;
import java.util.concurrent.Callable;
/**
 * 数据权限 Util
 *
 * @author iailab
 */
public class DataPermissionUtils {
    private static DataPermission DATA_PERMISSION_DISABLE;
    @DataPermission(enable = false)
    @SneakyThrows
    private static DataPermission getDisableDataPermissionDisable() {
        if (DATA_PERMISSION_DISABLE == null) {
            DATA_PERMISSION_DISABLE = DataPermissionUtils.class
                    .getDeclaredMethod("getDisableDataPermissionDisable")
                    .getAnnotation(DataPermission.class);
        }
        return DATA_PERMISSION_DISABLE;
    }
    /**
     * 忽略数据权限,执行对应的逻辑
     *
     * @param runnable 逻辑
     */
    public static void executeIgnore(Runnable runnable) {
        DataPermission dataPermission = getDisableDataPermissionDisable();
        DataPermissionContextHolder.add(dataPermission);
        try {
            // 执行 runnable
            runnable.run();
        } finally {
            DataPermissionContextHolder.remove();
        }
    }
    /**
     * 忽略数据权限,执行对应的逻辑
     *
     * @param callable 逻辑
     * @return 执行结果
     */
    @SneakyThrows
    public static <T> T executeIgnore(Callable<T> callable) {
        DataPermission dataPermission = getDisableDataPermissionDisable();
        DataPermissionContextHolder.add(dataPermission);
        try {
            // 执行 callable
            return callable.call();
        } finally {
            DataPermissionContextHolder.remove();
        }
    }
}
iailab-framework/iailab-common-biz-data-permission/src/main/java/com/iailab/framework/datapermission/package-info.java
对比新文件
@@ -0,0 +1,4 @@
/**
 * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件
 */
package com.iailab.framework.datapermission;
iailab-framework/iailab-common-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对比新文件
@@ -0,0 +1,3 @@
com.iailab.framework.datapermission.config.IailabDataPermissionAutoConfiguration
com.iailab.framework.datapermission.config.IailabDeptDataPermissionAutoConfiguration
com.iailab.framework.datapermission.config.IailabDataPermissionRpcAutoConfiguration
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java
对比新文件
@@ -0,0 +1,108 @@
package com.iailab.framework.datapermission.core.aop;
import cn.hutool.core.collection.CollUtil;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
/**
 * {@link DataPermissionAnnotationInterceptor} 的单元测试
 *
 * @author iailab
 */
public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest {
    @InjectMocks
    private DataPermissionAnnotationInterceptor interceptor;
    @Mock
    private MethodInvocation methodInvocation;
    @BeforeEach
    public void setUp() {
        interceptor.getDataPermissionCache().clear();
    }
    @Test // 无 @DataPermission 注解
    public void testInvoke_none() throws Throwable {
        // 参数
        mockMethodInvocation(TestNone.class);
        // 调用
        Object result = interceptor.invoke(methodInvocation);
        // 断言
        assertEquals("none", result);
        assertEquals(1, interceptor.getDataPermissionCache().size());
        assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
    }
    @Test // 在 Method 上有 @DataPermission 注解
    public void testInvoke_method() throws Throwable {
        // 参数
        mockMethodInvocation(TestMethod.class);
        // 调用
        Object result = interceptor.invoke(methodInvocation);
        // 断言
        assertEquals("method", result);
        assertEquals(1, interceptor.getDataPermissionCache().size());
        assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
    }
    @Test // 在 Class 上有 @DataPermission 注解
    public void testInvoke_class() throws Throwable {
        // 参数
        mockMethodInvocation(TestClass.class);
        // 调用
        Object result = interceptor.invoke(methodInvocation);
        // 断言
        assertEquals("class", result);
        assertEquals(1, interceptor.getDataPermissionCache().size());
        assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
    }
    private void mockMethodInvocation(Class<?> clazz) throws Throwable {
        Object targetObject = clazz.newInstance();
        Method method = targetObject.getClass().getMethod("echo");
        when(methodInvocation.getThis()).thenReturn(targetObject);
        when(methodInvocation.getMethod()).thenReturn(method);
        when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject));
    }
    static class TestMethod {
        @DataPermission(enable = false)
        public String echo() {
            return "method";
        }
    }
    @DataPermission(enable = false)
    static class TestClass {
        public String echo() {
            return "class";
        }
    }
    static class TestNone {
        public String echo() {
            return "none";
        }
    }
}
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/aop/DataPermissionContextHolderTest.java
对比新文件
@@ -0,0 +1,66 @@
package com.iailab.framework.datapermission.core.aop;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.mock;
/**
 * {@link DataPermissionContextHolder} 的单元测试
 *
 * @author iailab
 */
class DataPermissionContextHolderTest {
    @BeforeEach
    public void setUp() {
        DataPermissionContextHolder.clear();
    }
    @Test
    public void testGet() {
        // mock 方法
        DataPermission dataPermission01 = mock(DataPermission.class);
        DataPermissionContextHolder.add(dataPermission01);
        DataPermission dataPermission02 = mock(DataPermission.class);
        DataPermissionContextHolder.add(dataPermission02);
        // 调用
        DataPermission result = DataPermissionContextHolder.get();
        // 断言
        assertSame(result, dataPermission02);
    }
    @Test
    public void testPush() {
        // 调用
        DataPermission dataPermission01 = mock(DataPermission.class);
        DataPermissionContextHolder.add(dataPermission01);
        DataPermission dataPermission02 = mock(DataPermission.class);
        DataPermissionContextHolder.add(dataPermission02);
        // 断言
        DataPermission first = DataPermissionContextHolder.getAll().get(0);
        DataPermission second = DataPermissionContextHolder.getAll().get(1);
        assertSame(dataPermission01, first);
        assertSame(dataPermission02, second);
    }
    @Test
    public void testRemove() {
        // mock 方法
        DataPermission dataPermission01 = mock(DataPermission.class);
        DataPermissionContextHolder.add(dataPermission01);
        DataPermission dataPermission02 = mock(DataPermission.class);
        DataPermissionContextHolder.add(dataPermission02);
        // 调用
        DataPermission result = DataPermissionContextHolder.remove();
        // 断言
        assertSame(result, dataPermission02);
        assertEquals(1, DataPermissionContextHolder.getAll().size());
    }
}
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/db/DataPermissionRuleHandlerTest.java
对比新文件
@@ -0,0 +1,540 @@
package com.iailab.framework.datapermission.core.db;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.iailab.framework.datapermission.core.rule.DataPermissionRule;
import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactory;
import com.iailab.framework.mybatis.core.util.MyBatisUtils;
import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.Arrays;
import java.util.Set;
import static com.iailab.framework.common.util.collection.SetUtils.asSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
 * {@link DataPermissionRuleHandler} 的单元测试
 * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试
 * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~
 *
 * @author iailab
 */
public class DataPermissionRuleHandlerTest extends BaseMockitoUnitTest {
    @InjectMocks
    private DataPermissionRuleHandler handler;
    @Mock
    private DataPermissionRuleFactory ruleFactory;
    private DataPermissionInterceptor interceptor;
    @BeforeEach
    public void setUp() {
        interceptor = new DataPermissionInterceptor(handler);
        // 租户的数据权限规则
        DataPermissionRule tenantRule = new DataPermissionRule() {
            private static final String COLUMN = "tenant_id";
            @Override
            public Set<String> getTableNames() {
                return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试
                        "t_user", "t_role"); // 满足自己的单元测试
            }
            @Override
            public Expression getExpression(String tableName, Alias tableAlias) {
                Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
                LongValue value = new LongValue(1L);
                return new EqualsTo(column, value);
            }
        };
        // 部门的数据权限规则
        DataPermissionRule deptRule = new DataPermissionRule() {
            private static final String COLUMN = "dept_id";
            @Override
            public Set<String> getTableNames() {
                return asSet("t_user");  // 满足自己的单元测试
            }
            @Override
            public Expression getExpression(String tableName, Alias tableAlias) {
                Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
                ExpressionList<LongValue> values = new ExpressionList<>(new LongValue(10L),
                        new LongValue(20L));
                return new InExpression(column, new Parenthesis((values)));
            }
        };
        // 设置到上下文
        when(ruleFactory.getDataPermissionRule(any())).thenReturn(Arrays.asList(tenantRule, deptRule));
    }
    @Test
    void delete() {
        assertSql("delete from entity where id = ?",
                "DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1");
    }
    @Test
    void update() {
        assertSql("update entity set name = ? where id = ?",
                "UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1");
    }
    @Test
    void selectSingle() {
        // 单表
        assertSql("select * from entity where id = ?",
                "SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1");
        assertSql("select * from entity where id = ? or name = ?",
                "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
        assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)",
                "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
        /* not */
        assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)",
                "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1");
    }
    @Test
    void selectSubSelectIn() {
        /* in */
        assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
        // 在最前
        assertSql("SELECT * FROM entity e WHERE e.id IN " +
                        "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
                "SELECT * FROM entity e WHERE e.id IN " +
                        "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
        // 在最后
        assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
                        "(select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
                        "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
        // 在中间
        assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
                        "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
                "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
                        "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
    }
    @Test
    void selectSubSelectEq() {
        /* = */
        assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
    }
    @Test
    void selectSubSelectInnerNotEq() {
        /* inner not = */
        assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))",
                "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1");
        assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)",
                "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1");
    }
    @Test
    void selectSubSelectExists() {
        /* EXISTS */
        assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
        /* NOT EXISTS */
        assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
    }
    @Test
    void selectSubSelect() {
        /* >= */
        assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
        /* <= */
        assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
        /* <> */
        assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)",
                "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
    }
    @Test
    void selectFromSelect() {
        assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))",
                "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)");
    }
    @Test
    void selectBodySubSelect() {
        assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1",
                "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1");
    }
    @Test
    void selectLeftJoin() {
        // left join
        assertSql("SELECT * FROM entity e " +
                        "left join entity1 e1 on e1.id = e.id " +
                        "WHERE e.id = ? OR e.name = ?",
                "SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
        assertSql("SELECT * FROM entity e " +
                        "left join entity1 e1 on e1.id = e.id " +
                        "WHERE (e.id = ? OR e.name = ?)",
                "SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
        assertSql("SELECT * FROM entity e " +
                        "left join entity1 e1 on e1.id = e.id " +
                        "left join entity2 e2 on e1.id = e2.id",
                "SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
                        "WHERE e.tenant_id = 1");
    }
    @Test
    void selectRightJoin() {
        // right join
        assertSql("SELECT * FROM entity e " +
                        "right join entity1 e1 on e1.id = e.id",
                "SELECT * FROM entity e " +
                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
                        "WHERE e1.tenant_id = 1");
        assertSql("SELECT * FROM with_as_1 e " +
                        "right join entity1 e1 on e1.id = e.id",
                "SELECT * FROM with_as_1 e " +
                        "RIGHT JOIN entity1 e1 ON e1.id = e.id " +
                        "WHERE e1.tenant_id = 1");
        assertSql("SELECT * FROM entity e " +
                        "right join entity1 e1 on e1.id = e.id " +
                        "WHERE e.id = ? OR e.name = ?",
                "SELECT * FROM entity e " +
                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
        assertSql("SELECT * FROM entity e " +
                        "right join entity1 e1 on e1.id = e.id " +
                        "right join entity2 e2 on e1.id = e2.id ",
                "SELECT * FROM entity e " +
                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
                        "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " +
                        "WHERE e2.tenant_id = 1");
    }
    @Test
    void selectMixJoin() {
        assertSql("SELECT * FROM entity e " +
                        "right join entity1 e1 on e1.id = e.id " +
                        "left join entity2 e2 on e1.id = e2.id",
                "SELECT * FROM entity e " +
                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
                        "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
                        "WHERE e1.tenant_id = 1");
        assertSql("SELECT * FROM entity e " +
                        "left join entity1 e1 on e1.id = e.id " +
                        "right join entity2 e2 on e1.id = e2.id",
                "SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 " +
                        "WHERE e2.tenant_id = 1");
        assertSql("SELECT * FROM entity e " +
                        "left join entity1 e1 on e1.id = e.id " +
                        "inner join entity2 e2 on e1.id = e2.id",
                "SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1");
    }
    @Test
    void selectJoinSubSelect() {
        assertSql("select * from (select * from entity) e1 " +
                        "left join entity2 e2 on e1.id = e2.id",
                "SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " +
                        "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1");
        assertSql("select * from entity1 e1 " +
                        "left join (select * from entity2) e2 " +
                        "on e1.id = e2.id",
                "SELECT * FROM entity1 e1 " +
                        "LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " +
                        "ON e1.id = e2.id " +
                        "WHERE e1.tenant_id = 1");
    }
    @Test
    void selectSubJoin() {
        assertSql("select * FROM " +
                        "(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)",
                "SELECT * FROM " +
                        "(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
                        "WHERE e2.tenant_id = 1");
        assertSql("select * FROM " +
                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)",
                "SELECT * FROM " +
                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
                        "WHERE e1.tenant_id = 1");
        assertSql("select * FROM " +
                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " +
                        "right join entity3 e3 on e1.id = e3.id",
                "SELECT * FROM " +
                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
                        "RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " +
                        "WHERE e3.tenant_id = 1");
        assertSql("select * FROM entity e " +
                        "LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " +
                        "on e.id = e2.id",
                "SELECT * FROM entity e " +
                        "LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
                        "ON e.id = e2.id AND e2.tenant_id = 1 " +
                        "WHERE e.tenant_id = 1");
        assertSql("select * FROM entity e " +
                        "LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
                        "on e.id = e2.id",
                "SELECT * FROM entity e " +
                        "LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
                        "ON e.id = e2.id AND e1.tenant_id = 1 " +
                        "WHERE e.tenant_id = 1");
        assertSql("select * FROM entity e " +
                        "RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
                        "on e.id = e2.id",
                "SELECT * FROM entity e " +
                        "RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
                        "ON e.id = e2.id AND e.tenant_id = 1 " +
                        "WHERE e1.tenant_id = 1");
    }
    @Test
    void selectLeftJoinMultipleTrailingOn() {
        // 多个 on 尾缀的
        assertSql("SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 " +
                        "LEFT JOIN entity2 e2 ON e2.id = e1.id " +
                        "ON e1.id = e.id " +
                        "WHERE (e.id = ? OR e.NAME = ?)",
                "SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 " +
                        "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " +
                        "ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
        assertSql("SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 " +
                        "LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
                        "ON e1.id = e.id " +
                        "WHERE (e.id = ? OR e.NAME = ?)",
                "SELECT * FROM entity e " +
                        "LEFT JOIN entity1 e1 " +
                        "LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
                        "ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
    }
    @Test
    void selectInnerJoin() {
        // inner join
        assertSql("SELECT * FROM entity e " +
                        "inner join entity1 e1 on e1.id = e.id " +
                        "WHERE e.id = ? OR e.name = ?",
                "SELECT * FROM entity e " +
                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
                        "WHERE e.id = ? OR e.name = ?");
        assertSql("SELECT * FROM entity e " +
                        "inner join entity1 e1 on e1.id = e.id " +
                        "WHERE (e.id = ? OR e.name = ?)",
                "SELECT * FROM entity e " +
                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.name = ?)");
        // 隐式内连接
        assertSql("SELECT * FROM entity,entity1 " +
                        "WHERE entity.id = entity1.id",
                "SELECT * FROM entity, entity1 " +
                        "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
        // 隐式内连接
        assertSql("SELECT * FROM entity a, with_as_entity1 b " +
                        "WHERE a.id = b.id",
                "SELECT * FROM entity a, with_as_entity1 b " +
                        "WHERE a.id = b.id AND a.tenant_id = 1");
        assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " +
                        "WHERE a.id = b.id",
                "SELECT * FROM with_as_entity a, with_as_entity1 b " +
                        "WHERE a.id = b.id");
        // SubJoin with 隐式内连接
        assertSql("SELECT * FROM (entity,entity1) " +
                        "WHERE entity.id = entity1.id",
                "SELECT * FROM (entity, entity1) " +
                        "WHERE entity.id = entity1.id " +
                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
        assertSql("SELECT * FROM ((entity,entity1),entity2) " +
                        "WHERE entity.id = entity1.id and entity.id = entity2.id",
                "SELECT * FROM ((entity, entity1), entity2) " +
                        "WHERE entity.id = entity1.id AND entity.id = entity2.id " +
                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
        assertSql("SELECT * FROM (entity,(entity1,entity2)) " +
                        "WHERE entity.id = entity1.id and entity.id = entity2.id",
                "SELECT * FROM (entity, (entity1, entity2)) " +
                        "WHERE entity.id = entity1.id AND entity.id = entity2.id " +
                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
        // 沙雕的括号写法
        assertSql("SELECT * FROM (((entity,entity1))) " +
                        "WHERE entity.id = entity1.id",
                "SELECT * FROM (((entity, entity1))) " +
                        "WHERE entity.id = entity1.id " +
                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
    }
    @Test
    void selectWithAs() {
        assertSql("with with_as_A as (select * from entity) select * from with_as_A",
                "WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A");
    }
    @Test
    void selectIgnoreTable() {
        assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)",
                "SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)");
    }
    private void assertSql(String sql, String targetSql) {
        assertEquals(targetSql, interceptor.parserSingle(sql, null));
    }
    // ========== 额外的测试 ==========
    @Test
    public void testSelectSingle() {
        // 单表
        assertSql("select * from t_user where id = ?",
                "SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
        assertSql("select * from t_user where id = ? or name = ?",
                "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
        assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)",
                "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
        /* not */
        assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)",
                "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
    }
    @Test
    public void testSelectLeftJoin() {
        // left join
        assertSql("SELECT * FROM t_user e " +
                        "left join t_role e1 on e1.id = e.id " +
                        "WHERE e.id = ? OR e.name = ?",
                "SELECT * FROM t_user e " +
                        "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
        // 条件 e.id = ? OR e.name = ? 带括号
        assertSql("SELECT * FROM t_user e " +
                        "left join t_role e1 on e1.id = e.id " +
                        "WHERE (e.id = ? OR e.name = ?)",
                "SELECT * FROM t_user e " +
                        "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
    }
    @Test
    public void testSelectRightJoin() {
        // right join
        assertSql("SELECT * FROM t_user e " +
                        "right join t_role e1 on e1.id = e.id " +
                        "WHERE e.id = ? OR e.name = ?",
                "SELECT * FROM t_user e " +
                        "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
                        "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
        // 条件 e.id = ? OR e.name = ? 带括号
        assertSql("SELECT * FROM t_user e " +
                        "right join t_role e1 on e1.id = e.id " +
                        "WHERE (e.id = ? OR e.name = ?)",
                "SELECT * FROM t_user e " +
                        "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
                        "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
    }
    @Test
    public void testSelectInnerJoin() {
        // inner join
        assertSql("SELECT * FROM t_user e " +
                        "inner join entity1 e1 on e1.id = e.id " +
                        "WHERE e.id = ? OR e.name = ?",
                "SELECT * FROM t_user e " +
                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
                        "WHERE e.id = ? OR e.name = ?");
        // 条件 e.id = ? OR e.name = ? 带括号
        assertSql("SELECT * FROM t_user e " +
                        "inner join entity1 e1 on e1.id = e.id " +
                        "WHERE (e.id = ? OR e.name = ?)",
                "SELECT * FROM t_user e " +
                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
                        "WHERE (e.id = ? OR e.name = ?)");
        // 没有 On 的 inner join
        assertSql("SELECT * FROM entity,entity1 " +
                "WHERE entity.id = entity1.id",
            "SELECT * FROM entity, entity1 " +
                    "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
    }
}
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java
对比新文件
@@ -0,0 +1,145 @@
package com.iailab.framework.datapermission.core.rule;
import com.iailab.framework.datapermission.core.annotation.DataPermission;
import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder;
import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import org.springframework.core.annotation.AnnotationUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import static com.iailab.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
/**
 * {@link DataPermissionRuleFactoryImpl} 单元测试
 *
 * @author iailab
 */
class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest {
    @InjectMocks
    private DataPermissionRuleFactoryImpl dataPermissionRuleFactory;
    @Spy
    private List<DataPermissionRule> rules = Arrays.asList(new DataPermissionRule01(),
            new DataPermissionRule02());
    @BeforeEach
    public void setUp() {
        DataPermissionContextHolder.clear();
    }
    @Test
    public void testGetDataPermissionRule_02() {
        // 准备参数
        String mappedStatementId = randomString();
        // 调用
        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
        // 断言
        assertSame(rules, result);
    }
    @Test
    public void testGetDataPermissionRule_03() {
        // 准备参数
        String mappedStatementId = randomString();
        // mock 方法
        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class));
        // 调用
        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
        // 断言
        assertTrue(result.isEmpty());
    }
    @Test
    public void testGetDataPermissionRule_04() {
        // 准备参数
        String mappedStatementId = randomString();
        // mock 方法
        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class));
        // 调用
        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
        // 断言
        assertEquals(1, result.size());
        assertEquals(DataPermissionRule01.class, result.get(0).getClass());
    }
    @Test
    public void testGetDataPermissionRule_05() {
        // 准备参数
        String mappedStatementId = randomString();
        // mock 方法
        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class));
        // 调用
        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
        // 断言
        assertEquals(1, result.size());
        assertEquals(DataPermissionRule02.class, result.get(0).getClass());
    }
    @Test
    public void testGetDataPermissionRule_06() {
        // 准备参数
        String mappedStatementId = randomString();
        // mock 方法
        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class));
        // 调用
        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
        // 断言
        assertSame(rules, result);
    }
    @DataPermission(enable = false)
    static class TestClass03 {}
    @DataPermission(includeRules = DataPermissionRule01.class)
    static class TestClass04 {}
    @DataPermission(excludeRules = DataPermissionRule01.class)
    static class TestClass05 {}
    @DataPermission
    static class TestClass06 {}
    static class DataPermissionRule01 implements DataPermissionRule {
        @Override
        public Set<String> getTableNames() {
            return null;
        }
        @Override
        public Expression getExpression(String tableName, Alias tableAlias) {
            return null;
        }
    }
    static class DataPermissionRule02 implements DataPermissionRule {
        @Override
        public Set<String> getTableNames() {
            return null;
        }
        @Override
        public Expression getExpression(String tableName, Alias tableAlias) {
            return null;
        }
    }
}
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java
对比新文件
@@ -0,0 +1,239 @@
package com.iailab.framework.datapermission.core.rule.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import com.iailab.framework.common.enums.UserTypeEnum;
import com.iailab.framework.common.util.collection.SetUtils;
import com.iailab.framework.security.core.LoginUser;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
import com.iailab.module.system.api.permission.PermissionApi;
import com.iailab.module.system.api.permission.dto.DeptDataPermissionRespDTO;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import java.util.Map;
import static com.iailab.framework.common.pojo.CommonResult.success;
import static com.iailab.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL;
import static com.iailab.framework.test.core.util.RandomUtils.randomPojo;
import static com.iailab.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
/**
 * {@link DeptDataPermissionRule} 的单元测试
 *
 * @author iailab
 */
class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
    @InjectMocks
    private DeptDataPermissionRule rule;
    @Mock
    private PermissionApi permissionApi;
    @BeforeEach
    @SuppressWarnings("unchecked")
    public void setUp() {
        // 清空 rule
        rule.getTableNames().clear();
        ((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
        ((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
    }
    @Test // 无 LoginUser
    public void testGetExpression_noLoginUser() {
        // 准备参数
        String tableName = randomString();
        Alias tableAlias = new Alias(randomString());
        // mock 方法
        // 调用
        Expression expression = rule.getExpression(tableName, tableAlias);
        // 断言
        assertNull(expression);
    }
    @Test // 无数据权限时
    public void testGetExpression_noDeptDataPermission() {
        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
                     = mockStatic(SecurityFrameworkUtils.class)) {
            // 准备参数
            String tableName = "t_user";
            Alias tableAlias = new Alias("u");
            // mock 方法
            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
                    .setUserType(UserTypeEnum.ADMIN.getValue()));
            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
            // mock 方法(permissionApi 返回 null)
            when(permissionApi.getDeptDataPermission(eq(loginUser.getId()))).thenReturn(success(null));
            // 调用
            NullPointerException exception = assertThrows(NullPointerException.class,
                    () -> rule.getExpression(tableName, tableAlias));
            // 断言
            assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage());
        }
    }
    @Test // 全部数据权限
    public void testGetExpression_allDeptDataPermission() {
        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
                     = mockStatic(SecurityFrameworkUtils.class)) {
            // 准备参数
            String tableName = "t_user";
            Alias tableAlias = new Alias("u");
            // mock 方法(LoginUser)
            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
                    .setUserType(UserTypeEnum.ADMIN.getValue()));
            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
            // mock 方法(DeptDataPermissionRespDTO)
            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true);
            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
            // 调用
            Expression expression = rule.getExpression(tableName, tableAlias);
            // 断言
            assertNull(expression);
            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
        }
    }
    @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限
    public void testGetExpression_noDept_noSelf() {
        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
                     = mockStatic(SecurityFrameworkUtils.class)) {
            // 准备参数
            String tableName = "t_user";
            Alias tableAlias = new Alias("u");
            // mock 方法(LoginUser)
            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
                    .setUserType(UserTypeEnum.ADMIN.getValue()));
            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
            // mock 方法(DeptDataPermissionRespDTO)
            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO();
            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
            // 调用
            Expression expression = rule.getExpression(tableName, tableAlias);
            // 断言
            assertEquals("null = null", expression.toString());
            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
        }
    }
    @Test // 拼接 Dept 和 User 的条件(字段都不符合)
    public void testGetExpression_noDeptColumn_noSelfColumn() {
        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
                     = mockStatic(SecurityFrameworkUtils.class)) {
            // 准备参数
            String tableName = "t_user";
            Alias tableAlias = new Alias("u");
            // mock 方法(LoginUser)
            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
                    .setUserType(UserTypeEnum.ADMIN.getValue()));
            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
            // mock 方法(DeptDataPermissionRespDTO)
            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
                    .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true);
            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
            // 调用
            Expression expression = rule.getExpression(tableName, tableAlias);
            // 断言
            assertSame(EXPRESSION_NULL, expression);
            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
        }
    }
    @Test // 拼接 Dept 和 User 的条件(self 符合)
    public void testGetExpression_noDeptColumn_yesSelfColumn() {
        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
                     = mockStatic(SecurityFrameworkUtils.class)) {
            // 准备参数
            String tableName = "t_user";
            Alias tableAlias = new Alias("u");
            // mock 方法(LoginUser)
            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
                    .setUserType(UserTypeEnum.ADMIN.getValue()));
            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
            // mock 方法(DeptDataPermissionRespDTO)
            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
                    .setSelf(true);
            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
            // 添加 user 字段配置
            rule.addUserColumn("t_user", "id");
            // 调用
            Expression expression = rule.getExpression(tableName, tableAlias);
            // 断言
            assertEquals("u.id = 1", expression.toString());
            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
        }
    }
    @Test // 拼接 Dept 和 User 的条件(dept 符合)
    public void testGetExpression_yesDeptColumn_noSelfColumn() {
        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
                     = mockStatic(SecurityFrameworkUtils.class)) {
            // 准备参数
            String tableName = "t_user";
            Alias tableAlias = new Alias("u");
            // mock 方法(LoginUser)
            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
                    .setUserType(UserTypeEnum.ADMIN.getValue()));
            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
            // mock 方法(DeptDataPermissionRespDTO)
            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
                    .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L));
            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
            // 添加 dept 字段配置
            rule.addDeptColumn("t_user", "dept_id");
            // 调用
            Expression expression = rule.getExpression(tableName, tableAlias);
            // 断言
            assertEquals("u.dept_id IN (10, 20)", expression.toString());
            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
        }
    }
    @Test // 拼接 Dept 和 User 的条件(dept + self 符合)
    public void testGetExpression_yesDeptColumn_yesSelfColumn() {
        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
                     = mockStatic(SecurityFrameworkUtils.class)) {
            // 准备参数
            String tableName = "t_user";
            Alias tableAlias = new Alias("u");
            // mock 方法(LoginUser)
            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
                    .setUserType(UserTypeEnum.ADMIN.getValue()));
            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
            // mock 方法(DeptDataPermissionRespDTO)
            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
                    .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true);
            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
            // 添加 user 字段配置
            rule.addUserColumn("t_user", "id");
            // 添加 dept 字段配置
            rule.addDeptColumn("t_user", "dept_id");
            // 调用
            Expression expression = rule.getExpression(tableName, tableAlias);
            // 断言
            assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString());
            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
        }
    }
}
iailab-framework/iailab-common-biz-data-permission/src/test/java/com/iailab/framework/datapermission/core/util/DataPermissionUtilsTest.java
对比新文件
@@ -0,0 +1,15 @@
package com.iailab.framework.datapermission.core.util;
import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class DataPermissionUtilsTest {
    @Test
    public void testExecuteIgnore() {
        DataPermissionUtils.executeIgnore(() -> assertFalse(DataPermissionContextHolder.get().enable()));
    }
}
iailab-framework/iailab-common-biz-ip/pom.xml
对比新文件
@@ -0,0 +1,54 @@
<?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-biz-ip</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>IP 拓展,支持如下功能:
        1. IP 功能:查询 IP 对应的城市信息
            基于 https://gitee.com/lionsoul/ip2region 实现
        2. 城市功能:查询城市编码对应的城市信息
            基于 https://github.com/modood/Administrative-divisions-of-China 实现
    </description>
    <url>http://172.16.8.100:8888/summary/iailab-plat.git</url>
    <dependencies>
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common</artifactId>
        </dependency>
        <!-- IP地址检索 -->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>ip2region</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
        </dependency>
        <!-- Test 测试相关 -->
        <dependency>
            <groupId>com.iailab</groupId>
            <artifactId>iailab-common-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/Area.java
对比新文件
@@ -0,0 +1,60 @@
package com.iailab.framework.ip.core;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.iailab.framework.ip.core.enums.AreaTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.List;
/**
 * 区域节点,包括国家、省份、城市、地区等信息
 *
 * 数据可见 resources/area.csv 文件
 *
 * @author iailab
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = {"parent"})
public class Area {
    /**
     * 编号 - 全球,即根目录
     */
    public static final Integer ID_GLOBAL = 0;
    /**
     * 编号 - 中国
     */
    public static final Integer ID_CHINA = 1;
    /**
     * 编号
     */
    private Integer id;
    /**
     * 名字
     */
    private String name;
    /**
     * 类型
     *
     * 枚举 {@link AreaTypeEnum}
     */
    private Integer type;
    /**
     * 父节点
     */
    @JsonManagedReference
    private Area parent;
    /**
     * 子节点
     */
    @JsonManagedReference
    private List<Area> children;
}
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/enums/AreaTypeEnum.java
对比新文件
@@ -0,0 +1,39 @@
package com.iailab.framework.ip.core.enums;
import com.iailab.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * 区域类型枚举
 *
 * @author iailab
 */
@AllArgsConstructor
@Getter
public enum AreaTypeEnum implements IntArrayValuable {
    COUNTRY(1, "国家"),
    PROVINCE(2, "省份"),
    CITY(3, "城市"),
    DISTRICT(4, "地区"), // 县、镇、区等
    ;
    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AreaTypeEnum::getType).toArray();
    /**
     * 类型
     */
    private final Integer type;
    /**
     * 名字
     */
    private final String name;
    @Override
    public int[] array() {
        return ARRAYS;
    }
}
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/utils/AreaUtils.java
对比新文件
@@ -0,0 +1,214 @@
package com.iailab.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.csv.CsvRow;
import cn.hutool.core.text.csv.CsvUtil;
import com.iailab.framework.common.util.object.ObjectUtils;
import com.iailab.framework.ip.core.Area;
import com.iailab.framework.ip.core.enums.AreaTypeEnum;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
import static com.iailab.framework.common.util.collection.CollectionUtils.findFirst;
/**
 * 区域工具类
 *
 * @author iailab
 */
@Slf4j
public class AreaUtils {
    /**
     * 初始化 SEARCHER
     */
    @SuppressWarnings("InstantiationOfUtilityClass")
    private final static AreaUtils INSTANCE = new AreaUtils();
    /**
     * Area 内存缓存,提升访问速度
     */
    private static Map<Integer, Area> areas;
    private AreaUtils() {
        long now = System.currentTimeMillis();
        areas = new HashMap<>();
        areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0,
                null, new ArrayList<>()));
        // 从 csv 中加载数据
        List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
        rows.remove(0); // 删除 header
        for (CsvRow row : rows) {
            // 创建 Area 对象
            Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)),
                    null, new ArrayList<>());
            // 添加到 areas 中
            areas.put(area.getId(), area);
        }
        // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
        for (CsvRow row : rows) {
            Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
            Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
            Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
            area.setParent(parent);
            parent.getChildren().add(area);
        }
        log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
    }
    /**
     * 获得指定编号对应的区域
     *
     * @param id 区域编号
     * @return 区域
     */
    public static Area getArea(Integer id) {
        return areas.get(id);
    }
    /**
     * 获得指定区域对应的编号
     *
     * @param pathStr 区域路径,例如说:河南省/石家庄市/新华区
     * @return 区域
     */
    public static Area parseArea(String pathStr) {
        String[] paths = pathStr.split("/");
        Area area = null;
        for (String path : paths) {
            if (area == null) {
                area = findFirst(areas.values(), item -> item.getName().equals(path));
            } else {
                area = findFirst(area.getChildren(), item -> item.getName().equals(path));
            }
        }
        return area;
    }
    /**
     * 获取所有节点的全路径名称如:河南省/石家庄市/新华区
     *
     * @param areas 地区树
     * @return 所有节点的全路径名称
     */
    public static List<String> getAreaNodePathList(List<Area> areas) {
        List<String> paths = new ArrayList<>();
        areas.forEach(area -> getAreaNodePathList(area, "", paths));
        return paths;
    }
    /**
     * 构建一棵树的所有节点的全路径名称,并将其存储为 "祖先/父级/子级" 的形式
     *
     * @param node  父节点
     * @param path  全路径名称
     * @param paths 全路径名称列表,省份/城市/地区
     */
    private static void getAreaNodePathList(Area node, String path, List<String> paths) {
        if (node == null) {
            return;
        }
        // 构建当前节点的路径
        String currentPath = path.isEmpty() ? node.getName() : path + "/" + node.getName();
        paths.add(currentPath);
        // 递归遍历子节点
        for (Area child : node.getChildren()) {
            getAreaNodePathList(child, currentPath, paths);
        }
    }
    /**
     * 格式化区域
     *
     * @param id 区域编号
     * @return 格式化后的区域
     */
    public static String format(Integer id) {
        return format(id, " ");
    }
    /**
     * 格式化区域
     *
     * 例如说:
     * 1. id = “静安区”时:上海 上海市 静安区
     * 2. id = “上海市”时:上海 上海市
     * 3. id = “上海”时:上海
     * 4. id = “美国”时:美国
     * 当区域在中国时,默认不显示中国
     *
     * @param id        区域编号
     * @param separator 分隔符
     * @return 格式化后的区域
     */
    public static String format(Integer id, String separator) {
        // 获得区域
        Area area = areas.get(id);
        if (area == null) {
            return null;
        }
        // 格式化
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < AreaTypeEnum.values().length; i++) { // 避免死循环
            sb.insert(0, area.getName());
            // “递归”父节点
            area = area.getParent();
            if (area == null
                    || ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况
                break;
            }
            sb.insert(0, separator);
        }
        return sb.toString();
    }
    /**
     * 获取指定类型的区域列表
     *
     * @param type 区域类型
     * @param func 转换函数
     * @param <T>  结果类型
     * @return 区域列表
     */
    public static <T> List<T> getByType(AreaTypeEnum type, Function<Area, T> func) {
        return convertList(areas.values(), func, area -> type.getType().equals(area.getType()));
    }
    /**
     * 根据区域编号、上级区域类型,获取上级区域编号
     *
     * @param id   区域编号
     * @param type 区域类型
     * @return 上级区域编号
     */
    public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) {
        for (int i = 0; i < Byte.MAX_VALUE; i++) {
            Area area = AreaUtils.getArea(id);
            if (area == null) {
                return null;
            }
            // 情况一:匹配到,返回它
            if (type.getType().equals(area.getType())) {
                return area.getId();
            }
            // 情况二:找到根节点,返回空
            if (area.getParent() == null || area.getParent().getId() == null) {
                return null;
            }
            // 其它:继续向上查找
            id = area.getParent().getId();
        }
        return null;
    }
}
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/core/utils/IPUtils.java
对比新文件
@@ -0,0 +1,87 @@
package com.iailab.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil;
import com.iailab.framework.ip.core.Area;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.IOException;
/**
 * IP 工具类
 *
 * IP 数据源来自 ip2region.xdb 精简版,基于 <a href="https://gitee.com/zhijiantianya/ip2region"/> 项目
 *
 * @author wanglhup
 */
@Slf4j
public class IPUtils {
    /**
     * 初始化 SEARCHER
     */
    @SuppressWarnings("InstantiationOfUtilityClass")
    private final static IPUtils INSTANCE = new IPUtils();
    /**
     * IP 查询器,启动加载到内存中
     */
    private static Searcher SEARCHER;
    /**
     * 私有化构造
     */
    private IPUtils() {
        try {
            long now = System.currentTimeMillis();
            byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
            SEARCHER = Searcher.newWithBuffer(bytes);
            log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
        } catch (IOException e) {
            log.error("启动加载 IPUtils 失败", e);
        }
    }
    /**
     * 查询 IP 对应的地区编号
     *
     * @param ip IP 地址,格式为 127.0.0.1
     * @return 地区id
     */
    @SneakyThrows
    public static Integer getAreaId(String ip) {
        return Integer.parseInt(SEARCHER.search(ip.trim()));
    }
    /**
     * 查询 IP 对应的地区编号
     *
     * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回
     * @return 地区编号
     */
    @SneakyThrows
    public static Integer getAreaId(long ip) {
        return Integer.parseInt(SEARCHER.search(ip));
    }
    /**
     * 查询 IP 对应的地区
     *
     * @param ip IP 地址,格式为 127.0.0.1
     * @return 地区
     */
    public static Area getArea(String ip) {
        return AreaUtils.getArea(getAreaId(ip));
    }
    /**
     * 查询 IP 对应的地区
     *
     * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回
     * @return 地区
     */
    public static Area getArea(long ip) {
        return AreaUtils.getArea(getAreaId(ip));
    }
}
iailab-framework/iailab-common-biz-ip/src/main/java/com/iailab/framework/ip/package-info.java
对比新文件
@@ -0,0 +1,11 @@
/**
 * IP 拓展,支持如下功能:
 *
 * 1. IP 功能:查询 IP 对应的城市信息
 *      基于 https://gitee.com/lionsoul/ip2region 实现
 * 2. 城市功能:查询城市编码对应的城市信息
 *      基于 https://github.com/modood/Administrative-divisions-of-China 实现
 *
 * @author iailab
 */
package com.iailab.framework.ip;
iailab-framework/iailab-common-biz-ip/src/main/resources/area.csv
对比新文件
@@ -0,0 +1,3662 @@
id,name,type,parentId
1,中国,1,0
2,蒙古,1,0
3,朝鲜,1,0
4,韩国,1,0
5,日本,1,0
6,菲律宾,1,0
7,越南,1,0
8,老挝,1,0
9,柬埔寨,1,0
10,缅甸,1,0
11,泰国,1,0
12,马来西亚,1,0
13,文莱,1,0
14,新加坡,1,0
15,印度尼西亚,1,0
16,东帝汶,1,0
17,尼泊尔,1,0
18,不丹,1,0
19,孟加拉国,1,0
20,印度,1,0
21,巴基斯坦,1,0
22,斯里兰卡,1,0
23,马尔代夫,1,0
24,哈萨克斯坦,1,0
25,吉尔吉斯斯坦,1,0
26,塔吉克斯坦,1,0
27,乌兹别克斯坦,1,0
28,土库曼斯坦,1,0
29,阿富汗,1,0
30,伊拉克,1,0
31,伊朗,1,0
32,叙利亚,1,0
33,约旦,1,0
34,黎巴嫩,1,0
35,以色列,1,0
36,巴勒斯坦,1,0
37,沙特阿拉伯,1,0
38,巴林,1,0
39,卡塔尔,1,0
40,科威特,1,0
41,阿拉伯联合酋长国,1,0
42,阿曼,1,0
43,也门,1,0
44,格鲁吉亚,1,0
45,亚美尼亚,1,0
46,阿塞拜疆,1,0
47,土耳其,1,0
48,塞浦路斯,1,0
49,芬兰,1,0
50,瑞典,1,0
51,挪威,1,0
52,冰岛,1,0
53,丹麦,1,0
54,爱沙尼亚,1,0
55,拉脱维亚,1,0
56,立陶宛,1,0
57,白俄罗斯,1,0
58,俄罗斯,1,0
59,乌克兰,1,0
60,摩尔多瓦,1,0
61,波兰,1,0
62,捷克,1,0
63,斯洛伐克,1,0
64,匈牙利,1,0
65,德国,1,0
66,奥地利,1,0
67,瑞士,1,0
68,列支敦士登,1,0
69,英国,1,0
70,爱尔兰,1,0
71,荷兰,1,0
72,比利时,1,0
73,卢森堡,1,0
74,法国,1,0
75,摩纳哥,1,0
76,罗马尼亚,1,0
77,保加利亚,1,0
78,塞尔维亚,1,0
79,马其顿,1,0
80,阿尔巴尼亚,1,0
81,希腊,1,0
82,斯洛文尼亚,1,0
83,克罗地亚,1,0
84,波斯尼亚和墨塞哥维那,1,0
85,意大利,1,0
86,梵蒂冈,1,0
87,圣马力诺,1,0
88,马耳他,1,0
89,西班牙,1,0
90,葡萄牙,1,0
91,安道尔共和国,1,0
92,埃及,1,0
93,利比亚,1,0
94,苏丹,1,0
95,突尼斯,1,0
96,阿尔及利亚,1,0
97,摩洛哥,1,0
98,亚速尔群岛,1,0
99,马德拉群岛,1,0
100,埃塞俄比亚,1,0
101,厄立特里亚,1,0
102,索马里,1,0
103,吉布提,1,0
104,肯尼亚,1,0
105,坦桑尼亚,1,0
106,乌干达,1,0
107,卢旺达,1,0
108,布隆迪,1,0
109,塞舌尔,1,0
110,圣多美及普林西比,1,0
111,塞内加尔,1,0
112,冈比亚,1,0
113,马里,1,0
114,布基纳法索,1,0
115,几内亚,1,0
116,几内亚比绍,1,0
117,佛得角,1,0
118,塞拉利昂,1,0
119,利比里亚,1,0
120,科特迪瓦,1,0
121,加纳,1,0
122,多哥,1,0
123,贝宁,1,0
124,尼日尔,1,0
125,加那利群岛,1,0
126,赞比亚,1,0
127,安哥拉,1,0
128,津巴布韦,1,0
129,马拉维,1,0
130,莫桑比克,1,0
131,博茨瓦纳,1,0
132,纳米比亚,1,0
133,南非,1,0
134,斯威士兰,1,0
135,莱索托,1,0
136,马达加斯加,1,0
137,科摩罗,1,0
138,毛里求斯,1,0
139,留尼旺,1,0
140,圣赫勒拿,1,0
141,澳大利亚,1,0
142,新西兰,1,0
143,巴布亚新几内亚,1,0
144,所罗门群岛,1,0
145,瓦努阿图共和国,1,0
146,密克罗尼西亚,1,0
147,马绍尔群岛,1,0
148,帕劳,1,0
149,瑙鲁,1,0
150,基里巴斯,1,0
151,图瓦卢,1,0
152,萨摩亚,1,0
153,斐济,1,0
154,汤加,1,0
155,库克群岛,1,0
156,关岛,1,0
157,新喀里多尼亚,1,0
158,法属波利尼西亚,1,0
159,皮特凯恩岛,1,0
160,瓦利斯与富图纳,1,0
161,纽埃,1,0
162,托克劳,1,0
163,美属萨摩亚,1,0
164,北马里亚纳,1,0
165,加拿大,1,0
166,美国,1,0
167,墨西哥,1,0
168,格陵兰,1,0
169,危地马拉,1,0
170,伯利兹,1,0
171,萨尔瓦多,1,0
172,洪都拉斯,1,0
173,尼加拉瓜,1,0
174,哥斯达黎加,1,0
175,巴拿马,1,0
176,巴哈马,1,0
177,古巴,1,0
178,牙买加,1,0
179,海地,1,0
180,多米尼加共和国,1,0
181,安提瓜和巴布达,1,0
182,圣基茨和尼维斯,1,0
183,多米尼克,1,0
184,圣卢西亚,1,0
185,圣文森特和格林纳丁斯,1,0
186,格林纳达,1,0
187,巴巴多斯,1,0
188,特立尼达和多巴哥,1,0
189,波多黎各,1,0
190,英属维尔京群岛,1,0
191,美属维尔京群岛,1,0
192,安圭拉,1,0
193,蒙特塞拉特岛,1,0
194,瓜德罗普,1,0
195,马提尼克,1,0
196,荷属安的列斯,1,0
197,阿鲁巴,1,0
198,特克斯和凯科斯群岛,1,0
199,开曼群岛,1,0
200,百慕大,1,0
201,哥伦比亚,1,0
202,委内瑞拉,1,0
203,圭亚那,1,0
204,法属圭亚那,1,0
205,苏里南,1,0
206,厄瓜多尔,1,0
207,秘鲁,1,0
208,玻利维亚,1,0
209,巴西,1,0
210,智利,1,0
211,阿根廷,1,0
212,乌拉圭,1,0
213,巴拉圭,1,0
214,波黑,1,0
215,直布罗陀,1,0
216,新喀里多尼亚群岛,1,0
217,瓦利斯和富图纳群岛,1,0
218,泽西岛,1,0
219,黑山,1,0
220,英属马恩岛,1,0
221,尼日利亚,1,0
222,喀麦隆,1,0
223,加蓬,1,0
224,乍得,1,0
225,刚果共和国,1,0
226,中非共和国,1,0
227,南苏丹,1,0
228,赤道几内亚,1,0
229,毛里塔尼亚,1,0
230,刚果民主共和国,1,0
231,留尼汪岛,1,0
232,格陵兰岛,1,0
233,法罗群岛,1,0
234,根西岛,1,0
235,百慕大群岛,1,0
236,圣皮埃尔和密克隆群岛,1,0
237,法属圣马丁,1,0
238,奥兰群岛,1,0
239,北马里亚纳群岛,1,0
240,库拉索,1,0
241,博内尔岛,1,0
242,圣马丁岛,1,0
243,圣巴泰勒米岛,1,0
244,福克兰群岛,1,0
245,圣多美和普林西比,1,0
246,英属印度洋领地,1,0
247,东萨摩亚,1,0
248,诺福克岛,1,0
110000,北京,2,1
120000,天津,2,1
130000,河北省,2,1
140000,山西省,2,1
150000,内蒙古自治区,2,1
210000,辽宁省,2,1
220000,吉林省,2,1
230000,黑龙江省,2,1
310000,上海,2,1
320000,江苏省,2,1
330000,浙江省,2,1
340000,安徽省,2,1
350000,福建省,2,1
360000,江西省,2,1
370000,山东省,2,1
410000,河南省,2,1
420000,湖北省,2,1
430000,湖南省,2,1
440000,广东省,2,1
450000,广西壮族自治区,2,1
460000,海南省,2,1
500000,重庆,2,1
510000,四川省,2,1
520000,贵州省,2,1
530000,云南省,2,1
540000,西藏自治区,2,1
610000,陕西省,2,1
620000,甘肃省,2,1
630000,青海省,2,1
640000,宁夏回族自治区,2,1
650000,新疆维吾尔自治区,2,1
110100,北京市,3,110000
120100,天津市,3,120000
130100,石家庄市,3,130000
130200,唐山市,3,130000
130300,秦皇岛市,3,130000
130400,邯郸市,3,130000
130500,邢台市,3,130000
130600,保定市,3,130000
130700,张家口市,3,130000
130800,承德市,3,130000
130900,沧州市,3,130000
131000,廊坊市,3,130000
131100,衡水市,3,130000
140100,太原市,3,140000
140200,大同市,3,140000
140300,阳泉市,3,140000
140400,长治市,3,140000
140500,晋城市,3,140000
140600,朔州市,3,140000
140700,晋中市,3,140000
140800,运城市,3,140000
140900,忻州市,3,140000
141000,临汾市,3,140000
141100,吕梁市,3,140000
150100,呼和浩特市,3,150000
150200,包头市,3,150000
150300,乌海市,3,150000
150400,赤峰市,3,150000
150500,通辽市,3,150000
150600,鄂尔多斯市,3,150000
150700,呼伦贝尔市,3,150000
150800,巴彦淖尔市,3,150000
150900,乌兰察布市,3,150000
152200,兴安盟,3,150000
152500,锡林郭勒盟,3,150000
152900,阿拉善盟,3,150000
210100,沈阳市,3,210000
210200,大连市,3,210000
210300,鞍山市,3,210000
210400,抚顺市,3,210000
210500,本溪市,3,210000
210600,丹东市,3,210000
210700,锦州市,3,210000
210800,营口市,3,210000
210900,阜新市,3,210000
211000,辽阳市,3,210000
211100,盘锦市,3,210000
211200,铁岭市,3,210000
211300,朝阳市,3,210000
211400,葫芦岛市,3,210000
220100,长春市,3,220000
220200,吉林市,3,220000
220300,四平市,3,220000
220400,辽源市,3,220000
220500,通化市,3,220000
220600,白山市,3,220000
220700,松原市,3,220000
220800,白城市,3,220000
222400,延边朝鲜族自治州,3,220000
230100,哈尔滨市,3,230000
230200,齐齐哈尔市,3,230000
230300,鸡西市,3,230000
230400,鹤岗市,3,230000
230500,双鸭山市,3,230000
230600,大庆市,3,230000
230700,伊春市,3,230000
230800,佳木斯市,3,230000
230900,七台河市,3,230000
231000,牡丹江市,3,230000
231100,黑河市,3,230000
231200,绥化市,3,230000
232700,大兴安岭地区,3,230000
310100,上海市,3,310000
320100,南京市,3,320000
320200,无锡市,3,320000
320300,徐州市,3,320000
320400,常州市,3,320000
320500,苏州市,3,320000
320600,南通市,3,320000
320700,连云港市,3,320000
320800,淮安市,3,320000
320900,盐城市,3,320000
321000,扬州市,3,320000
321100,镇江市,3,320000
321200,泰州市,3,320000
321300,宿迁市,3,320000
330100,杭州市,3,330000
330200,宁波市,3,330000
330300,温州市,3,330000
330400,嘉兴市,3,330000
330500,湖州市,3,330000
330600,绍兴市,3,330000
330700,金华市,3,330000
330800,衢州市,3,330000
330900,舟山市,3,330000
331000,台州市,3,330000
331100,丽水市,3,330000
340100,合肥市,3,340000
340200,芜湖市,3,340000
340300,蚌埠市,3,340000
340400,淮南市,3,340000
340500,马鞍山市,3,340000
340600,淮北市,3,340000
340700,铜陵市,3,340000
340800,安庆市,3,340000
341000,黄山市,3,340000
341100,滁州市,3,340000
341200,阜阳市,3,340000
341300,宿州市,3,340000
341500,六安市,3,340000
341600,亳州市,3,340000
341700,池州市,3,340000
341800,宣城市,3,340000
350100,福州市,3,350000
350200,厦门市,3,350000
350300,莆田市,3,350000
350400,三明市,3,350000
350500,泉州市,3,350000
350600,漳州市,3,350000
350700,南平市,3,350000
350800,龙岩市,3,350000
350900,宁德市,3,350000
360100,南昌市,3,360000
360200,景德镇市,3,360000
360300,萍乡市,3,360000
360400,九江市,3,360000
360500,新余市,3,360000
360600,鹰潭市,3,360000
360700,赣州市,3,360000
360800,吉安市,3,360000
360900,宜春市,3,360000
361000,抚州市,3,360000
361100,上饶市,3,360000
370100,济南市,3,370000
370200,青岛市,3,370000
370300,淄博市,3,370000
370400,枣庄市,3,370000
370500,东营市,3,370000
370600,烟台市,3,370000
370700,潍坊市,3,370000
370800,济宁市,3,370000
370900,泰安市,3,370000
371000,威海市,3,370000
371100,日照市,3,370000
371300,临沂市,3,370000
371400,德州市,3,370000
371500,聊城市,3,370000
371600,滨州市,3,370000
371700,菏泽市,3,370000
410100,郑州市,3,410000
410200,开封市,3,410000
410300,洛阳市,3,410000
410400,平顶山市,3,410000
410500,安阳市,3,410000
410600,鹤壁市,3,410000
410700,新乡市,3,410000
410800,焦作市,3,410000
410900,濮阳市,3,410000
411000,许昌市,3,410000
411100,漯河市,3,410000
411200,三门峡市,3,410000
411300,南阳市,3,410000
411400,商丘市,3,410000
411500,信阳市,3,410000
411600,周口市,3,410000
411700,驻马店市,3,410000
419000,省直辖县级行政区划,3,410000
420100,武汉市,3,420000
420200,黄石市,3,420000
420300,十堰市,3,420000
420500,宜昌市,3,420000
420600,襄阳市,3,420000
420700,鄂州市,3,420000
420800,荆门市,3,420000
420900,孝感市,3,420000
421000,荆州市,3,420000
421100,黄冈市,3,420000
421200,咸宁市,3,420000
421300,随州市,3,420000
422800,恩施土家族苗族自治州,3,420000
429000,省直辖县级行政区划,3,420000
430100,长沙市,3,430000
430200,株洲市,3,430000
430300,湘潭市,3,430000
430400,衡阳市,3,430000
430500,邵阳市,3,430000
430600,岳阳市,3,430000
430700,常德市,3,430000
430800,张家界市,3,430000
430900,益阳市,3,430000
431000,郴州市,3,430000
431100,永州市,3,430000
431200,怀化市,3,430000
431300,娄底市,3,430000
433100,湘西土家族苗族自治州,3,430000
440100,广州市,3,440000
440200,韶关市,3,440000
440300,深圳市,3,440000
440400,珠海市,3,440000
440500,汕头市,3,440000
440600,佛山市,3,440000
440700,江门市,3,440000
440800,湛江市,3,440000
440900,茂名市,3,440000
441200,肇庆市,3,440000
441300,惠州市,3,440000
441400,梅州市,3,440000
441500,汕尾市,3,440000
441600,河源市,3,440000
441700,阳江市,3,440000
441800,清远市,3,440000
441900,东莞市,3,440000
441901,莞城区,4,441900
441902,南城区,4,441900
441904,万江区,4,441900
441905,石碣镇,4,441900
441906,石龙镇,4,441900
441907,茶山镇,4,441900
441908,石排镇,4,441900
441909,企石镇,4,441900
441910,横沥镇,4,441900
441911,桥头镇,4,441900
441912,谢岗镇,4,441900
441913,东坑镇,4,441900
441914,常平镇,4,441900
441915,寮步镇,4,441900
441916,大朗镇,4,441900
441917,麻涌镇,4,441900
441918,中堂镇,4,441900
441919,高埗镇,4,441900
441920,樟木头镇,4,441900
441921,大岭山镇,4,441900
441922,望牛墩镇,4,441900
441923,黄江镇,4,441900
441924,洪梅镇,4,441900
441925,清溪镇,4,441900
441926,沙田镇,4,441900
441927,道滘镇,4,441900
441928,塘厦镇,4,441900
441929,虎门镇,4,441900
441930,厚街镇,4,441900
441931,凤岗镇,4,441900
441932,长安镇,4,441900
442000,中山市,3,440000
442001,石岐街道,4,442000
442002,东区街道,4,442000
442003,中山港街道,4,442000
442004,西区街道,4,442000
442005,南区街道,4,442000
442006,五桂山街道,4,442000
442007,民众街道,4,442000
442008,南朗街道,4,442000
442009,黄圃镇,4,442000
442010,东凤镇,4,442000
442011,古镇镇,4,442000
442012,沙溪镇,4,442000
442013,坦洲镇,4,442000
442014,港口镇,4,442000
442015,三角镇,4,442000
442016,横栏镇,4,442000
442017,南头镇,4,442000
442018,阜沙镇,4,442000
442019,三乡镇,4,442000
442020,板芙镇,4,442000
442021,大涌镇,4,442000
442022,神湾镇,4,442000
442023,小榄镇,4,442000
445100,潮州市,3,440000
445200,揭阳市,3,440000
445300,云浮市,3,440000
450100,南宁市,3,450000
450200,柳州市,3,450000
450300,桂林市,3,450000
450400,梧州市,3,450000
450500,北海市,3,450000
450600,防城港市,3,450000
450700,钦州市,3,450000
450800,贵港市,3,450000
450900,玉林市,3,450000
451000,百色市,3,450000
451100,贺州市,3,450000
451200,河池市,3,450000
451300,来宾市,3,450000
451400,崇左市,3,450000
460100,海口市,3,460000
460200,三亚市,3,460000
460300,三沙市,3,460000
460400,儋州市,3,460000
469000,省直辖县级行政区划,3,460000
500100,重庆市,3,500000
510100,成都市,3,510000
510300,自贡市,3,510000
510400,攀枝花市,3,510000
510500,泸州市,3,510000
510600,德阳市,3,510000
510700,绵阳市,3,510000
510800,广元市,3,510000
510900,遂宁市,3,510000
511000,内江市,3,510000
511100,乐山市,3,510000
511300,南充市,3,510000
511400,眉山市,3,510000
511500,宜宾市,3,510000
511600,广安市,3,510000
511700,达州市,3,510000
511800,雅安市,3,510000
511900,巴中市,3,510000
512000,资阳市,3,510000
513200,阿坝藏族羌族自治州,3,510000
513300,甘孜藏族自治州,3,510000
513400,凉山彝族自治州,3,510000
520100,贵阳市,3,520000
520200,六盘水市,3,520000
520300,遵义市,3,520000
520400,安顺市,3,520000
520500,毕节市,3,520000
520600,铜仁市,3,520000
522300,黔西南布依族苗族自治州,3,520000
522600,黔东南苗族侗族自治州,3,520000
522700,黔南布依族苗族自治州,3,520000
530100,昆明市,3,530000
530300,曲靖市,3,530000
530400,玉溪市,3,530000
530500,保山市,3,530000
530600,昭通市,3,530000
530700,丽江市,3,530000
530800,普洱市,3,530000
530900,临沧市,3,530000
532300,楚雄彝族自治州,3,530000
532500,红河哈尼族彝族自治州,3,530000
532600,文山壮族苗族自治州,3,530000
532800,西双版纳傣族自治州,3,530000
532900,大理白族自治州,3,530000
533100,德宏傣族景颇族自治州,3,530000
533300,怒江傈僳族自治州,3,530000
533400,迪庆藏族自治州,3,530000
540100,拉萨市,3,540000
540200,日喀则市,3,540000
540300,昌都市,3,540000
540400,林芝市,3,540000
540500,山南市,3,540000
540600,那曲市,3,540000
542500,阿里地区,3,540000
610100,西安市,3,610000
610200,铜川市,3,610000
610300,宝鸡市,3,610000
610400,咸阳市,3,610000
610500,渭南市,3,610000
610600,延安市,3,610000
610700,汉中市,3,610000
610800,榆林市,3,610000
610900,安康市,3,610000
611000,商洛市,3,610000
620100,兰州市,3,620000
620200,嘉峪关市,3,620000
620300,金昌市,3,620000
620400,白银市,3,620000
620500,天水市,3,620000
620600,武威市,3,620000
620700,张掖市,3,620000
620800,平凉市,3,620000
620900,酒泉市,3,620000
621000,庆阳市,3,620000
621100,定西市,3,620000
621200,陇南市,3,620000
622900,临夏回族自治州,3,620000
623000,甘南藏族自治州,3,620000
630100,西宁市,3,630000
630200,海东市,3,630000
632200,海北藏族自治州,3,630000
632300,黄南藏族自治州,3,630000
632500,海南藏族自治州,3,630000
632600,果洛藏族自治州,3,630000
632700,玉树藏族自治州,3,630000
632800,海西蒙古族藏族自治州,3,630000
640100,银川市,3,640000
640200,石嘴山市,3,640000
640300,吴忠市,3,640000
640400,固原市,3,640000
640500,中卫市,3,640000
650100,乌鲁木齐市,3,650000
650200,克拉玛依市,3,650000
650400,吐鲁番市,3,650000
650500,哈密市,3,650000
652300,昌吉回族自治州,3,650000
652700,博尔塔拉蒙古自治州,3,650000
652800,巴音郭楞蒙古自治州,3,650000
652900,阿克苏地区,3,650000
653000,克孜勒苏柯尔克孜自治州,3,650000
653100,喀什地区,3,650000
653200,和田地区,3,650000
654000,伊犁哈萨克自治州,3,650000
654200,塔城地区,3,650000
654300,阿勒泰地区,3,650000
659000,自治区直辖县级行政区划,3,650000
110101,东城区,4,110100
110102,西城区,4,110100
110105,朝阳区,4,110100
110106,丰台区,4,110100
110107,石景山区,4,110100
110108,海淀区,4,110100
110109,门头沟区,4,110100
110111,房山区,4,110100
110112,通州区,4,110100
110113,顺义区,4,110100
110114,昌平区,4,110100
110115,大兴区,4,110100
110116,怀柔区,4,110100
110117,平谷区,4,110100
110118,密云区,4,110100
110119,延庆区,4,110100
120101,和平区,4,120100
120102,河东区,4,120100
120103,河西区,4,120100
120104,南开区,4,120100
120105,河北区,4,120100
120106,红桥区,4,120100
120110,东丽区,4,120100
120111,西青区,4,120100
120112,津南区,4,120100
120113,北辰区,4,120100
120114,武清区,4,120100
120115,宝坻区,4,120100
120116,滨海新区,4,120100
120117,宁河区,4,120100
120118,静海区,4,120100
120119,蓟州区,4,120100
130102,长安区,4,130100
130104,桥西区,4,130100
130105,新华区,4,130100
130107,井陉矿区,4,130100
130108,裕华区,4,130100
130109,藁城区,4,130100
130110,鹿泉区,4,130100
130111,栾城区,4,130100
130121,井陉县,4,130100
130123,正定县,4,130100
130125,行唐县,4,130100
130126,灵寿县,4,130100
130127,高邑县,4,130100
130128,深泽县,4,130100
130129,赞皇县,4,130100
130130,无极县,4,130100
130131,平山县,4,130100
130132,元氏县,4,130100
130133,赵县,4,130100
130171,石家庄高新技术产业开发区,4,130100
130172,石家庄循环化工园区,4,130100
130181,辛集市,4,130100
130183,晋州市,4,130100
130184,新乐市,4,130100
130202,路南区,4,130200
130203,路北区,4,130200
130204,古冶区,4,130200
130205,开平区,4,130200
130207,丰南区,4,130200
130208,丰润区,4,130200
130209,曹妃甸区,4,130200
130224,滦南县,4,130200
130225,乐亭县,4,130200
130227,迁西县,4,130200
130229,玉田县,4,130200
130271,河北唐山芦台经济开发区,4,130200
130272,唐山市汉沽管理区,4,130200
130273,唐山高新技术产业开发区,4,130200
130274,河北唐山海港经济开发区,4,130200
130281,遵化市,4,130200
130283,迁安市,4,130200
130284,滦州市,4,130200
130302,海港区,4,130300
130303,山海关区,4,130300
130304,北戴河区,4,130300
130306,抚宁区,4,130300
130321,青龙满族自治县,4,130300
130322,昌黎县,4,130300
130324,卢龙县,4,130300
130371,秦皇岛市经济技术开发区,4,130300
130372,北戴河新区,4,130300
130402,邯山区,4,130400
130403,丛台区,4,130400
130404,复兴区,4,130400
130406,峰峰矿区,4,130400
130407,肥乡区,4,130400
130408,永年区,4,130400
130423,临漳县,4,130400
130424,成安县,4,130400
130425,大名县,4,130400
130426,涉县,4,130400
130427,磁县,4,130400
130430,邱县,4,130400
130431,鸡泽县,4,130400
130432,广平县,4,130400
130433,馆陶县,4,130400
130434,魏县,4,130400
130435,曲周县,4,130400
130471,邯郸经济技术开发区,4,130400
130473,邯郸冀南新区,4,130400
130481,武安市,4,130400
130502,襄都区,4,130500
130503,信都区,4,130500
130505,任泽区,4,130500
130506,南和区,4,130500
130522,临城县,4,130500
130523,内丘县,4,130500
130524,柏乡县,4,130500
130525,隆尧县,4,130500
130528,宁晋县,4,130500
130529,巨鹿县,4,130500
130530,新河县,4,130500
130531,广宗县,4,130500
130532,平乡县,4,130500
130533,威县,4,130500
130534,清河县,4,130500
130535,临西县,4,130500
130571,河北邢台经济开发区,4,130500
130581,南宫市,4,130500
130582,沙河市,4,130500
130602,竞秀区,4,130600
130606,莲池区,4,130600
130607,满城区,4,130600
130608,清苑区,4,130600
130609,徐水区,4,130600
130623,涞水县,4,130600
130624,阜平县,4,130600
130626,定兴县,4,130600
130627,唐县,4,130600
130628,高阳县,4,130600
130629,容城县,4,130600
130630,涞源县,4,130600
130631,望都县,4,130600
130632,安新县,4,130600
130633,易县,4,130600
130634,曲阳县,4,130600
130635,蠡县,4,130600
130636,顺平县,4,130600
130637,博野县,4,130600
130638,雄县,4,130600
130671,保定高新技术产业开发区,4,130600
130672,保定白沟新城,4,130600
130681,涿州市,4,130600
130682,定州市,4,130600
130683,安国市,4,130600
130684,高碑店市,4,130600
130702,桥东区,4,130700
130703,桥西区,4,130700
130705,宣化区,4,130700
130706,下花园区,4,130700
130708,万全区,4,130700
130709,崇礼区,4,130700
130722,张北县,4,130700
130723,康保县,4,130700
130724,沽源县,4,130700
130725,尚义县,4,130700
130726,蔚县,4,130700
130727,阳原县,4,130700
130728,怀安县,4,130700
130730,怀来县,4,130700
130731,涿鹿县,4,130700
130732,赤城县,4,130700
130771,张家口经济开发区,4,130700
130772,张家口市察北管理区,4,130700
130773,张家口市塞北管理区,4,130700
130802,双桥区,4,130800
130803,双滦区,4,130800
130804,鹰手营子矿区,4,130800
130821,承德县,4,130800
130822,兴隆县,4,130800
130824,滦平县,4,130800
130825,隆化县,4,130800
130826,丰宁满族自治县,4,130800
130827,宽城满族自治县,4,130800
130828,围场满族蒙古族自治县,4,130800
130871,承德高新技术产业开发区,4,130800
130881,平泉市,4,130800
130902,新华区,4,130900
130903,运河区,4,130900
130921,沧县,4,130900
130922,青县,4,130900
130923,东光县,4,130900
130924,海兴县,4,130900
130925,盐山县,4,130900
130926,肃宁县,4,130900
130927,南皮县,4,130900
130928,吴桥县,4,130900
130929,献县,4,130900
130930,孟村回族自治县,4,130900
130971,河北沧州经济开发区,4,130900
130972,沧州高新技术产业开发区,4,130900
130973,沧州渤海新区,4,130900
130981,泊头市,4,130900
130982,任丘市,4,130900
130983,黄骅市,4,130900
130984,河间市,4,130900
131002,安次区,4,131000
131003,广阳区,4,131000
131022,固安县,4,131000
131023,永清县,4,131000
131024,香河县,4,131000
131025,大城县,4,131000
131026,文安县,4,131000
131028,大厂回族自治县,4,131000
131071,廊坊经济技术开发区,4,131000
131081,霸州市,4,131000
131082,三河市,4,131000
131102,桃城区,4,131100
131103,冀州区,4,131100
131121,枣强县,4,131100
131122,武邑县,4,131100
131123,武强县,4,131100
131124,饶阳县,4,131100
131125,安平县,4,131100
131126,故城县,4,131100
131127,景县,4,131100
131128,阜城县,4,131100
131171,河北衡水高新技术产业开发区,4,131100
131172,衡水滨湖新区,4,131100
131182,深州市,4,131100
140105,小店区,4,140100
140106,迎泽区,4,140100
140107,杏花岭区,4,140100
140108,尖草坪区,4,140100
140109,万柏林区,4,140100
140110,晋源区,4,140100
140121,清徐县,4,140100
140122,阳曲县,4,140100
140123,娄烦县,4,140100
140171,山西转型综合改革示范区,4,140100
140181,古交市,4,140100
140212,新荣区,4,140200
140213,平城区,4,140200
140214,云冈区,4,140200
140215,云州区,4,140200
140221,阳高县,4,140200
140222,天镇县,4,140200
140223,广灵县,4,140200
140224,灵丘县,4,140200
140225,浑源县,4,140200
140226,左云县,4,140200
140271,山西大同经济开发区,4,140200
140302,城区,4,140300
140303,矿区,4,140300
140311,郊区,4,140300
140321,平定县,4,140300
140322,盂县,4,140300
140403,潞州区,4,140400
140404,上党区,4,140400
140405,屯留区,4,140400
140406,潞城区,4,140400
140423,襄垣县,4,140400
140425,平顺县,4,140400
140426,黎城县,4,140400
140427,壶关县,4,140400
140428,长子县,4,140400
140429,武乡县,4,140400
140430,沁县,4,140400
140431,沁源县,4,140400
140471,山西长治高新技术产业园区,4,140400
140502,城区,4,140500
140521,沁水县,4,140500
140522,阳城县,4,140500
140524,陵川县,4,140500
140525,泽州县,4,140500
140581,高平市,4,140500
140602,朔城区,4,140600
140603,平鲁区,4,140600
140621,山阴县,4,140600
140622,应县,4,140600
140623,右玉县,4,140600
140671,山西朔州经济开发区,4,140600
140681,怀仁市,4,140600
140702,榆次区,4,140700
140703,太谷区,4,140700
140721,榆社县,4,140700
140722,左权县,4,140700
140723,和顺县,4,140700
140724,昔阳县,4,140700
140725,寿阳县,4,140700
140727,祁县,4,140700
140728,平遥县,4,140700
140729,灵石县,4,140700
140781,介休市,4,140700
140802,盐湖区,4,140800
140821,临猗县,4,140800
140822,万荣县,4,140800
140823,闻喜县,4,140800
140824,稷山县,4,140800
140825,新绛县,4,140800
140826,绛县,4,140800
140827,垣曲县,4,140800
140828,夏县,4,140800
140829,平陆县,4,140800
140830,芮城县,4,140800
140881,永济市,4,140800
140882,河津市,4,140800
140902,忻府区,4,140900
140921,定襄县,4,140900
140922,五台县,4,140900
140923,代县,4,140900
140924,繁峙县,4,140900
140925,宁武县,4,140900
140926,静乐县,4,140900
140927,神池县,4,140900
140928,五寨县,4,140900
140929,岢岚县,4,140900
140930,河曲县,4,140900
140931,保德县,4,140900
140932,偏关县,4,140900
140971,五台山风景名胜区,4,140900
140981,原平市,4,140900
141002,尧都区,4,141000
141021,曲沃县,4,141000
141022,翼城县,4,141000
141023,襄汾县,4,141000
141024,洪洞县,4,141000
141025,古县,4,141000
141026,安泽县,4,141000
141027,浮山县,4,141000
141028,吉县,4,141000
141029,乡宁县,4,141000
141030,大宁县,4,141000
141031,隰县,4,141000
141032,永和县,4,141000
141033,蒲县,4,141000
141034,汾西县,4,141000
141081,侯马市,4,141000
141082,霍州市,4,141000
141102,离石区,4,141100
141121,文水县,4,141100
141122,交城县,4,141100
141123,兴县,4,141100
141124,临县,4,141100
141125,柳林县,4,141100
141126,石楼县,4,141100
141127,岚县,4,141100
141128,方山县,4,141100
141129,中阳县,4,141100
141130,交口县,4,141100
141181,孝义市,4,141100
141182,汾阳市,4,141100
150102,新城区,4,150100
150103,回民区,4,150100
150104,玉泉区,4,150100
150105,赛罕区,4,150100
150121,土默特左旗,4,150100
150122,托克托县,4,150100
150123,和林格尔县,4,150100
150124,清水河县,4,150100
150125,武川县,4,150100
150172,呼和浩特经济技术开发区,4,150100
150202,东河区,4,150200
150203,昆都仑区,4,150200
150204,青山区,4,150200
150205,石拐区,4,150200
150206,白云鄂博矿区,4,150200
150207,九原区,4,150200
150221,土默特右旗,4,150200
150222,固阳县,4,150200
150223,达尔罕茂明安联合旗,4,150200
150271,包头稀土高新技术产业开发区,4,150200
150302,海勃湾区,4,150300
150303,海南区,4,150300
150304,乌达区,4,150300
150402,红山区,4,150400
150403,元宝山区,4,150400
150404,松山区,4,150400
150421,阿鲁科尔沁旗,4,150400
150422,巴林左旗,4,150400
150423,巴林右旗,4,150400
150424,林西县,4,150400
150425,克什克腾旗,4,150400
150426,翁牛特旗,4,150400
150428,喀喇沁旗,4,150400
150429,宁城县,4,150400
150430,敖汉旗,4,150400
150502,科尔沁区,4,150500
150521,科尔沁左翼中旗,4,150500
150522,科尔沁左翼后旗,4,150500
150523,开鲁县,4,150500
150524,库伦旗,4,150500
150525,奈曼旗,4,150500
150526,扎鲁特旗,4,150500
150571,通辽经济技术开发区,4,150500
150581,霍林郭勒市,4,150500
150602,东胜区,4,150600
150603,康巴什区,4,150600
150621,达拉特旗,4,150600
150622,准格尔旗,4,150600
150623,鄂托克前旗,4,150600
150624,鄂托克旗,4,150600
150625,杭锦旗,4,150600
150626,乌审旗,4,150600
150627,伊金霍洛旗,4,150600
150702,海拉尔区,4,150700
150703,扎赉诺尔区,4,150700
150721,阿荣旗,4,150700
150722,莫力达瓦达斡尔族自治旗,4,150700
150723,鄂伦春自治旗,4,150700
150724,鄂温克族自治旗,4,150700
150725,陈巴尔虎旗,4,150700
150726,新巴尔虎左旗,4,150700
150727,新巴尔虎右旗,4,150700
150781,满洲里市,4,150700
150782,牙克石市,4,150700
150783,扎兰屯市,4,150700
150784,额尔古纳市,4,150700
150785,根河市,4,150700
150802,临河区,4,150800
150821,五原县,4,150800
150822,磴口县,4,150800
150823,乌拉特前旗,4,150800
150824,乌拉特中旗,4,150800
150825,乌拉特后旗,4,150800
150826,杭锦后旗,4,150800
150902,集宁区,4,150900
150921,卓资县,4,150900
150922,化德县,4,150900
150923,商都县,4,150900
150924,兴和县,4,150900
150925,凉城县,4,150900
150926,察哈尔右翼前旗,4,150900
150927,察哈尔右翼中旗,4,150900
150928,察哈尔右翼后旗,4,150900
150929,四子王旗,4,150900
150981,丰镇市,4,150900
152201,乌兰浩特市,4,152200
152202,阿尔山市,4,152200
152221,科尔沁右翼前旗,4,152200
152222,科尔沁右翼中旗,4,152200
152223,扎赉特旗,4,152200
152224,突泉县,4,152200
152501,二连浩特市,4,152500
152502,锡林浩特市,4,152500
152522,阿巴嘎旗,4,152500
152523,苏尼特左旗,4,152500
152524,苏尼特右旗,4,152500
152525,东乌珠穆沁旗,4,152500
152526,西乌珠穆沁旗,4,152500
152527,太仆寺旗,4,152500
152528,镶黄旗,4,152500
152529,正镶白旗,4,152500
152530,正蓝旗,4,152500
152531,多伦县,4,152500
152571,乌拉盖管委会,4,152500
152921,阿拉善左旗,4,152900
152922,阿拉善右旗,4,152900
152923,额济纳旗,4,152900
152971,内蒙古阿拉善高新技术产业开发区,4,152900
210102,和平区,4,210100
210103,沈河区,4,210100
210104,大东区,4,210100
210105,皇姑区,4,210100
210106,铁西区,4,210100
210111,苏家屯区,4,210100
210112,浑南区,4,210100
210113,沈北新区,4,210100
210114,于洪区,4,210100
210115,辽中区,4,210100
210123,康平县,4,210100
210124,法库县,4,210100
210181,新民市,4,210100
210202,中山区,4,210200
210203,西岗区,4,210200
210204,沙河口区,4,210200
210211,甘井子区,4,210200
210212,旅顺口区,4,210200
210213,金州区,4,210200
210214,普兰店区,4,210200
210224,长海县,4,210200
210281,瓦房店市,4,210200
210283,庄河市,4,210200
210302,铁东区,4,210300
210303,铁西区,4,210300
210304,立山区,4,210300
210311,千山区,4,210300
210321,台安县,4,210300
210323,岫岩满族自治县,4,210300
210381,海城市,4,210300
210402,新抚区,4,210400
210403,东洲区,4,210400
210404,望花区,4,210400
210411,顺城区,4,210400
210421,抚顺县,4,210400
210422,新宾满族自治县,4,210400
210423,清原满族自治县,4,210400
210502,平山区,4,210500
210503,溪湖区,4,210500
210504,明山区,4,210500
210505,南芬区,4,210500
210521,本溪满族自治县,4,210500
210522,桓仁满族自治县,4,210500
210602,元宝区,4,210600
210603,振兴区,4,210600
210604,振安区,4,210600
210624,宽甸满族自治县,4,210600
210681,东港市,4,210600
210682,凤城市,4,210600
210702,古塔区,4,210700
210703,凌河区,4,210700
210711,太和区,4,210700
210726,黑山县,4,210700
210727,义县,4,210700
210781,凌海市,4,210700
210782,北镇市,4,210700
210802,站前区,4,210800
210803,西市区,4,210800
210804,鲅鱼圈区,4,210800
210811,老边区,4,210800
210881,盖州市,4,210800
210882,大石桥市,4,210800
210902,海州区,4,210900
210903,新邱区,4,210900
210904,太平区,4,210900
210905,清河门区,4,210900
210911,细河区,4,210900
210921,阜新蒙古族自治县,4,210900
210922,彰武县,4,210900
211002,白塔区,4,211000
211003,文圣区,4,211000
211004,宏伟区,4,211000
211005,弓长岭区,4,211000
211011,太子河区,4,211000
211021,辽阳县,4,211000
211081,灯塔市,4,211000
211102,双台子区,4,211100
211103,兴隆台区,4,211100
211104,大洼区,4,211100
211122,盘山县,4,211100
211202,银州区,4,211200
211204,清河区,4,211200
211221,铁岭县,4,211200
211223,西丰县,4,211200
211224,昌图县,4,211200
211281,调兵山市,4,211200
211282,开原市,4,211200
211302,双塔区,4,211300
211303,龙城区,4,211300
211321,朝阳县,4,211300
211322,建平县,4,211300
211324,喀喇沁左翼蒙古族自治县,4,211300
211381,北票市,4,211300
211382,凌源市,4,211300
211402,连山区,4,211400
211403,龙港区,4,211400
211404,南票区,4,211400
211421,绥中县,4,211400
211422,建昌县,4,211400
211481,兴城市,4,211400
220102,南关区,4,220100
220103,宽城区,4,220100
220104,朝阳区,4,220100
220105,二道区,4,220100
220106,绿园区,4,220100
220112,双阳区,4,220100
220113,九台区,4,220100
220122,农安县,4,220100
220171,长春经济技术开发区,4,220100
220172,长春净月高新技术产业开发区,4,220100
220173,长春高新技术产业开发区,4,220100
220174,长春汽车经济技术开发区,4,220100
220182,榆树市,4,220100
220183,德惠市,4,220100
220184,公主岭市,4,220100
220202,昌邑区,4,220200
220203,龙潭区,4,220200
220204,船营区,4,220200
220211,丰满区,4,220200
220221,永吉县,4,220200
220271,吉林经济开发区,4,220200
220272,吉林高新技术产业开发区,4,220200
220273,吉林中国新加坡食品区,4,220200
220281,蛟河市,4,220200
220282,桦甸市,4,220200
220283,舒兰市,4,220200
220284,磐石市,4,220200
220302,铁西区,4,220300
220303,铁东区,4,220300
220322,梨树县,4,220300
220323,伊通满族自治县,4,220300
220382,双辽市,4,220300
220402,龙山区,4,220400
220403,西安区,4,220400
220421,东丰县,4,220400
220422,东辽县,4,220400
220502,东昌区,4,220500
220503,二道江区,4,220500
220521,通化县,4,220500
220523,辉南县,4,220500
220524,柳河县,4,220500
220581,梅河口市,4,220500
220582,集安市,4,220500
220602,浑江区,4,220600
220605,江源区,4,220600
220621,抚松县,4,220600
220622,靖宇县,4,220600
220623,长白朝鲜族自治县,4,220600
220681,临江市,4,220600
220702,宁江区,4,220700
220721,前郭尔罗斯蒙古族自治县,4,220700
220722,长岭县,4,220700
220723,乾安县,4,220700
220771,吉林松原经济开发区,4,220700
220781,扶余市,4,220700
220802,洮北区,4,220800
220821,镇赉县,4,220800
220822,通榆县,4,220800
220871,吉林白城经济开发区,4,220800
220881,洮南市,4,220800
220882,大安市,4,220800
222401,延吉市,4,222400
222402,图们市,4,222400
222403,敦化市,4,222400
222404,珲春市,4,222400
222405,龙井市,4,222400
222406,和龙市,4,222400
222424,汪清县,4,222400
222426,安图县,4,222400
230102,道里区,4,230100
230103,南岗区,4,230100
230104,道外区,4,230100
230108,平房区,4,230100
230109,松北区,4,230100
230110,香坊区,4,230100
230111,呼兰区,4,230100
230112,阿城区,4,230100
230113,双城区,4,230100
230123,依兰县,4,230100
230124,方正县,4,230100
230125,宾县,4,230100
230126,巴彦县,4,230100
230127,木兰县,4,230100
230128,通河县,4,230100
230129,延寿县,4,230100
230183,尚志市,4,230100
230184,五常市,4,230100
230202,龙沙区,4,230200
230203,建华区,4,230200
230204,铁锋区,4,230200
230205,昂昂溪区,4,230200
230206,富拉尔基区,4,230200
230207,碾子山区,4,230200
230208,梅里斯达斡尔族区,4,230200
230221,龙江县,4,230200
230223,依安县,4,230200
230224,泰来县,4,230200
230225,甘南县,4,230200
230227,富裕县,4,230200
230229,克山县,4,230200
230230,克东县,4,230200
230231,拜泉县,4,230200
230281,讷河市,4,230200
230302,鸡冠区,4,230300
230303,恒山区,4,230300
230304,滴道区,4,230300
230305,梨树区,4,230300
230306,城子河区,4,230300
230307,麻山区,4,230300
230321,鸡东县,4,230300
230381,虎林市,4,230300
230382,密山市,4,230300
230402,向阳区,4,230400
230403,工农区,4,230400
230404,南山区,4,230400
230405,兴安区,4,230400
230406,东山区,4,230400
230407,兴山区,4,230400
230421,萝北县,4,230400
230422,绥滨县,4,230400
230502,尖山区,4,230500
230503,岭东区,4,230500
230505,四方台区,4,230500
230506,宝山区,4,230500
230521,集贤县,4,230500
230522,友谊县,4,230500
230523,宝清县,4,230500
230524,饶河县,4,230500
230602,萨尔图区,4,230600
230603,龙凤区,4,230600
230604,让胡路区,4,230600
230605,红岗区,4,230600
230606,大同区,4,230600
230621,肇州县,4,230600
230622,肇源县,4,230600
230623,林甸县,4,230600
230624,杜尔伯特蒙古族自治县,4,230600
230671,大庆高新技术产业开发区,4,230600
230717,伊美区,4,230700
230718,乌翠区,4,230700
230719,友好区,4,230700
230722,嘉荫县,4,230700
230723,汤旺县,4,230700
230724,丰林县,4,230700
230725,大箐山县,4,230700
230726,南岔县,4,230700
230751,金林区,4,230700
230781,铁力市,4,230700
230803,向阳区,4,230800
230804,前进区,4,230800
230805,东风区,4,230800
230811,郊区,4,230800
230822,桦南县,4,230800
230826,桦川县,4,230800
230828,汤原县,4,230800
230881,同江市,4,230800
230882,富锦市,4,230800
230883,抚远市,4,230800
230902,新兴区,4,230900
230903,桃山区,4,230900
230904,茄子河区,4,230900
230921,勃利县,4,230900
231002,东安区,4,231000
231003,阳明区,4,231000
231004,爱民区,4,231000
231005,西安区,4,231000
231025,林口县,4,231000
231071,牡丹江经济技术开发区,4,231000
231081,绥芬河市,4,231000
231083,海林市,4,231000
231084,宁安市,4,231000
231085,穆棱市,4,231000
231086,东宁市,4,231000
231102,爱辉区,4,231100
231123,逊克县,4,231100
231124,孙吴县,4,231100
231181,北安市,4,231100
231182,五大连池市,4,231100
231183,嫩江市,4,231100
231202,北林区,4,231200
231221,望奎县,4,231200
231222,兰西县,4,231200
231223,青冈县,4,231200
231224,庆安县,4,231200
231225,明水县,4,231200
231226,绥棱县,4,231200
231281,安达市,4,231200
231282,肇东市,4,231200
231283,海伦市,4,231200
232701,漠河市,4,232700
232721,呼玛县,4,232700
232722,塔河县,4,232700
232761,加格达奇区,4,232700
232762,松岭区,4,232700
232763,新林区,4,232700
232764,呼中区,4,232700
310101,黄浦区,4,310100
310104,徐汇区,4,310100
310105,长宁区,4,310100
310106,静安区,4,310100
310107,普陀区,4,310100
310109,虹口区,4,310100
310110,杨浦区,4,310100
310112,闵行区,4,310100
310113,宝山区,4,310100
310114,嘉定区,4,310100
310115,浦东新区,4,310100
310116,金山区,4,310100
310117,松江区,4,310100
310118,青浦区,4,310100
310120,奉贤区,4,310100
310151,崇明区,4,310100
320102,玄武区,4,320100
320104,秦淮区,4,320100
320105,建邺区,4,320100
320106,鼓楼区,4,320100
320111,浦口区,4,320100
320113,栖霞区,4,320100
320114,雨花台区,4,320100
320115,江宁区,4,320100
320116,六合区,4,320100
320117,溧水区,4,320100
320118,高淳区,4,320100
320205,锡山区,4,320200
320206,惠山区,4,320200
320211,滨湖区,4,320200
320213,梁溪区,4,320200
320214,新吴区,4,320200
320281,江阴市,4,320200
320282,宜兴市,4,320200
320302,鼓楼区,4,320300
320303,云龙区,4,320300
320305,贾汪区,4,320300
320311,泉山区,4,320300
320312,铜山区,4,320300
320321,丰县,4,320300
320322,沛县,4,320300
320324,睢宁县,4,320300
320371,徐州经济技术开发区,4,320300
320381,新沂市,4,320300
320382,邳州市,4,320300
320402,天宁区,4,320400
320404,钟楼区,4,320400
320411,新北区,4,320400
320412,武进区,4,320400
320413,金坛区,4,320400
320481,溧阳市,4,320400
320505,虎丘区,4,320500
320506,吴中区,4,320500
320507,相城区,4,320500
320508,姑苏区,4,320500
320509,吴江区,4,320500
320571,苏州工业园区,4,320500
320581,常熟市,4,320500
320582,张家港市,4,320500
320583,昆山市,4,320500
320585,太仓市,4,320500
320612,通州区,4,320600
320613,崇川区,4,320600
320614,海门区,4,320600
320623,如东县,4,320600
320671,南通经济技术开发区,4,320600
320681,启东市,4,320600
320682,如皋市,4,320600
320685,海安市,4,320600
320703,连云区,4,320700
320706,海州区,4,320700
320707,赣榆区,4,320700
320722,东海县,4,320700
320723,灌云县,4,320700
320724,灌南县,4,320700
320771,连云港经济技术开发区,4,320700
320772,连云港高新技术产业开发区,4,320700
320803,淮安区,4,320800
320804,淮阴区,4,320800
320812,清江浦区,4,320800
320813,洪泽区,4,320800
320826,涟水县,4,320800
320830,盱眙县,4,320800
320831,金湖县,4,320800
320871,淮安经济技术开发区,4,320800
320902,亭湖区,4,320900
320903,盐都区,4,320900
320904,大丰区,4,320900
320921,响水县,4,320900
320922,滨海县,4,320900
320923,阜宁县,4,320900
320924,射阳县,4,320900
320925,建湖县,4,320900
320971,盐城经济技术开发区,4,320900
320981,东台市,4,320900
321002,广陵区,4,321000
321003,邗江区,4,321000
321012,江都区,4,321000
321023,宝应县,4,321000
321071,扬州经济技术开发区,4,321000
321081,仪征市,4,321000
321084,高邮市,4,321000
321102,京口区,4,321100
321111,润州区,4,321100
321112,丹徒区,4,321100
321171,镇江新区,4,321100
321181,丹阳市,4,321100
321182,扬中市,4,321100
321183,句容市,4,321100
321202,海陵区,4,321200
321203,高港区,4,321200
321204,姜堰区,4,321200
321271,泰州医药高新技术产业开发区,4,321200
321281,兴化市,4,321200
321282,靖江市,4,321200
321283,泰兴市,4,321200
321302,宿城区,4,321300
321311,宿豫区,4,321300
321322,沭阳县,4,321300
321323,泗阳县,4,321300
321324,泗洪县,4,321300
321371,宿迁经济技术开发区,4,321300
330102,上城区,4,330100
330105,拱墅区,4,330100
330106,西湖区,4,330100
330108,滨江区,4,330100
330109,萧山区,4,330100
330110,余杭区,4,330100
330111,富阳区,4,330100
330112,临安区,4,330100
330113,临平区,4,330100
330114,钱塘区,4,330100
330122,桐庐县,4,330100
330127,淳安县,4,330100
330182,建德市,4,330100
330203,海曙区,4,330200
330205,江北区,4,330200
330206,北仑区,4,330200
330211,镇海区,4,330200
330212,鄞州区,4,330200
330213,奉化区,4,330200
330225,象山县,4,330200
330226,宁海县,4,330200
330281,余姚市,4,330200
330282,慈溪市,4,330200
330302,鹿城区,4,330300
330303,龙湾区,4,330300
330304,瓯海区,4,330300
330305,洞头区,4,330300
330324,永嘉县,4,330300
330326,平阳县,4,330300
330327,苍南县,4,330300
330328,文成县,4,330300
330329,泰顺县,4,330300
330371,温州经济技术开发区,4,330300
330381,瑞安市,4,330300
330382,乐清市,4,330300
330383,龙港市,4,330300
330402,南湖区,4,330400
330411,秀洲区,4,330400
330421,嘉善县,4,330400
330424,海盐县,4,330400
330481,海宁市,4,330400
330482,平湖市,4,330400
330483,桐乡市,4,330400
330502,吴兴区,4,330500
330503,南浔区,4,330500
330521,德清县,4,330500
330522,长兴县,4,330500
330523,安吉县,4,330500
330602,越城区,4,330600
330603,柯桥区,4,330600
330604,上虞区,4,330600
330624,新昌县,4,330600
330681,诸暨市,4,330600
330683,嵊州市,4,330600
330702,婺城区,4,330700
330703,金东区,4,330700
330723,武义县,4,330700
330726,浦江县,4,330700
330727,磐安县,4,330700
330781,兰溪市,4,330700
330782,义乌市,4,330700
330783,东阳市,4,330700
330784,永康市,4,330700
330802,柯城区,4,330800
330803,衢江区,4,330800
330822,常山县,4,330800
330824,开化县,4,330800
330825,龙游县,4,330800
330881,江山市,4,330800
330902,定海区,4,330900
330903,普陀区,4,330900
330921,岱山县,4,330900
330922,嵊泗县,4,330900
331002,椒江区,4,331000
331003,黄岩区,4,331000
331004,路桥区,4,331000
331022,三门县,4,331000
331023,天台县,4,331000
331024,仙居县,4,331000
331081,温岭市,4,331000
331082,临海市,4,331000
331083,玉环市,4,331000
331102,莲都区,4,331100
331121,青田县,4,331100
331122,缙云县,4,331100
331123,遂昌县,4,331100
331124,松阳县,4,331100
331125,云和县,4,331100
331126,庆元县,4,331100
331127,景宁畲族自治县,4,331100
331181,龙泉市,4,331100
340102,瑶海区,4,340100
340103,庐阳区,4,340100
340104,蜀山区,4,340100
340111,包河区,4,340100
340121,长丰县,4,340100
340122,肥东县,4,340100
340123,肥西县,4,340100
340124,庐江县,4,340100
340171,合肥高新技术产业开发区,4,340100
340172,合肥经济技术开发区,4,340100
340173,合肥新站高新技术产业开发区,4,340100
340181,巢湖市,4,340100
340202,镜湖区,4,340200
340207,鸠江区,4,340200
340209,弋江区,4,340200
340210,湾沚区,4,340200
340212,繁昌区,4,340200
340223,南陵县,4,340200
340271,芜湖经济技术开发区,4,340200
340272,安徽芜湖三山经济开发区,4,340200
340281,无为市,4,340200
340302,龙子湖区,4,340300
340303,蚌山区,4,340300
340304,禹会区,4,340300
340311,淮上区,4,340300
340321,怀远县,4,340300
340322,五河县,4,340300
340323,固镇县,4,340300
340371,蚌埠市高新技术开发区,4,340300
340372,蚌埠市经济开发区,4,340300
340402,大通区,4,340400
340403,田家庵区,4,340400
340404,谢家集区,4,340400
340405,八公山区,4,340400
340406,潘集区,4,340400
340421,凤台县,4,340400
340422,寿县,4,340400
340503,花山区,4,340500
340504,雨山区,4,340500
340506,博望区,4,340500
340521,当涂县,4,340500
340522,含山县,4,340500
340523,和县,4,340500
340602,杜集区,4,340600
340603,相山区,4,340600
340604,烈山区,4,340600
340621,濉溪县,4,340600
340705,铜官区,4,340700
340706,义安区,4,340700
340711,郊区,4,340700
340722,枞阳县,4,340700
340802,迎江区,4,340800
340803,大观区,4,340800
340811,宜秀区,4,340800
340822,怀宁县,4,340800
340825,太湖县,4,340800
340826,宿松县,4,340800
340827,望江县,4,340800
340828,岳西县,4,340800
340871,安徽安庆经济开发区,4,340800
340881,桐城市,4,340800
340882,潜山市,4,340800
341002,屯溪区,4,341000
341003,黄山区,4,341000
341004,徽州区,4,341000
341021,歙县,4,341000
341022,休宁县,4,341000
341023,黟县,4,341000
341024,祁门县,4,341000
341102,琅琊区,4,341100
341103,南谯区,4,341100
341122,来安县,4,341100
341124,全椒县,4,341100
341125,定远县,4,341100
341126,凤阳县,4,341100
341171,中新苏滁高新技术产业开发区,4,341100
341172,滁州经济技术开发区,4,341100
341181,天长市,4,341100
341182,明光市,4,341100
341202,颍州区,4,341200
341203,颍东区,4,341200
341204,颍泉区,4,341200
341221,临泉县,4,341200
341222,太和县,4,341200
341225,阜南县,4,341200
341226,颍上县,4,341200
341271,阜阳合肥现代产业园区,4,341200
341272,阜阳经济技术开发区,4,341200
341282,界首市,4,341200
341302,埇桥区,4,341300
341321,砀山县,4,341300
341322,萧县,4,341300
341323,灵璧县,4,341300
341324,泗县,4,341300
341371,宿州马鞍山现代产业园区,4,341300
341372,宿州经济技术开发区,4,341300
341502,金安区,4,341500
341503,裕安区,4,341500
341504,叶集区,4,341500
341522,霍邱县,4,341500
341523,舒城县,4,341500
341524,金寨县,4,341500
341525,霍山县,4,341500
341602,谯城区,4,341600
341621,涡阳县,4,341600
341622,蒙城县,4,341600
341623,利辛县,4,341600
341702,贵池区,4,341700
341721,东至县,4,341700
341722,石台县,4,341700
341723,青阳县,4,341700
341802,宣州区,4,341800
341821,郎溪县,4,341800
341823,泾县,4,341800
341824,绩溪县,4,341800
341825,旌德县,4,341800
341871,宣城市经济开发区,4,341800
341881,宁国市,4,341800
341882,广德市,4,341800
350102,鼓楼区,4,350100
350103,台江区,4,350100
350104,仓山区,4,350100
350105,马尾区,4,350100
350111,晋安区,4,350100
350112,长乐区,4,350100
350121,闽侯县,4,350100
350122,连江县,4,350100
350123,罗源县,4,350100
350124,闽清县,4,350100
350125,永泰县,4,350100
350128,平潭县,4,350100
350181,福清市,4,350100
350203,思明区,4,350200
350205,海沧区,4,350200
350206,湖里区,4,350200
350211,集美区,4,350200
350212,同安区,4,350200
350213,翔安区,4,350200
350302,城厢区,4,350300
350303,涵江区,4,350300
350304,荔城区,4,350300
350305,秀屿区,4,350300
350322,仙游县,4,350300
350404,三元区,4,350400
350405,沙县区,4,350400
350421,明溪县,4,350400
350423,清流县,4,350400
350424,宁化县,4,350400
350425,大田县,4,350400
350426,尤溪县,4,350400
350428,将乐县,4,350400
350429,泰宁县,4,350400
350430,建宁县,4,350400
350481,永安市,4,350400
350502,鲤城区,4,350500
350503,丰泽区,4,350500
350504,洛江区,4,350500
350505,泉港区,4,350500
350521,惠安县,4,350500
350524,安溪县,4,350500
350525,永春县,4,350500
350526,德化县,4,350500
350527,金门县,4,350500
350581,石狮市,4,350500
350582,晋江市,4,350500
350583,南安市,4,350500
350602,芗城区,4,350600
350603,龙文区,4,350600
350604,龙海区,4,350600
350605,长泰区,4,350600
350622,云霄县,4,350600
350623,漳浦县,4,350600
350624,诏安县,4,350600
350626,东山县,4,350600
350627,南靖县,4,350600
350628,平和县,4,350600
350629,华安县,4,350600
350702,延平区,4,350700
350703,建阳区,4,350700
350721,顺昌县,4,350700
350722,浦城县,4,350700
350723,光泽县,4,350700
350724,松溪县,4,350700
350725,政和县,4,350700
350781,邵武市,4,350700
350782,武夷山市,4,350700
350783,建瓯市,4,350700
350802,新罗区,4,350800
350803,永定区,4,350800
350821,长汀县,4,350800
350823,上杭县,4,350800
350824,武平县,4,350800
350825,连城县,4,350800
350881,漳平市,4,350800
350902,蕉城区,4,350900
350921,霞浦县,4,350900
350922,古田县,4,350900
350923,屏南县,4,350900
350924,寿宁县,4,350900
350925,周宁县,4,350900
350926,柘荣县,4,350900
350981,福安市,4,350900
350982,福鼎市,4,350900
360102,东湖区,4,360100
360103,西湖区,4,360100
360104,青云谱区,4,360100
360111,青山湖区,4,360100
360112,新建区,4,360100
360113,红谷滩区,4,360100
360121,南昌县,4,360100
360123,安义县,4,360100
360124,进贤县,4,360100
360202,昌江区,4,360200
360203,珠山区,4,360200
360222,浮梁县,4,360200
360281,乐平市,4,360200
360302,安源区,4,360300
360313,湘东区,4,360300
360321,莲花县,4,360300
360322,上栗县,4,360300
360323,芦溪县,4,360300
360402,濂溪区,4,360400
360403,浔阳区,4,360400
360404,柴桑区,4,360400
360423,武宁县,4,360400
360424,修水县,4,360400
360425,永修县,4,360400
360426,德安县,4,360400
360428,都昌县,4,360400
360429,湖口县,4,360400
360430,彭泽县,4,360400
360481,瑞昌市,4,360400
360482,共青城市,4,360400
360483,庐山市,4,360400
360502,渝水区,4,360500
360521,分宜县,4,360500
360602,月湖区,4,360600
360603,余江区,4,360600
360681,贵溪市,4,360600
360702,章贡区,4,360700
360703,南康区,4,360700
360704,赣县区,4,360700
360722,信丰县,4,360700
360723,大余县,4,360700
360724,上犹县,4,360700
360725,崇义县,4,360700
360726,安远县,4,360700
360728,定南县,4,360700
360729,全南县,4,360700
360730,宁都县,4,360700
360731,于都县,4,360700
360732,兴国县,4,360700
360733,会昌县,4,360700
360734,寻乌县,4,360700
360735,石城县,4,360700
360781,瑞金市,4,360700
360783,龙南市,4,360700
360802,吉州区,4,360800
360803,青原区,4,360800
360821,吉安县,4,360800
360822,吉水县,4,360800
360823,峡江县,4,360800
360824,新干县,4,360800
360825,永丰县,4,360800
360826,泰和县,4,360800
360827,遂川县,4,360800
360828,万安县,4,360800
360829,安福县,4,360800
360830,永新县,4,360800
360881,井冈山市,4,360800
360902,袁州区,4,360900
360921,奉新县,4,360900
360922,万载县,4,360900
360923,上高县,4,360900
360924,宜丰县,4,360900
360925,靖安县,4,360900
360926,铜鼓县,4,360900
360981,丰城市,4,360900
360982,樟树市,4,360900
360983,高安市,4,360900
361002,临川区,4,361000
361003,东乡区,4,361000
361021,南城县,4,361000
361022,黎川县,4,361000
361023,南丰县,4,361000
361024,崇仁县,4,361000
361025,乐安县,4,361000
361026,宜黄县,4,361000
361027,金溪县,4,361000
361028,资溪县,4,361000
361030,广昌县,4,361000
361102,信州区,4,361100
361103,广丰区,4,361100
361104,广信区,4,361100
361123,玉山县,4,361100
361124,铅山县,4,361100
361125,横峰县,4,361100
361126,弋阳县,4,361100
361127,余干县,4,361100
361128,鄱阳县,4,361100
361129,万年县,4,361100
361130,婺源县,4,361100
361181,德兴市,4,361100
370102,历下区,4,370100
370103,市中区,4,370100
370104,槐荫区,4,370100
370105,天桥区,4,370100
370112,历城区,4,370100
370113,长清区,4,370100
370114,章丘区,4,370100
370115,济阳区,4,370100
370116,莱芜区,4,370100
370117,钢城区,4,370100
370124,平阴县,4,370100
370126,商河县,4,370100
370171,济南高新技术产业开发区,4,370100
370202,市南区,4,370200
370203,市北区,4,370200
370211,黄岛区,4,370200
370212,崂山区,4,370200
370213,李沧区,4,370200
370214,城阳区,4,370200
370215,即墨区,4,370200
370271,青岛高新技术产业开发区,4,370200
370281,胶州市,4,370200
370283,平度市,4,370200
370285,莱西市,4,370200
370302,淄川区,4,370300
370303,张店区,4,370300
370304,博山区,4,370300
370305,临淄区,4,370300
370306,周村区,4,370300
370321,桓台县,4,370300
370322,高青县,4,370300
370323,沂源县,4,370300
370402,市中区,4,370400
370403,薛城区,4,370400
370404,峄城区,4,370400
370405,台儿庄区,4,370400
370406,山亭区,4,370400
370481,滕州市,4,370400
370502,东营区,4,370500
370503,河口区,4,370500
370505,垦利区,4,370500
370522,利津县,4,370500
370523,广饶县,4,370500
370571,东营经济技术开发区,4,370500
370572,东营港经济开发区,4,370500
370602,芝罘区,4,370600
370611,福山区,4,370600
370612,牟平区,4,370600
370613,莱山区,4,370600
370614,蓬莱区,4,370600
370671,烟台高新技术产业开发区,4,370600
370672,烟台经济技术开发区,4,370600
370681,龙口市,4,370600
370682,莱阳市,4,370600
370683,莱州市,4,370600
370685,招远市,4,370600
370686,栖霞市,4,370600
370687,海阳市,4,370600
370702,潍城区,4,370700
370703,寒亭区,4,370700
370704,坊子区,4,370700
370705,奎文区,4,370700
370724,临朐县,4,370700
370725,昌乐县,4,370700
370772,潍坊滨海经济技术开发区,4,370700
370781,青州市,4,370700
370782,诸城市,4,370700
370783,寿光市,4,370700
370784,安丘市,4,370700
370785,高密市,4,370700
370786,昌邑市,4,370700
370811,任城区,4,370800
370812,兖州区,4,370800
370826,微山县,4,370800
370827,鱼台县,4,370800
370828,金乡县,4,370800
370829,嘉祥县,4,370800
370830,汶上县,4,370800
370831,泗水县,4,370800
370832,梁山县,4,370800
370871,济宁高新技术产业开发区,4,370800
370881,曲阜市,4,370800
370883,邹城市,4,370800
370902,泰山区,4,370900
370911,岱岳区,4,370900
370921,宁阳县,4,370900
370923,东平县,4,370900
370982,新泰市,4,370900
370983,肥城市,4,370900
371002,环翠区,4,371000
371003,文登区,4,371000
371071,威海火炬高技术产业开发区,4,371000
371072,威海经济技术开发区,4,371000
371073,威海临港经济技术开发区,4,371000
371082,荣成市,4,371000
371083,乳山市,4,371000
371102,东港区,4,371100
371103,岚山区,4,371100
371121,五莲县,4,371100
371122,莒县,4,371100
371171,日照经济技术开发区,4,371100
371302,兰山区,4,371300
371311,罗庄区,4,371300
371312,河东区,4,371300
371321,沂南县,4,371300
371322,郯城县,4,371300
371323,沂水县,4,371300
371324,兰陵县,4,371300
371325,费县,4,371300
371326,平邑县,4,371300
371327,莒南县,4,371300
371328,蒙阴县,4,371300
371329,临沭县,4,371300
371371,临沂高新技术产业开发区,4,371300
371402,德城区,4,371400
371403,陵城区,4,371400
371422,宁津县,4,371400
371423,庆云县,4,371400
371424,临邑县,4,371400
371425,齐河县,4,371400
371426,平原县,4,371400
371427,夏津县,4,371400
371428,武城县,4,371400
371471,德州经济技术开发区,4,371400
371472,德州运河经济开发区,4,371400
371481,乐陵市,4,371400
371482,禹城市,4,371400
371502,东昌府区,4,371500
371503,茌平区,4,371500
371521,阳谷县,4,371500
371522,莘县,4,371500
371524,东阿县,4,371500
371525,冠县,4,371500
371526,高唐县,4,371500
371581,临清市,4,371500
371602,滨城区,4,371600
371603,沾化区,4,371600
371621,惠民县,4,371600
371622,阳信县,4,371600
371623,无棣县,4,371600
371625,博兴县,4,371600
371681,邹平市,4,371600
371702,牡丹区,4,371700
371703,定陶区,4,371700
371721,曹县,4,371700
371722,单县,4,371700
371723,成武县,4,371700
371724,巨野县,4,371700
371725,郓城县,4,371700
371726,鄄城县,4,371700
371728,东明县,4,371700
371771,菏泽经济技术开发区,4,371700
371772,菏泽高新技术开发区,4,371700
410102,中原区,4,410100
410103,二七区,4,410100
410104,管城回族区,4,410100
410105,金水区,4,410100
410106,上街区,4,410100
410108,惠济区,4,410100
410122,中牟县,4,410100
410171,郑州经济技术开发区,4,410100
410172,郑州高新技术产业开发区,4,410100
410173,郑州航空港经济综合实验区,4,410100
410181,巩义市,4,410100
410182,荥阳市,4,410100
410183,新密市,4,410100
410184,新郑市,4,410100
410185,登封市,4,410100
410202,龙亭区,4,410200
410203,顺河回族区,4,410200
410204,鼓楼区,4,410200
410205,禹王台区,4,410200
410212,祥符区,4,410200
410221,杞县,4,410200
410222,通许县,4,410200
410223,尉氏县,4,410200
410225,兰考县,4,410200
410302,老城区,4,410300
410303,西工区,4,410300
410304,瀍河回族区,4,410300
410305,涧西区,4,410300
410307,偃师区,4,410300
410308,孟津区,4,410300
410311,洛龙区,4,410300
410323,新安县,4,410300
410324,栾川县,4,410300
410325,嵩县,4,410300
410326,汝阳县,4,410300
410327,宜阳县,4,410300
410328,洛宁县,4,410300
410329,伊川县,4,410300
410371,洛阳高新技术产业开发区,4,410300
410402,新华区,4,410400
410403,卫东区,4,410400
410404,石龙区,4,410400
410411,湛河区,4,410400
410421,宝丰县,4,410400
410422,叶县,4,410400
410423,鲁山县,4,410400
410425,郏县,4,410400
410471,平顶山高新技术产业开发区,4,410400
410472,平顶山市城乡一体化示范区,4,410400
410481,舞钢市,4,410400
410482,汝州市,4,410400
410502,文峰区,4,410500
410503,北关区,4,410500
410505,殷都区,4,410500
410506,龙安区,4,410500
410522,安阳县,4,410500
410523,汤阴县,4,410500
410526,滑县,4,410500
410527,内黄县,4,410500
410571,安阳高新技术产业开发区,4,410500
410581,林州市,4,410500
410602,鹤山区,4,410600
410603,山城区,4,410600
410611,淇滨区,4,410600
410621,浚县,4,410600
410622,淇县,4,410600
410671,鹤壁经济技术开发区,4,410600
410702,红旗区,4,410700
410703,卫滨区,4,410700
410704,凤泉区,4,410700
410711,牧野区,4,410700
410721,新乡县,4,410700
410724,获嘉县,4,410700
410725,原阳县,4,410700
410726,延津县,4,410700
410727,封丘县,4,410700
410771,新乡高新技术产业开发区,4,410700
410772,新乡经济技术开发区,4,410700
410773,新乡市平原城乡一体化示范区,4,410700
410781,卫辉市,4,410700
410782,辉县市,4,410700
410783,长垣市,4,410700
410802,解放区,4,410800
410803,中站区,4,410800
410804,马村区,4,410800
410811,山阳区,4,410800
410821,修武县,4,410800
410822,博爱县,4,410800
410823,武陟县,4,410800
410825,温县,4,410800
410871,焦作城乡一体化示范区,4,410800
410882,沁阳市,4,410800
410883,孟州市,4,410800
410902,华龙区,4,410900
410922,清丰县,4,410900
410923,南乐县,4,410900
410926,范县,4,410900
410927,台前县,4,410900
410928,濮阳县,4,410900
410971,河南濮阳工业园区,4,410900
410972,濮阳经济技术开发区,4,410900
411002,魏都区,4,411000
411003,建安区,4,411000
411024,鄢陵县,4,411000
411025,襄城县,4,411000
411071,许昌经济技术开发区,4,411000
411081,禹州市,4,411000
411082,长葛市,4,411000
411102,源汇区,4,411100
411103,郾城区,4,411100
411104,召陵区,4,411100
411121,舞阳县,4,411100
411122,临颍县,4,411100
411171,漯河经济技术开发区,4,411100
411202,湖滨区,4,411200
411203,陕州区,4,411200
411221,渑池县,4,411200
411224,卢氏县,4,411200
411271,河南三门峡经济开发区,4,411200
411281,义马市,4,411200
411282,灵宝市,4,411200
411302,宛城区,4,411300
411303,卧龙区,4,411300
411321,南召县,4,411300
411322,方城县,4,411300
411323,西峡县,4,411300
411324,镇平县,4,411300
411325,内乡县,4,411300
411326,淅川县,4,411300
411327,社旗县,4,411300
411328,唐河县,4,411300
411329,新野县,4,411300
411330,桐柏县,4,411300
411371,南阳高新技术产业开发区,4,411300
411372,南阳市城乡一体化示范区,4,411300
411381,邓州市,4,411300
411402,梁园区,4,411400
411403,睢阳区,4,411400
411421,民权县,4,411400
411422,睢县,4,411400
411423,宁陵县,4,411400
411424,柘城县,4,411400
411425,虞城县,4,411400
411426,夏邑县,4,411400
411471,豫东综合物流产业聚集区,4,411400
411472,河南商丘经济开发区,4,411400
411481,永城市,4,411400
411502,浉河区,4,411500
411503,平桥区,4,411500
411521,罗山县,4,411500
411522,光山县,4,411500
411523,新县,4,411500
411524,商城县,4,411500
411525,固始县,4,411500
411526,潢川县,4,411500
411527,淮滨县,4,411500
411528,息县,4,411500
411571,信阳高新技术产业开发区,4,411500
411602,川汇区,4,411600
411603,淮阳区,4,411600
411621,扶沟县,4,411600
411622,西华县,4,411600
411623,商水县,4,411600
411624,沈丘县,4,411600
411625,郸城县,4,411600
411627,太康县,4,411600
411628,鹿邑县,4,411600
411671,河南周口经济开发区,4,411600
411681,项城市,4,411600
411702,驿城区,4,411700
411721,西平县,4,411700
411722,上蔡县,4,411700
411723,平舆县,4,411700
411724,正阳县,4,411700
411725,确山县,4,411700
411726,泌阳县,4,411700
411727,汝南县,4,411700
411728,遂平县,4,411700
411729,新蔡县,4,411700
411771,河南驻马店经济开发区,4,411700
419001,济源市,4,419000
420102,江岸区,4,420100
420103,江汉区,4,420100
420104,硚口区,4,420100
420105,汉阳区,4,420100
420106,武昌区,4,420100
420107,青山区,4,420100
420111,洪山区,4,420100
420112,东西湖区,4,420100
420113,汉南区,4,420100
420114,蔡甸区,4,420100
420115,江夏区,4,420100
420116,黄陂区,4,420100
420117,新洲区,4,420100
420202,黄石港区,4,420200
420203,西塞山区,4,420200
420204,下陆区,4,420200
420205,铁山区,4,420200
420222,阳新县,4,420200
420281,大冶市,4,420200
420302,茅箭区,4,420300
420303,张湾区,4,420300
420304,郧阳区,4,420300
420322,郧西县,4,420300
420323,竹山县,4,420300
420324,竹溪县,4,420300
420325,房县,4,420300
420381,丹江口市,4,420300
420502,西陵区,4,420500
420503,伍家岗区,4,420500
420504,点军区,4,420500
420505,猇亭区,4,420500
420506,夷陵区,4,420500
420525,远安县,4,420500
420526,兴山县,4,420500
420527,秭归县,4,420500
420528,长阳土家族自治县,4,420500
420529,五峰土家族自治县,4,420500
420581,宜都市,4,420500
420582,当阳市,4,420500
420583,枝江市,4,420500
420602,襄城区,4,420600
420606,樊城区,4,420600
420607,襄州区,4,420600
420624,南漳县,4,420600
420625,谷城县,4,420600
420626,保康县,4,420600
420682,老河口市,4,420600
420683,枣阳市,4,420600
420684,宜城市,4,420600
420702,梁子湖区,4,420700
420703,华容区,4,420700
420704,鄂城区,4,420700
420802,东宝区,4,420800
420804,掇刀区,4,420800
420822,沙洋县,4,420800
420881,钟祥市,4,420800
420882,京山市,4,420800
420902,孝南区,4,420900
420921,孝昌县,4,420900
420922,大悟县,4,420900
420923,云梦县,4,420900
420981,应城市,4,420900
420982,安陆市,4,420900
420984,汉川市,4,420900
421002,沙市区,4,421000
421003,荆州区,4,421000
421022,公安县,4,421000
421024,江陵县,4,421000
421071,荆州经济技术开发区,4,421000
421081,石首市,4,421000
421083,洪湖市,4,421000
421087,松滋市,4,421000
421088,监利市,4,421000
421102,黄州区,4,421100
421121,团风县,4,421100
421122,红安县,4,421100
421123,罗田县,4,421100
421124,英山县,4,421100
421125,浠水县,4,421100
421126,蕲春县,4,421100
421127,黄梅县,4,421100
421171,龙感湖管理区,4,421100
421181,麻城市,4,421100
421182,武穴市,4,421100
421202,咸安区,4,421200
421221,嘉鱼县,4,421200
421222,通城县,4,421200
421223,崇阳县,4,421200
421224,通山县,4,421200
421281,赤壁市,4,421200
421303,曾都区,4,421300
421321,随县,4,421300
421381,广水市,4,421300
422801,恩施市,4,422800
422802,利川市,4,422800
422822,建始县,4,422800
422823,巴东县,4,422800
422825,宣恩县,4,422800
422826,咸丰县,4,422800
422827,来凤县,4,422800
422828,鹤峰县,4,422800
429004,仙桃市,4,429000
429005,潜江市,4,429000
429006,天门市,4,429000
429021,神农架林区,4,429000
430102,芙蓉区,4,430100
430103,天心区,4,430100
430104,岳麓区,4,430100
430105,开福区,4,430100
430111,雨花区,4,430100
430112,望城区,4,430100
430121,长沙县,4,430100
430181,浏阳市,4,430100
430182,宁乡市,4,430100
430202,荷塘区,4,430200
430203,芦淞区,4,430200
430204,石峰区,4,430200
430211,天元区,4,430200
430212,渌口区,4,430200
430223,攸县,4,430200
430224,茶陵县,4,430200
430225,炎陵县,4,430200
430271,云龙示范区,4,430200
430281,醴陵市,4,430200
430302,雨湖区,4,430300
430304,岳塘区,4,430300
430321,湘潭县,4,430300
430371,湖南湘潭高新技术产业园区,4,430300
430372,湘潭昭山示范区,4,430300
430373,湘潭九华示范区,4,430300
430381,湘乡市,4,430300
430382,韶山市,4,430300
430405,珠晖区,4,430400
430406,雁峰区,4,430400
430407,石鼓区,4,430400
430408,蒸湘区,4,430400
430412,南岳区,4,430400
430421,衡阳县,4,430400
430422,衡南县,4,430400
430423,衡山县,4,430400
430424,衡东县,4,430400
430426,祁东县,4,430400
430471,衡阳综合保税区,4,430400
430472,湖南衡阳高新技术产业园区,4,430400
430473,湖南衡阳松木经济开发区,4,430400
430481,耒阳市,4,430400
430482,常宁市,4,430400
430502,双清区,4,430500
430503,大祥区,4,430500
430511,北塔区,4,430500
430522,新邵县,4,430500
430523,邵阳县,4,430500
430524,隆回县,4,430500
430525,洞口县,4,430500
430527,绥宁县,4,430500
430528,新宁县,4,430500
430529,城步苗族自治县,4,430500
430581,武冈市,4,430500
430582,邵东市,4,430500
430602,岳阳楼区,4,430600
430603,云溪区,4,430600
430611,君山区,4,430600
430621,岳阳县,4,430600
430623,华容县,4,430600
430624,湘阴县,4,430600
430626,平江县,4,430600
430671,岳阳市屈原管理区,4,430600
430681,汨罗市,4,430600
430682,临湘市,4,430600
430702,武陵区,4,430700
430703,鼎城区,4,430700
430721,安乡县,4,430700
430722,汉寿县,4,430700
430723,澧县,4,430700
430724,临澧县,4,430700
430725,桃源县,4,430700
430726,石门县,4,430700
430771,常德市西洞庭管理区,4,430700
430781,津市市,4,430700
430802,永定区,4,430800
430811,武陵源区,4,430800
430821,慈利县,4,430800
430822,桑植县,4,430800
430902,资阳区,4,430900
430903,赫山区,4,430900
430921,南县,4,430900
430922,桃江县,4,430900
430923,安化县,4,430900
430971,益阳市大通湖管理区,4,430900
430972,湖南益阳高新技术产业园区,4,430900
430981,沅江市,4,430900
431002,北湖区,4,431000
431003,苏仙区,4,431000
431021,桂阳县,4,431000
431022,宜章县,4,431000
431023,永兴县,4,431000
431024,嘉禾县,4,431000
431025,临武县,4,431000
431026,汝城县,4,431000
431027,桂东县,4,431000
431028,安仁县,4,431000
431081,资兴市,4,431000
431102,零陵区,4,431100
431103,冷水滩区,4,431100
431122,东安县,4,431100
431123,双牌县,4,431100
431124,道县,4,431100
431125,江永县,4,431100
431126,宁远县,4,431100
431127,蓝山县,4,431100
431128,新田县,4,431100
431129,江华瑶族自治县,4,431100
431171,永州经济技术开发区,4,431100
431173,永州市回龙圩管理区,4,431100
431181,祁阳市,4,431100
431202,鹤城区,4,431200
431221,中方县,4,431200
431222,沅陵县,4,431200
431223,辰溪县,4,431200
431224,溆浦县,4,431200
431225,会同县,4,431200
431226,麻阳苗族自治县,4,431200
431227,新晃侗族自治县,4,431200
431228,芷江侗族自治县,4,431200
431229,靖州苗族侗族自治县,4,431200
431230,通道侗族自治县,4,431200
431271,怀化市洪江管理区,4,431200
431281,洪江市,4,431200
431302,娄星区,4,431300
431321,双峰县,4,431300
431322,新化县,4,431300
431381,冷水江市,4,431300
431382,涟源市,4,431300
433101,吉首市,4,433100
433122,泸溪县,4,433100
433123,凤凰县,4,433100
433124,花垣县,4,433100
433125,保靖县,4,433100
433126,古丈县,4,433100
433127,永顺县,4,433100
433130,龙山县,4,433100
440103,荔湾区,4,440100
440104,越秀区,4,440100
440105,海珠区,4,440100
440106,天河区,4,440100
440111,白云区,4,440100
440112,黄埔区,4,440100
440113,番禺区,4,440100
440114,花都区,4,440100
440115,南沙区,4,440100
440117,从化区,4,440100
440118,增城区,4,440100
440203,武江区,4,440200
440204,浈江区,4,440200
440205,曲江区,4,440200
440222,始兴县,4,440200
440224,仁化县,4,440200
440229,翁源县,4,440200
440232,乳源瑶族自治县,4,440200
440233,新丰县,4,440200
440281,乐昌市,4,440200
440282,南雄市,4,440200
440303,罗湖区,4,440300
440304,福田区,4,440300
440305,南山区,4,440300
440306,宝安区,4,440300
440307,龙岗区,4,440300
440308,盐田区,4,440300
440309,龙华区,4,440300
440310,坪山区,4,440300
440311,光明区,4,440300
440402,香洲区,4,440400
440403,斗门区,4,440400
440404,金湾区,4,440400
440507,龙湖区,4,440500
440511,金平区,4,440500
440512,濠江区,4,440500
440513,潮阳区,4,440500
440514,潮南区,4,440500
440515,澄海区,4,440500
440523,南澳县,4,440500
440604,禅城区,4,440600
440605,南海区,4,440600
440606,顺德区,4,440600
440607,三水区,4,440600
440608,高明区,4,440600
440703,蓬江区,4,440700
440704,江海区,4,440700
440705,新会区,4,440700
440781,台山市,4,440700
440783,开平市,4,440700
440784,鹤山市,4,440700
440785,恩平市,4,440700
440802,赤坎区,4,440800
440803,霞山区,4,440800
440804,坡头区,4,440800
440811,麻章区,4,440800
440823,遂溪县,4,440800
440825,徐闻县,4,440800
440881,廉江市,4,440800
440882,雷州市,4,440800
440883,吴川市,4,440800
440902,茂南区,4,440900
440904,电白区,4,440900
440981,高州市,4,440900
440982,化州市,4,440900
440983,信宜市,4,440900
441202,端州区,4,441200
441203,鼎湖区,4,441200
441204,高要区,4,441200
441223,广宁县,4,441200
441224,怀集县,4,441200
441225,封开县,4,441200
441226,德庆县,4,441200
441284,四会市,4,441200
441302,惠城区,4,441300
441303,惠阳区,4,441300
441322,博罗县,4,441300
441323,惠东县,4,441300
441324,龙门县,4,441300
441402,梅江区,4,441400
441403,梅县区,4,441400
441422,大埔县,4,441400
441423,丰顺县,4,441400
441424,五华县,4,441400
441426,平远县,4,441400
441427,蕉岭县,4,441400
441481,兴宁市,4,441400
441502,城区,4,441500
441521,海丰县,4,441500
441523,陆河县,4,441500
441581,陆丰市,4,441500
441602,源城区,4,441600
441621,紫金县,4,441600
441622,龙川县,4,441600
441623,连平县,4,441600
441624,和平县,4,441600
441625,东源县,4,441600
441702,江城区,4,441700
441704,阳东区,4,441700
441721,阳西县,4,441700
441781,阳春市,4,441700
441802,清城区,4,441800
441803,清新区,4,441800
441821,佛冈县,4,441800
441823,阳山县,4,441800
441825,连山壮族瑶族自治县,4,441800
441826,连南瑶族自治县,4,441800
441881,英德市,4,441800
441882,连州市,4,441800
445102,湘桥区,4,445100
445103,潮安区,4,445100
445122,饶平县,4,445100
445202,榕城区,4,445200
445203,揭东区,4,445200
445222,揭西县,4,445200
445224,惠来县,4,445200
445281,普宁市,4,445200
445302,云城区,4,445300
445303,云安区,4,445300
445321,新兴县,4,445300
445322,郁南县,4,445300
445381,罗定市,4,445300
450102,兴宁区,4,450100
450103,青秀区,4,450100
450105,江南区,4,450100
450107,西乡塘区,4,450100
450108,良庆区,4,450100
450109,邕宁区,4,450100
450110,武鸣区,4,450100
450123,隆安县,4,450100
450124,马山县,4,450100
450125,上林县,4,450100
450126,宾阳县,4,450100
450181,横州市,4,450100
450202,城中区,4,450200
450203,鱼峰区,4,450200
450204,柳南区,4,450200
450205,柳北区,4,450200
450206,柳江区,4,450200
450222,柳城县,4,450200
450223,鹿寨县,4,450200
450224,融安县,4,450200
450225,融水苗族自治县,4,450200
450226,三江侗族自治县,4,450200
450302,秀峰区,4,450300
450303,叠彩区,4,450300
450304,象山区,4,450300
450305,七星区,4,450300
450311,雁山区,4,450300
450312,临桂区,4,450300
450321,阳朔县,4,450300
450323,灵川县,4,450300
450324,全州县,4,450300
450325,兴安县,4,450300
450326,永福县,4,450300
450327,灌阳县,4,450300
450328,龙胜各族自治县,4,450300
450329,资源县,4,450300
450330,平乐县,4,450300
450332,恭城瑶族自治县,4,450300
450381,荔浦市,4,450300
450403,万秀区,4,450400
450405,长洲区,4,450400
450406,龙圩区,4,450400
450421,苍梧县,4,450400
450422,藤县,4,450400
450423,蒙山县,4,450400
450481,岑溪市,4,450400
450502,海城区,4,450500
450503,银海区,4,450500
450512,铁山港区,4,450500
450521,合浦县,4,450500
450602,港口区,4,450600
450603,防城区,4,450600
450621,上思县,4,450600
450681,东兴市,4,450600
450702,钦南区,4,450700
450703,钦北区,4,450700
450721,灵山县,4,450700
450722,浦北县,4,450700
450802,港北区,4,450800
450803,港南区,4,450800
450804,覃塘区,4,450800
450821,平南县,4,450800
450881,桂平市,4,450800
450902,玉州区,4,450900
450903,福绵区,4,450900
450921,容县,4,450900
450922,陆川县,4,450900
450923,博白县,4,450900
450924,兴业县,4,450900
450981,北流市,4,450900
451002,右江区,4,451000
451003,田阳区,4,451000
451022,田东县,4,451000
451024,德保县,4,451000
451026,那坡县,4,451000
451027,凌云县,4,451000
451028,乐业县,4,451000
451029,田林县,4,451000
451030,西林县,4,451000
451031,隆林各族自治县,4,451000
451081,靖西市,4,451000
451082,平果市,4,451000
451102,八步区,4,451100
451103,平桂区,4,451100
451121,昭平县,4,451100
451122,钟山县,4,451100
451123,富川瑶族自治县,4,451100
451202,金城江区,4,451200
451203,宜州区,4,451200
451221,南丹县,4,451200
451222,天峨县,4,451200
451223,凤山县,4,451200
451224,东兰县,4,451200
451225,罗城仫佬族自治县,4,451200
451226,环江毛南族自治县,4,451200
451227,巴马瑶族自治县,4,451200
451228,都安瑶族自治县,4,451200
451229,大化瑶族自治县,4,451200
451302,兴宾区,4,451300
451321,忻城县,4,451300
451322,象州县,4,451300
451323,武宣县,4,451300
451324,金秀瑶族自治县,4,451300
451381,合山市,4,451300
451402,江州区,4,451400
451421,扶绥县,4,451400
451422,宁明县,4,451400
451423,龙州县,4,451400
451424,大新县,4,451400
451425,天等县,4,451400
451481,凭祥市,4,451400
460105,秀英区,4,460100
460106,龙华区,4,460100
460107,琼山区,4,460100
460108,美兰区,4,460100
460202,海棠区,4,460200
460203,吉阳区,4,460200
460204,天涯区,4,460200
460205,崖州区,4,460200
460321,西沙群岛,4,460300
460322,南沙群岛,4,460300
460323,中沙群岛的岛礁及其海域,4,460300
469001,五指山市,4,469000
469002,琼海市,4,469000
469005,文昌市,4,469000
469006,万宁市,4,469000
469007,东方市,4,469000
469021,定安县,4,469000
469022,屯昌县,4,469000
469023,澄迈县,4,469000
469024,临高县,4,469000
469025,白沙黎族自治县,4,469000
469026,昌江黎族自治县,4,469000
469027,乐东黎族自治县,4,469000
469028,陵水黎族自治县,4,469000
469029,保亭黎族苗族自治县,4,469000
469030,琼中黎族苗族自治县,4,469000
500101,万州区,4,500100
500102,涪陵区,4,500100
500103,渝中区,4,500100
500104,大渡口区,4,500100
500105,江北区,4,500100
500106,沙坪坝区,4,500100
500107,九龙坡区,4,500100
500108,南岸区,4,500100
500109,北碚区,4,500100
500110,綦江区,4,500100
500111,大足区,4,500100
500112,渝北区,4,500100
500113,巴南区,4,500100
500114,黔江区,4,500100
500115,长寿区,4,500100
500116,江津区,4,500100
500117,合川区,4,500100
500118,永川区,4,500100
500119,南川区,4,500100
500120,璧山区,4,500100
500151,铜梁区,4,500100
500152,潼南区,4,500100
500153,荣昌区,4,500100
500154,开州区,4,500100
500155,梁平区,4,500100
500156,武隆区,4,500100
500229,城口县,4,500100
500230,丰都县,4,500100
500231,垫江县,4,500100
500233,忠县,4,500100
500235,云阳县,4,500100
500236,奉节县,4,500100
500237,巫山县,4,500100
500238,巫溪县,4,500100
500240,石柱土家族自治县,4,500100
500241,秀山土家族苗族自治县,4,500100
500242,酉阳土家族苗族自治县,4,500100
500243,彭水苗族土家族自治县,4,500100
510104,锦江区,4,510100
510105,青羊区,4,510100
510106,金牛区,4,510100
510107,武侯区,4,510100
510108,成华区,4,510100
510112,龙泉驿区,4,510100
510113,青白江区,4,510100
510114,新都区,4,510100
510115,温江区,4,510100
510116,双流区,4,510100
510117,郫都区,4,510100
510118,新津区,4,510100
510121,金堂县,4,510100
510129,大邑县,4,510100
510131,蒲江县,4,510100
510181,都江堰市,4,510100
510182,彭州市,4,510100
510183,邛崃市,4,510100
510184,崇州市,4,510100
510185,简阳市,4,510100
510302,自流井区,4,510300
510303,贡井区,4,510300
510304,大安区,4,510300
510311,沿滩区,4,510300
510321,荣县,4,510300
510322,富顺县,4,510300
510402,东区,4,510400
510403,西区,4,510400
510411,仁和区,4,510400
510421,米易县,4,510400
510422,盐边县,4,510400
510502,江阳区,4,510500
510503,纳溪区,4,510500
510504,龙马潭区,4,510500
510521,泸县,4,510500
510522,合江县,4,510500
510524,叙永县,4,510500
510525,古蔺县,4,510500
510603,旌阳区,4,510600
510604,罗江区,4,510600
510623,中江县,4,510600
510681,广汉市,4,510600
510682,什邡市,4,510600
510683,绵竹市,4,510600
510703,涪城区,4,510700
510704,游仙区,4,510700
510705,安州区,4,510700
510722,三台县,4,510700
510723,盐亭县,4,510700
510725,梓潼县,4,510700
510726,北川羌族自治县,4,510700
510727,平武县,4,510700
510781,江油市,4,510700
510802,利州区,4,510800
510811,昭化区,4,510800
510812,朝天区,4,510800
510821,旺苍县,4,510800
510822,青川县,4,510800
510823,剑阁县,4,510800
510824,苍溪县,4,510800
510903,船山区,4,510900
510904,安居区,4,510900
510921,蓬溪县,4,510900
510923,大英县,4,510900
510981,射洪市,4,510900
511002,市中区,4,511000
511011,东兴区,4,511000
511024,威远县,4,511000
511025,资中县,4,511000
511071,内江经济开发区,4,511000
511083,隆昌市,4,511000
511102,市中区,4,511100
511111,沙湾区,4,511100
511112,五通桥区,4,511100
511113,金口河区,4,511100
511123,犍为县,4,511100
511124,井研县,4,511100
511126,夹江县,4,511100
511129,沐川县,4,511100
511132,峨边彝族自治县,4,511100
511133,马边彝族自治县,4,511100
511181,峨眉山市,4,511100
511302,顺庆区,4,511300
511303,高坪区,4,511300
511304,嘉陵区,4,511300
511321,南部县,4,511300
511322,营山县,4,511300
511323,蓬安县,4,511300
511324,仪陇县,4,511300
511325,西充县,4,511300
511381,阆中市,4,511300
511402,东坡区,4,511400
511403,彭山区,4,511400
511421,仁寿县,4,511400
511423,洪雅县,4,511400
511424,丹棱县,4,511400
511425,青神县,4,511400
511502,翠屏区,4,511500
511503,南溪区,4,511500
511504,叙州区,4,511500
511523,江安县,4,511500
511524,长宁县,4,511500
511525,高县,4,511500
511526,珙县,4,511500
511527,筠连县,4,511500
511528,兴文县,4,511500
511529,屏山县,4,511500
511602,广安区,4,511600
511603,前锋区,4,511600
511621,岳池县,4,511600
511622,武胜县,4,511600
511623,邻水县,4,511600
511681,华蓥市,4,511600
511702,通川区,4,511700
511703,达川区,4,511700
511722,宣汉县,4,511700
511723,开江县,4,511700
511724,大竹县,4,511700
511725,渠县,4,511700
511771,达州经济开发区,4,511700
511781,万源市,4,511700
511802,雨城区,4,511800
511803,名山区,4,511800
511822,荥经县,4,511800
511823,汉源县,4,511800
511824,石棉县,4,511800
511825,天全县,4,511800
511826,芦山县,4,511800
511827,宝兴县,4,511800
511902,巴州区,4,511900
511903,恩阳区,4,511900
511921,通江县,4,511900
511922,南江县,4,511900
511923,平昌县,4,511900
511971,巴中经济开发区,4,511900
512002,雁江区,4,512000
512021,安岳县,4,512000
512022,乐至县,4,512000
513201,马尔康市,4,513200
513221,汶川县,4,513200
513222,理县,4,513200
513223,茂县,4,513200
513224,松潘县,4,513200
513225,九寨沟县,4,513200
513226,金川县,4,513200
513227,小金县,4,513200
513228,黑水县,4,513200
513230,壤塘县,4,513200
513231,阿坝县,4,513200
513232,若尔盖县,4,513200
513233,红原县,4,513200
513301,康定市,4,513300
513322,泸定县,4,513300
513323,丹巴县,4,513300
513324,九龙县,4,513300
513325,雅江县,4,513300
513326,道孚县,4,513300
513327,炉霍县,4,513300
513328,甘孜县,4,513300
513329,新龙县,4,513300
513330,德格县,4,513300
513331,白玉县,4,513300
513332,石渠县,4,513300
513333,色达县,4,513300
513334,理塘县,4,513300
513335,巴塘县,4,513300
513336,乡城县,4,513300
513337,稻城县,4,513300
513338,得荣县,4,513300
513401,西昌市,4,513400
513402,会理市,4,513400
513422,木里藏族自治县,4,513400
513423,盐源县,4,513400
513424,德昌县,4,513400
513426,会东县,4,513400
513427,宁南县,4,513400
513428,普格县,4,513400
513429,布拖县,4,513400
513430,金阳县,4,513400
513431,昭觉县,4,513400
513432,喜德县,4,513400
513433,冕宁县,4,513400
513434,越西县,4,513400
513435,甘洛县,4,513400
513436,美姑县,4,513400
513437,雷波县,4,513400
520102,南明区,4,520100
520103,云岩区,4,520100
520111,花溪区,4,520100
520112,乌当区,4,520100
520113,白云区,4,520100
520115,观山湖区,4,520100
520121,开阳县,4,520100
520122,息烽县,4,520100
520123,修文县,4,520100
520181,清镇市,4,520100
520201,钟山区,4,520200
520203,六枝特区,4,520200
520204,水城区,4,520200
520281,盘州市,4,520200
520302,红花岗区,4,520300
520303,汇川区,4,520300
520304,播州区,4,520300
520322,桐梓县,4,520300
520323,绥阳县,4,520300
520324,正安县,4,520300
520325,道真仡佬族苗族自治县,4,520300
520326,务川仡佬族苗族自治县,4,520300
520327,凤冈县,4,520300
520328,湄潭县,4,520300
520329,余庆县,4,520300
520330,习水县,4,520300
520381,赤水市,4,520300
520382,仁怀市,4,520300
520402,西秀区,4,520400
520403,平坝区,4,520400
520422,普定县,4,520400
520423,镇宁布依族苗族自治县,4,520400
520424,关岭布依族苗族自治县,4,520400
520425,紫云苗族布依族自治县,4,520400
520502,七星关区,4,520500
520521,大方县,4,520500
520523,金沙县,4,520500
520524,织金县,4,520500
520525,纳雍县,4,520500
520526,威宁彝族回族苗族自治县,4,520500
520527,赫章县,4,520500
520581,黔西市,4,520500
520602,碧江区,4,520600
520603,万山区,4,520600
520621,江口县,4,520600
520622,玉屏侗族自治县,4,520600
520623,石阡县,4,520600
520624,思南县,4,520600
520625,印江土家族苗族自治县,4,520600
520626,德江县,4,520600
520627,沿河土家族自治县,4,520600
520628,松桃苗族自治县,4,520600
522301,兴义市,4,522300
522302,兴仁市,4,522300
522323,普安县,4,522300
522324,晴隆县,4,522300
522325,贞丰县,4,522300
522326,望谟县,4,522300
522327,册亨县,4,522300
522328,安龙县,4,522300
522601,凯里市,4,522600
522622,黄平县,4,522600
522623,施秉县,4,522600
522624,三穗县,4,522600
522625,镇远县,4,522600
522626,岑巩县,4,522600
522627,天柱县,4,522600
522628,锦屏县,4,522600
522629,剑河县,4,522600
522630,台江县,4,522600
522631,黎平县,4,522600
522632,榕江县,4,522600
522633,从江县,4,522600
522634,雷山县,4,522600
522635,麻江县,4,522600
522636,丹寨县,4,522600
522701,都匀市,4,522700
522702,福泉市,4,522700
522722,荔波县,4,522700
522723,贵定县,4,522700
522725,瓮安县,4,522700
522726,独山县,4,522700
522727,平塘县,4,522700
522728,罗甸县,4,522700
522729,长顺县,4,522700
522730,龙里县,4,522700
522731,惠水县,4,522700
522732,三都水族自治县,4,522700
530102,五华区,4,530100
530103,盘龙区,4,530100
530111,官渡区,4,530100
530112,西山区,4,530100
530113,东川区,4,530100
530114,呈贡区,4,530100
530115,晋宁区,4,530100
530124,富民县,4,530100
530125,宜良县,4,530100
530126,石林彝族自治县,4,530100
530127,嵩明县,4,530100
530128,禄劝彝族苗族自治县,4,530100
530129,寻甸回族彝族自治县,4,530100
530181,安宁市,4,530100
530302,麒麟区,4,530300
530303,沾益区,4,530300
530304,马龙区,4,530300
530322,陆良县,4,530300
530323,师宗县,4,530300
530324,罗平县,4,530300
530325,富源县,4,530300
530326,会泽县,4,530300
530381,宣威市,4,530300
530402,红塔区,4,530400
530403,江川区,4,530400
530423,通海县,4,530400
530424,华宁县,4,530400
530425,易门县,4,530400
530426,峨山彝族自治县,4,530400
530427,新平彝族傣族自治县,4,530400
530428,元江哈尼族彝族傣族自治县,4,530400
530481,澄江市,4,530400
530502,隆阳区,4,530500
530521,施甸县,4,530500
530523,龙陵县,4,530500
530524,昌宁县,4,530500
530581,腾冲市,4,530500
530602,昭阳区,4,530600
530621,鲁甸县,4,530600
530622,巧家县,4,530600
530623,盐津县,4,530600
530624,大关县,4,530600
530625,永善县,4,530600
530626,绥江县,4,530600
530627,镇雄县,4,530600
530628,彝良县,4,530600
530629,威信县,4,530600
530681,水富市,4,530600
530702,古城区,4,530700
530721,玉龙纳西族自治县,4,530700
530722,永胜县,4,530700
530723,华坪县,4,530700
530724,宁蒗彝族自治县,4,530700
530802,思茅区,4,530800
530821,宁洱哈尼族彝族自治县,4,530800
530822,墨江哈尼族自治县,4,530800
530823,景东彝族自治县,4,530800
530824,景谷傣族彝族自治县,4,530800
530825,镇沅彝族哈尼族拉祜族自治县,4,530800
530826,江城哈尼族彝族自治县,4,530800
530827,孟连傣族拉祜族佤族自治县,4,530800
530828,澜沧拉祜族自治县,4,530800
530829,西盟佤族自治县,4,530800
530902,临翔区,4,530900
530921,凤庆县,4,530900
530922,云县,4,530900
530923,永德县,4,530900
530924,镇康县,4,530900
530925,双江拉祜族佤族布朗族傣族自治县,4,530900
530926,耿马傣族佤族自治县,4,530900
530927,沧源佤族自治县,4,530900
532301,楚雄市,4,532300
532302,禄丰市,4,532300
532322,双柏县,4,532300
532323,牟定县,4,532300
532324,南华县,4,532300
532325,姚安县,4,532300
532326,大姚县,4,532300
532327,永仁县,4,532300
532328,元谋县,4,532300
532329,武定县,4,532300
532501,个旧市,4,532500
532502,开远市,4,532500
532503,蒙自市,4,532500
532504,弥勒市,4,532500
532523,屏边苗族自治县,4,532500
532524,建水县,4,532500
532525,石屏县,4,532500
532527,泸西县,4,532500
532528,元阳县,4,532500
532529,红河县,4,532500
532530,金平苗族瑶族傣族自治县,4,532500
532531,绿春县,4,532500
532532,河口瑶族自治县,4,532500
532601,文山市,4,532600
532622,砚山县,4,532600
532623,西畴县,4,532600
532624,麻栗坡县,4,532600
532625,马关县,4,532600
532626,丘北县,4,532600
532627,广南县,4,532600
532628,富宁县,4,532600
532801,景洪市,4,532800
532822,勐海县,4,532800
532823,勐腊县,4,532800
532901,大理市,4,532900
532922,漾濞彝族自治县,4,532900
532923,祥云县,4,532900
532924,宾川县,4,532900
532925,弥渡县,4,532900
532926,南涧彝族自治县,4,532900
532927,巍山彝族回族自治县,4,532900
532928,永平县,4,532900
532929,云龙县,4,532900
532930,洱源县,4,532900
532931,剑川县,4,532900
532932,鹤庆县,4,532900
533102,瑞丽市,4,533100
533103,芒市,4,533100
533122,梁河县,4,533100
533123,盈江县,4,533100
533124,陇川县,4,533100
533301,泸水市,4,533300
533323,福贡县,4,533300
533324,贡山独龙族怒族自治县,4,533300
533325,兰坪白族普米族自治县,4,533300
533401,香格里拉市,4,533400
533422,德钦县,4,533400
533423,维西傈僳族自治县,4,533400
540102,城关区,4,540100
540103,堆龙德庆区,4,540100
540104,达孜区,4,540100
540121,林周县,4,540100
540122,当雄县,4,540100
540123,尼木县,4,540100
540124,曲水县,4,540100
540127,墨竹工卡县,4,540100
540171,格尔木藏青工业园区,4,540100
540172,拉萨经济技术开发区,4,540100
540173,西藏文化旅游创意园区,4,540100
540174,达孜工业园区,4,540100
540202,桑珠孜区,4,540200
540221,南木林县,4,540200
540222,江孜县,4,540200
540223,定日县,4,540200
540224,萨迦县,4,540200
540225,拉孜县,4,540200
540226,昂仁县,4,540200
540227,谢通门县,4,540200
540228,白朗县,4,540200
540229,仁布县,4,540200
540230,康马县,4,540200
540231,定结县,4,540200
540232,仲巴县,4,540200
540233,亚东县,4,540200
540234,吉隆县,4,540200
540235,聂拉木县,4,540200
540236,萨嘎县,4,540200
540237,岗巴县,4,540200
540302,卡若区,4,540300
540321,江达县,4,540300
540322,贡觉县,4,540300
540323,类乌齐县,4,540300
540324,丁青县,4,540300
540325,察雅县,4,540300
540326,八宿县,4,540300
540327,左贡县,4,540300
540328,芒康县,4,540300
540329,洛隆县,4,540300
540330,边坝县,4,540300
540402,巴宜区,4,540400
540421,工布江达县,4,540400
540422,米林县,4,540400
540423,墨脱县,4,540400
540424,波密县,4,540400
540425,察隅县,4,540400
540426,朗县,4,540400
540502,乃东区,4,540500
540521,扎囊县,4,540500
540522,贡嘎县,4,540500
540523,桑日县,4,540500
540524,琼结县,4,540500
540525,曲松县,4,540500
540526,措美县,4,540500
540527,洛扎县,4,540500
540528,加查县,4,540500
540529,隆子县,4,540500
540530,错那县,4,540500
540531,浪卡子县,4,540500
540602,色尼区,4,540600
540621,嘉黎县,4,540600
540622,比如县,4,540600
540623,聂荣县,4,540600
540624,安多县,4,540600
540625,申扎县,4,540600
540626,索县,4,540600
540627,班戈县,4,540600
540628,巴青县,4,540600
540629,尼玛县,4,540600
540630,双湖县,4,540600
542521,普兰县,4,542500
542522,札达县,4,542500
542523,噶尔县,4,542500
542524,日土县,4,542500
542525,革吉县,4,542500
542526,改则县,4,542500
542527,措勤县,4,542500
610102,新城区,4,610100
610103,碑林区,4,610100
610104,莲湖区,4,610100
610111,灞桥区,4,610100
610112,未央区,4,610100
610113,雁塔区,4,610100
610114,阎良区,4,610100
610115,临潼区,4,610100
610116,长安区,4,610100
610117,高陵区,4,610100
610118,鄠邑区,4,610100
610122,蓝田县,4,610100
610124,周至县,4,610100
610202,王益区,4,610200
610203,印台区,4,610200
610204,耀州区,4,610200
610222,宜君县,4,610200
610302,渭滨区,4,610300
610303,金台区,4,610300
610304,陈仓区,4,610300
610305,凤翔区,4,610300
610323,岐山县,4,610300
610324,扶风县,4,610300
610326,眉县,4,610300
610327,陇县,4,610300
610328,千阳县,4,610300
610329,麟游县,4,610300
610330,凤县,4,610300
610331,太白县,4,610300
610402,秦都区,4,610400
610403,杨陵区,4,610400
610404,渭城区,4,610400
610422,三原县,4,610400
610423,泾阳县,4,610400
610424,乾县,4,610400
610425,礼泉县,4,610400
610426,永寿县,4,610400
610428,长武县,4,610400
610429,旬邑县,4,610400
610430,淳化县,4,610400
610431,武功县,4,610400
610481,兴平市,4,610400
610482,彬州市,4,610400
610502,临渭区,4,610500
610503,华州区,4,610500
610522,潼关县,4,610500
610523,大荔县,4,610500
610524,合阳县,4,610500
610525,澄城县,4,610500
610526,蒲城县,4,610500
610527,白水县,4,610500
610528,富平县,4,610500
610581,韩城市,4,610500
610582,华阴市,4,610500
610602,宝塔区,4,610600
610603,安塞区,4,610600
610621,延长县,4,610600
610622,延川县,4,610600
610625,志丹县,4,610600
610626,吴起县,4,610600
610627,甘泉县,4,610600
610628,富县,4,610600
610629,洛川县,4,610600
610630,宜川县,4,610600
610631,黄龙县,4,610600
610632,黄陵县,4,610600
610681,子长市,4,610600
610702,汉台区,4,610700
610703,南郑区,4,610700
610722,城固县,4,610700
610723,洋县,4,610700
610724,西乡县,4,610700
610725,勉县,4,610700
610726,宁强县,4,610700
610727,略阳县,4,610700
610728,镇巴县,4,610700
610729,留坝县,4,610700
610730,佛坪县,4,610700
610802,榆阳区,4,610800
610803,横山区,4,610800
610822,府谷县,4,610800
610824,靖边县,4,610800
610825,定边县,4,610800
610826,绥德县,4,610800
610827,米脂县,4,610800
610828,佳县,4,610800
610829,吴堡县,4,610800
610830,清涧县,4,610800
610831,子洲县,4,610800
610881,神木市,4,610800
610902,汉滨区,4,610900
610921,汉阴县,4,610900
610922,石泉县,4,610900
610923,宁陕县,4,610900
610924,紫阳县,4,610900
610925,岚皋县,4,610900
610926,平利县,4,610900
610927,镇坪县,4,610900
610929,白河县,4,610900
610981,旬阳市,4,610900
611002,商州区,4,611000
611021,洛南县,4,611000
611022,丹凤县,4,611000
611023,商南县,4,611000
611024,山阳县,4,611000
611025,镇安县,4,611000
611026,柞水县,4,611000
620102,城关区,4,620100
620103,七里河区,4,620100
620104,西固区,4,620100
620105,安宁区,4,620100
620111,红古区,4,620100
620121,永登县,4,620100
620122,皋兰县,4,620100
620123,榆中县,4,620100
620171,兰州新区,4,620100
620201,嘉峪关市,4,620200
620302,金川区,4,620300
620321,永昌县,4,620300
620402,白银区,4,620400
620403,平川区,4,620400
620421,靖远县,4,620400
620422,会宁县,4,620400
620423,景泰县,4,620400
620502,秦州区,4,620500
620503,麦积区,4,620500
620521,清水县,4,620500
620522,秦安县,4,620500
620523,甘谷县,4,620500
620524,武山县,4,620500
620525,张家川回族自治县,4,620500
620602,凉州区,4,620600
620621,民勤县,4,620600
620622,古浪县,4,620600
620623,天祝藏族自治县,4,620600
620702,甘州区,4,620700
620721,肃南裕固族自治县,4,620700
620722,民乐县,4,620700
620723,临泽县,4,620700
620724,高台县,4,620700
620725,山丹县,4,620700
620802,崆峒区,4,620800
620821,泾川县,4,620800
620822,灵台县,4,620800
620823,崇信县,4,620800
620825,庄浪县,4,620800
620826,静宁县,4,620800
620881,华亭市,4,620800
620902,肃州区,4,620900
620921,金塔县,4,620900
620922,瓜州县,4,620900
620923,肃北蒙古族自治县,4,620900
620924,阿克塞哈萨克族自治县,4,620900
620981,玉门市,4,620900
620982,敦煌市,4,620900
621002,西峰区,4,621000
621021,庆城县,4,621000
621022,环县,4,621000
621023,华池县,4,621000
621024,合水县,4,621000
621025,正宁县,4,621000
621026,宁县,4,621000
621027,镇原县,4,621000
621102,安定区,4,621100
621121,通渭县,4,621100
621122,陇西县,4,621100
621123,渭源县,4,621100
621124,临洮县,4,621100
621125,漳县,4,621100
621126,岷县,4,621100
621202,武都区,4,621200
621221,成县,4,621200
621222,文县,4,621200
621223,宕昌县,4,621200
621224,康县,4,621200
621225,西和县,4,621200
621226,礼县,4,621200
621227,徽县,4,621200
621228,两当县,4,621200
622901,临夏市,4,622900
622921,临夏县,4,622900
622922,康乐县,4,622900
622923,永靖县,4,622900
622924,广河县,4,622900
622925,和政县,4,622900
622926,东乡族自治县,4,622900
622927,积石山保安族东乡族撒拉族自治县,4,622900
623001,合作市,4,623000
623021,临潭县,4,623000
623022,卓尼县,4,623000
623023,舟曲县,4,623000
623024,迭部县,4,623000
623025,玛曲县,4,623000
623026,碌曲县,4,623000
623027,夏河县,4,623000
630102,城东区,4,630100
630103,城中区,4,630100
630104,城西区,4,630100
630105,城北区,4,630100
630106,湟中区,4,630100
630121,大通回族土族自治县,4,630100
630123,湟源县,4,630100
630202,乐都区,4,630200
630203,平安区,4,630200
630222,民和回族土族自治县,4,630200
630223,互助土族自治县,4,630200
630224,化隆回族自治县,4,630200
630225,循化撒拉族自治县,4,630200
632221,门源回族自治县,4,632200
632222,祁连县,4,632200
632223,海晏县,4,632200
632224,刚察县,4,632200
632301,同仁市,4,632300
632322,尖扎县,4,632300
632323,泽库县,4,632300
632324,河南蒙古族自治县,4,632300
632521,共和县,4,632500
632522,同德县,4,632500
632523,贵德县,4,632500
632524,兴海县,4,632500
632525,贵南县,4,632500
632621,玛沁县,4,632600
632622,班玛县,4,632600
632623,甘德县,4,632600
632624,达日县,4,632600
632625,久治县,4,632600
632626,玛多县,4,632600
632701,玉树市,4,632700
632722,杂多县,4,632700
632723,称多县,4,632700
632724,治多县,4,632700
632725,囊谦县,4,632700
632726,曲麻莱县,4,632700
632801,格尔木市,4,632800
632802,德令哈市,4,632800
632803,茫崖市,4,632800
632821,乌兰县,4,632800
632822,都兰县,4,632800
632823,天峻县,4,632800
632857,大柴旦行政委员会,4,632800
640104,兴庆区,4,640100
640105,西夏区,4,640100
640106,金凤区,4,640100
640121,永宁县,4,640100
640122,贺兰县,4,640100
640181,灵武市,4,640100
640202,大武口区,4,640200
640205,惠农区,4,640200
640221,平罗县,4,640200
640302,利通区,4,640300
640303,红寺堡区,4,640300
640323,盐池县,4,640300
640324,同心县,4,640300
640381,青铜峡市,4,640300
640402,原州区,4,640400
640422,西吉县,4,640400
640423,隆德县,4,640400
640424,泾源县,4,640400
640425,彭阳县,4,640400
640502,沙坡头区,4,640500
640521,中宁县,4,640500
640522,海原县,4,640500
650102,天山区,4,650100
650103,沙依巴克区,4,650100
650104,新市区,4,650100
650105,水磨沟区,4,650100
650106,头屯河区,4,650100
650107,达坂城区,4,650100
650109,米东区,4,650100
650121,乌鲁木齐县,4,650100
650202,独山子区,4,650200
650203,克拉玛依区,4,650200
650204,白碱滩区,4,650200
650205,乌尔禾区,4,650200
650402,高昌区,4,650400
650421,鄯善县,4,650400
650422,托克逊县,4,650400
650502,伊州区,4,650500
650521,巴里坤哈萨克自治县,4,650500
650522,伊吾县,4,650500
652301,昌吉市,4,652300
652302,阜康市,4,652300
652323,呼图壁县,4,652300
652324,玛纳斯县,4,652300
652325,奇台县,4,652300
652327,吉木萨尔县,4,652300
652328,木垒哈萨克自治县,4,652300
652701,博乐市,4,652700
652702,阿拉山口市,4,652700
652722,精河县,4,652700
652723,温泉县,4,652700
652801,库尔勒市,4,652800
652822,轮台县,4,652800
652823,尉犁县,4,652800
652824,若羌县,4,652800
652825,且末县,4,652800
652826,焉耆回族自治县,4,652800
652827,和静县,4,652800
652828,和硕县,4,652800
652829,博湖县,4,652800
652871,库尔勒经济技术开发区,4,652800
652901,阿克苏市,4,652900
652902,库车市,4,652900
652922,温宿县,4,652900
652924,沙雅县,4,652900
652925,新和县,4,652900
652926,拜城县,4,652900
652927,乌什县,4,652900
652928,阿瓦提县,4,652900
652929,柯坪县,4,652900
653001,阿图什市,4,653000
653022,阿克陶县,4,653000
653023,阿合奇县,4,653000
653024,乌恰县,4,653000
653101,喀什市,4,653100
653121,疏附县,4,653100
653122,疏勒县,4,653100
653123,英吉沙县,4,653100
653124,泽普县,4,653100
653125,莎车县,4,653100
653126,叶城县,4,653100
653127,麦盖提县,4,653100
653128,岳普湖县,4,653100
653129,伽师县,4,653100
653130,巴楚县,4,653100
653131,塔什库尔干塔吉克自治县,4,653100
653201,和田市,4,653200
653221,和田县,4,653200
653222,墨玉县,4,653200
653223,皮山县,4,653200
653224,洛浦县,4,653200
653225,策勒县,4,653200
653226,于田县,4,653200
653227,民丰县,4,653200
654002,伊宁市,4,654000
654003,奎屯市,4,654000
654004,霍尔果斯市,4,654000
654021,伊宁县,4,654000
654022,察布查尔锡伯自治县,4,654000
654023,霍城县,4,654000
654024,巩留县,4,654000
654025,新源县,4,654000
654026,昭苏县,4,654000
654027,特克斯县,4,654000
654028,尼勒克县,4,654000
654201,塔城市,4,654200
654202,乌苏市,4,654200
654203,沙湾市,4,654200
654221,额敏县,4,654200
654224,托里县,4,654200
654225,裕民县,4,654200
654226,和布克赛尔蒙古自治县,4,654200
654301,阿勒泰市,4,654300
654321,布尔津县,4,654300
654322,富蕴县,4,654300
654323,福海县,4,654300
654324,哈巴河县,4,654300
654325,青河县,4,654300
654326,吉木乃县,4,654300
659001,石河子市,4,659000
659002,阿拉尔市,4,659000
659003,图木舒克市,4,659000
659004,五家渠市,4,659000
659005,北屯市,4,659000
659006,铁门关市,4,659000
659007,双河市,4,659000
659008,可克达拉市,4,659000
659009,昆玉市,4,659000
659010,胡杨河市,4,659000
659011,新星市,4,659000
iailab-framework/iailab-common-biz-ip/src/main/resources/ip2region.xdb
Binary files differ
iailab-framework/iailab-common-biz-ip/src/test/java/com/iailab/framework/ip/core/utils/AreaUtilsTest.java
对比新文件
@@ -0,0 +1,36 @@
package com.iailab.framework.ip.core.utils;
import com.iailab.framework.ip.core.Area;
import com.iailab.framework.ip.core.enums.AreaTypeEnum;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
 * {@link AreaUtils} 的单元测试
 *
 * @author iailab
 */
public class AreaUtilsTest {
    @Test
    public void testGetArea() {
        // 调用:北京
        Area area = AreaUtils.getArea(110100);
        // 断言
        assertEquals(area.getId(), 110100);
        assertEquals(area.getName(), "北京市");
        assertEquals(area.getType(), AreaTypeEnum.CITY.getType());
        assertEquals(area.getParent().getId(), 110000);
        assertEquals(area.getChildren().size(), 16);
    }
    @Test
    public void testFormat() {
        assertEquals(AreaUtils.format(110105), "北京 北京市 朝阳区");
        assertEquals(AreaUtils.format(1), "中国");
        assertEquals(AreaUtils.format(2), "蒙古");
    }
}
iailab-framework/iailab-common-biz-ip/src/test/java/com/iailab/framework/ip/core/utils/IPUtilsTest.java
对比新文件
@@ -0,0 +1,47 @@
package com.iailab.framework.ip.core.utils;
import com.iailab.framework.ip.core.Area;
import org.junit.jupiter.api.Test;
import org.lionsoul.ip2region.xdb.Searcher;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
 * {@link IPUtils} 的单元测试
 *
 * @author wanglhup
 */
public class IPUtilsTest {
    @Test
    public void testGetAreaId_string() {
        // 120.202.4.0|120.202.4.255|420600
        Integer areaId = IPUtils.getAreaId("120.202.4.50");
        assertEquals(420600, areaId);
    }
    @Test
    public void testGetAreaId_long() throws Exception {
        // 120.203.123.0|120.203.133.255|360900
        long ip = Searcher.checkIP("120.203.123.250");
        Integer areaId = IPUtils.getAreaId(ip);
        assertEquals(360900, areaId);
    }
    @Test
    public void testGetArea_string() {
        // 120.202.4.0|120.202.4.255|420600
        Area area = IPUtils.getArea("120.202.4.50");
        assertEquals("襄阳市", area.getName());
    }
    @Test
    public void testGetArea_long() throws Exception {
        // 120.203.123.0|120.203.133.255|360900
        long ip = Searcher.checkIP("120.203.123.252");
        Area area = IPUtils.getArea(ip);
        assertEquals("宜春市", area.getName());
    }
}
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 iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports iailab-framework/iailab-common-web/src/main/resources/banner.txt iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java iailab-framework/iailab-common-websocket/pom.xml iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports iailab-framework/iailab-common/pom.xml iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java 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 pom.xml