Procházet zdrojové kódy

代码生成的表3.0

hyy před 5 měsíci
rodič
revize
c21b73a5c9
100 změnil soubory, kde provedl 10368 přidání a 0 odebrání
  1. 53 0
      .gitignore
  2. 20 0
      LICENSE
  3. 364 0
      README.md
  4. 4 0
      lombok.config
  5. 169 0
      pom.xml
  6. 661 0
      yudao-dependencies/pom.xml
  7. 45 0
      yudao-framework/pom.xml
  8. 149 0
      yudao-framework/yudao-common/pom.xml
  9. 15 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/IntArrayValuable.java
  10. 22 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java
  11. 46 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java
  12. 46 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java
  13. 21 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java
  14. 40 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java
  15. 39 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java
  16. 34 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
  17. 32 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java
  18. 60 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServerException.java
  19. 60 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java
  20. 41 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java
  21. 48 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java
  22. 77 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java
  23. 6 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java
  24. 112 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java
  25. 36 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java
  26. 41 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java
  27. 19 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortablePageParam.java
  28. 37 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java
  29. 49 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java
  30. 58 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java
  31. 322 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  32. 68 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java
  33. 19 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java
  34. 149 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java
  35. 309 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java
  36. 126 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
  37. 84 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
  38. 28 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java
  39. 202 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
  40. 37 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/NumberSerializer.java
  41. 27 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
  42. 26 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java
  43. 30 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java
  44. 131 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java
  45. 64 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java
  46. 62 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java
  47. 63 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java
  48. 67 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java
  49. 7 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java
  50. 119 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java
  51. 89 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java
  52. 24 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringUtils.java
  53. 80 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java
  54. 55 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
  55. 35 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java
  56. 42 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumCollectionValidator.java
  57. 44 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java
  58. 28 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java
  59. 25 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java
  60. 28 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java
  61. 25 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java
  62. 4 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java
  63. 1 0
      yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md
  64. 46 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml
  65. 46 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java
  66. 34 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java
  67. 35 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java
  68. 36 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java
  69. 72 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java
  70. 72 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java
  71. 57 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java
  72. 36 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java
  73. 28 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java
  74. 62 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java
  75. 206 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
  76. 20 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java
  77. 6 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java
  78. 63 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java
  79. 4 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java
  80. 2 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  81. 48 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml
  82. 61 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java
  83. 39 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/enums/AreaTypeEnum.java
  84. 214 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java
  85. 87 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java
  86. 11 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/package-info.java
  87. 3662 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv
  88. binární
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb
  89. 76 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml
  90. 49 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java
  91. 133 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java
  92. 18 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java
  93. 35 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java
  94. 68 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java
  95. 21 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java
  96. 43 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java
  97. 14 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java
  98. 56 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java
  99. 37 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java
  100. 47 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java

+ 53 - 0
.gitignore

@@ -0,0 +1,53 @@
+######################################################################
+# Build Tools
+
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+.flattened-pom.xml
+
+######################################################################
+# IDE
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+nbproject/private/
+build/*
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+
+######################################################################
+# Others
+*.log
+*.xml.versionsBackup
+*.swp
+
+!*/build/*.java
+!*/build/*.html
+!*/build/*.xml
+
+### JRebel ###
+rebel.xml
+
+application-my.yaml
+
+/yudao-ui-app/unpackage/

+ 20 - 0
LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2021 ruoyi-vue-pro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 364 - 0
README.md

@@ -0,0 +1,364 @@
+<p align="center">
+ <img src="https://img.shields.io/badge/Spring%20Boot-2.7.18-blue.svg" alt="Downloads">
+ <img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
+ <img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro"/>
+</p>
+
+**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!!**
+
+**「我喜欢写代码,乐此不疲」**  
+**「我喜欢做开源,以此为乐」**
+
+我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。
+
+如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
+
+## 🐶 新手必读
+
+* 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.yudao.iocoder.cn>
+* 演示地址【Vue3 + vben(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
+* 演示地址【Vue2 + element-ui】:<http://dashboard.yudao.iocoder.cn>
+* 启动文档:<https://doc.iocoder.cn/quick-start/>
+* 视频教程:<https://doc.iocoder.cn/video/>
+
+## 🐰 版本说明
+
+| 版本                                                                  | JDK 8 + Spring Boot 2.7                                                   | JDK 17/21 + Spring Boot 3.2                                                           |
+|---------------------------------------------------------------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
+| 【完整版】[ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-jdk17/) 分支 |
+| 【精简版】[yudao-boot-mini](https://gitee.com/yudaocode/yudao-boot-mini) | [`master`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master/) 分支   | [`master-jdk17`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master-jdk17/) 分支   |
+
+* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
+* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
+
+可参考 [《迁移文档》](https://doc.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
+
+## 🐯 平台简介
+
+**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
+
+> 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。
+>
+> 😜 给项目点点 Star 吧,这对我们真的很重要!
+
+![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
+
+* Java 后端:`master` 分支为 JDK 8 + Spring Boot 2.7,`master-jdk17` 分支为 JDK 17/21 + Spring Boot 3.2
+* 管理后台的电脑端:Vue3 提供 `element-plus`、`vben(ant-design-vue)` 两个版本,Vue2 提供 `element-ui` 版本
+* 管理后台的移动端:采用 `uni-app` 方案,一份代码多终端适配,同时支持 APP、小程序、H5!
+* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson
+* 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等
+* 消息队列可使用 Event、Redis、RabbitMQ、Kafka、RocketMQ 等
+* 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统,支持 SSO 单点登录
+* 支持加载动态权限菜单,按钮级别权限控制,Redis 缓存提升性能
+* 支持 SaaS 多租户,可自定义每个租户的权限,提供透明化的多租户底层封装
+* 工作流使用 Flowable,支持动态表单、在线设计流程、会签 / 或签、多种任务分配方式
+* 高效率开发,使用代码生成器可以一键生成 Java、Vue 前后端代码、SQL 脚本、接口文档,支持单表、树表、主子表
+* 实时通信,采用 Spring WebSocket 实现,内置 Token 身份校验,支持 WebSocket 集群
+* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款
+* 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务
+* 集成报表设计器、大屏设计器,通过拖拽即可生成酷炫的报表与大屏
+
+##  🐳 项目关系
+
+![架构演进](/.image/common/yudao-roadmap.png)
+
+三个项目的功能对比,可见社区共同整理的 [国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) 表格。
+
+### 后端项目
+
+| 项目                                                              | Star                                                                                                                                                                                                                                                                                             | 简介                          |
+|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
+| [ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro)  | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro)       | 基于 Spring Boot 多模块架构        |
+| [yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud)      | [![Gitee star](https://gitee.com/zhijiantianya/yudao-cloud/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yudao-cloud) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/yudao-cloud.svg?style=social&label=Stars)](https://github.com/YunaiV/yudao-cloud)               | 基于 Spring Cloud 微服务架构       |
+| [Spring-Boot-Labs](https://gitee.com/yudaocode/SpringBoot-Labs) | [![Gitee star](https://gitee.com/yudaocode/SpringBoot-Labs/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yudao-cloud) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/SpringBoot-Labs.svg?style=social&label=Stars)](https://github.com/yudaocode/SpringBoot-Labs) | 系统学习 Spring Boot & Cloud 专栏 |
+
+### 前端项目
+
+| 项目                                                                         | Star                                                                                                                                                                                                                                                                                                                     | 简介                                     |
+|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------|
+| [yudao-ui-admin-vue3](https://gitee.com/yudaocode/yudao-ui-admin-vue3)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue3/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue3) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue3.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue3)         | 基于 Vue3 + element-plus 实现的管理后台         |
+| [yudao-ui-admin-vben](https://gitee.com/yudaocode/yudao-ui-admin-vben)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vben/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vben) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vben.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vben)         | 基于 Vue3 + vben(ant-design-vue) 实现的管理后台 |
+| [yudao-mall-uniapp](https://gitee.com/yudaocode/yudao-mall-uniapp)         | [![Gitee star](https://gitee.com/yudaocode/yudao-mall-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-mall-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-mall-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-mall-uniapp)                 | 基于 uni-app 实现的商城小程序                    |
+| [yudao-ui-admin-vue2](https://gitee.com/yudaocode/yudao-ui-admin-vue2)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue2/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue2) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue2.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue2)         | 基于 Vue2 + element-ui 实现的管理后台           |
+| [yudao-ui-admin-uniapp](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-uniapp) | 基于 Vue2 + element-ui 实现的管理后台           |
+| [yudao-ui-go-view](https://gitee.com/yudaocode/yudao-ui-go-view)           | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-go-view.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-go-view)                     | 基于 Vue3 + naive-ui 实现的大屏报表             |
+
+## 😎 开源协议
+
+**为什么推荐使用本项目?**
+
+① 本项目采用比 Apache 2.0 更宽松的 [MIT License](https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE) 开源协议,个人与企业可 100% 免费使用,不用保留类作者、Copyright 信息。
+
+② 代码全部开源,不会像其他项目一样,只开源部分代码,让你无法了解整个项目的架构设计。[国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn)
+
+![开源项目对比](/.image/common/project-vs.png)
+
+③ 代码整洁、架构整洁,遵循《阿里巴巴 Java 开发手册》规范,代码注释详细,113770 行 Java 代码,42462 行代码注释。
+
+## 🤝 项目外包
+
+我们也是接外包滴,如果你有项目想要外包,可以微信联系【**Aix9975**】。
+
+团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
+
+项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。
+
+## 🐼 内置功能
+
+系统内置多种多种业务功能,可以用于快速你的业务系统:
+
+![功能分层](/.image/common/ruoyi-vue-pro-biz.png)
+
+* 通用模块(必选):系统功能、基础设施
+* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
+* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
+
+> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
+>
+> * 额外新增的功能,我们使用 🚀 标记。
+> * 重新实现的功能,我们使用 ⭐️ 标记。
+
+🙂 所有功能,都通过 **单元测试** 保证高质量。
+
+### 系统功能
+
+|     | 功能    | 描述                              |
+|-----|-------|---------------------------------|
+|     | 用户管理  | 用户是系统操作者,该功能主要完成系统用户配置          |
+| ⭐️  | 在线用户  | 当前系统中活跃用户状态监控,支持手动踢下线           |
+|     | 角色管理  | 角色菜单权限分配、设置角色按机构进行数据范围权限划分      |
+|     | 菜单管理  | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能    |
+|     | 部门管理  | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限  |
+|     | 岗位管理  | 配置系统用户所属担任职务                    |
+| 🚀  | 租户管理  | 配置系统租户,支持 SaaS 场景下的多租户功能        |
+| 🚀  | 租户套餐  | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限       |
+|     | 字典管理  | 对系统中经常使用的一些较为固定的数据进行维护          |
+| 🚀  | 短信管理  | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 |
+| 🚀  | 邮件管理  | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台       |
+| 🚀  | 站内信   | 系统内的消息通知,提供站内信模版、站内信消息          |
+| 🚀  | 操作日志  | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
+| ⭐️  | 登录日志  | 系统登录日志记录查询,包含登录异常               |
+| 🚀  | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务     |
+|     | 通知公告  | 系统通知公告信息发布维护                    |
+| 🚀  | 敏感词   | 配置系统敏感词,支持标签分组                  |
+| 🚀  | 应用管理  | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
+| 🚀  | 地区管理  | 展示省份、城市、区镇等城市信息,支持 IP 对应城市      |
+
+![功能图](/.image/common/system-feature.png)
+
+### 工作流程
+
+|     | 功能    | 描述                                     |
+|-----|-------|----------------------------------------|
+| 🚀  | 流程模型  | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 |
+| 🚀  | 流程表单  | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
+| 🚀  | 用户分组  | 自定义用户分组,可用于工作流的审批分组                    |
+| 🚀  | 我的流程  | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线    |
+| 🚀  | 待办任务  | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作    |
+| 🚀  | 已办任务  | 查看自己【已】审批的工作任务,未来会支持回退操作               |
+| 🚀  | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
+
+![功能图](/.image/common/bpm-feature.png)
+
+### 支付系统
+
+|     | 功能   | 描述                        |
+|-----|------|---------------------------|
+| 🚀  | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
+| 🚀  | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单     |
+| 🚀  | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单     |
+| 🚀  | 回调通知 | 查看支付回调业务的【支付】【退款】的通知结果    |
+| 🚀  | 接入示例 | 提供接入支付系统的【支付】【退款】的功能实战    |
+
+### 基础设施
+
+|     | 功能        | 描述                                           |
+|-----|-----------|----------------------------------------------|
+| 🚀  | 代码生成      | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载       |
+| 🚀  | 系统接口      | 基于 Swagger 自动生成相关的 RESTful API 接口文档          |
+| 🚀  | 数据库文档     | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式      |
+|     | 表单构建      | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件         |
+| 🚀  | 配置管理      | 对系统动态配置常用参数,支持 SpringBoot 加载                 |
+| ⭐️  | 定时任务      | 在线(添加、修改、删除)任务调度包含执行结果日志                     |
+| 🚀  | 文件服务      | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等   | 
+| 🚀  | WebSocket | 提供 WebSocket 接入示例,支持一对一、一对多发送方式              | 
+| 🚀  | API 日志    | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题   |
+|     | MySQL 监控  | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈              |
+|     | Redis 监控  | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理           |
+| 🚀  | 消息队列      | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
+| 🚀  | Java 监控   | 基于 Spring Boot Admin 实现 Java 应用的监控           |
+| 🚀  | 链路追踪      | 接入 SkyWalking 组件,实现链路追踪                      |
+| 🚀  | 日志中心      | 接入 SkyWalking 组件,实现日志中心                      |
+| 🚀  | 服务保障      | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景              |
+| 🚀  | 日志服务      | 轻量级日志中心,查看远程服务器的日志                           |
+| 🚀  | 单元测试      | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等    |
+
+![功能图](/.image/common/infra-feature.png)
+
+### 数据报表
+
+|     | 功能    | 描述                 |
+|-----|-------|--------------------|
+| 🚀  | 报表设计器 | 支持数据报表、图形报表、打印设计等  |
+| 🚀  | 大屏设计器 | 拖拽生成数据大屏,内置几十种图表组件 |
+
+### 微信公众号
+
+|     | 功能     | 描述                            |
+|-----|--------|-------------------------------|
+| 🚀  | 账号管理   | 配置接入的微信公众号,可支持多个公众号           |
+| 🚀  | 数据统计   | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据  |
+| 🚀  | 粉丝管理   | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
+| 🚀  | 消息管理   | 查看粉丝发送的消息列表,可主动回复粉丝消息         |
+| 🚀  | 自动回复   | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
+| 🚀  | 标签管理   | 对公众号的标签进行创建、查询、修改、删除等操作       |
+| 🚀  | 菜单管理   | 自定义公众号的菜单,也可以从公众号同步菜单         |
+| 🚀  | 素材管理   | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
+| 🚀  | 图文草稿箱  | 新增常用的图文素材到草稿箱,可发布到公众号         |
+| 🚀  | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作           |
+
+### 商城系统
+
+演示地址:<https://doc.iocoder.cn/mall-preview/>
+
+![功能图](/.image/common/mall-feature.png)
+
+![功能图](/.image/common/mall-preview.png)
+
+### 会员中心
+
+|     | 功能   | 描述                               |
+|-----|------|----------------------------------|
+| 🚀  | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理        |
+| 🚀  | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作           |
+| 🚀  | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益      |
+| 🚀  | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段         |
+| 🚀  | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 |
+
+### ERP 系统
+
+演示地址:<https://doc.iocoder.cn/erp-preview/>
+
+![功能图](/.image/common/erp-feature.png)
+
+### CRM 系统
+
+演示地址:<https://doc.iocoder.cn/crm-preview/>
+
+![功能图](/.image/common/crm-feature.png)
+
+### AI 大模型
+
+演示地址:<https://doc.iocoder.cn/ai-preview/>
+
+![功能图](/.image/common/ai-feature.png)
+
+![功能图](/.image/common/ai-preview.gif)
+
+## 🐨 技术栈
+
+### 模块
+
+| 项目                    | 说明                 |
+|-----------------------|--------------------|
+| `yudao-dependencies`  | Maven 依赖版本管理       |
+| `yudao-framework`     | Java 框架拓展          |
+| `yudao-server`        | 管理后台 + 用户 APP 的服务端 |
+| `yudao-module-system` | 系统功能的 Module 模块    |
+| `yudao-module-member` | 会员中心的 Module 模块    |
+| `yudao-module-infra`  | 基础设施的 Module 模块    |
+| `yudao-module-bpm`    | 工作流程的 Module 模块    |
+| `yudao-module-pay`    | 支付系统的 Module 模块    |
+| `yudao-module-mall`   | 商城系统的 Module 模块    |
+| `yudao-module-erp`    | ERP 系统的 Module 模块  |
+| `yudao-module-crm`    | CRM 系统的 Module 模块  |
+| `yudao-module-ai`     | AI 大模型的 Module 模块  |
+| `yudao-module-mp`     | 微信公众号的 Module 模块   |
+| `yudao-module-report` | 大屏报表 Module 模块     |
+
+### 框架
+
+| 框架                                                                                          | 说明               | 版本             | 学习指南                                                           |
+|---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
+| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 2.7.18         | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
+| [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器           | 5.7 / 8.0+     |                                                                |
+| [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件    | 1.2.23         | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
+| [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.7          | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |
+| [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源            | 3.6.1          | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
+| [Redis](https://redis.io/)                                                                  | key-value 数据库    | 5.0 / 6.0 /7.0 |                                                                |
+| [Redisson](https://github.com/redisson/redisson)                                            | Redis 客户端        | 3.32.0         | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao)           |
+| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架           | 5.3.24         | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao)               |
+| [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架      | 5.7.11         | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
+| [Hibernate Validator](https://github.com/hibernate/hibernate-validator)                     | 参数校验组件           | 6.2.5          | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao)      |
+| [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎            | 6.8.0          | [文档](https://doc.iocoder.cn/bpm/)                              |
+| [Quartz](https://github.com/quartz-scheduler)                                               | 任务调度组件           | 2.3.2          | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao)             |
+| [Springdoc](https://springdoc.org/)                                                         | Swagger 文档       | 1.7.0          | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao)         |
+| [SkyWalking](https://skywalking.apache.org/)                                                | 分布式应用追踪系统        | 8.12.0         | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao)      |
+| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台 | 2.7.10         | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao)           |
+| [Jackson](https://github.com/FasterXML/jackson)                                             | JSON 工具库         | 2.13.5         |                                                                |
+| [MapStruct](https://mapstruct.org/)                                                         | Java Bean 转换     | 1.5.5.Final    | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao)       |
+| [Lombok](https://projectlombok.org/)                                                        | 消除冗长的 Java 代码    | 1.18.34        | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao)          |
+| [JUnit](https://junit.org/junit5/)                                                          | Java 单元测试框架      | 5.8.2          | -                                                              |
+| [Mockito](https://github.com/mockito/mockito)                                               | Java Mock 框架     | 4.8.0          | -                                                              |
+
+## 🐷 演示图
+
+### 系统功能
+
+| 模块       | biu                         | biu                       | biu                      |
+|----------|-----------------------------|---------------------------|--------------------------|
+| 登录 & 首页  | ![登录](/.image/登录.jpg)       | ![首页](/.image/首页.jpg)     | ![个人中心](/.image/个人中心.jpg) |
+| 用户 & 应用  | ![用户管理](/.image/用户管理.jpg)   | ![令牌管理](/.image/令牌管理.jpg) | ![应用管理](/.image/应用管理.jpg) |
+| 租户 & 套餐  | ![租户管理](/.image/租户管理.jpg)   | ![租户套餐](/.image/租户套餐.png) | -                        |
+| 部门 & 岗位  | ![部门管理](/.image/部门管理.jpg)   | ![岗位管理](/.image/岗位管理.jpg) | -                        |
+| 菜单 & 角色  | ![菜单管理](/.image/菜单管理.jpg)   | ![角色管理](/.image/角色管理.jpg) | -                        |
+| 审计日志     | ![操作日志](/.image/操作日志.jpg)   | ![登录日志](/.image/登录日志.jpg) | -                        |
+| 短信       | ![短信渠道](/.image/短信渠道.jpg)   | ![短信模板](/.image/短信模板.jpg) | ![短信日志](/.image/短信日志.jpg) |
+| 字典 & 敏感词 | ![字典类型](/.image/字典类型.jpg)   | ![字典数据](/.image/字典数据.jpg) | ![敏感词](/.image/敏感词.jpg)  |
+| 错误码 & 通知 | ![错误码管理](/.image/错误码管理.jpg) | ![通知公告](/.image/通知公告.jpg) | -                        |
+
+### 工作流程
+
+| 模块      | biu                             | biu                             | biu                             |
+|---------|---------------------------------|---------------------------------|---------------------------------|
+| 流程模型    | ![流程模型-列表](/.image/流程模型-列表.jpg) | ![流程模型-设计](/.image/流程模型-设计.jpg) | ![流程模型-定义](/.image/流程模型-定义.jpg) |
+| 表单 & 分组 | ![流程表单](/.image/流程表单.jpg)       | ![用户分组](/.image/用户分组.jpg)       | -                               |
+| 我的流程    | ![我的流程-列表](/.image/我的流程-列表.jpg) | ![我的流程-发起](/.image/我的流程-发起.jpg) | ![我的流程-详情](/.image/我的流程-详情.jpg) |
+| 待办 & 已办 | ![任务列表-审批](/.image/任务列表-审批.jpg) | ![任务列表-待办](/.image/任务列表-待办.jpg) | ![任务列表-已办](/.image/任务列表-已办.jpg) |
+| OA 请假   | ![OA请假-列表](/.image/OA请假-列表.jpg) | ![OA请假-发起](/.image/OA请假-发起.jpg) | ![OA请假-详情](/.image/OA请假-详情.jpg) |
+
+### 基础设施
+
+| 模块            | biu                           | biu                         | biu                       |
+|---------------|-------------------------------|-----------------------------|---------------------------|
+| 代码生成          | ![代码生成](/.image/代码生成.jpg)     | ![生成效果](/.image/生成效果.jpg)   | -                         |
+| 文档            | ![系统接口](/.image/系统接口.jpg)     | ![数据库文档](/.image/数据库文档.jpg) | -                         |
+| 文件 & 配置       | ![文件配置](/.image/文件配置.jpg)     | ![文件管理](/.image/文件管理2.jpg)  | ![配置管理](/.image/配置管理.jpg) |
+| 定时任务          | ![定时任务](/.image/定时任务.jpg)     | ![任务日志](/.image/任务日志.jpg)   | -                         |
+| API 日志        | ![访问日志](/.image/访问日志.jpg)     | ![错误日志](/.image/错误日志.jpg)   | -                         |
+| MySQL & Redis | ![MySQL](/.image/MySQL.jpg)   | ![Redis](/.image/Redis.jpg) | -                         |
+| 监控平台          | ![Java监控](/.image/Java监控.jpg) | ![链路追踪](/.image/链路追踪.jpg)   | ![日志中心](/.image/日志中心.jpg) |
+
+### 支付系统
+
+| 模块      | biu                       | biu                             | biu                             |
+|---------|---------------------------|---------------------------------|---------------------------------|
+| 商家 & 应用 | ![商户信息](/.image/商户信息.jpg) | ![应用信息-列表](/.image/应用信息-列表.jpg) | ![应用信息-编辑](/.image/应用信息-编辑.jpg) |
+| 支付 & 退款 | ![支付订单](/.image/支付订单.jpg) | ![退款订单](/.image/退款订单.jpg)       | ---                             |
+### 数据报表
+
+| 模块    | biu                             | biu                             | biu                                   |
+|-------|---------------------------------|---------------------------------|---------------------------------------|
+| 报表设计器 | ![数据报表](/.image/报表设计器-数据报表.jpg) | ![图形报表](/.image/报表设计器-图形报表.jpg) | ![报表设计器-打印设计](/.image/报表设计器-打印设计.jpg) |
+| 大屏设计器 | ![大屏列表](/.image/大屏设计器-列表.jpg)   | ![大屏预览](/.image/大屏设计器-预览.jpg)   | ![大屏编辑](/.image/大屏设计器-编辑.jpg)         |
+
+### 移动端(管理后台)
+
+| biu                              | biu                              | biu                              |
+|----------------------------------|----------------------------------|----------------------------------|
+| ![](/.image/admin-uniapp/01.png) | ![](/.image/admin-uniapp/02.png) | ![](/.image/admin-uniapp/03.png) |
+| ![](/.image/admin-uniapp/04.png) | ![](/.image/admin-uniapp/05.png) | ![](/.image/admin-uniapp/06.png) |
+| ![](/.image/admin-uniapp/07.png) | ![](/.image/admin-uniapp/08.png) | ![](/.image/admin-uniapp/09.png) |
+
+目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。

+ 4 - 0
lombok.config

@@ -0,0 +1,4 @@
+config.stopBubbling = true
+lombok.tostring.callsuper=CALL
+lombok.equalsandhashcode.callsuper=CALL
+lombok.accessors.chain=true

+ 169 - 0
pom.xml

@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>cn.iocoder.boot</groupId>
+    <artifactId>yudao</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+    <modules>
+        <module>yudao-dependencies</module>
+        <module>yudao-framework</module>
+        <!-- Server 主项目 -->
+        <module>yudao-server</module>
+        <!-- 各种 module 拓展 -->
+        <module>yudao-module-system</module>
+        <module>yudao-module-infra</module>
+        <module>yudao-module-museums</module>
+
+<!--        <module>yudao-module-museums-api</module>-->
+<!--        <module>yudao-module-museums-biz</module>-->
+        <!--        <module>yudao-module-member</module>-->
+<!--        <module>yudao-module-bpm</module>-->
+<!--        <module>yudao-module-report</module>-->
+<!--        <module>yudao-module-mp</module>-->
+<!--        <module>yudao-module-pay</module>-->
+<!--        <module>yudao-module-mall</module>-->
+<!--        <module>yudao-module-crm</module>-->
+<!--        <module>yudao-module-erp</module>-->
+<!--        <module>yudao-module-ai</module>-->
+    </modules>
+
+    <name>${project.artifactId}</name>
+    <description>芋道项目基础脚手架</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <properties>
+        <revision>2.2.0-jdk8-snapshot</revision>
+        <!-- Maven 相关 -->
+        <java.version>1.8</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
+        <maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
+        <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
+        <!-- 看看咋放到 bom 里 -->
+        <lombok.version>1.18.34</lombok.version>
+        <spring.boot.version>2.7.18</spring.boot.version>
+        <mapstruct.version>1.5.5.Final</mapstruct.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-dependencies</artifactId>
+                <version>${revision}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <!-- maven-surefire-plugin 插件,用于运行单元测试。 -->
+                <!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${maven-surefire-plugin.version}</version>
+                </plugin>
+                <!-- maven-compiler-plugin 插件,解决 spring-boot-configuration-processor + Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>${maven-compiler-plugin.version}</version>
+                    <configuration>
+                        <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring.boot.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok</artifactId>
+                                <version>${lombok.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.mapstruct</groupId>
+                                <artifactId>mapstruct-processor</artifactId>
+                                <version>${mapstruct.version}</version>
+                            </path>
+                        </annotationProcessorPaths>
+                    </configuration>
+                </plugin>
+                <plugin>
+                    <groupId>org.codehaus.mojo</groupId>
+                    <artifactId>flatten-maven-plugin</artifactId>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+            <!-- 统一 revision 版本 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <flattenMode>resolveCiFriendliesOnly</flattenMode>
+                    <updatePomFile>true</updatePomFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                    </execution>
+                    <execution>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <!-- 使用 huawei / aliyun 的 Maven 源,提升下载速度 -->
+    <repositories>
+        <repository>
+            <id>huaweicloud</id>
+            <name>huawei</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
+        </repository>
+        <repository>
+            <id>aliyunmaven</id>
+            <name>aliyun</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+        </repository>
+
+        <repository>
+            <id>spring-milestones</id>
+            <name>Spring Milestones</name>
+            <url>https://repo.spring.io/milestone</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>spring-snapshots</id>
+            <name>Spring Snapshots</name>
+            <url>https://repo.spring.io/snapshot</url>
+            <releases>
+                <enabled>false</enabled>
+            </releases>
+        </repository>
+    </repositories>
+
+</project>

+ 661 - 0
yudao-dependencies/pom.xml

@@ -0,0 +1,661 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>cn.iocoder.boot</groupId>
+    <artifactId>yudao-dependencies</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>基础 bom 文件,管理整个项目的依赖版本</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <properties>
+        <revision>2.2.0-jdk8-snapshot</revision>
+        <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
+        <!-- 统一依赖管理 -->
+        <spring.boot.version>2.7.18</spring.boot.version>
+        <!-- Web 相关 -->
+        <springdoc.version>1.7.0</springdoc.version>
+        <knife4j.version>4.5.0</knife4j.version>
+        <servlet.versoin>2.5</servlet.versoin>
+        <!-- DB 相关 -->
+        <druid.version>1.2.23</druid.version>
+        <mybatis.version>3.5.16</mybatis.version>
+        <mybatis-plus.version>3.5.7</mybatis-plus.version>
+        <mybatis-plus-generator.version>3.5.7</mybatis-plus-generator.version>
+        <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
+        <mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
+        <easy-trans.version>3.0.5</easy-trans.version>
+        <redisson.version>3.32.0</redisson.version>
+        <dm8.jdbc.version>8.1.3.62</dm8.jdbc.version>
+        <kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
+        <opengauss.jdbc.version>5.0.2</opengauss.jdbc.version>
+        <!-- 消息队列 -->
+        <rocketmq-spring.version>2.3.0</rocketmq-spring.version>
+        <!-- 服务保障相关 -->
+        <lock4j.version>2.2.7</lock4j.version>
+        <!-- 监控相关 -->
+        <skywalking.version>8.12.0</skywalking.version>
+        <spring-boot-admin.version>2.7.15</spring-boot-admin.version>
+        <opentracing.version>0.33.0</opentracing.version>
+        <!-- Test 测试相关 -->
+        <podam.version>7.2.11.RELEASE</podam.version> <!-- Spring Boot 2.X 最多使用 7.2.11 版本 -->
+        <jedis-mock.version>1.1.2</jedis-mock.version>
+        <mockito-inline.version>4.11.0</mockito-inline.version>
+        <!-- Bpm 工作流相关 -->
+        <flowable.version>6.8.0</flowable.version>
+        <!-- 工具类相关 -->
+        <captcha-plus.version>1.0.10</captcha-plus.version>
+        <jsoup.version>1.18.1</jsoup.version>
+        <lombok.version>1.18.34</lombok.version>
+        <mapstruct.version>1.5.5.Final</mapstruct.version>
+        <hutool.version>5.8.29</hutool.version>
+        <easyexcel.verion>3.3.4</easyexcel.verion>
+        <velocity.version>2.3</velocity.version>
+        <fastjson.version>1.2.83</fastjson.version>
+        <guava.version>33.2.1-jre</guava.version>
+        <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
+        <commons-net.version>3.11.1</commons-net.version>
+        <jsch.version>0.1.55</jsch.version>
+        <tika-core.version>2.9.2</tika-core.version>
+        <ip2region.version>2.7.0</ip2region.version>
+        <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
+        <!-- 三方云服务相关 -->
+        <okio.version>3.5.0</okio.version>
+        <okhttp3.version>4.11.0</okhttp3.version>
+        <commons-io.version>2.15.1</commons-io.version>
+        <minio.version>8.5.7</minio.version>
+        <aliyun-java-sdk-core.version>4.6.4</aliyun-java-sdk-core.version>
+        <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
+        <tencentcloud-sdk-java.version>3.1.880</tencentcloud-sdk-java.version>
+        <justauth.version>1.0.8</justauth.version>
+        <jimureport.version>1.7.8</jimureport.version>
+        <xercesImpl.version>2.12.2</xercesImpl.version>
+        <weixin-java.version>4.6.0</weixin-java.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- 统一依赖管理 -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring.boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- 业务组件 -->
+            <dependency>
+                <groupId>io.github.mouzt</groupId>
+                <artifactId>bizlog-sdk</artifactId>
+                <version>${bizlog-sdk.version}</version>
+                <exclusions>
+                    <exclusion> <!-- 排除掉springboot依赖使用项目的 -->
+                        <groupId>org.springframework.boot</groupId>
+                        <artifactId>spring-boot-starter</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- Spring 核心 -->
+            <dependency>
+                <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-configuration-processor</artifactId>
+                <version>${spring.boot.version}</version>
+            </dependency>
+
+            <!-- Web 相关 -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-web</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-security</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-websocket</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
+                <version>${knife4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springdoc</groupId>
+                <artifactId>springdoc-openapi-ui</artifactId>
+                <version>${springdoc.version}</version>
+            </dependency>
+
+            <!-- DB 相关 -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>druid-spring-boot-starter</artifactId>
+                <version>${druid.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mybatis</groupId>
+                <artifactId>mybatis</artifactId>
+                <version>${mybatis.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-boot-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
+                <version>${mybatis-plus-generator.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
+                <version>${dynamic-datasource.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.yulichang</groupId>
+                <artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
+                <version>${mybatis-plus-join.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
+                <artifactId>easy-trans-spring-boot-starter</artifactId>
+                <version>${easy-trans.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.springframework</groupId>
+                        <artifactId>spring-context</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.springframework.cloud</groupId>
+                        <artifactId>spring-cloud-commons</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>com.fhs-opensource</groupId>
+                <artifactId>easy-trans-mybatis-plus-extend</artifactId>
+                <version>${easy-trans.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.fhs-opensource</groupId>
+                <artifactId>easy-trans-anno</artifactId>
+                <version>${easy-trans.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-redis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.springframework.boot</groupId>
+                        <artifactId>spring-boot-starter-actuator</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.redisson</groupId>
+                        <!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
+                        <artifactId>redisson-spring-data-33</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-data-27</artifactId>
+                <version>${redisson.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.dameng</groupId>
+                <artifactId>DmJdbcDriver18</artifactId>
+                <version>${dm8.jdbc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.opengauss</groupId>
+                <artifactId>opengauss-jdbc</artifactId>
+                <version>${opengauss.jdbc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.com.kingbase</groupId>
+                <artifactId>kingbase8</artifactId>
+                <version>${kingbase.jdbc.version}</version>
+            </dependency>
+
+            <!-- Job 定时任务相关 -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-job</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 消息队列相关 -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-mq</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.rocketmq</groupId>
+                <artifactId>rocketmq-spring-boot-starter</artifactId>
+                <version>${rocketmq-spring.version}</version>
+            </dependency>
+
+            <!-- 服务保障相关 -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-protection</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
+                <version>${lock4j.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>redisson-spring-boot-starter</artifactId>
+                        <groupId>org.redisson</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!-- 监控相关 -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-monitor</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-trace</artifactId>
+                <version>${skywalking.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-logback-1.x</artifactId>
+                <version>${skywalking.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-opentracing</artifactId>
+                <version>${skywalking.version}</version>
+                <!--                <exclusions>-->
+                <!--                    <exclusion>-->
+                <!--                        <artifactId>opentracing-api</artifactId>-->
+                <!--                        <groupId>io.opentracing</groupId>-->
+                <!--                    </exclusion>-->
+                <!--                    <exclusion>-->
+                <!--                        <artifactId>opentracing-util</artifactId>-->
+                <!--                        <groupId>io.opentracing</groupId>-->
+                <!--                    </exclusion>-->
+                <!--                </exclusions>-->
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-api</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-util</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-noop</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>de.codecentric</groupId>
+                <artifactId>spring-boot-admin-starter-server</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
+                <version>${spring-boot-admin.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>de.codecentric</groupId>
+                        <artifactId>spring-boot-admin-server-cloud</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>de.codecentric</groupId>
+                <artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
+                <version>${spring-boot-admin.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.mockito</groupId>
+                <artifactId>mockito-inline</artifactId>
+                <version>${mockito-inline.version}</version> <!-- 支持 Mockito 的 final 类与 static 方法的 mock -->
+            </dependency>
+
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-test</artifactId>
+                <version>${spring.boot.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>asm</artifactId>
+                        <groupId>org.ow2.asm</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.mockito</groupId>
+                        <artifactId>mockito-core</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
+                <artifactId>jedis-mock</artifactId>
+                <version>${jedis-mock.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
+                <artifactId>podam</artifactId>
+                <version>${podam.version}</version>
+            </dependency>
+
+            <!-- 工作流相关 -->
+            <dependency>
+                <groupId>org.flowable</groupId>
+                <artifactId>flowable-spring-boot-starter-process</artifactId>
+                <version>${flowable.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.flowable</groupId>
+                <artifactId>flowable-spring-boot-starter-actuator</artifactId>
+                <version>${flowable.version}</version>
+            </dependency>
+            <!-- 工作流相关结束 -->
+
+            <!-- 工具类相关 -->
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-common</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-excel</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct-jdk8</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct-processor</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>easyexcel</artifactId>
+                <version>${easyexcel.verion}</version>
+            </dependency>
+            <dependency>
+                <groupId>commons-io</groupId>
+                <artifactId>commons-io</artifactId>
+                <version>${commons-io.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.tika</groupId>
+                <artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
+                <version>${tika-core.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.velocity</groupId>
+                <artifactId>velocity-engine-core</artifactId>
+                <version>${velocity.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>fastjson</artifactId>
+                <version>${fastjson.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>${guava.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.google.inject</groupId>
+                <artifactId>guice</artifactId>
+                <version>${guice.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 -->
+                <version>${transmittable-thread-local.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>commons-net</groupId>
+                <artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
+                <version>${commons-net.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.jcraft</groupId>
+                <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
+                <version>${jsch.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xingyuv</groupId>
+                <artifactId>spring-boot-starter-captcha-plus</artifactId>
+                <version>${captcha-plus.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.lionsoul</groupId>
+                <artifactId>ip2region</artifactId>
+                <version>${ip2region.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.jsoup</groupId>
+                <artifactId>jsoup</artifactId>
+                <version>${jsoup.version}</version>
+            </dependency>
+
+            <!-- 三方云服务相关 -->
+            <dependency>
+                <groupId>com.squareup.okio</groupId>
+                <artifactId>okio</artifactId>
+                <version>${okio.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>okhttp</artifactId>
+                <version>${okhttp3.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.minio</groupId>
+                <artifactId>minio</artifactId>
+                <version>${minio.version}</version>
+            </dependency>
+
+            <!-- SMS SDK begin -->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>aliyun-java-sdk-core</artifactId>
+                <version>${aliyun-java-sdk-core.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>opentracing-api</artifactId>
+                        <groupId>io.opentracing</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>opentracing-util</artifactId>
+                        <groupId>io.opentracing</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
+                <version>${aliyun-java-sdk-dysmsapi.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.tencentcloudapi</groupId>
+                <artifactId>tencentcloud-sdk-java-sms</artifactId>
+                <version>${tencentcloud-sdk-java.version}</version>
+            </dependency>
+            <!-- SMS SDK end -->
+
+            <dependency>
+                <groupId>com.xingyuv</groupId>
+                <artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
+                <version>${justauth.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>cn.hutool</groupId>
+                        <artifactId>hutool-core</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>weixin-java-pay</artifactId>
+                <version>${weixin-java.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>wx-java-mp-spring-boot-starter</artifactId>
+                <version>${weixin-java.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
+                <version>${weixin-java.version}</version>
+            </dependency>
+
+            <!-- 积木报表-->
+            <dependency>
+                <groupId>org.jeecgframework.jimureport</groupId>
+                <artifactId>jimureport-spring-boot-starter</artifactId>
+                <version>${jimureport.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>com.alibaba</groupId>
+                        <artifactId>druid</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>xerces</groupId>
+                <artifactId>xercesImpl</artifactId>
+                <version>${xercesImpl.version}</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <!-- 统一 revision 版本 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <flattenMode>resolveCiFriendliesOnly</flattenMode>
+                    <updatePomFile>true</updatePomFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                    </execution>
+                    <execution>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 45 - 0
yudao-framework/pom.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>yudao</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <packaging>pom</packaging>
+    <modules>
+        <module>yudao-common</module>
+        <module>yudao-spring-boot-starter-mybatis</module>
+        <module>yudao-spring-boot-starter-redis</module>
+        <module>yudao-spring-boot-starter-web</module>
+        <module>yudao-spring-boot-starter-security</module>
+        <module>yudao-spring-boot-starter-websocket</module>
+
+        <module>yudao-spring-boot-starter-monitor</module>
+        <module>yudao-spring-boot-starter-protection</module>
+        <module>yudao-spring-boot-starter-job</module>
+        <module>yudao-spring-boot-starter-mq</module>
+
+        <module>yudao-spring-boot-starter-excel</module>
+
+        <module>yudao-spring-boot-starter-biz-tenant</module>
+        <module>yudao-spring-boot-starter-biz-data-permission</module>
+        <module>yudao-spring-boot-starter-biz-ip</module>
+    </modules>
+
+    <artifactId>yudao-framework</artifactId>
+    <description>
+        该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
+            1. core 包:是该组件的核心封装
+            2. config 包:是该组件基于 Spring 的配置
+
+        技术组件,也分成两类:
+            1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
+            2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
+        如果是业务组件,Maven 名字会包含 biz
+    </description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+</project>

+ 149 - 0
yudao-framework/yudao-common/pom.xml

@@ -0,0 +1,149 @@
+<?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>cn.iocoder.boot</groupId>
+        <artifactId>yudao-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-common</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>定义基础 pojo 类、枚举、工具类等等</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <dependencies>
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-expression</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aop</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
+        </dependency>
+
+        <!-- 监控相关 -->
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-trace</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>transmittable-thread-local</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
+            <artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 15 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/IntArrayValuable.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.framework.common.core;
+
+/**
+ * 可生成 Int 数组的接口
+ *
+ * @author 芋道源码
+ */
+public interface IntArrayValuable {
+
+    /**
+     * @return int 数组
+     */
+    int[] array();
+
+}

+ 22 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.framework.common.core;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Key Value 的键值对
+ *
+ * @author 芋道源码
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class KeyValue<K, V> implements Serializable {
+
+    private K key;
+    private V value;
+
+}

+ 46 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.framework.common.enums;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 通用状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum CommonStatusEnum implements IntArrayValuable {
+
+    ENABLE(0, "开启"),
+    DISABLE(1, "关闭");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray();
+
+    /**
+     * 状态值
+     */
+    private final Integer status;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static boolean isEnable(Integer status) {
+        return ObjUtil.equal(ENABLE.status, status);
+    }
+
+    public static boolean isDisable(Integer status) {
+        return ObjUtil.equal(DISABLE.status, status);
+    }
+
+}

+ 46 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 时间间隔的枚举
+ *
+ * @author dhb52
+ */
+@Getter
+@AllArgsConstructor
+public enum DateIntervalEnum implements IntArrayValuable {
+
+    DAY(1, "天"),
+    WEEK(2, "周"),
+    MONTH(3, "月"),
+    QUARTER(4, "季度"),
+    YEAR(5, "年")
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DateIntervalEnum::getInterval).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer interval;
+    /**
+     * 名称
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static DateIntervalEnum valueOf(Integer interval) {
+        return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values());
+    }
+
+}

+ 21 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.framework.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 文档地址
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum DocumentEnum {
+
+    REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"),
+    TENANT("https://doc.iocoder.cn", "SaaS 多租户文档");
+
+    private final String url;
+    private final String memo;
+
+}

+ 40 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.framework.common.enums;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 终端的枚举
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Getter
+public enum TerminalEnum implements IntArrayValuable {
+
+    UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
+    WECHAT_MINI_PROGRAM(10, "微信小程序"),
+    WECHAT_WAP(11, "微信公众号"),
+    H5(20, "H5 网页"),
+    APP(31, "手机 App"),
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray();
+
+    /**
+     * 终端
+     */
+    private final Integer terminal;
+    /**
+     * 终端名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 39 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 全局用户类型枚举
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum implements IntArrayValuable {
+
+    MEMBER(1, "会员"), // 面向 c 端,普通用户
+    ADMIN(2, "管理员"); // 面向 b 端,管理后台
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer value;
+    /**
+     * 类型名
+     */
+    private final String name;
+
+    public static UserTypeEnum valueOf(Integer value) {
+        return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 34 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.common.enums;
+
+/**
+ * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下
+ *
+ * @author 芋道源码
+ */
+public interface WebFilterOrderEnum {
+
+    int CORS_FILTER = Integer.MIN_VALUE;
+
+    int TRACE_FILTER = CORS_FILTER + 1;
+
+    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+
+    // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
+
+    int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
+
+    int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
+
+    int XSS_FILTER = -102;  // 需要保证在 RequestBodyCacheFilter 后面
+
+    // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
+
+    int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
+
+    int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    int DEMO_FILTER = Integer.MAX_VALUE;
+
+}

+ 32 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.framework.common.exception;
+
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.common.exception.enums.ServiceErrorCodeRange;
+import lombok.Data;
+
+/**
+ * 错误码对象
+ *
+ * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
+ * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
+ *
+ * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
+ */
+@Data
+public class ErrorCode {
+
+    /**
+     * 错误码
+     */
+    private final Integer code;
+    /**
+     * 错误提示
+     */
+    private final String msg;
+
+    public ErrorCode(Integer code, String message) {
+        this.code = code;
+        this.msg = message;
+    }
+
+}

+ 60 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServerException.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.framework.common.exception;
+
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 服务器异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServerException extends RuntimeException {
+
+    /**
+     * 全局错误码
+     *
+     * @see GlobalErrorCodeConstants
+     */
+    private Integer code;
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServerException() {
+    }
+
+    public ServerException(ErrorCode errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMsg();
+    }
+
+    public ServerException(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public ServerException setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServerException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}

+ 60 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.framework.common.exception;
+
+import cn.iocoder.yudao.framework.common.exception.enums.ServiceErrorCodeRange;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 业务逻辑异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServiceException extends RuntimeException {
+
+    /**
+     * 业务错误码
+     *
+     * @see ServiceErrorCodeRange
+     */
+    private Integer code;
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServiceException() {
+    }
+
+    public ServiceException(ErrorCode errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMsg();
+    }
+
+    public ServiceException(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public ServiceException setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}

+ 41 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.framework.common.exception.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * 全局错误码枚举
+ * 0-999 系统异常编码保留
+ *
+ * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
+ * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
+ * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
+ *
+ * @author 芋道源码
+ */
+public interface GlobalErrorCodeConstants {
+
+    ErrorCode SUCCESS = new ErrorCode(0, "成功");
+
+    // ========== 客户端错误段 ==========
+
+    ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
+    ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
+    ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
+    ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
+    ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
+    ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
+    ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
+
+    // ========== 服务端错误段 ==========
+
+    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
+    ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
+    ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项");
+
+    // ========== 自定义错误段 ==========
+    ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
+    ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
+
+    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
+
+}

+ 48 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.framework.common.exception.enums;
+
+/**
+ * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
+ *
+ * 一共 10 位,分成四段
+ *
+ * 第一段,1 位,类型
+ *      1 - 业务级别异常
+ *      x - 预留
+ * 第二段,3 位,系统类型
+ *      001 - 用户系统
+ *      002 - 商品系统
+ *      003 - 订单系统
+ *      004 - 支付系统
+ *      005 - 优惠劵系统
+ *      ... - ...
+ * 第三段,3 位,模块
+ *      不限制规则。
+ *      一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
+ *          001 - OAuth2 模块
+ *          002 - User 模块
+ *          003 - MobileCode 模块
+ * 第四段,3 位,错误码
+ *       不限制规则。
+ *       一般建议,每个模块自增。
+ *
+ * @author 芋道源码
+ */
+public class ServiceErrorCodeRange {
+
+    // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
+    // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
+    // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000)
+    // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
+    // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
+    // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
+    // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
+
+    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
+    // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
+    // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
+
+    // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
+
+    // 模块 ai 错误码区间 [1-022-000-000 ~ 1-023-000-000)
+
+}

+ 77 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.framework.common.exception.util;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * {@link ServiceException} 工具类
+ *
+ * 目的在于,格式化异常信息提示。
+ * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
+ *
+ */
+@Slf4j
+public class ServiceExceptionUtil {
+
+    // ========== 和 ServiceException 的集成 ==========
+
+    public static ServiceException exception(ErrorCode errorCode) {
+        return exception0(errorCode.getCode(), errorCode.getMsg());
+    }
+
+    public static ServiceException exception(ErrorCode errorCode, Object... params) {
+        return exception0(errorCode.getCode(), errorCode.getMsg(), params);
+    }
+
+    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
+        String message = doFormat(code, messagePattern, params);
+        return new ServiceException(code, message);
+    }
+
+    public static ServiceException invalidParamException(String messagePattern, Object... params) {
+        return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
+    }
+
+    // ========== 格式化方法 ==========
+
+    /**
+     * 将错误编号对应的消息使用 params 进行格式化。
+     *
+     * @param code           错误编号
+     * @param messagePattern 消息模版
+     * @param params         参数
+     * @return 格式化后的提示
+     */
+    @VisibleForTesting
+    public static String doFormat(int code, String messagePattern, Object... params) {
+        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
+        int i = 0;
+        int j;
+        int l;
+        for (l = 0; l < params.length; l++) {
+            j = messagePattern.indexOf("{}", i);
+            if (j == -1) {
+                log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+                if (i == 0) {
+                    return messagePattern;
+                } else {
+                    sbuf.append(messagePattern.substring(i));
+                    return sbuf.toString();
+                }
+            } else {
+                sbuf.append(messagePattern, i, j);
+                sbuf.append(params[l]);
+                i = j + 2;
+            }
+        }
+        if (messagePattern.indexOf("{}", i) != -1) {
+            log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+        }
+        sbuf.append(messagePattern.substring(i));
+        return sbuf.toString();
+    }
+
+}

+ 6 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 基础的通用类,和框架无关
+ *
+ * 例如说,CommonResult 为通用返回
+ */
+package cn.iocoder.yudao.framework.common;

+ 112 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java

@@ -0,0 +1,112 @@
+package cn.iocoder.yudao.framework.common.pojo;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 通用返回
+ *
+ * @param <T> 数据泛型
+ */
+@Data
+public class CommonResult<T> implements Serializable {
+
+    /**
+     * 错误码
+     *
+     * @see ErrorCode#getCode()
+     */
+    private Integer code;
+    /**
+     * 返回数据
+     */
+    private T data;
+    /**
+     * 错误提示,用户可阅读
+     *
+     * @see ErrorCode#getMsg() ()
+     */
+    private String msg;
+
+    /**
+     * 将传入的 result 对象,转换成另外一个泛型结果的对象
+     *
+     * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
+     *
+     * @param result 传入的 result 对象
+     * @param <T>    返回的泛型
+     * @return 新的 CommonResult 对象
+     */
+    public static <T> CommonResult<T> error(CommonResult<?> result) {
+        return error(result.getCode(), result.getMsg());
+    }
+
+    public static <T> CommonResult<T> error(Integer code, String message) {
+        Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = code;
+        result.msg = message;
+        return result;
+    }
+
+    public static <T> CommonResult<T> error(ErrorCode errorCode) {
+        return error(errorCode.getCode(), errorCode.getMsg());
+    }
+
+    public static <T> CommonResult<T> success(T data) {
+        CommonResult<T> result = new CommonResult<>();
+        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
+        result.data = data;
+        result.msg = "";
+        return result;
+    }
+
+    public static boolean isSuccess(Integer code) {
+        return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
+    }
+
+    @JsonIgnore // 避免 jackson 序列化
+    public boolean isSuccess() {
+        return isSuccess(code);
+    }
+
+    @JsonIgnore // 避免 jackson 序列化
+    public boolean isError() {
+        return !isSuccess();
+    }
+
+    // ========= 和 Exception 异常体系集成 =========
+
+    /**
+     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
+     */
+    public void checkError() throws ServiceException {
+        if (isSuccess()) {
+            return;
+        }
+        // 业务异常
+        throw new ServiceException(code, msg);
+    }
+
+    /**
+     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
+     * 如果没有,则返回 {@link #data} 数据
+     */
+    @JsonIgnore // 避免 jackson 序列化
+    public T getCheckedData() {
+        checkError();
+        return data;
+    }
+
+    public static <T> CommonResult<T> error(ServiceException serviceException) {
+        return error(serviceException.getCode(), serviceException.getMessage());
+    }
+
+}

+ 36 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Schema(description="分页参数")
+@Data
+public class PageParam implements Serializable {
+
+    private static final Integer PAGE_NO = 1;
+    private static final Integer PAGE_SIZE = 10;
+
+    /**
+     * 每页条数 - 不分页
+     *
+     * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
+     */
+    public static final Integer PAGE_SIZE_NONE = -1;
+
+    @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
+    @NotNull(message = "页码不能为空")
+    @Min(value = 1, message = "页码最小值为 1")
+    private Integer pageNo = PAGE_NO;
+
+    @Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "每页条数不能为空")
+    @Min(value = 1, message = "每页条数最小值为 1")
+    @Max(value = 100, message = "每页条数最大值为 100")
+    private Integer pageSize = PAGE_SIZE;
+
+}

+ 41 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "分页结果")
+@Data
+public final class PageResult<T> implements Serializable {
+
+    @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<T> list;
+
+    @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long total;
+
+    public PageResult() {
+    }
+
+    public PageResult(List<T> list, Long total) {
+        this.list = list;
+        this.total = total;
+    }
+
+    public PageResult(Long total) {
+        this.list = new ArrayList<>();
+        this.total = total;
+    }
+
+    public static <T> PageResult<T> empty() {
+        return new PageResult<>(0L);
+    }
+
+    public static <T> PageResult<T> empty(Long total) {
+        return new PageResult<>(total);
+    }
+
+}

+ 19 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortablePageParam.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.List;
+
+@Schema(description = "可排序的分页参数")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class SortablePageParam extends PageParam {
+
+    @Schema(description = "排序字段")
+    private List<SortingField> sortingFields;
+
+}

+ 37 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.framework.common.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 排序字段 DTO
+ *
+ * 类名加了 ing 的原因是,避免和 ES SortField 重名。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SortingField implements Serializable {
+
+    /**
+     * 顺序 - 升序
+     */
+    public static final String ORDER_ASC = "asc";
+    /**
+     * 顺序 - 降序
+     */
+    public static final String ORDER_DESC = "desc";
+
+    /**
+     * 字段
+     */
+    private String field;
+    /**
+     * 顺序
+     */
+    private String order;
+
+}

+ 49 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.framework.common.util.cache;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+/**
+ * Cache 工具类
+ *
+ * @author 芋道源码
+ */
+public class CacheUtils {
+
+    /**
+     * 构建异步刷新的 LoadingCache 对象
+     *
+     * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法
+     *
+     * 或者简单理解:
+     * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法
+     * 2、和“全局”、“系统”相关的,使用当前缓存方法
+     *
+     * @param duration 过期时间
+     * @param loader  CacheLoader 对象
+     * @return LoadingCache 对象
+     */
+    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder()
+                // 只阻塞当前数据加载线程,其他线程返回旧值
+                .refreshAfterWrite(duration)
+                // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
+                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
+    }
+
+    /**
+     * 构建同步刷新的 LoadingCache 对象
+     *
+     * @param duration 过期时间
+     * @param loader  CacheLoader 对象
+     * @return LoadingCache 对象
+     */
+    public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader);
+    }
+
+}

+ 58 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.util.ArrayUtil;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * Array 工具类
+ *
+ * @author 芋道源码
+ */
+public class ArrayUtils {
+
+    /**
+     * 将 object 和 newElements 合并成一个数组
+     *
+     * @param object 对象
+     * @param newElements 数组
+     * @param <T> 泛型
+     * @return 结果数组
+     */
+    @SafeVarargs
+    public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
+        if (object == null) {
+            return newElements;
+        }
+        Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
+        result[0] = object;
+        System.arraycopy(newElements, 0, result, 1, newElements.length);
+        return result;
+    }
+
+    public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) {
+        return toArray(convertList(from, mapper));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T[] toArray(Collection<T> from) {
+        if (CollectionUtil.isEmpty(from)) {
+            return (T[]) (new Object[0]);
+        }
+        return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
+    }
+
+    public static <T> T get(T[] array, int index) {
+        if (null == array || index >= array.length) {
+            return null;
+        }
+        return array[index];
+    }
+
+}

+ 322 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java

@@ -0,0 +1,322 @@
+package cn.iocoder.yudao.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Collection 工具类
+ *
+ * @author 芋道源码
+ */
+public class CollectionUtils {
+
+    public static boolean containsAny(Object source, Object... targets) {
+        return asList(targets).contains(source);
+    }
+
+    public static boolean isAnyEmpty(Collection<?>... collections) {
+        return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
+    }
+
+    public static <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) {
+        return from.stream().anyMatch(predicate);
+    }
+
+    public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(predicate).collect(Collectors.toList());
+    }
+
+    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return distinct(from, keyMapper, (t1, t2) -> t1);
+    }
+
+    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
+    }
+
+    public static <T, U> List<U> convertList(T[] from, Function<T, U> func) {
+        if (ArrayUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return convertList(Arrays.asList(from), func);
+    }
+
+    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
+                                                      Function<T, ? extends Stream<? extends U>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
+                                                         Function<? super T, ? extends U> mapper,
+                                                         Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
+        return map.values()
+                .stream()
+                .flatMap(List::stream)
+                .collect(Collectors.toList());
+    }
+
+    public static <T> Set<T> convertSet(Collection<T> from) {
+        return convertSet(from, v -> v);
+    }
+
+    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v));
+    }
+
+    public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
+                                                    Function<T, ? extends Stream<? extends U>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
+                                                       Function<? super T, ? extends U> mapper,
+                                                       Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, Function.identity());
+    }
+
+    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return supplier.get();
+        }
+        return convertMap(from, keyFunc, Function.identity(), supplier);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return supplier.get();
+        }
+        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
+    }
+
+    public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
+    }
+
+    public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream()
+                .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
+    }
+
+    // 暂时没想好名字,先以 2 结尾噶
+    public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
+    }
+
+    public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return Collections.emptyMap();
+        }
+        ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
+        from.forEach(item -> builder.put(keyFunc.apply(item), item));
+        return builder.build();
+    }
+
+    /**
+     * 对比老、新两个列表,找出新增、修改、删除的数据
+     *
+     * @param oldList  老列表
+     * @param newList  新列表
+     * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同
+     *                 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据
+     * @return [新增列表、修改列表、删除列表]
+     */
+    public static <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList,
+                                             BiFunction<T, T, Boolean> sameFunc) {
+        List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
+        List<T> updateList = new ArrayList<>();
+        List<T> deleteList = new ArrayList<>();
+
+        // 通过以 oldList 为主遍历,找出 updateList 和 deleteList
+        for (T oldObj : oldList) {
+            // 1. 寻找是否有匹配的
+            T foundObj = null;
+            for (Iterator<T> iterator = createList.iterator(); iterator.hasNext(); ) {
+                T newObj = iterator.next();
+                // 1.1 不匹配,则直接跳过
+                if (!sameFunc.apply(oldObj, newObj)) {
+                    continue;
+                }
+                // 1.2 匹配,则移除,并结束寻找
+                iterator.remove();
+                foundObj = newObj;
+                break;
+            }
+            // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中
+            if (foundObj != null) {
+                updateList.add(foundObj);
+            } else {
+                deleteList.add(oldObj);
+            }
+        }
+        return asList(createList, updateList, deleteList);
+    }
+
+    public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
+        return org.springframework.util.CollectionUtils.containsAny(source, candidates);
+    }
+
+    public static <T> T getFirst(List<T> from) {
+        return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
+    }
+
+    public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) {
+        return findFirst(from, predicate, Function.identity());
+    }
+
+    public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        return from.stream().filter(predicate).findFirst().map(func).orElse(null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert !from.isEmpty(); // 断言,避免告警
+        T t = from.stream().max(Comparator.comparing(valueFunc)).get();
+        return valueFunc.apply(t);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        T t = from.stream().min(Comparator.comparing(valueFunc)).get();
+        return valueFunc.apply(t);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc,
+                                                                     BinaryOperator<V> accumulator) {
+        return getSumValue(from, valueFunc, accumulator, null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
+                                                                     BinaryOperator<V> accumulator, V defaultValue) {
+        if (CollUtil.isEmpty(from)) {
+            return defaultValue;
+        }
+        assert !from.isEmpty(); // 断言,避免告警
+        return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue);
+    }
+
+    public static <T> void addIfNotNull(Collection<T> coll, T item) {
+        if (item == null) {
+            return;
+        }
+        coll.add(item);
+    }
+
+    public static <T> Collection<T> singleton(T obj) {
+        return obj == null ? Collections.emptyList() : Collections.singleton(obj);
+    }
+
+    public static <T> List<T> newArrayList(List<List<T>> list) {
+        return list.stream().flatMap(Collection::stream).collect(Collectors.toList());
+    }
+
+}

+ 68 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java

@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Map 工具类
+ *
+ * @author 芋道源码
+ */
+public class MapUtils {
+
+    /**
+     * 从哈希表表中,获得 keys 对应的所有 value 数组
+     *
+     * @param multimap 哈希表
+     * @param keys keys
+     * @return value 数组
+     */
+    public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) {
+        List<V> result = new ArrayList<>();
+        keys.forEach(k -> {
+            Collection<V> values = multimap.get(k);
+            if (CollectionUtil.isEmpty(values)) {
+                return;
+            }
+            result.addAll(values);
+        });
+        return result;
+    }
+
+    /**
+     * 从哈希表查找到 key 对应的 value,然后进一步处理
+     * key 为 null 时, 不处理
+     * 注意,如果查找到的 value 为 null 时,不进行处理
+     *
+     * @param map 哈希表
+     * @param key key
+     * @param consumer 进一步处理的逻辑
+     */
+    public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) {
+        if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) {
+            return;
+        }
+        V value = map.get(key);
+        if (value == null) {
+            return;
+        }
+        consumer.accept(value);
+    }
+
+    public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) {
+        Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size());
+        keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue()));
+        return map;
+    }
+
+}

+ 19 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.Set;
+
+/**
+ * Set 工具类
+ *
+ * @author 芋道源码
+ */
+public class SetUtils {
+
+    @SafeVarargs
+    public static <T> Set<T> asSet(T... objs) {
+        return CollUtil.newHashSet(objs);
+    }
+
+}

+ 149 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java

@@ -0,0 +1,149 @@
+package cn.iocoder.yudao.framework.common.util.date;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+
+import java.time.*;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ *
+ * @author 芋道源码
+ */
+public class DateUtils {
+
+    /**
+     * 时区 - 默认
+     */
+    public static final String TIME_ZONE_DEFAULT = "GMT+8";
+
+    /**
+     * 秒转换成毫秒
+     */
+    public static final long SECOND_MILLIS = 1000;
+
+    public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd";
+
+    public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
+
+    /**
+     * 将 LocalDateTime 转换成 Date
+     *
+     * @param date LocalDateTime
+     * @return LocalDateTime
+     */
+    public static Date of(LocalDateTime date) {
+        if (date == null) {
+            return null;
+        }
+        // 将此日期时间与时区相结合以创建 ZonedDateTime
+        ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault());
+        // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳
+        Instant instant = zonedDateTime.toInstant();
+        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
+        return Date.from(instant);
+    }
+
+    /**
+     * 将 Date 转换成 LocalDateTime
+     *
+     * @param date Date
+     * @return LocalDateTime
+     */
+    public static LocalDateTime of(Date date) {
+        if (date == null) {
+            return null;
+        }
+        // 转为时间戳
+        Instant instant = date.toInstant();
+        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
+        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+    }
+
+    public static Date addTime(Duration duration) {
+        return new Date(System.currentTimeMillis() + duration.toMillis());
+    }
+
+    public static boolean isExpired(LocalDateTime time) {
+        LocalDateTime now = LocalDateTime.now();
+        return now.isAfter(time);
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year  年
+     * @param mouth 月
+     * @param day   日
+     * @return 指定时间
+     */
+    public static Date buildTime(int year, int mouth, int day) {
+        return buildTime(year, mouth, day, 0, 0, 0);
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year   年
+     * @param mouth  月
+     * @param day    日
+     * @param hour   小时
+     * @param minute 分钟
+     * @param second 秒
+     * @return 指定时间
+     */
+    public static Date buildTime(int year, int mouth, int day,
+                                 int hour, int minute, int second) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.YEAR, year);
+        calendar.set(Calendar.MONTH, mouth - 1);
+        calendar.set(Calendar.DAY_OF_MONTH, day);
+        calendar.set(Calendar.HOUR_OF_DAY, hour);
+        calendar.set(Calendar.MINUTE, minute);
+        calendar.set(Calendar.SECOND, second);
+        calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒
+        return calendar.getTime();
+    }
+
+    public static Date max(Date a, Date b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+        return a.compareTo(b) > 0 ? a : b;
+    }
+
+    public static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+        return a.isAfter(b) ? a : b;
+    }
+
+    /**
+     * 是否今天
+     *
+     * @param date 日期
+     * @return 是否
+     */
+    public static boolean isToday(LocalDateTime date) {
+        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now());
+    }
+
+    /**
+     * 是否昨天
+     *
+     * @param date 日期
+     * @return 是否
+     */
+    public static boolean isYesterday(LocalDateTime date) {
+        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1));
+    }
+
+}

+ 309 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java

@@ -0,0 +1,309 @@
+package cn.iocoder.yudao.framework.common.util.date;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
+
+import java.time.*;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 时间工具类,用于 {@link java.time.LocalDateTime}
+ *
+ * @author 芋道源码
+ */
+public class LocalDateTimeUtils {
+
+    /**
+     * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
+     */
+    public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
+
+    /**
+     * 解析时间
+     *
+     * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功
+     *
+     * @param time 时间
+     * @return 时间字符串
+     */
+    public static LocalDateTime parse(String time) {
+        try {
+            return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN);
+        } catch (DateTimeParseException e) {
+            return LocalDateTimeUtil.parse(time);
+        }
+    }
+
+    public static LocalDateTime addTime(Duration duration) {
+        return LocalDateTime.now().plus(duration);
+    }
+
+    public static LocalDateTime minusTime(Duration duration) {
+        return LocalDateTime.now().minus(duration);
+    }
+
+    public static boolean beforeNow(LocalDateTime date) {
+        return date.isBefore(LocalDateTime.now());
+    }
+
+    public static boolean afterNow(LocalDateTime date) {
+        return date.isAfter(LocalDateTime.now());
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year  年
+     * @param mouth 月
+     * @param day   日
+     * @return 指定时间
+     */
+    public static LocalDateTime buildTime(int year, int mouth, int day) {
+        return LocalDateTime.of(year, mouth, day, 0, 0, 0);
+    }
+
+    public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1,
+                                                   int year2, int mouth2, int day2) {
+        return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)};
+    }
+
+    /**
+     * 判指定断时间,是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @param time 指定时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) {
+        if (startTime == null || endTime == null || time == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(parse(time), startTime, endTime);
+    }
+
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) {
+        if (startTime == null || endTime == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime);
+    }
+
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 是否
+     */
+    public static boolean isBetween(String startTime, String endTime) {
+        if (startTime == null || endTime == null) {
+            return false;
+        }
+        LocalDate nowDate = LocalDate.now();
+        return LocalDateTimeUtil.isIn(LocalDateTime.now(),
+                LocalDateTime.of(nowDate, LocalTime.parse(startTime)),
+                LocalDateTime.of(nowDate, LocalTime.parse(endTime)));
+    }
+
+    /**
+     * 判断时间段是否重叠
+     *
+     * @param startTime1 开始 time1
+     * @param endTime1   结束 time1
+     * @param startTime2 开始 time2
+     * @param endTime2   结束 time2
+     * @return 重叠:true 不重叠:false
+     */
+    public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) {
+        LocalDate nowDate = LocalDate.now();
+        return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1),
+                LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
+    }
+
+    /**
+     * 获取指定日期所在的月份的开始时间
+     * 例如:2023-09-30 00:00:00,000
+     *
+     * @param date 日期
+     * @return 月份的开始时间
+     */
+    public static LocalDateTime beginOfMonth(LocalDateTime date) {
+        return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
+    }
+
+    /**
+     * 获取指定日期所在的月份的最后时间
+     * 例如:2023-09-30 23:59:59,999
+     *
+     * @param date 日期
+     * @return 月份的结束时间
+     */
+    public static LocalDateTime endOfMonth(LocalDateTime date) {
+        return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
+    }
+
+    /**
+     * 获得指定日期所在季度
+     *
+     * @param date 日期
+     * @return 所在季度
+     */
+    public static int getQuarterOfYear(LocalDateTime date) {
+        return (date.getMonthValue() - 1) / 3 + 1;
+    }
+
+    /**
+     * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负
+     *
+     * @param dateTime 日期
+     * @return 相差天数
+     */
+    public static Long between(LocalDateTime dateTime) {
+        return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS);
+    }
+
+    /**
+     * 获取今天的开始时间
+     *
+     * @return 今天
+     */
+    public static LocalDateTime getToday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
+    }
+
+    /**
+     * 获取昨天的开始时间
+     *
+     * @return 昨天
+     */
+    public static LocalDateTime getYesterday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1));
+    }
+
+    /**
+     * 获取本月的开始时间
+     *
+     * @return 本月
+     */
+    public static LocalDateTime getMonth() {
+        return beginOfMonth(LocalDateTime.now());
+    }
+
+    /**
+     * 获取本年的开始时间
+     *
+     * @return 本年
+     */
+    public static LocalDateTime getYear() {
+        return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
+    }
+
+    public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
+                                                         LocalDateTime endTime,
+                                                         Integer interval) {
+        // 1.1 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+        // 1.2 将时间对齐
+        startTime = LocalDateTimeUtil.beginOfDay(startTime);
+        endTime = LocalDateTimeUtil.endOfDay(endTime);
+
+        // 2. 循环,生成时间范围
+        List<LocalDateTime[]> timeRanges = new ArrayList<>();
+        switch (intervalEnum) {
+            case DAY:
+                while (startTime.isBefore(endTime)) {
+                    timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
+                    startTime = startTime.plusDays(1);
+                }
+                break;
+            case WEEK:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfWeek});
+                    startTime = endOfWeek.plusNanos(1);
+                }
+                break;
+            case MONTH:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfMonth});
+                    startTime = endOfMonth.plusNanos(1);
+                }
+                break;
+            case QUARTER:
+                while (startTime.isBefore(endTime)) {
+                    int quarterOfYear = getQuarterOfYear(startTime);
+                    LocalDateTime quarterEnd = quarterOfYear == 4
+                            ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1)
+                            : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, quarterEnd});
+                    startTime = quarterEnd.plusNanos(1);
+                }
+                break;
+            case YEAR:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfYear});
+                    startTime = endOfYear.plusNanos(1);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+        // 3. 兜底,最后一个时间,需要保持在 endTime 之前
+        LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges);
+        if (lastTimeRange != null) {
+            lastTimeRange[1] = endTime;
+        }
+        return timeRanges;
+    }
+
+    /**
+     * 格式化时间范围
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @param interval  时间间隔
+     * @return 时间范围
+     */
+    public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) {
+        // 1. 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+
+        // 2. 循环,生成时间范围
+        switch (intervalEnum) {
+            case DAY:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
+            case WEEK:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN)
+                        + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime));
+            case MONTH:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN);
+            case QUARTER:
+                return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime));
+            case YEAR:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN);
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+    }
+
+}

+ 126 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java

@@ -0,0 +1,126 @@
+package cn.iocoder.yudao.framework.common.util.http;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.map.TableMap;
+import cn.hutool.core.net.url.UrlBuilder;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.Map;
+
+/**
+ * HTTP 工具类
+ *
+ * @author 芋道源码
+ */
+public class HttpUtils {
+
+    @SuppressWarnings("unchecked")
+    public static String replaceUrlQuery(String url, String key, String value) {
+        UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
+        // 先移除
+        TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
+                ReflectUtil.getFieldValue(builder.getQuery(), "query");
+        query.remove(key);
+        // 后添加
+        builder.addQuery(key, value);
+        return builder.build();
+    }
+
+    private String append(String base, Map<String, ?> query, boolean fragment) {
+        return append(base, query, null, fragment);
+    }
+
+    /**
+     * 拼接 URL
+     *
+     * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
+     *
+     * @param base 基础 URL
+     * @param query 查询参数
+     * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
+     * @param fragment URL 的 fragment,即拼接到 # 中
+     * @return 拼接后的 URL
+     */
+    public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
+        UriComponentsBuilder template = UriComponentsBuilder.newInstance();
+        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
+        URI redirectUri;
+        try {
+            // assume it's encoded to start with (if it came in over the wire)
+            redirectUri = builder.build(true).toUri();
+        } catch (Exception e) {
+            // ... but allow client registrations to contain hard-coded non-encoded values
+            redirectUri = builder.build().toUri();
+            builder = UriComponentsBuilder.fromUri(redirectUri);
+        }
+        template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
+                .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
+
+        if (fragment) {
+            StringBuilder values = new StringBuilder();
+            if (redirectUri.getFragment() != null) {
+                String append = redirectUri.getFragment();
+                values.append(append);
+            }
+            for (String key : query.keySet()) {
+                if (values.length() > 0) {
+                    values.append("&");
+                }
+                String name = key;
+                if (keys != null && keys.containsKey(key)) {
+                    name = keys.get(key);
+                }
+                values.append(name).append("={").append(key).append("}");
+            }
+            if (values.length() > 0) {
+                template.fragment(values.toString());
+            }
+            UriComponents encoded = template.build().expand(query).encode();
+            builder.fragment(encoded.getFragment());
+        } else {
+            for (String key : query.keySet()) {
+                String name = key;
+                if (keys != null && keys.containsKey(key)) {
+                    name = keys.get(key);
+                }
+                template.queryParam(name, "{" + key + "}");
+            }
+            template.fragment(redirectUri.getFragment());
+            UriComponents encoded = template.build().expand(query).encode();
+            builder.query(encoded.getQuery());
+        }
+        return builder.build().toUriString();
+    }
+
+    public static String[] obtainBasicAuthorization(HttpServletRequest request) {
+        String clientId;
+        String clientSecret;
+        // 先从 Header 中获取
+        String authorization = request.getHeader("Authorization");
+        authorization = StrUtil.subAfter(authorization, "Basic ", true);
+        if (StringUtils.hasText(authorization)) {
+            authorization = Base64.decodeStr(authorization);
+            clientId = StrUtil.subBefore(authorization, ":", false);
+            clientSecret = StrUtil.subAfter(authorization, ":", false);
+        // 再从 Param 中获取
+        } else {
+            clientId = request.getParameter("client_id");
+            clientSecret = request.getParameter("client_secret");
+        }
+
+        // 如果两者非空,则返回
+        if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
+            return new String[]{clientId, clientSecret};
+        }
+        return null;
+    }
+
+
+}

+ 84 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java

@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.framework.common.util.io;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import lombok.SneakyThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author 芋道源码
+ */
+public class FileUtils {
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(String data) {
+        File file = createTempFile();
+        // 写入内容
+        FileUtil.writeUtf8String(data, file);
+        return file;
+    }
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(byte[] data) {
+        File file = createTempFile();
+        // 写入内容
+        FileUtil.writeBytes(data, file);
+        return file;
+    }
+
+    /**
+     * 创建临时文件,无内容
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile() {
+        // 创建文件,通过 UUID 保证唯一
+        File file = File.createTempFile(IdUtil.simpleUUID(), null);
+        // 标记 JVM 退出时,自动删除
+        file.deleteOnExit();
+        return file;
+    }
+
+    /**
+     * 生成文件路径
+     *
+     * @param content      文件内容
+     * @param originalName 原始文件名
+     * @return path,唯一不可重复
+     */
+    public static String generatePath(byte[] content, String originalName) {
+        String sha256Hex = DigestUtil.sha256Hex(content);
+        // 情况一:如果存在 name,则优先使用 name 的后缀
+        if (StrUtil.isNotBlank(originalName)) {
+            String extName = FileNameUtil.extName(originalName);
+            return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
+        }
+        // 情况二:基于 content 计算
+        return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
+    }
+
+}

+ 28 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.util.io;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.InputStream;
+
+/**
+ * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法
+ *
+ * @author 芋道源码
+ */
+public class IoUtils {
+
+    /**
+     * 从流中读取 UTF8 编码的内容
+     *
+     * @param in 输入流
+     * @param isClose 是否关闭
+     * @return 内容
+     * @throws IORuntimeException IO 异常
+     */
+    public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
+        return StrUtil.utf8Str(IoUtil.read(in, isClose));
+    }
+
+}

+ 202 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java

@@ -0,0 +1,202 @@
+package cn.iocoder.yudao.framework.common.util.json;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class JsonUtils {
+
+    private static ObjectMapper objectMapper = new ObjectMapper();
+
+    static {
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
+        objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
+    }
+
+    /**
+     * 初始化 objectMapper 属性
+     * <p>
+     * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+     *
+     * @param objectMapper ObjectMapper 对象
+     */
+    public static void init(ObjectMapper objectMapper) {
+        JsonUtils.objectMapper = objectMapper;
+    }
+
+    @SneakyThrows
+    public static String toJsonString(Object object) {
+        return objectMapper.writeValueAsString(object);
+    }
+
+    @SneakyThrows
+    public static byte[] toJsonByte(Object object) {
+        return objectMapper.writeValueAsBytes(object);
+    }
+
+    @SneakyThrows
+    public static String toJsonPrettyString(Object object) {
+        return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+    }
+
+    public static <T> T parseObject(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, Type type) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 将字符串解析成指定类型的对象
+     * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
+     * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
+     *
+     * @param text 字符串
+     * @param clazz 类型
+     * @return 对象
+     */
+    public static <T> T parseObject2(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        return JSONUtil.toBean(text, clazz);
+    }
+
+    public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
+        if (ArrayUtil.isEmpty(bytes)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(bytes, clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", bytes, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, TypeReference<T> typeReference) {
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
+     *
+     * @param text 字符串
+     * @param typeReference 类型引用
+     * @return 指定类型的对象
+     */
+    public static <T> T parseObjectQuietly(String text, TypeReference<T> typeReference) {
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    public static <T> List<T> parseArray(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return new ArrayList<>();
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> List<T> parseArray(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static JsonNode parseTree(String text) {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static JsonNode parseTree(byte[] text) {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static boolean isJson(String text) {
+        return JSONUtil.isTypeJSON(text);
+    }
+
+}

+ 37 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/NumberSerializer.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.framework.common.util.json.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());
+        }
+    }
+}

+ 27 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.framework.common.util.json.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());
+    }
+
+}

+ 26 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.framework.common.util.json.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());
+    }
+
+}

+ 30 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.framework.common.util.monitor;
+
+import org.apache.skywalking.apm.toolkit.trace.TraceContext;
+
+/**
+ * 链路追踪工具类
+ *
+ * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
+ *
+ * @author 芋道源码
+ */
+public class TracerUtils {
+
+    /**
+     * 私有化构造方法
+     */
+    private TracerUtils() {
+    }
+
+    /**
+     * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
+     * 如果不存在的话为空字符串!!!
+     *
+     * @return 链路追踪编号
+     */
+    public static String getTraceId() {
+        return TraceContext.traceId();
+    }
+
+}

+ 131 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java

@@ -0,0 +1,131 @@
+package cn.iocoder.yudao.framework.common.util.number;
+
+import cn.hutool.core.math.Money;
+import cn.hutool.core.util.NumberUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 金额工具类
+ *
+ * @author 芋道源码
+ */
+public class MoneyUtils {
+
+    /**
+     * 金额的小数位数
+     */
+    private static final int PRICE_SCALE = 2;
+
+    /**
+     * 百分比对应的 BigDecimal 对象
+     */
+    public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100);
+
+    /**
+     * 计算百分比金额,四舍五入
+     *
+     * @param price 金额
+     * @param rate  百分比,例如说 56.77% 则传入 56.77
+     * @return 百分比金额
+     */
+    public static Integer calculateRatePrice(Integer price, Double rate) {
+        return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue();
+    }
+
+    /**
+     * 计算百分比金额,向下传入
+     *
+     * @param price 金额
+     * @param rate  百分比,例如说 56.77% 则传入 56.77
+     * @return 百分比金额
+     */
+    public static Integer calculateRatePriceFloor(Integer price, Double rate) {
+        return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
+    }
+
+    /**
+     * 计算百分比金额
+     *
+     * @param price   金额(单位分)
+     * @param count   数量
+     * @param percent 折扣(单位分),列如 60.2%,则传入 6020
+     * @return 商品总价
+     */
+    public static Integer calculator(Integer price, Integer count, Integer percent) {
+        price = price * count;
+        if (percent == null) {
+            return price;
+        }
+        return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100));
+    }
+
+    /**
+     * 计算百分比金额
+     *
+     * @param price        金额
+     * @param rate         百分比,例如说 56.77% 则传入 56.77
+     * @param scale        保留小数位数
+     * @param roundingMode 舍入模式
+     */
+    public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
+        return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
+                .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
+    }
+
+    /**
+     * 分转元
+     *
+     * @param fen 分
+     * @return 元
+     */
+    public static BigDecimal fenToYuan(int fen) {
+        return new Money(0, fen).getAmount();
+    }
+
+    /**
+     * 分转元(字符串)
+     *
+     * 例如说 fen 为 1 时,则结果为 0.01
+     *
+     * @param fen 分
+     * @return 元
+     */
+    public static String fenToYuanStr(int fen) {
+        return new Money(0, fen).toString();
+    }
+
+    /**
+     * 金额相乘,默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price 金额
+     * @param count 数量
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) {
+        if (price == null || count == null) {
+            return null;
+        }
+        return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 金额相乘(百分比),默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price  金额
+     * @param percent 百分比
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) {
+        if (price == null || percent == null) {
+            return null;
+        }
+        return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
+}

+ 64 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.framework.common.util.number;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.math.BigDecimal;
+
+/**
+ * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
+ *
+ * @author 芋道源码
+ */
+public class NumberUtils {
+
+    public static Long parseLong(String str) {
+        return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
+    }
+
+    public static Integer parseInt(String str) {
+        return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null;
+    }
+
+    /**
+     * 通过经纬度获取地球上两点之间的距离
+     *
+     * 参考 <<a href="https://gitee.com/dromara/hutool/blob/1caabb586b1f95aec66a21d039c5695df5e0f4c1/hutool-core/src/main/java/cn/hutool/core/util/DistanceUtil.java">DistanceUtil</a>> 实现,目前它已经被 hutool 删除
+     *
+     * @param lat1 经度1
+     * @param lng1 纬度1
+     * @param lat2 经度2
+     * @param lng2 纬度2
+     * @return 距离,单位:千米
+     */
+    public static double getDistance(double lat1, double lng1, double lat2, double lng2) {
+        double radLat1 = lat1 * Math.PI / 180.0;
+        double radLat2 = lat2 * Math.PI / 180.0;
+        double a = radLat1 - radLat2;
+        double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
+        double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+                + Math.cos(radLat1) * Math.cos(radLat2)
+                * Math.pow(Math.sin(b / 2), 2)));
+        distance = distance * 6378.137;
+        distance = Math.round(distance * 10000d) / 10000d;
+        return distance;
+    }
+
+    /**
+     * 提供精确的乘法运算
+     *
+     * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null
+     *
+     * @param values 多个被乘值
+     * @return 积
+     */
+    public static BigDecimal mul(BigDecimal... values) {
+        for (BigDecimal value : values) {
+            if (value == null) {
+                return null;
+            }
+        }
+        return NumberUtil.mul(values);
+    }
+
+}

+ 62 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.framework.common.util.object;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Bean 工具类
+ *
+ * 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
+ * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
+ *
+ * @author 芋道源码
+ */
+public class BeanUtils {
+
+    public static <T> T toBean(Object source, Class<T> targetClass) {
+        return BeanUtil.toBean(source, targetClass);
+    }
+
+    public static <T> T toBean(Object source, Class<T> targetClass, Consumer<T> peek) {
+        T target = toBean(source, targetClass);
+        if (target != null) {
+            peek.accept(target);
+        }
+        return target;
+    }
+
+    public static <S, T> List<T> toBean(List<S> source, Class<T> targetType) {
+        if (source == null) {
+            return null;
+        }
+        return CollectionUtils.convertList(source, s -> toBean(s, targetType));
+    }
+
+    public static <S, T> List<T> toBean(List<S> source, Class<T> targetType, Consumer<T> peek) {
+        List<T> list = toBean(source, targetType);
+        if (list != null) {
+            list.forEach(peek);
+        }
+        return list;
+    }
+
+    public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
+        return toBean(source, targetType, null);
+    }
+
+    public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType, Consumer<T> peek) {
+        if (source == null) {
+            return null;
+        }
+        List<T> list = toBean(source.getList(), targetType);
+        if (peek != null) {
+            list.forEach(peek);
+        }
+        return new PageResult<>(list, source.getTotal());
+    }
+
+}

+ 63 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java

@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.framework.common.util.object;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+/**
+ * Object 工具类
+ *
+ * @author 芋道源码
+ */
+public class ObjectUtils {
+
+    /**
+     * 复制对象,并忽略 Id 编号
+     *
+     * @param object 被复制对象
+     * @param consumer 消费者,可以二次编辑被复制对象
+     * @return 复制后的对象
+     */
+    public static <T> T cloneIgnoreId(T object, Consumer<T> consumer) {
+        T result = ObjectUtil.clone(object);
+        // 忽略 id 编号
+        Field field = ReflectUtil.getField(object.getClass(), "id");
+        if (field != null) {
+            ReflectUtil.setFieldValue(result, field, null);
+        }
+        // 二次编辑
+        if (result != null) {
+            consumer.accept(result);
+        }
+        return result;
+    }
+
+    public static <T extends Comparable<T>> T max(T obj1, T obj2) {
+        if (obj1 == null) {
+            return obj2;
+        }
+        if (obj2 == null) {
+            return obj1;
+        }
+        return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+    }
+
+    @SafeVarargs
+    public static <T> T defaultIfNull(T... array) {
+        for (T item : array) {
+            if (item != null) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    @SafeVarargs
+    public static <T> boolean equalsAny(T obj, T... array) {
+        return Arrays.asList(array).contains(obj);
+    }
+
+}

+ 67 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.framework.common.util.object;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.func.Func1;
+import cn.hutool.core.lang.func.LambdaUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
+import cn.iocoder.yudao.framework.common.pojo.SortingField;
+import org.springframework.util.Assert;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * {@link cn.iocoder.yudao.framework.common.pojo.PageParam} 工具类
+ *
+ * @author 芋道源码
+ */
+public class PageUtils {
+
+    private static final Object[] ORDER_TYPES = new String[]{SortingField.ORDER_ASC, SortingField.ORDER_DESC};
+
+    public static int getStart(PageParam pageParam) {
+        return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
+    }
+
+    /**
+     * 构建排序字段(默认倒序)
+     *
+     * @param func 排序字段的 Lambda 表达式
+     * @param <T>  排序字段所属的类型
+     * @return 排序字段
+     */
+    public static <T> SortingField buildSortingField(Func1<T, ?> func) {
+        return buildSortingField(func, SortingField.ORDER_DESC);
+    }
+
+    /**
+     * 构建排序字段
+     *
+     * @param func  排序字段的 Lambda 表达式
+     * @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC}
+     * @param <T>   排序字段所属的类型
+     * @return 排序字段
+     */
+    public static <T> SortingField buildSortingField(Func1<T, ?> func, String order) {
+        Assert.isTrue(ArrayUtil.contains(ORDER_TYPES, order), String.format("字段的排序类型只能是 %s/%s", ORDER_TYPES));
+
+        String fieldName = LambdaUtil.getFieldName(func);
+        return new SortingField(fieldName, order);
+    }
+
+    /**
+     * 构建默认的排序字段
+     * 如果排序字段为空,则设置排序字段;否则忽略
+     *
+     * @param sortablePageParam 排序分页查询参数
+     * @param func              排序字段的 Lambda 表达式
+     * @param <T>               排序字段所属的类型
+     */
+    public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) {
+        if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) {
+            sortablePageParam.setSortingFields(singletonList(buildSortingField(func)));
+        }
+    }
+
+}

+ 7 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * 对于工具类的选择,优先查找 Hutool 中有没对应的方法
+ * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分
+ *
+ * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。
+ */
+package cn.iocoder.yudao.framework.common.util;

+ 119 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java

@@ -0,0 +1,119 @@
+package cn.iocoder.yudao.framework.common.util.servlet;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import org.springframework.http.MediaType;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author 芋道源码
+ */
+public class ServletUtils {
+
+    /**
+     * 返回 JSON 字符串
+     *
+     * @param response 响应
+     * @param object   对象,会序列化成 JSON 字符串
+     */
+    @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
+    public static void writeJSON(HttpServletResponse response, Object object) {
+        String content = JsonUtils.toJsonString(object);
+        ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
+    }
+
+    /**
+     * 返回附件
+     *
+     * @param response 响应
+     * @param filename 文件名
+     * @param content  附件内容
+     */
+    public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
+        // 设置 header 和 contentType
+        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+        // 输出附件
+        IoUtil.write(response.getOutputStream(), false, content);
+    }
+
+    /**
+     * @param request 请求
+     * @return ua
+     */
+    public static String getUserAgent(HttpServletRequest request) {
+        String ua = request.getHeader("User-Agent");
+        return ua != null ? ua : "";
+    }
+
+    /**
+     * 获得请求
+     *
+     * @return HttpServletRequest
+     */
+    public static HttpServletRequest getRequest() {
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (!(requestAttributes instanceof ServletRequestAttributes)) {
+            return null;
+        }
+        return ((ServletRequestAttributes) requestAttributes).getRequest();
+    }
+
+    public static String getUserAgent() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return getUserAgent(request);
+    }
+
+    public static String getClientIP() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return ServletUtil.getClientIP(request);
+    }
+
+    public static boolean isJsonRequest(ServletRequest request) {
+        return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+    }
+
+    public static String getBody(HttpServletRequest request) {
+        // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
+        if (isJsonRequest(request)) {
+            return ServletUtil.getBody(request);
+        }
+        return null;
+    }
+
+    public static byte[] getBodyBytes(HttpServletRequest request) {
+        // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
+        if (isJsonRequest(request)) {
+            return ServletUtil.getBodyBytes(request);
+        }
+        return null;
+    }
+
+    public static String getClientIP(HttpServletRequest request) {
+        return ServletUtil.getClientIP(request);
+    }
+
+    public static Map<String, String> getParamMap(HttpServletRequest request) {
+        return ServletUtil.getParamMap(request);
+    }
+
+}

+ 89 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java

@@ -0,0 +1,89 @@
+package cn.iocoder.yudao.framework.common.util.spring;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+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.EvaluationContext;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Spring EL 表达式的工具类
+ *
+ * @author mashu
+ */
+public class SpringExpressionUtils {
+
+    /**
+     * Spring EL 表达式解析器
+     */
+    private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
+    /**
+     * 参数名发现器
+     */
+    private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+    private SpringExpressionUtils() {
+    }
+
+    /**
+     * 从切面中,单个解析 EL 表达式的结果
+     *
+     * @param joinPoint        切面点
+     * @param expressionString EL 表达式数组
+     * @return 执行界面
+     */
+    public static Object parseExpression(JoinPoint joinPoint, String expressionString) {
+        Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
+        return result.get(expressionString);
+    }
+
+    /**
+     * 从切面中,批量解析 EL 表达式的结果
+     *
+     * @param joinPoint         切面点
+     * @param expressionStrings EL 表达式数组
+     * @return 结果,key 为表达式,value 为对应值
+     */
+    public static Map<String, Object> parseExpressions(JoinPoint joinPoint, List<String> expressionStrings) {
+        // 如果为空,则不进行解析
+        if (CollUtil.isEmpty(expressionStrings)) {
+            return MapUtil.newHashMap();
+        }
+
+        // 第一步,构建解析的上下文 EvaluationContext
+        // 通过 joinPoint 获取被注解方法
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        Method method = methodSignature.getMethod();
+        // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
+        String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
+        // Spring 的表达式上下文对象
+        EvaluationContext context = new StandardEvaluationContext();
+        // 给上下文赋值
+        if (ArrayUtil.isNotEmpty(paramNames)) {
+            Object[] args = joinPoint.getArgs();
+            for (int i = 0; i < paramNames.length; i++) {
+                context.setVariable(paramNames[i], args[i]);
+            }
+        }
+
+        // 第二步,逐个参数解析
+        Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true);
+        expressionStrings.forEach(key -> {
+            Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
+            result.put(key, value);
+        });
+        return result;
+    }
+
+}

+ 24 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringUtils.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.common.util.spring;
+
+import cn.hutool.extra.spring.SpringUtil;
+
+import java.util.Objects;
+
+/**
+ * Spring 工具类
+ *
+ * @author 芋道源码
+ */
+public class SpringUtils extends SpringUtil {
+
+    /**
+     * 是否为生产环境
+     *
+     * @return 是否生产环境
+     */
+    public static boolean isProd() {
+        String activeProfile = getActiveProfile();
+        return Objects.equals("prod", activeProfile);
+    }
+
+}

+ 80 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.framework.common.util.string;
+
+import cn.hutool.core.text.StrPool;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author 芋道源码
+ */
+public class StrUtils {
+
+    public static String maxLength(CharSequence str, int maxLength) {
+        return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
+    }
+
+    /**
+     * 给定字符串是否以任何一个字符串开始
+     * 给定字符串和数组为空都返回 false
+     *
+     * @param str      给定字符串
+     * @param prefixes 需要检测的开始字符串
+     * @since 3.0.6
+     */
+    public static boolean startWithAny(String str, Collection<String> prefixes) {
+        if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
+            return false;
+        }
+
+        for (CharSequence suffix : prefixes) {
+            if (StrUtil.startWith(str, suffix, false)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static List<Long> splitToLong(String value, CharSequence separator) {
+        long[] longs = StrUtil.splitToLong(value, separator);
+        return Arrays.stream(longs).boxed().collect(Collectors.toList());
+    }
+
+    public static Set<Long> splitToLongSet(String value) {
+        return splitToLongSet(value, StrPool.COMMA);
+    }
+
+    public static Set<Long> splitToLongSet(String value, CharSequence separator) {
+        long[] longs = StrUtil.splitToLong(value, separator);
+        return Arrays.stream(longs).boxed().collect(Collectors.toSet());
+    }
+
+    public static List<Integer> splitToInteger(String value, CharSequence separator) {
+        int[] integers = StrUtil.splitToInt(value, separator);
+        return Arrays.stream(integers).boxed().collect(Collectors.toList());
+    }
+
+    /**
+     * 移除字符串中,包含指定字符串的行
+     *
+     * @param content 字符串
+     * @param sequence 包含的字符串
+     * @return 移除后的字符串
+     */
+    public static String removeLineContains(String content, String sequence) {
+        if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
+            return content;
+        }
+        return Arrays.stream(content.split("\n"))
+                .filter(line -> !line.contains(sequence))
+                .collect(Collectors.joining("\n"));
+    }
+
+}

+ 55 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.framework.common.util.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import org.springframework.util.StringUtils;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 校验工具类
+ *
+ * @author 芋道源码
+ */
+public class ValidationUtils {
+
+    private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$");
+
+    private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
+
+    private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
+
+    public static boolean isMobile(String mobile) {
+        return StringUtils.hasText(mobile)
+                && PATTERN_MOBILE.matcher(mobile).matches();
+    }
+
+    public static boolean isURL(String url) {
+        return StringUtils.hasText(url)
+                && PATTERN_URL.matcher(url).matches();
+    }
+
+    public static boolean isXmlNCName(String str) {
+        return StringUtils.hasText(str)
+                && PATTERN_XML_NCNAME.matcher(str).matches();
+    }
+
+    public static void validate(Object object, Class<?>... groups) {
+        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+        Assert.notNull(validator);
+        validate(validator, object, groups);
+    }
+
+    public static void validate(Validator validator, Object object, Class<?>... groups) {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+        if (CollUtil.isNotEmpty(constraintViolations)) {
+            throw new ConstraintViolationException(constraintViolations);
+        }
+    }
+
+}

+ 35 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class}
+)
+public @interface InEnum {
+
+    /**
+     * @return 实现 EnumValuable 接口的
+     */
+    Class<? extends IntArrayValuable> value();
+
+    String message() default "必须在指定范围 {value}";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 42 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumCollectionValidator.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumCollectionValidator implements ConstraintValidator<InEnum, Collection<Integer>> {
+
+    private List<Integer> values;
+
+    @Override
+    public void initialize(InEnum annotation) {
+        IntArrayValuable[] values = annotation.value().getEnumConstants();
+        if (values.length == 0) {
+            this.values = Collections.emptyList();
+        } else {
+            this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList());
+        }
+    }
+
+    @Override
+    public boolean isValid(Collection<Integer> list, ConstraintValidatorContext context) {
+        // 校验通过
+        if (CollUtil.containsAll(values, list)) {
+            return true;
+        }
+        // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+                .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
+        return false;
+    }
+
+}
+

+ 44 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {
+
+    private List<Integer> values;
+
+    @Override
+    public void initialize(InEnum annotation) {
+        IntArrayValuable[] values = annotation.value().getEnumConstants();
+        if (values.length == 0) {
+            this.values = Collections.emptyList();
+        } else {
+            this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList());
+        }
+    }
+
+    @Override
+    public boolean isValid(Integer value, ConstraintValidatorContext context) {
+        // 为空时,默认不校验,即认为通过
+        if (value == null) {
+            return true;
+        }
+        // 校验通过
+        if (values.contains(value)) {
+            return true;
+        }
+        // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+                .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
+        return false;
+    }
+
+}
+

+ 28 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = MobileValidator.class
+)
+public @interface Mobile {
+
+    String message() default "手机号格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 25 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class MobileValidator implements ConstraintValidator<Mobile, String> {
+
+    @Override
+    public void initialize(Mobile annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (StrUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return ValidationUtils.isMobile(value);
+    }
+
+}

+ 28 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = TelephoneValidator.class
+)
+public @interface Telephone {
+
+    String message() default "电话格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 25 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.PhoneUtil;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
+
+    @Override
+    public void initialize(Telephone annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (CharSequenceUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value);
+    }
+
+}

+ 4 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 使用 Hibernate Validator 实现参数校验
+ */
+package cn.iocoder.yudao.framework.common.validation;

+ 1 - 0
yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md

@@ -0,0 +1 @@
+<http://www.iocoder.cn/Spring-Boot/Validation/?yudao>

+ 46 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml

@@ -0,0 +1,46 @@
+<?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>yudao-framework</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>数据权限</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+            <optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 -->
+            <version>${revision}</version>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 46 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.framework.datapermission.config;
+
+import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
+import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionRuleHandler;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+import java.util.List;
+
+/**
+ * 数据权限的自动配置类
+ *
+ * @author 芋道源码
+ */
+@AutoConfiguration
+public class YudaoDataPermissionAutoConfiguration {
+
+    @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();
+    }
+
+}

+ 34 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.datapermission.config;
+
+import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.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 芋道源码
+ */
+@AutoConfiguration
+@ConditionalOnClass(LoginUser.class)
+@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
+public class YudaoDeptDataPermissionAutoConfiguration {
+
+    @Bean
+    public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
+                                                         List<DeptDataPermissionRuleCustomizer> customizers) {
+        // 创建 DeptDataPermissionRule 对象
+        DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
+        // 补全表配置
+        customizers.forEach(customizer -> customizer.customize(rule));
+        return rule;
+    }
+
+}

+ 35 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.datapermission.core.annotation;
+
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限注解
+ * 可声明在类或者方法上,标识使用的数据权限规则
+ *
+ * @author 芋道源码
+ */
+@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 {};
+
+}

+ 36 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.iocoder.yudao.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 cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类
+ *
+ * @author 芋道源码
+ */
+@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);
+    }
+
+}

+ 72 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.iocoder.yudao.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 芋道源码
+ */
+@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;
+    }
+
+}

+ 72 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * {@link DataPermission} 注解的 Context 上下文
+ *
+ * @author 芋道源码
+ */
+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();
+    }
+
+}

+ 57 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.framework.datapermission.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
+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 芋道源码
+ */
+@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;
+    }
+
+}

+ 36 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.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 芋道源码
+ */
+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);
+
+}

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule;
+
+import java.util.List;
+
+/**
+ * {@link DataPermissionRule} 工厂接口
+ * 作为 {@link DataPermissionRule} 的容器,提供管理能力
+ *
+ * @author 芋道源码
+ */
+public interface DataPermissionRuleFactory {
+
+    /**
+     * 获得所有数据权限规则数组
+     *
+     * @return 数据权限规则数组
+     */
+    List<DataPermissionRule> getDataPermissionRules();
+
+    /**
+     * 获得指定 Mapper 的数据权限规则数组
+     *
+     * @param mappedStatementId 指定 Mapper 的编号
+     * @return 数据权限规则数组
+     */
+    List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);
+
+}

+ 62 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.yudao.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 芋道源码
+ */
+@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;
+    }
+
+}

+ 206 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java

@@ -0,0 +1,206 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+import cn.iocoder.yudao.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 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 不进行修改,则会导致用户看不到之前的数据。【yudao-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 芋道源码
+ */
+@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());
+            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 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
+            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 Parenthesis(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 Parenthesis(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);
+    }
+
+}

+ 20 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
+
+/**
+ * {@link DeptDataPermissionRule} 的自定义配置接口
+ *
+ * @author 芋道源码
+ */
+@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);
+
+}

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 基于部门的数据权限规则
+ *
+ * @author 芋道源码
+ */
+package cn.iocoder.yudao.framework.datapermission.core.rule.dept;

+ 63 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java

@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.framework.datapermission.core.util;
+
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
+import lombok.SneakyThrows;
+
+import java.util.concurrent.Callable;
+
+/**
+ * 数据权限 Util
+ *
+ * @author 芋道源码
+ */
+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();
+        }
+    }
+
+}

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件
+ */
+package cn.iocoder.yudao.framework.datapermission;

+ 2 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,2 @@
+cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration
+cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration

+ 48 - 0
yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml

@@ -0,0 +1,48 @@
+<?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>yudao-framework</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-spring-boot-starter-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>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-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>
+
+    </dependencies>
+
+</project>

+ 61 - 0
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.framework.ip.core;
+
+import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.util.List;
+
+/**
+ * 区域节点,包括国家、省份、城市、地区等信息
+ *
+ * 数据可见 resources/area.csv 文件
+ *
+ * @author 芋道源码
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@ToString(exclude = {"parent"}) // 参见 https://gitee.com/yudaocode/yudao-cloud-mini/pulls/2 原因
+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;
+    /**
+     * 子节点
+     */
+    @JsonBackReference
+    private List<Area> children;
+
+}

+ 39 - 0
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/enums/AreaTypeEnum.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.framework.ip.core.enums;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 区域类型枚举
+ *
+ * @author 芋道源码
+ */
+@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;
+    }
+}

+ 214 - 0
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java

@@ -0,0 +1,214 @@
+package cn.iocoder.yudao.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 cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.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 cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.findFirst;
+
+/**
+ * 区域工具类
+ *
+ * @author 芋道源码
+ */
+@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;
+    }
+
+}

+ 87 - 0
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java

@@ -0,0 +1,87 @@
+package cn.iocoder.yudao.framework.ip.core.utils;
+
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.iocoder.yudao.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));
+    }
+}

+ 11 - 0
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/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 芋道源码
+ */
+package cn.iocoder.yudao.framework.ip;

+ 3662 - 0
yudao-framework/yudao-spring-boot-starter-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

binární
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb


+ 76 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml

@@ -0,0 +1,76 @@
+<?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>yudao-framework</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>多租户</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-redis</artifactId>
+        </dependency>
+
+        <!-- Job 定时任务相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-job</artifactId>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-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>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 49 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.framework.tenant.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 多租户配置
+ *
+ * @author 芋道源码
+ */
+@ConfigurationProperties(prefix = "yudao.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();
+
+}

+ 133 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java

@@ -0,0 +1,133 @@
+package cn.iocoder.yudao.framework.tenant.config;
+
+import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
+import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect;
+import cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
+import cn.iocoder.yudao.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor;
+import cn.iocoder.yudao.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
+import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager;
+import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
+import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
+import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
+import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
+import cn.iocoder.yudao.framework.web.config.WebProperties;
+import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
+import cn.iocoder.yudao.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.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 = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
+@EnableConfigurationProperties(TenantProperties.class)
+public class YudaoTenantAutoConfiguration {
+
+    @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;
+    }
+
+    // ========== 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;
+    }
+
+    // ========== MQ ==========
+
+    @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();
+    }
+
+    // ========== Job ==========
+
+    @Bean
+    public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
+        return new TenantJobAspect(tenantFrameworkService);
+    }
+
+    // ========== Redis ==========
+
+    @Bean
+    @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
+    public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
+                                                     RedisCacheConfiguration redisCacheConfiguration,
+                                                     YudaoCacheProperties yudaoCacheProperties,
+                                                     TenantProperties tenantProperties) {
+        // 创建 RedisCacheWriter 对象
+        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
+        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
+                BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
+        // 创建 TenantRedisCacheManager 对象
+        return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
+    }
+
+}

+ 18 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.framework.tenant.core.aop;
+
+import java.lang.annotation.*;
+
+/**
+ * 忽略租户,标记指定方法不进行租户的自动过滤
+ *
+ * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
+ * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
+ * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TenantIgnore {
+}

+ 35 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.tenant.core.aop;
+
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.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 芋道源码
+ */
+@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);
+        }
+    }
+
+}

+ 68 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java

@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.framework.tenant.core.context;
+
+import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author 芋道源码
+ */
+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();
+    }
+
+}

+ 21 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.framework.tenant.core.db;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 拓展多租户的 BaseDO 基类
+ *
+ * @author 芋道源码
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class TenantBaseDO extends BaseDO {
+
+    /**
+     * 多租户编号
+     */
+    private Long tenantId;
+
+}

+ 43 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.framework.tenant.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
+import cn.iocoder.yudao.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 芋道源码
+ */
+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); // 情况二,忽略多租户的表
+    }
+
+}

+ 14 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.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 {
+}

+ 56 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.framework.tenant.core.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import lombok.RequiredArgsConstructor;
+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.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 多租户 JobHandler AOP
+ * 任务执行时,会按照租户逐个执行 Job 的逻辑
+ *
+ * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
+ *
+ * @author 芋道源码
+ */
+@Aspect
+@RequiredArgsConstructor
+@Slf4j
+public class TenantJobAspect {
+
+    private final TenantFrameworkService tenantFrameworkService;
+
+    @Around("@annotation(tenantJob)")
+    public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) {
+        // 获得租户列表
+        List<Long> tenantIds = tenantFrameworkService.getTenantIds();
+        if (CollUtil.isEmpty(tenantIds)) {
+            return null;
+        }
+
+        // 逐个租户,执行 Job
+        Map<Long, String> results = new ConcurrentHashMap<>();
+        tenantIds.parallelStream().forEach(tenantId -> {
+            // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
+            TenantUtils.execute(tenantId, () -> {
+                try {
+                    joinPoint.proceed();
+                } catch (Throwable e) {
+                    results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
+                }
+            });
+        });
+        return JsonUtils.toJsonString(results);
+    }
+
+}

+ 37 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.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 芋道源码
+ */
+@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 依赖
+        }
+    }
+
+}

+ 47 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.framework.tenant.core.mq.kafka;
+
+import cn.hutool.core.util.ReflectUtil;
+import cn.iocoder.yudao.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 cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author 芋道源码
+ */
+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) {
+    }
+
+}

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů