18339543638 2 роки тому
батько
коміт
49e46837bc
100 змінених файлів з 6912 додано та 0 видалено
  1. 102 0
      pom.xml
  2. 261 0
      tr-dependencies/pom.xml
  3. 56 0
      tr-framework/pom.xml
  4. 24 0
      tr-framework/src/main/java/cn/tr/core/annotation/Mobile.java
  5. 25 0
      tr-framework/src/main/java/cn/tr/core/annotation/MobileValidator.java
  6. 73 0
      tr-framework/src/main/java/cn/tr/core/context/SecurityContextHolder.java
  7. 15 0
      tr-framework/src/main/java/cn/tr/core/enums/IEnum.java
  8. 53 0
      tr-framework/src/main/java/cn/tr/core/enums/WebFilterOrderEnum.java
  9. 23 0
      tr-framework/src/main/java/cn/tr/core/exception/BaseCode.java
  10. 61 0
      tr-framework/src/main/java/cn/tr/core/exception/ServiceException.java
  11. 211 0
      tr-framework/src/main/java/cn/tr/core/exception/TRExcCode.java
  12. 94 0
      tr-framework/src/main/java/cn/tr/core/pojo/CommonResult.java
  13. 28 0
      tr-framework/src/main/java/cn/tr/core/pojo/LoginResult.java
  14. 32 0
      tr-framework/src/main/java/cn/tr/core/pojo/PageDomain.java
  15. 41 0
      tr-framework/src/main/java/cn/tr/core/pojo/PageInfo.java
  16. 64 0
      tr-framework/src/main/java/cn/tr/core/pojo/TableDataInfo.java
  17. 55 0
      tr-framework/src/main/java/cn/tr/core/strategy/DeptDataPermissionStrategy.java
  18. 37 0
      tr-framework/src/main/java/cn/tr/core/strategy/ExceptionStrategy.java
  19. 16 0
      tr-framework/src/main/java/cn/tr/core/strategy/ILoginUser.java
  20. 56 0
      tr-framework/src/main/java/cn/tr/core/strategy/LoginUserStrategy.java
  21. 39 0
      tr-framework/src/main/java/cn/tr/core/strategy/PageStrategy.java
  22. 26 0
      tr-framework/src/main/java/cn/tr/core/strategy/TenantStrategy.java
  23. 58 0
      tr-framework/src/main/java/cn/tr/core/utils/IpUtil.java
  24. 146 0
      tr-framework/src/main/java/cn/tr/core/utils/JsonUtils.java
  25. 111 0
      tr-framework/src/main/java/cn/tr/core/utils/MediaTypeUtils.java
  26. 140 0
      tr-framework/src/main/java/cn/tr/core/utils/ServletUtils.java
  27. BIN
      tr-framework/src/main/resources/ip2region.xdb
  28. 16 0
      tr-modules/pom.xml
  29. 34 0
      tr-plugins/pom.xml
  30. 29 0
      tr-plugins/tr-spring-boot-starter-plugin-banner/pom.xml
  31. 17 0
      tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/java/cn/tr/plugin/banner/TrBannerAutoConfiguration.java
  32. 42 0
      tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/java/cn/tr/plugin/banner/config/BannerConfig.java
  33. 1 0
      tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  34. 15 0
      tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/resources/banner.txt
  35. 34 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/pom.xml
  36. 58 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/TrDataPermissionAutoConfiguration.java
  37. 32 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/TrDeptPermissionAutoConfiguration.java
  38. 36 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/annotation/DataPermission.java
  39. 36 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/aop/DataPermissionAnnotationAdvisor.java
  40. 73 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/aop/DataPermissionAnnotationInterceptor.java
  41. 44 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/db/TrDataPermissionHandler.java
  42. 36 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/DataPermissionRule.java
  43. 28 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/DataPermissionRuleFactory.java
  44. 66 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/DataPermissionRuleFactoryImpl.java
  45. 170 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/dept/DeptDataPermissionRule.java
  46. 20 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/dept/DeptDataPermissionRuleCustomizer.java
  47. 24 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/constant/DataPermissionConstant.java
  48. 62 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/context/DataPermissionContextHolder.java
  49. 105 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/config/aop/DataPermissionAnnotationInterceptorTest.java
  50. 258 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/config/db/TrDataPermissionHandlerTest.java
  51. 59 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/context/DataPermissionContextHolderTest.java
  52. 110 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/rule/DataPermissionRuleFactoryImplTest.java
  53. 59 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/pom.xml
  54. 19 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/TrExcelAutoConfiguration.java
  55. 180 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/annotation/Excel.java
  56. 18 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/annotation/Excels.java
  57. 19 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelHandlerAdapter.java
  58. 1441 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelHelper.java
  59. 36 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelHelperFactory.java
  60. 46 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelThreadFactory.java
  61. 1 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  62. 95 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/test/java/cn/tr/plugin/excel/ExcelTest.java
  63. 55 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/pom.xml
  64. 96 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/TrTenantAutoConfiguration.java
  65. 17 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/TenantStrategyConfig.java
  66. 18 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/aop/TenantIgnore.java
  67. 33 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/aop/TenantIgnoreAspect.java
  68. 61 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/db/TenantDatabaseInterceptor.java
  69. 62 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/ignore/TenantIgnoreUrlConfig.java
  70. 119 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/secutiry/TenantSecurityWebFilter.java
  71. 26 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/service/TenantFrameworkService.java
  72. 34 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/web/TenantContextWebFilter.java
  73. 26 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/constant/TenantConstant.java
  74. 52 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/context/TenantContextHolder.java
  75. 38 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/properties/TenantProperties.java
  76. 77 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/utils/TenantUtils.java
  77. 2 0
      tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  78. 51 0
      tr-plugins/tr-spring-boot-starter-plugin-cache/pom.xml
  79. 56 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/pom.xml
  80. 24 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/TrDictAutoConfiguration.java
  81. 43 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/annotation/Dict.java
  82. 61 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/bo/DictBO.java
  83. 85 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/config/cache/DictManager.java
  84. 45 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/config/jackson/DictJacksonDeserializer.java
  85. 57 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/config/jackson/DictJacksonSerializer.java
  86. 17 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/constant/DictConstant.java
  87. 1 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  88. 17 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/resources/application-unit-test.yml
  89. 74 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/test/java/cn/tr/plugin/dict/config/cache/CaffeineDictManagerTest.java
  90. 115 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/test/java/cn/tr/plugin/dict/config/cache/RedisDictManagerTest.java
  91. 75 0
      tr-plugins/tr-spring-boot-starter-plugin-dict/src/test/java/cn/tr/plugin/dict/config/jackson/DictJacksonSerializerTest.java
  92. 32 0
      tr-plugins/tr-spring-boot-starter-plugin-eventbus/pom.xml
  93. 22 0
      tr-plugins/tr-spring-boot-starter-plugin-eventbus/src/main/java/cn/tr/plugin/eventbus/TrEventBusAutoConfiguration.java
  94. 1 0
      tr-plugins/tr-spring-boot-starter-plugin-eventbus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  95. 40 0
      tr-plugins/tr-spring-boot-starter-plugin-eventbus/src/test/java/cn/tr/plugin/eventbus/EventBusTest.java
  96. 46 0
      tr-plugins/tr-spring-boot-starter-plugin-file/pom.xml
  97. 40 0
      tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/bo/FileBO.java
  98. 60 0
      tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/config/AbstractFileClient.java
  99. 54 0
      tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/config/FileClient.java
  100. 14 0
      tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/config/FileClientConfig.java

+ 102 - 0
pom.xml

@@ -0,0 +1,102 @@
+<?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.tr</groupId>
+    <artifactId>tr-footstone</artifactId>
+
+    <version>${revision}</version>
+
+    <packaging>pom</packaging>
+    <description>驼人通用型管理框架</description>
+    <modules>
+        <module>tr-plugins</module>
+        <module>tr-framework</module>
+        <module>tr-modules</module>
+        <module>tr-test</module>
+    </modules>
+
+    <properties>
+        <revision>0.0.9</revision>
+        <java.version>1.8</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <versions-maven-plugin.version>2.7</versions-maven-plugin.version>
+        <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
+        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+        <skipTests>true</skipTests>
+        <lombok.version>1.18.24</lombok.version>
+        <mapstruct.version>1.5.3.Final</mapstruct.version>
+        <spring-boot.version>2.7.8</spring-boot.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-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 插件,解决 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>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+
+    <!--&lt;!&ndash; 使用 aliyun 的 Maven 源,提升下载速度 &ndash;&gt;-->
+    <!--<repositories>-->
+        <!--<repository>-->
+            <!--<id>aliyunmaven</id>-->
+            <!--<name>aliyun</name>-->
+            <!--<url>https://maven.aliyun.com/repository/public</url>-->
+        <!--</repository>-->
+    <!--</repositories>-->
+
+</project>

+ 261 - 0
tr-dependencies/pom.xml

@@ -0,0 +1,261 @@
+<?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">
+
+
+    <groupId>cn.tr</groupId>
+    <artifactId>tr-dependencies</artifactId>
+    <version>${revision}</version>
+    <modelVersion>4.0.0</modelVersion>
+
+
+    <name>${project.artifactId}</name>
+    <description>基础 bom 文件,管理整个项目的依赖版本</description>
+
+    <properties>
+        <revision>0.0.9</revision>
+
+        <spring-boot.version>2.7.8</spring-boot.version>
+        <hutool.version>5.8.12</hutool.version>
+        <mapstruct.version>1.5.3.Final</mapstruct.version>
+        <lombok.version>1.18.20</lombok.version>
+        <ip2region.version>2.6.6</ip2region.version>
+        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
+        <mybatis-plus-generator.version>3.5.3.1</mybatis-plus-generator.version>
+        <druid.version>1.2.8</druid.version>
+        <pagehelper.boot.version>1.4.5</pagehelper.boot.version>
+        <!--权限相关-->
+        <satoken.version>1.34.0</satoken.version>
+        <!--TEST测试相关-->
+        <podam.version>7.2.11.RELEASE</podam.version>
+        <mockito-inline.version>4.11.0</mockito-inline.version>
+
+        <caffeine.version>2.6.2</caffeine.version>
+        <!--redission-->
+        <redisson.version>3.19.0</redisson.version>
+
+        <!--文件存储-->
+        <aliyun-oss.version>3.15.0</aliyun-oss.version>
+        <minio.version>8.5.1</minio.version>
+        <qiniu.version>7.9.5</qiniu.version>
+        <!--文件导入导出-->
+        <poi.version>4.1.2</poi.version>
+        <!--websocket-->
+        <tio-websocket.version>3.6.0.v20200315-RELEASE</tio-websocket.version>
+
+        <!--事件主线-->
+        <eventbus.version>3.3.1</eventbus.version>
+    </properties>
+
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-test</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-mybatis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--缓存插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-cache</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+
+            <!--文件插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-file</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+
+            <!--字典插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-dict</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--redis客户端-->
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-framework</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--springboot pom文件-->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-spring-boot-starter</artifactId>
+                <version>${satoken.version}</version>
+            </dependency>
+
+            <!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-dao-redis</artifactId>
+                <version>${satoken.version}</version>
+            </dependency>
+
+
+            <!--hutool工具类包-->
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
+
+            <!--lombok-->
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+            </dependency>
+
+            <!-- ip地址查询 -->
+            <dependency>
+                <groupId>org.lionsoul</groupId>
+                <artifactId>ip2region</artifactId>
+                <version>${ip2region.version}</version>
+            </dependency>
+
+            <!--mybatis-plus-->
+            <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-extension</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
+                <version>${mybatis-plus-generator.version}</version>
+            </dependency>
+            <!--mybatis-plus-->
+
+            <!--阿里数据库连接池 -->
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>druid-spring-boot-starter</artifactId>
+                <version>${druid.version}</version>
+            </dependency>
+
+            <!-- pagehelper 分页插件 -->
+            <dependency>
+                <groupId>com.github.pagehelper</groupId>
+                <artifactId>pagehelper-spring-boot-starter</artifactId>
+                <version>${pagehelper.boot.version}</version>
+            </dependency>
+
+
+            <!--Test测试自动填充对象属性-->
+            <dependency>
+                <groupId>uk.co.jemos.podam</groupId>
+                <artifactId>podam</artifactId>
+                <version>${podam.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.ben-manes.caffeine</groupId>
+                <artifactId>caffeine</artifactId>
+                <version>${caffeine.version}</version>
+            </dependency>
+
+
+            <!--OSS模块-->
+            <!--ali OSS存储-->
+            <dependency>
+                <groupId>com.aliyun.oss</groupId>
+                <artifactId>aliyun-sdk-oss</artifactId>
+                <version>${aliyun-oss.version}</version>
+            </dependency>
+            <!-- minio -->
+            <dependency>
+                <groupId>io.minio</groupId>
+                <artifactId>minio</artifactId>
+                <version>${minio.version}</version>
+            </dependency>
+            <!--七牛云-->
+            <dependency>
+                <groupId>com.qiniu</groupId>
+                <artifactId>qiniu-java-sdk</artifactId>
+                <version>${qiniu.version}</version>
+            </dependency>
+            <!--OSS模块-->
+
+
+            <!-- excel导入导出工具 -->
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi-ooxml</artifactId>
+                <version>${poi.version}</version>
+            </dependency>
+
+            <!--websocket-->
+            <dependency>
+                <groupId>org.t-io</groupId>
+                <artifactId>tio-websocket-spring-boot-starter</artifactId>
+                <version>${tio-websocket.version}</version>
+            </dependency>
+
+            <!--事件主线-->
+            <dependency>
+                <groupId>org.greenrobot</groupId>
+                <artifactId>eventbus-java</artifactId>
+                <version>${eventbus.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>

+ 56 - 0
tr-framework/pom.xml

@@ -0,0 +1,56 @@
+<?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>tr-footstone</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-framework</artifactId>
+    <packaging>pom</packaging>
+    <version>${revision}</version>
+
+    <description>系统内置的核心框架功能</description>
+    <modules>
+
+    </modules>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-log4j2</artifactId>
+        </dependency>
+
+        <!-- ip2region -->
+        <dependency>
+            <groupId>org.lionsoul</groupId>
+            <artifactId>ip2region</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 24 - 0
tr-framework/src/main/java/cn/tr/core/annotation/Mobile.java

@@ -0,0 +1,24 @@
+package cn.tr.core.annotation;
+
+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 value() default "手机号格式不正确";
+
+}

+ 25 - 0
tr-framework/src/main/java/cn/tr/core/annotation/MobileValidator.java

@@ -0,0 +1,25 @@
+package cn.tr.core.annotation;
+
+import cn.hutool.core.util.PhoneUtil;
+import cn.hutool.core.util.StrUtil;
+
+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 PhoneUtil.isPhone(value);
+    }
+
+}

+ 73 - 0
tr-framework/src/main/java/cn/tr/core/context/SecurityContextHolder.java

@@ -0,0 +1,73 @@
+package cn.tr.core.context;
+
+import cn.hutool.core.util.ObjectUtil;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 获取当前线程变量中的 用户id、用户名称、Token等信息 
+ * 注意: 必须在网关通过请求头的方法传入,同时在HeaderInterceptor拦截器设置值。 否则这里无法获取
+ *
+ * @author lifang
+ */
+public class SecurityContextHolder {
+    private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL = new ThreadLocal<>();
+
+
+    public static void set(String key, Object value) {
+        if (ObjectUtil.isNull(value)) {
+            return;
+        }
+        Map<String, Object> map = getLocalMap();
+        map.put(key, value);
+    }
+
+    public static String getStr(String key)
+    {
+        return get(key,String.class);
+    }
+
+    public static <T> T get(String key, Class<T> clazz) {
+        Map<String,  Object> map = getLocalMap();
+        Object result = map.getOrDefault(key, null);
+        if(result==null){
+            return null;
+        }
+        if(result.getClass() .equals(clazz) ){
+            return (T) result;
+        }else {
+            return null;
+        }
+    }
+
+
+    public static <T> List<T> getList(String key,Class<T> clazz){
+        return (List<T>) getLocalMap().getOrDefault(key, null);
+    }
+
+    public static <T> Map getMap(String key){
+        return (Map) getLocalMap().getOrDefault(key, null);
+    }
+
+
+
+
+
+    private static Map<String, Object> getLocalMap() {
+        Map<String,  Object> map = THREAD_LOCAL.get();
+        if (map == null)
+        {
+            map = new ConcurrentHashMap<String,Object>();
+            THREAD_LOCAL.set(map);
+        }
+        return map;
+    }
+
+    public static void remove()
+    {
+        THREAD_LOCAL.remove();
+    }
+
+}

+ 15 - 0
tr-framework/src/main/java/cn/tr/core/enums/IEnum.java

@@ -0,0 +1,15 @@
+package cn.tr.core.enums;
+
+/**
+ * @ClassName : IEnum
+ * @Description :
+ * @Author : JR
+ * @Date: 2022年11月12日
+ */
+
+public interface IEnum<T> {
+    T getValue();
+
+    String getLabel();
+
+}

+ 53 - 0
tr-framework/src/main/java/cn/tr/core/enums/WebFilterOrderEnum.java

@@ -0,0 +1,53 @@
+package cn.tr.core.enums;
+
+/**
+ * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
+ * order值越小越先执行
+ * @author 芋道源码
+ */
+public interface WebFilterOrderEnum {
+
+    int CORS_FILTER = Integer.MIN_VALUE;
+
+    int EXCEPTION_FILTER=CORS_FILTER+1;
+
+    int TRACE_FILTER = EXCEPTION_FILTER + 1;
+
+    int ENV_TAG_FILTER = TRACE_FILTER + 1;
+
+    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+
+    /**
+     * 执行租户过滤器
+     */
+    int TENANT_CONTEXT_FILTER = - 104;
+
+    /**
+     * 租户接口安全,在租户过滤器之后
+     */
+    int TENANT_SECURITY_FILTER = -99;
+
+    /**
+     * 请求记录过滤器
+     */
+    int API_ACCESS_LOG_FILTER = -103;
+
+    int XSS_FILTER = -102;  // 需要保证在 RequestBodyCacheFilter 后面
+
+    // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
+
+
+
+    int ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    /**
+     * 填充登录方式,放在最后
+     */
+    int FILL_LOGIN_TYPE=9999;
+    int DEMO_FILTER = Integer.MAX_VALUE;
+
+}

+ 23 - 0
tr-framework/src/main/java/cn/tr/core/exception/BaseCode.java

@@ -0,0 +1,23 @@
+package cn.tr.core.exception;
+
+/**
+ * @Enum : BaseCode
+ * @Description : 基础错误代码
+ * @Author : JR
+ * @Date: 2022年11月18日
+ */
+
+public interface BaseCode {
+
+    /**
+     * 错误码
+     * @return
+     */
+    String getErrCode();
+
+    /**
+     * 错误信息
+     * @return
+     */
+    String getErrMsg();
+}

+ 61 - 0
tr-framework/src/main/java/cn/tr/core/exception/ServiceException.java

@@ -0,0 +1,61 @@
+package cn.tr.core.exception;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 业务逻辑异常 Exception
+ * @author JR
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServiceException extends RuntimeException {
+
+    private static final long serialVersionUID = 7657422049507503668L;
+    /**
+     * 业务错误码
+     *
+     */
+    private TRExcCode code;
+
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServiceException() {
+    }
+
+    public ServiceException(TRExcCode errorCode) {
+        this.code = errorCode;
+        this.message = errorCode.getErrMsg();
+    }
+
+    public ServiceException(TRExcCode errCode, String message) {
+        this.code = errCode;
+        this.message = message;
+    }
+
+    public TRExcCode getCode() {
+        return code;
+    }
+
+    public ServiceException setCode(TRExcCode code) {
+        this.code = code;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}

+ 211 - 0
tr-framework/src/main/java/cn/tr/core/exception/TRExcCode.java

@@ -0,0 +1,211 @@
+package cn.tr.core.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @ClassName : TRExcCode
+ * @Description : 根据阿里巴巴编码规约(泰山版)整理的错误码
+ * @Author : JR
+ * @Date: 2022年11月18日
+ */
+@AllArgsConstructor
+@Getter
+public enum TRExcCode implements BaseCode {
+    SUCCESS("00000", "成功"),
+    USER_ERROR_0001("A0001", "用户端错误"),
+    USER_ERROR_A0100("A0100", "用户注册错误"),
+    USER_ERROR_A0101("A0101", "用户未同意隐私协议"),
+    USER_ERROR_A0102("A0102", "注册国家或地区受限"),
+    USER_ERROR_A0110("A0110", "用户名校验失败"),
+    USER_ERROR_A0111("A0111", "用户名已存在"),
+    USER_ERROR_A0112("A0112", "用户名包含敏感词"),
+    USER_ERROR_A0113("A0113", "用户名包含特殊字符"),
+    USER_ERROR_A0120("A0120", "密码校验失败"),
+    USER_ERROR_A0121("A0121", "密码长度不够"),
+    USER_ERROR_A0122("A0122", "密码强度不够"),
+    USER_ERROR_A0130("A0130", "校验码输入错误"),
+    USER_ERROR_A0131("A0131", "短信校验码输入错误"),
+    USER_ERROR_A0132("A0132", "邮件校验码输入错误"),
+    USER_ERROR_A0133("A0133", "语音校验码输入错误"),
+    USER_ERROR_A0140("A0140", "用户证件异常"),
+    USER_ERROR_A0141("A0141", "用户证件类型未选择"),
+    USER_ERROR_A0142("A0142", "大陆身份证编号校验非法"),
+    USER_ERROR_A0143("A0143", "护照编号校验非法"),
+    USER_ERROR_A0144("A0144", "军官证编号校验非法"),
+    USER_ERROR_A0150("A0150", "用户基本信息校验失败"),
+    USER_ERROR_A0151("A0151", "手机格式校验失败"),
+    USER_ERROR_A0152("A0152", "地址格式校验失败"),
+    USER_ERROR_A0153("A0153", "邮箱格式校验失败"),
+
+    /**** 登录 **/
+    USER_ERROR_A0200("A0200", "用户登陆异常"),
+    USER_ERROR_A0201("A0201", "用户账户不存在"),
+    USER_ERROR_A0202("A0202", "用户账户被禁用"),
+    USER_ERROR_A0203("A0203", "用户账户已作废"),
+    USER_ERROR_A0210("A0210", "用户密码错误"),
+    USER_ERROR_A0211("A0211", "用户输入密码次数超限"),
+    USER_ERROR_A0220("A0220", "用户身份校验失败"),
+    USER_ERROR_A0221("A0221", "用户指纹识别失败"),
+    USER_ERROR_A0222("A0222", "用户面容识别失败"),
+    USER_ERROR_A0223("A0223", "用户未获得第三方登陆授权"),
+    USER_ERROR_A0230("A0230", "用户登陆已过期"),
+    USER_ERROR_A0240("A0240", "用户验证码错误"),
+    USER_ERROR_A0241("A0241", "用户验证码尝试次数超限"),
+    USER_ERROR_A0242("A0242", "用户未登录"),
+
+    /**** 权限 **/
+    USER_ERROR_A0300("A0300", "访问权限异常"),
+    USER_ERROR_A0301("A0301", "访问未授权"),
+    USER_ERROR_A0302("A0302", "正在授权中"),
+    USER_ERROR_A0303("A0303", "用户授权申请被拒绝"),
+    USER_ERROR_A0310("A0310", "因访问对象隐私设置被拦截"),
+    USER_ERROR_A0311("A0311", "授权已过期"),
+    USER_ERROR_A0312("A0312", "无权限使用API"),
+    USER_ERROR_A0320("A0320", "用户访问被拦截"),
+    USER_ERROR_A0321("A0321", "黑名单用户"),
+    USER_ERROR_A0322("A0322", "账号被禁用"),
+    USER_ERROR_A0323("A0323", "非法IP地址"),
+    USER_ERROR_A0324("A0324", "网关访问受限"),
+    USER_ERROR_A0325("A0325", "地域黑名单"),
+    USER_ERROR_A0330("A0330", "服务已欠费"),
+    USER_ERROR_A0340("A0340", "用户签名异常"),
+    USER_ERROR_A0341("A0341", "RSA签名错误"),
+
+
+    /**** 输入错误 **/
+    USER_ERROR_A0400("A0400", "用户请求参数错误"),
+    USER_ERROR_A0401("A0401", "包含非法恶意跳转链接"),
+    USER_ERROR_A0402("A0402", "无效的用户输入"),
+    USER_ERROR_A0403("A0403", "权限不足"),
+    USER_ERROR_A0404("A0404", "用户请求地址不存在"),
+    USER_ERROR_A0410("A0410", "请求必填参数为空"),
+    USER_ERROR_A0411("A0411", "用户订单号为空"),
+    USER_ERROR_A0412("A0412", "订购数量为空"),
+    USER_ERROR_A0413("A0413", "缺少时间戳参数"),
+    USER_ERROR_A0414("A0414", "非法的时间戳参数"),
+    USER_ERROR_A0420("A0420", "请求参数值超出允许的范围"),
+    USER_ERROR_A0421("A0421", "参数格式不匹配"),
+    USER_ERROR_A0422("A0422", "地址不在服务范围"),
+    USER_ERROR_A0423("A0423", "时间不在服务范围"),
+    USER_ERROR_A0424("A0424", "金额超出限制"),
+    USER_ERROR_A0425("A0425", "数量超出限制"),
+    USER_ERROR_A0426("A0426", "请求批量处理总个数超出限制"),
+    USER_ERROR_A0427("A0427", "请求JSON解析失败"),
+    USER_ERROR_A0430("A0430", "用户输入内容非法"),
+    USER_ERROR_A0431("A0431", "包含违禁敏感词"),
+    USER_ERROR_A0432("A0432", "图片包含违禁信息"),
+    USER_ERROR_A0433("A0433", "文件侵犯版权"),
+    USER_ERROR_A0440("A0440", "用户操作异常"),
+    USER_ERROR_A0441("A0441", "用户支付超时"),
+    USER_ERROR_A0442("A0442", "确认订单超时"),
+    USER_ERROR_A0443("A0443", "订单已关闭"),
+    USER_ERROR_A0500("A0500", "用户请求服务异常"),
+    USER_ERROR_A0501("A0501", "请求次数超出限制"),
+    USER_ERROR_A0502("A0502", "请求并发数超出限制"),
+    USER_ERROR_A0503("A0503", "用户操作请等待"),
+    USER_ERROR_A0504("A0504", "WebSocket连接异常"),
+    USER_ERROR_A0505("A0505", "WebSocket连接断开"),
+    USER_ERROR_A0506("A0506", "用户重复请求"),
+    USER_ERROR_A0600("A0600", "用户资源异常"),
+    USER_ERROR_A0601("A0601", "账户余额不足"),
+    USER_ERROR_A0602("A0602", "用户磁盘空间不足"),
+    USER_ERROR_A0603("A0603", "用户内存空间不足"),
+    USER_ERROR_A0604("A0604", "用户OSS容量不足"),
+    USER_ERROR_A0605("A0605", "用户配额已用光"),
+    USER_ERROR_A0700("A0700", "用户上传文件异常"),
+    USER_ERROR_A0701("A0701", "用户上传文件类型不匹配"),
+    USER_ERROR_A0702("A0702", "用户上传文件太大"),
+    USER_ERROR_A0703("A0703", "用户上传图片太大"),
+    USER_ERROR_A0704("A0704", "用户上传视频太大"),
+    USER_ERROR_A0705("A0705", "用户上传压缩文件太大"),
+    USER_ERROR_A0706("A0706", "不支持预览该类型文件预览"),
+    USER_ERROR_A0800("A0800", "用户当前版本异常"),
+    USER_ERROR_A0801("A0801", "用户安装版本与系统不匹配"),
+    USER_ERROR_A0802("A0802", "用户安装版本过低"),
+    USER_ERROR_A0803("A0803", "用户安装版本过高"),
+    USER_ERROR_A0804("A0804", "用户安装版本已过期"),
+    USER_ERROR_A0805("A0805", "用户API请求版本不匹配"),
+    USER_ERROR_A0806("A0806", "用户API请求版本过高"),
+    USER_ERROR_A0807("A0807", "用户API请求版本过低"),
+    USER_ERROR_A0900("A0900", "用户隐私未授权"),
+    USER_ERROR_A0901("A0901", "用户隐私未签署"),
+    USER_ERROR_A0902("A0902", "用户摄像头未授权"),
+    USER_ERROR_A0903("A0903", "用户相机未授权"),
+    USER_ERROR_A0904("A0904", "用户图片库未授权"),
+    USER_ERROR_A0905("A0905", "用户文件未授权"),
+    USER_ERROR_A0906("A0906", "用户位置信息未授权"),
+    USER_ERROR_A0907("A0907", "用户通讯录未授权"),
+    USER_ERROR_A1000("A1000", "用户设备异常"),
+    USER_ERROR_A1001("A1001", "用户相机异常"),
+    USER_ERROR_A1002("A1002", "用户麦克风异常"),
+    USER_ERROR_A1003("A1003", "用户听筒异常"),
+    USER_ERROR_A1004("A1004", "用户扬声器异常"),
+    USER_ERROR_A1005("A1005", "用户GPS定位异常"),
+
+    /**** 服务端错误 **/
+    SYSTEM_ERROR_B0001("B0001", "系统执行出错"),
+    SYSTEM_ERROR_B0100("B0100", "系统执行超时"),
+    SYSTEM_ERROR_B0101("B0101", "系统订单处理超时"),
+    SYSTEM_ERROR_B0200("B0200", "系统容灾功能被触发"),
+    SYSTEM_ERROR_B0210("B0210", "系统限流"),
+    SYSTEM_ERROR_B0220("B0220", "系统功能降级"),
+    SYSTEM_ERROR_B0300("B0300", "系统资源异常"),
+    SYSTEM_ERROR_B0310("B0310", "系统资源耗尽"),
+    SYSTEM_ERROR_B0311("B0311", "系统磁盘空间耗尽"),
+    SYSTEM_ERROR_B0312("B0312", "系统内存耗尽"),
+    SYSTEM_ERROR_B0313("B0313", "文件句柄耗尽"),
+    SYSTEM_ERROR_B0314("B0314", "系统连接池耗尽"),
+    SYSTEM_ERROR_B0315("B0315", "系统线程池耗尽"),
+    SYSTEM_ERROR_B0320("B0320", "系统资源访问异常"),
+    SYSTEM_ERROR_B0321("B0321", "系统读取磁盘文件失败"),
+    SYSTEM_ERROR_B0404("B0321", "资源未找到"),
+
+    /**** 第三方服务错误 **/
+    SERVICE_ERROR_C0001("C0001", "调用第三方服务出错"),
+    SERVICE_ERROR_C0100("C0100", "中间件服务出错"),
+    SERVICE_ERROR_C0110("C0110", "RPC服务出错"),
+    SERVICE_ERROR_C0111("C0111", "RPC服务未找到"),
+    SERVICE_ERROR_C0112("C0112", "RPC服务未注册"),
+    SERVICE_ERROR_C0113("C0113", "接口不存在"),
+    SERVICE_ERROR_C0120("C0120", "消息服务出错"),
+    SERVICE_ERROR_C0121("C0121", "消息投递出错"),
+    SERVICE_ERROR_C0122("C0122", "消息消费出错"),
+    SERVICE_ERROR_C0123("C0123", "消息订阅出错"),
+    SERVICE_ERROR_C0124("C0124", "消息分组未查到"),
+    SERVICE_ERROR_C0130("C0130", "缓存服务出错"),
+    SERVICE_ERROR_C0131("C0131", "key长度超过限制"),
+    SERVICE_ERROR_C0132("C0132", "value长度超过限制"),
+    SERVICE_ERROR_C0133("C0133", "存储容量已满"),
+    SERVICE_ERROR_C0134("C0134", "不支持的数据格式"),
+    SERVICE_ERROR_C0140("C0140", "配置服务出错"),
+    SERVICE_ERROR_C0150("C0150", "网络资源服务出错"),
+    SERVICE_ERROR_C0151("C0151", "VPN服务出错"),
+    SERVICE_ERROR_C0152("C0152", "CDN服务出错"),
+    SERVICE_ERROR_C0153("C0153", "域名解析服务出错"),
+    SERVICE_ERROR_C0154("C0154", "网关服务出错"),
+    SERVICE_ERROR_C0200("C0200", "第三方系统执行超时"),
+    SERVICE_ERROR_C0210("C0210", "RPC执行超时"),
+    SERVICE_ERROR_C0220("C0220", "消息投递超时"),
+    SERVICE_ERROR_C0230("C0230", "缓存服务超时"),
+    SERVICE_ERROR_C0240("C0240", "配置服务超时"),
+    SERVICE_ERROR_C0250("C0250", "数据库服务超时"),
+    SERVICE_ERROR_C0300("C0300", "数据库服务出错"),
+    SERVICE_ERROR_C0311("C0311", "表不存在"),
+    SERVICE_ERROR_C0312("C0312", "列不存在"),
+    SERVICE_ERROR_C0321("C0321", "多表关联中存在多个相同名称的列"),
+    SERVICE_ERROR_C0331("C0331", "数据库死锁"),
+    SERVICE_ERROR_C0341("C0341", "主键冲突"),
+    SERVICE_ERROR_C0400("C0400", "第三方容灾系统被触发"),
+    SERVICE_ERROR_C0401("C0401", "第三方系统限流"),
+    SERVICE_ERROR_C0402("C0402", "第三方功能降级"),
+    SERVICE_ERROR_C0500("C0500", "通知服务出错"),
+    SERVICE_ERROR_C0501("C0501", "短信提醒服务失败"),
+    SERVICE_ERROR_C0502("C0502", "语音提醒服务失败"),
+    SERVICE_ERROR_C0503("C0503", "邮件提醒服务失败"),
+    ;
+    @Getter
+    private String errCode;
+    @Getter
+    private String errMsg;
+}

+ 94 - 0
tr-framework/src/main/java/cn/tr/core/pojo/CommonResult.java

@@ -0,0 +1,94 @@
+package cn.tr.core.pojo;
+
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.exception.BaseCode;
+import cn.tr.core.exception.ServiceException;
+import cn.tr.core.exception.TRExcCode;
+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 {
+
+    private static final long serialVersionUID = -2619760277112039632L;
+
+    /**
+     * 错误码
+     *
+     * @see TRExcCode ()
+     */
+    private String code;
+    /**
+     * 返回数据
+     */
+    private T data;
+    /**
+     * 错误提示,用户可阅读
+     *
+     * @see TRExcCode#getErrMsg()
+     */
+    private String errorMsg;
+
+    public static <T> CommonResult<T> error(BaseCode excCode, String detailErrorMsg) {
+        String errCode = excCode.getErrCode();
+        Assert.isTrue(!TRExcCode.SUCCESS.getErrCode().equals(errCode), "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = errCode;
+        result.errorMsg = StrUtil.isNotBlank(detailErrorMsg)?detailErrorMsg:excCode.getErrMsg();
+        return result;
+    }
+
+    public static <T> CommonResult<T> error(BaseCode errorCode) {
+        return error(errorCode, errorCode.getErrMsg());
+    }
+
+    public static <T> CommonResult<T> success() {
+        return CommonResult.success(null);
+    }
+
+    public static <T> CommonResult<T> success(T data) {
+        CommonResult<T> result = new CommonResult<>();
+        result.code = TRExcCode.SUCCESS.getErrCode();
+        result.data = data;
+        result.errorMsg = "";
+        return result;
+    }
+
+    public static boolean isSuccess(String code) {
+        return Objects.equals(code, TRExcCode.SUCCESS.getErrCode());
+    }
+
+    /**
+     *  避免 jackson 序列化
+     * @return
+     */
+    @JsonIgnore
+    public boolean isSuccess() {
+        return isSuccess(code);
+    }
+
+    /**
+     *  避免 jackson 序列化
+     * @return
+     */
+    @JsonIgnore
+    public boolean isError() {
+        return !isSuccess();
+    }
+
+    // ========= 和 Exception 异常体系集成 =========
+
+    public static <T> CommonResult<T> error(ServiceException serviceException) {
+        return error(TRExcCode.SYSTEM_ERROR_B0001, serviceException.getMessage());
+    }
+
+}

+ 28 - 0
tr-framework/src/main/java/cn/tr/core/pojo/LoginResult.java

@@ -0,0 +1,28 @@
+package cn.tr.core.pojo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @ClassName : LoginResult
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月02日
+ */
+@Data
+public class LoginResult implements Serializable {
+    private static final long serialVersionUID = 6736183242095354849L;
+
+    /**
+     * 登录token
+     */
+    private String token;
+
+    /**
+     * token过期时间
+     */
+    private long expireTime;
+
+
+}

+ 32 - 0
tr-framework/src/main/java/cn/tr/core/pojo/PageDomain.java

@@ -0,0 +1,32 @@
+package cn.tr.core.pojo;
+
+import cn.hutool.db.sql.Order;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 分页数据
+ * 
+ * @author lifang
+ */
+@Data
+public class PageDomain
+{
+    /** 当前记录起始索引 */
+    private Integer current;
+
+    /** 每页显示记录数 */
+    private Integer size;
+
+    private List<Order> orders;
+
+    /** 分页参数合理化 */
+    private Boolean reasonable = true;
+
+
+    public PageDomain(Integer current, Integer size) {
+        this.current = current;
+        this.size = size;
+    }
+}

+ 41 - 0
tr-framework/src/main/java/cn/tr/core/pojo/PageInfo.java

@@ -0,0 +1,41 @@
+package cn.tr.core.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * @ClassName : PageInfo
+ * @Description :
+ * @Author : LF
+ * @Date: 2022年11月25日
+ */
+
+@AllArgsConstructor(staticName = "of")
+@NoArgsConstructor
+@Data
+public class PageInfo<T> implements Serializable {
+
+    /**
+     * 总条数
+     */
+    private Long total;
+
+    /**
+     * 当前记录起始索引
+     */
+    private Integer pageNum;
+
+    /**
+     * 每页显示记录数
+     */
+    private Integer pageSize;
+
+    /**
+     * 数据
+     */
+    private Collection<T> data;
+}

+ 64 - 0
tr-framework/src/main/java/cn/tr/core/pojo/TableDataInfo.java

@@ -0,0 +1,64 @@
+package cn.tr.core.pojo;
+
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.exception.TRExcCode;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 表格分页数据对象
+ * 
+ * @author lifang
+ */
+@Data
+public class TableDataInfo<T> implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 列表数据 */
+    private PageInfo<T> data;
+
+    /** 消息状态码 */
+    private String code;
+
+    /** 消息内容 */
+    private String errorMsg;
+
+    private TableDataInfo(PageInfo<T> data, TRExcCode code, String errorMsg) {
+        this.data = data;
+        this.code = code.getErrCode();
+        this.errorMsg = StrUtil.isNotEmpty(errorMsg)?errorMsg:code.getErrMsg();
+    }
+
+    private TableDataInfo(PageInfo<T> data, TRExcCode code) {
+        this.data = data;
+        this.code = code.getErrCode();
+        this.errorMsg =code.getErrMsg();
+    }
+
+
+    /**
+     * 表格数据对象
+     */
+    private TableDataInfo() {
+    }
+
+    public  static <C> TableDataInfo success(PageInfo<C> page){
+        return new TableDataInfo(page,TRExcCode.SUCCESS);
+    }
+
+    public  static <C> TableDataInfo fail(PageInfo<C> page, TRExcCode code){
+        return new TableDataInfo(page,code);
+    }
+
+
+    public  static <C> TableDataInfo fail(PageInfo<C> page, TRExcCode code,String errorMsg){
+        return new TableDataInfo(page,code,errorMsg);
+    }
+
+
+    public String getErrorMsg() {
+        return errorMsg==null?"":errorMsg;
+    }
+}

+ 55 - 0
tr-framework/src/main/java/cn/tr/core/strategy/DeptDataPermissionStrategy.java

@@ -0,0 +1,55 @@
+package cn.tr.core.strategy;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+/**
+ * @ClassName : DeptDataPermissionStrategy
+ * @Description : 数据权限策略
+ * @Author : LF
+ * @Date: 2023年03月03日
+ */
+
+public class DeptDataPermissionStrategy {
+    private DeptDataPermissionStrategy(){
+
+    }
+
+    public static DeptDataPermissionStrategy tr=new DeptDataPermissionStrategy();
+
+
+    /**
+     * 当前用户的部门id
+     */
+    public Supplier<Set<String>> currentUserDeptIdsSupplier=()->null;
+
+    /**
+     * 是否允许查看所有数据
+     */
+    public Supplier<Boolean> allowAllDataSupplier=()->false;
+
+    /**
+     * 是否允许查看自己的数据
+     */
+    public Supplier<Boolean> allowSelfDataSupplier=()->false;
+
+    public Set<String> getCurrentUserDeptIds(){
+        return currentUserDeptIdsSupplier==null?null:currentUserDeptIdsSupplier.get();
+    }
+
+    /**
+     * 查看所有数据
+     * @return
+     */
+    public Boolean getAll(){
+        return allowAllDataSupplier==null?null:allowAllDataSupplier.get();
+    }
+
+    /**
+     * 查看用户自身的数据
+     * @return
+     */
+    public Boolean getSelf(){
+        return allowSelfDataSupplier==null?null:allowSelfDataSupplier.get();
+    }
+}

+ 37 - 0
tr-framework/src/main/java/cn/tr/core/strategy/ExceptionStrategy.java

@@ -0,0 +1,37 @@
+package cn.tr.core.strategy;
+
+import cn.tr.core.exception.TRExcCode;
+import cn.tr.core.pojo.CommonResult;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+/**
+ * @ClassName : ExceptionStrategy
+ * @Description : 异常处理策略
+ * @Author : LF
+ * @Date: 2023年02月28日
+ */
+
+public class ExceptionStrategy {
+    private ExceptionStrategy(){
+
+    }
+    private Map<Class<? extends Throwable>,BiFunction<HttpServletRequest, Throwable,CommonResult>> exceptionHandlerMap=new HashMap<>();
+
+    public static ExceptionStrategy tr =new ExceptionStrategy();
+
+
+    public <T extends Throwable> void  registerThrowableHandler(Class<T> aClass,BiFunction<HttpServletRequest,Throwable,CommonResult> exceptionSupplier){
+        exceptionHandlerMap.put(aClass,exceptionSupplier);
+    }
+
+    public CommonResult<?> exceptionHandle(HttpServletRequest request, Throwable ex){
+        //兜底处理
+        return exceptionHandlerMap
+                .getOrDefault(ex.getClass(), ((re, t) -> CommonResult.error(TRExcCode.SYSTEM_ERROR_B0001, t.getLocalizedMessage())))
+                .apply(request,ex);
+    }
+}

+ 16 - 0
tr-framework/src/main/java/cn/tr/core/strategy/ILoginUser.java

@@ -0,0 +1,16 @@
+package cn.tr.core.strategy;
+
+/**
+ * @ClassName : ILoginUser
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public interface ILoginUser {
+    String getUserId();
+
+    String getUsername();
+
+    String getTenantId();
+}

+ 56 - 0
tr-framework/src/main/java/cn/tr/core/strategy/LoginUserStrategy.java

@@ -0,0 +1,56 @@
+package cn.tr.core.strategy;
+
+
+import cn.tr.core.pojo.LoginResult;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * @ClassName : LoginUserStrategy
+ * @Description : 登录用户策略
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public class LoginUserStrategy {
+    private LoginUserStrategy(){
+
+    }
+
+    public static LoginUserStrategy tr =new LoginUserStrategy();
+
+    public Supplier<? extends  ILoginUser> loginUserSupplier;
+    /**
+     * 当前地址是否允许匿名登录,默认不允许
+     */
+    public Supplier<Boolean> anonymousLoginSupplier=()->false;
+
+    /**
+     * 用户登录返回token
+     */
+    public Function<? super ILoginUser, LoginResult> loginTokenSupplier;
+
+    public LoginResult login(ILoginUser loginUser){
+        if(loginTokenSupplier==null){
+            throw new RuntimeException("LoginUserStrategy.loginTokenSupplier is null");
+        }
+        return loginTokenSupplier.apply(loginUser);
+    }
+
+    public String getCurrentUserId(){
+        return loginUserSupplier==null?null:loginUserSupplier.get().getUserId();
+    }
+
+    public String getCurrentUsername(){
+        return loginUserSupplier==null?null:loginUserSupplier.get().getUsername();
+    }
+
+    public String getTenantId(){
+        return loginUserSupplier==null?null:loginUserSupplier.get().getTenantId();
+    }
+
+    public boolean isAnonymous(){
+        return anonymousLoginSupplier==null||Boolean.TRUE.equals(anonymousLoginSupplier.get());
+    }
+}

+ 39 - 0
tr-framework/src/main/java/cn/tr/core/strategy/PageStrategy.java

@@ -0,0 +1,39 @@
+package cn.tr.core.strategy;
+
+import cn.tr.core.pojo.PageDomain;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * @ClassName : PageStrategy
+ * @Description : 分页策略
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public class PageStrategy {
+    private PageStrategy(){
+
+    }
+
+
+    public static final PageStrategy tr = new PageStrategy();
+
+
+    /**
+     * 从请求中创建分页参数
+     */
+    public Supplier<PageDomain> createPage=()-> new PageDomain(1,10);
+
+    /**
+     * 开始实现分页
+     */
+    public Consumer<PageDomain> startPage= page->{};
+
+    /**
+     * 获取当前最终分页参数
+     */
+    public Supplier<PageDomain> getPage=()-> new PageDomain(1,10);
+
+}

+ 26 - 0
tr-framework/src/main/java/cn/tr/core/strategy/TenantStrategy.java

@@ -0,0 +1,26 @@
+package cn.tr.core.strategy;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.function.Function;
+
+/**
+ * @ClassName : TenantStrategy
+ * @Description : 租户策略
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public class TenantStrategy {
+    private TenantStrategy(){
+
+    }
+
+    public static TenantStrategy tr=new TenantStrategy();
+
+    public Function<HttpServletRequest,String> tenantIdSupplier;
+
+    public String getTenantId(HttpServletRequest request){
+        return tenantIdSupplier==null?null:tenantIdSupplier.apply(request);
+    }
+
+}

+ 58 - 0
tr-framework/src/main/java/cn/tr/core/utils/IpUtil.java

@@ -0,0 +1,58 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.io.FileUtil;
+import org.lionsoul.ip2region.xdb.Searcher;
+
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ * Ip地址查询类
+ */
+public class IpUtil {
+    private static Searcher searcher;
+
+    static {
+        String fileName = "/ip2region.xdb";
+        File existFile = FileUtil.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + fileName);
+        if(!FileUtil.exist(existFile)) {
+            InputStream resourceAsStream = IpUtil.class.getResourceAsStream(fileName);
+            FileUtil.writeFromStream(resourceAsStream, existFile);
+        }
+
+        String dbPath = existFile.getPath();
+
+        // 1、从 dbPath 加载整个 xdb 到内存。
+        byte[] cBuff;
+        try {
+            cBuff = Searcher.loadContentFromFile(dbPath);
+        } catch (Exception e) {
+            throw new RuntimeException(String.format("IPUtil初始化失败,原因:%s", e.getMessage()));
+        }
+
+        // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
+        try {
+            searcher = Searcher.newWithBuffer(cBuff);
+        } catch (Exception e) {
+            throw new RuntimeException(String.format("IPUtil初始化失败,原因:%s", e.getMessage()));
+        }
+    }
+
+    /**
+     * 根据IP地址离线获取城市
+     *
+     * @author xuyuxiang
+     * @date 2022/4/27 23:14
+     */
+    public static String getCityInfo(String ip) {
+        try {
+            ip = ip.trim();
+            // 3、执行查询
+            String region = searcher.search(ip);
+            return region.replace("0|", "").replace("|0", "");
+        } catch (Exception e) {
+            return "未知";
+        }
+    }
+
+}

+ 146 - 0
tr-framework/src/main/java/cn/tr/core/utils/JsonUtils.java

@@ -0,0 +1,146 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.experimental.UtilityClass;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * JSON 工具类
+ *
+ * @author 芋道源码
+ */
+@UtilityClass
+public class JsonUtils {
+
+    private static ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 初始化 objectMapper 属性
+     * <p>
+     * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+     *
+     * @param objectMapper ObjectMapper 对象
+     */
+    public static void init(ObjectMapper objectMapper) {
+        JsonUtils.objectMapper = objectMapper;
+    }
+
+    public static String toJsonString(Object object) {
+        try {
+            return objectMapper.writeValueAsString(object);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static byte[] toJsonByte(Object object) throws JsonProcessingException {
+        return objectMapper.writeValueAsBytes(object);
+    }
+
+    public static String toJsonPrettyString(Object object) throws JsonProcessingException {
+        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) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    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) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, TypeReference<T> typeReference) throws JsonProcessingException {
+        if (!isJson(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            throw e;
+        }
+    }
+
+    public static Map<String,Object> parseMap(String text){
+        if (StrUtil.isEmpty(text)) {
+            return new HashMap<String,Object>();
+        }
+        try {
+            return objectMapper.readValue(text, Map.class);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    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) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static JsonNode parseTree(String text) throws JsonProcessingException {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            throw e;
+        }
+    }
+
+    public static JsonNode parseTree(byte[] text) throws IOException {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            throw e;
+        }
+    }
+
+    public static boolean isJson(String text) {
+        return JSONUtil.isTypeJSON(text);
+    }
+
+    /**
+     * 是否为JSONArray类型的字符串,首尾都为中括号判定为JSONArray字符串
+     *
+     * @param str 字符串
+     * @return 是否为JSONArray类型字符串
+     * @since 5.7.22
+     */
+    public static boolean isTypeJSONArray(String str) {
+        if (StrUtil.isBlank(str)) {
+            return false;
+        }
+        return StrUtil.isWrap(StrUtil.trim(str), '[', ']');
+    }
+}

+ 111 - 0
tr-framework/src/main/java/cn/tr/core/utils/MediaTypeUtils.java

@@ -0,0 +1,111 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @ClassName : MediaTypeUtils
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月13日
+ */
+@Slf4j
+public class MediaTypeUtils {
+    private static final Map<String,String> contentTypeMap=new HashMap<>();
+    /**
+     * 默认文件下载
+     */
+    private static final String DEFAULT_CONTENT_TYPE="application/octet-stream ";
+
+    static {
+        for (MediaType value : MediaType.values()) {
+            log.debug("[register]---->fileType:[{}],ContentType:[{}]",value.getFileType(),value.getContentType());
+            contentTypeMap.put(value.getFileType(),value.getContentType());
+        }
+    }
+
+    public static void registerContentType(String fileType,String contentType){
+        log.debug("[register]---->fileType:[{}],ContentType:[{}]",fileType,contentType);
+        contentTypeMap.put(fileType,contentType);
+    }
+
+    /**
+     *  解析文件锁对应的Content-Type
+     * @param fileName
+     * @return
+     */
+    public static String parseContentType(String fileName) {
+        if(!StrUtil.contains(fileName,".")){
+            return DEFAULT_CONTENT_TYPE;
+        }
+        String fileType= CollectionUtil.getLast(StrUtil.split(fileName,"."));
+        return contentTypeMap.getOrDefault(fileType,DEFAULT_CONTENT_TYPE);
+    }
+
+    @AllArgsConstructor
+    @Getter
+    private static enum MediaType{
+        DOC("doc","application/msword"),
+        DOCX("docx","application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
+
+        RTF("rtf","application/rtf"),
+
+        XLS("xls","application/vnd.ms-excel application/x-excel"),
+        XLSX("xlsx","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
+
+        PPT("ppt","application/vnd.ms-powerpoint"),
+        PPTX("pptx","application/vnd.openxmlformats-officedocument.presentationml.presentation"),
+
+        PPS("pps","application/vnd.ms-powerpoint"),
+        PPSX("ppsx","application/vnd.openxmlformats-officedocument.presentationml.slideshow"),
+
+        PDF("pdf","application/pdf"),
+
+        SWF("swf","application/x-shockwave-flash"),
+
+        DDL("ddl","application/x-msdownload"),
+
+        TAR("tar","application/x-tar"),
+
+        TGZ("tgz","application/x-compressed"),
+
+        ZIP("zip","application/x-zip-compressed"),
+
+        z("z","application/x-compress"),
+
+        WAV("wav","audio/wav"),
+
+        WMA("wma","audio/x-ms-wma"),
+
+        WMV("wmv","video/x-ms-wmv"),
+
+
+        BMP("bmp","image/bmp"),
+
+        GIF("gif","image/gif"),
+
+        JPEG("jpeg","image/jpg"),
+        JPG("jpg","image/jpg"),
+        PNG("png","image/jpg"),
+
+        HTML("html","text/html"),
+
+        TXT("txt","text/plain"),
+
+        VSD("vsd","application/vnd.visio"),
+
+        XML("xml","text/xml"),
+
+
+
+        ;
+        private String fileType;
+        private String contentType;
+    }
+}

+ 140 - 0
tr-framework/src/main/java/cn/tr/core/utils/ServletUtils.java

@@ -0,0 +1,140 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+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;
+
+/**
+ * 客户端工具类
+ *
+ * @author 芋道源码
+ */
+public class ServletUtils {
+
+    /**
+     * 获取String参数
+     */
+    public static String getParameter(String name)
+    {
+        return getRequest().getParameter(name);
+    }
+
+    /**
+     * 获取String参数
+     */
+    public static String getHeader(String name)
+    {
+        return getRequest().getHeader(name);
+    }
+    /**
+     * 返回 JSON 字符串
+     *
+     * @param response 响应
+     * @param object 对象,会序列化成 JSON 字符串
+     */
+    @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
+    public static void writeJSON(HttpServletResponse response, Object object) throws JsonProcessingException {
+        String content = JsonUtils.toJsonString(object);
+        ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
+    }
+
+    /**
+     * 返回附件
+     *
+     * @param response 响应
+     * @param filename 文件名
+     * @param content 附件内容
+     * @throws IOException
+     */
+    public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
+        writeAttachment(response,filename,content,false);
+    }
+
+    /**
+     * 返回附件
+     *
+     * @param response 响应
+     * @param filename 文件名
+     * @param content 附件内容
+     * @param close 是否关闭流
+     * @throws IOException
+     */
+    public static void writeAttachment(HttpServletResponse response, String filename, byte[] content, boolean close) 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(), close, 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() {
+        try {
+            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+            if (!(requestAttributes instanceof ServletRequestAttributes)) {
+                return null;
+            }
+            return ((ServletRequestAttributes) requestAttributes).getRequest();
+        }catch (Exception e){
+            return null;
+        }
+    }
+
+    /**
+     * 获取ua
+     * @return
+     */
+    public static String getUserAgent() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return getUserAgent(request);
+    }
+
+    /**
+     * 获取客户端ip地址
+     * @return
+     */
+    public static String getClientIP() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return ServletUtil.getClientIP(request);
+    }
+
+    /**
+     * 判断请求内容类型是为json
+     * @param request
+     * @return
+     */
+    public static boolean isJsonRequest(ServletRequest request) {
+        return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+    }
+
+}

BIN
tr-framework/src/main/resources/ip2region.xdb


+ 16 - 0
tr-modules/pom.xml

@@ -0,0 +1,16 @@
+<?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>tr-footstone</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-modules</artifactId>
+    <version>${revision}</version>
+
+
+</project>

+ 34 - 0
tr-plugins/pom.xml

@@ -0,0 +1,34 @@
+<?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>tr-footstone</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-plugins</artifactId>
+    <packaging>pom</packaging>
+    <version>${revision}</version>
+
+    <description></description>
+    <modules>
+        <module>tr-spring-boot-starter-plugin-biz-tenant</module>
+        <module>tr-spring-boot-starter-plugin-web</module>
+        <module>tr-spring-boot-starter-plugin-mybatis</module>
+        <module>tr-spring-boot-starter-plugin-banner</module>
+        <module>tr-spring-boot-starter-plugin-satoken</module>
+        <module>tr-spring-boot-starter-plugin-biz-data-permission</module>
+        <module>tr-spring-boot-starter-plugin-test</module>
+        <module>tr-spring-boot-starter-plugin-dict</module>
+        <module>tr-spring-boot-starter-plugin-cache</module>
+        <module>tr-spring-boot-starter-plugin-file</module>
+        <module>tr-spring-boot-starter-plugin-biz-excel</module>
+        <module>tr-spring-boot-starter-plugin-websocket</module>
+        <module>tr-spring-boot-starter-plugin-sms</module>
+        <module>tr-spring-boot-starter-plugin-eventbus</module>
+    </modules>
+
+</project>

+ 29 - 0
tr-plugins/tr-spring-boot-starter-plugin-banner/pom.xml

@@ -0,0 +1,29 @@
+<?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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-spring-boot-starter-plugin-banner</artifactId>
+
+
+    <description>打印banner</description>
+    <dependencies>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 17 - 0
tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/java/cn/tr/plugin/banner/TrBannerAutoConfiguration.java

@@ -0,0 +1,17 @@
+package cn.tr.plugin.banner;
+
+import cn.tr.plugin.banner.config.BannerConfig;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * @ClassName : TrBannerAutoConfiguration
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月01日
+ */
+public class TrBannerAutoConfiguration {
+    @Bean
+    public BannerConfig bannerConfig(){
+        return new BannerConfig();
+    }
+}

+ 42 - 0
tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/java/cn/tr/plugin/banner/config/BannerConfig.java

@@ -0,0 +1,42 @@
+package cn.tr.plugin.banner.config;
+
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.context.WebServerApplicationContext;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.boot.web.server.WebServer;
+import org.springframework.context.ApplicationListener;
+import org.springframework.core.env.Environment;
+
+/**
+ * @ClassName : BannerConfig
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月01日
+ */
+@Slf4j
+public class BannerConfig implements ApplicationListener<WebServerInitializedEvent> {
+
+    @Override
+    public void onApplicationEvent(WebServerInitializedEvent event) {
+        WebServer server = event.getWebServer();
+        WebServerApplicationContext context = event.getApplicationContext();
+        Environment env = context.getEnvironment();
+        String contextPath = env.getProperty("server.servlet.context-path");
+        String applicationName = env.getProperty("spring.application.name");
+        String profilesActive = env.getProperty("spring.profiles.active");
+        String localhostStr = NetUtil.getLocalhostStr();
+        int port = server.getPort();
+        String urlPrefix= StrUtil.isNullOrUndefined(contextPath)? "http://"+localhostStr+":"+port:"http://"+localhostStr+":"+port+"/"+contextPath;
+        log.info("\n----------------------------------------------------------\n\t" +
+                        "项目启动成功!\n\t" +
+                        "项目名称: \t{}\n\t" +
+                        "接口文档: \t{} \n\t" +
+                        "启动环境: \t{} \n\t" +
+                        "\n----------------------------------------------------------\n\t",
+                StrUtil.isEmpty(applicationName)?"tr":applicationName,
+                urlPrefix+"/doc.html",
+                StrUtil.isNullOrUndefined(profilesActive)?"default":profilesActive);
+    }
+}

+ 1 - 0
tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.tr.plugin.banner.TrBannerAutoConfiguration

+ 15 - 0
tr-plugins/tr-spring-boot-starter-plugin-banner/src/main/resources/banner.txt

@@ -0,0 +1,15 @@
+Spring Boot Version: ${spring-boot.version}
+Java        Version: ${java.version}
+.__   __.   ______      .______    __    __    _______
+|  \ |  |  /  __  \     |   _  \  |  |  |  |  /  _____|
+|   \|  | |  |  |  |    |  |_)  | |  |  |  | |  |  __
+|  . `  | |  |  |  |    |   _  <  |  |  |  | |  | |_ |
+|  |\   | |  `--'  |    |  |_)  | |  `--'  | |  |__| |
+|__| \__|  \______/     |______/   \______/   \______|
+
+███╗   ██╗ ██████╗     ██████╗ ██╗   ██╗ ██████╗
+████╗  ██║██╔═══██╗    ██╔══██╗██║   ██║██╔════╝
+██╔██╗ ██║██║   ██║    ██████╔╝██║   ██║██║  ███╗
+██║╚██╗██║██║   ██║    ██╔══██╗██║   ██║██║   ██║
+██║ ╚████║╚██████╔╝    ██████╔╝╚██████╔╝╚██████╔╝
+╚═╝  ╚═══╝ ╚═════╝     ╚═════╝  ╚═════╝  ╚═════╝

+ 34 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/pom.xml

@@ -0,0 +1,34 @@
+<?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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-spring-boot-starter-plugin-biz-datasource-permission</artifactId>
+
+
+    <description>数据权限</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-test</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+    </dependencies>
+</project>

+ 58 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/TrDataPermissionAutoConfiguration.java

@@ -0,0 +1,58 @@
+package cn.tr.plugin.biz.data.permission;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.plugin.biz.data.permission.config.aop.DataPermissionAnnotationAdvisor;
+import cn.tr.plugin.biz.data.permission.config.db.TrDataPermissionHandler;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRule;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRuleFactory;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRuleFactoryImpl;
+import cn.tr.plugin.biz.data.permission.config.rule.dept.DeptDataPermissionRule;
+import cn.tr.plugin.biz.data.permission.config.rule.dept.DeptDataPermissionRuleCustomizer;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+
+import java.util.*;
+/**
+ * @ClassName : TrDataPermissionAutoConfiguration
+ * @Description : 数据权限隔离自动装配
+ * @Author : LF
+ * @Date: 2023年03月03日
+ */
+
+public class TrDataPermissionAutoConfiguration {
+
+
+    @Bean
+    public InnerInterceptor dataPermissionInterceptor(DataPermissionRuleFactory dataPermissionRuleFactory){
+        DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
+        dataPermissionInterceptor.setDataPermissionHandler(new TrDataPermissionHandler(dataPermissionRuleFactory));
+        return dataPermissionInterceptor;
+    }
+
+    @Bean
+    public DataPermissionRuleFactory dataPermissionRuleFactory(@Autowired(required = false) List<DataPermissionRule> rules){
+        return new DataPermissionRuleFactoryImpl(rules);
+    }
+
+    @Bean
+    public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor(){
+        return new DataPermissionAnnotationAdvisor();
+    }
+
+     /**
+     * 部门数据权限
+     * @param ruleCustomizers
+     * @return
+     */
+    @Bean
+    public DeptDataPermissionRule deptDataPermissionRule(@Autowired(required = false) List<DeptDataPermissionRuleCustomizer> ruleCustomizers){
+        DeptDataPermissionRule rule = new DeptDataPermissionRule();
+        if(CollectionUtil.isNotEmpty(ruleCustomizers)){
+            ruleCustomizers.forEach(customizer->customizer.customize(rule));
+        }
+        return rule;
+    }
+}

+ 32 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/TrDeptPermissionAutoConfiguration.java

@@ -0,0 +1,32 @@
+package cn.tr.plugin.biz.data.permission;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.plugin.biz.data.permission.config.rule.dept.DeptDataPermissionRule;
+import cn.tr.plugin.biz.data.permission.config.rule.dept.DeptDataPermissionRuleCustomizer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+
+import java.util.List;
+
+/**
+ * @ClassName : TrDataPermissionAutoConfiguration
+ * @Description : 基于部门的权限配置
+ * @Author : LF
+ * @Date: 2023年03月03日
+ */
+
+public class TrDeptPermissionAutoConfiguration {
+     /**
+     * 部门数据权限
+     * @param ruleCustomizers
+     * @return
+     */
+    @Bean
+    public DeptDataPermissionRule deptDataPermissionRule(@Autowired(required = false) List<DeptDataPermissionRuleCustomizer> ruleCustomizers){
+        DeptDataPermissionRule rule = new DeptDataPermissionRule();
+        if(CollectionUtil.isNotEmpty(ruleCustomizers)){
+            ruleCustomizers.forEach(customizer->customizer.customize(rule));
+        }
+        return rule;
+    }
+}

+ 36 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/annotation/DataPermission.java

@@ -0,0 +1,36 @@
+package cn.tr.plugin.biz.data.permission.config.annotation;
+
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRule;
+import lombok.EqualsAndHashCode;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限注解
+ * 可声明在类或者方法上,标识使用的数据权限规则
+ *
+ * @author tr
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataPermission {
+
+    /**
+     * 当前类或方法是否开启数据权限
+     * 即使不添加 @DataPermission 注解,默认是开启状态
+     * 可通过设置 enable 为 false 禁用
+     */
+    boolean enable() default true;
+
+    /**
+     * 当触发多个规则{@link DataPermissionRule}时,生效的数据权限规则数组 ,优先级高于 {@link #excludeRules()}
+     */
+    Class<? extends DataPermissionRule>[] includeRules() default {};
+
+    /**
+     * 当触发多个规则{@link DataPermissionRule}时,排除的数据权限规则数组,优先级最低
+     */
+    Class<? extends DataPermissionRule>[] excludeRules() default {};
+
+}

+ 36 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/aop/DataPermissionAnnotationAdvisor.java

@@ -0,0 +1,36 @@
+package cn.tr.plugin.biz.data.permission.config.aop;
+
+import cn.tr.plugin.biz.data.permission.config.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 DataPermission} 注解的 Advisor 实现类
+ *
+ * @author tr
+ */
+@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);
+    }
+
+}

+ 73 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/aop/DataPermissionAnnotationInterceptor.java

@@ -0,0 +1,73 @@
+package cn.tr.plugin.biz.data.permission.config.aop;
+
+import cn.tr.plugin.biz.data.permission.config.annotation.DataPermission;
+import cn.tr.plugin.biz.data.permission.context.DataPermissionContextHolder;
+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 tr
+ */
+@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;
+    }
+
+}

+ 44 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/db/TrDataPermissionHandler.java

@@ -0,0 +1,44 @@
+package cn.tr.plugin.biz.data.permission.config.db;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRule;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRuleFactory;
+import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.schema.Table;
+
+import java.util.*;
+
+/**
+ *
+ * 数据权限处理器,通过{@link DataPermissionRule} 数据权限规则,重写sql的方式来实现
+ * 通过MP插件 {@link DataPermissionInterceptor} 来实现,此类主要提供sql语句的数据权限后缀
+ *
+ */
+public class TrDataPermissionHandler implements MultiDataPermissionHandler {
+
+    private final DataPermissionRuleFactory ruleFactory;
+
+    public TrDataPermissionHandler(DataPermissionRuleFactory ruleFactory) {
+        this.ruleFactory=ruleFactory;
+    }
+
+    @Override
+    public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
+        String tableName = table.getName();
+        List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
+        Expression allExpression = null;
+        if(CollectionUtil.isNotEmpty(rules)){
+            for (DataPermissionRule rule : rules) {
+                Expression oneExpress = rule.getExpression(tableName, table.getAlias());
+                // 拼接到 allExpression 中
+                allExpression = allExpression == null ? oneExpress
+                        : new AndExpression(allExpression, oneExpress);
+            }
+        }
+        return allExpression;
+    }
+}

+ 36 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/DataPermissionRule.java

@@ -0,0 +1,36 @@
+package cn.tr.plugin.biz.data.permission.config.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 tr
+ */
+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
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/DataPermissionRuleFactory.java

@@ -0,0 +1,28 @@
+package cn.tr.plugin.biz.data.permission.config.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);
+
+}

+ 66 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/DataPermissionRuleFactoryImpl.java

@@ -0,0 +1,66 @@
+package cn.tr.plugin.biz.data.permission.config.rule;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.tr.plugin.biz.data.permission.config.annotation.DataPermission;
+import cn.tr.plugin.biz.data.permission.context.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;
+    }
+
+    /**
+     * @param mappedStatementId 暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
+     * @return
+     */
+    @Override
+    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;
+    }
+
+}

+ 170 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/dept/DeptDataPermissionRule.java

@@ -0,0 +1,170 @@
+package cn.tr.plugin.biz.data.permission.config.rule.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.strategy.DeptDataPermissionStrategy;
+import cn.tr.core.strategy.LoginUserStrategy;
+import cn.tr.core.utils.JsonUtils;
+import cn.tr.plugin.biz.data.permission.constant.DataPermissionConstant;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRule;
+import cn.tr.plugin.mybatis.config.expression.OrExpressionX;
+import cn.tr.plugin.mybatis.pojo.BaseDO;
+import cn.tr.plugin.mybatis.utils.MyBatisUtils;
+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.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;
+import java.util.stream.Collectors;
+
+/**
+ * 基于部门的 {@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 tr
+ */
+@AllArgsConstructor
+@Slf4j
+public class DeptDataPermissionRule implements DataPermissionRule {
+
+
+    /**
+     * 基于部门的表字段配置
+     * 一般情况下,每个表的部门编号字段是 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) {
+        // 只有有登陆用户的情况下,才进行数据权限的处理
+        if (LoginUserStrategy.tr.isAnonymous()&&StrUtil.isEmpty(LoginUserStrategy.tr.getCurrentUserId())) {
+            //用户未登录,则不执行数据权限的处理
+            return null;
+        }
+
+        // 情况一,如果是 ALL 可查看全部,则无需拼接条件
+        if (Boolean.TRUE.equals(DeptDataPermissionStrategy.tr.getAll())) {
+            return null;
+        }
+        Set<String> deptIds = DeptDataPermissionStrategy.tr.getCurrentUserDeptIds();
+        // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
+        if (CollUtil.isEmpty(deptIds)
+                && !Boolean.TRUE.equals(DeptDataPermissionStrategy.tr.getSelf())) {
+            return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
+        }
+
+        // 情况三,拼接 Dept 和 User 的条件,最后组合
+        Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptIds);
+        Expression userExpression = buildUserExpression(tableName, tableAlias, DeptDataPermissionStrategy.tr.getSelf(),LoginUserStrategy.tr.getCurrentUserId());
+        if (deptExpression == null && userExpression == null) {
+//            log.warn("[getExpression][LoginUser Id({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
+//                    LoginUserStrategy.tr.getCurrentUserId(), tableName, tableAlias, JsonUtils.toJsonString(deptIds));
+            return DataPermissionConstant.EXPRESSION_NULL;
+        }
+        if (deptExpression == null) {
+            return userExpression;
+        }
+        if (userExpression == null) {
+            return deptExpression;
+        }
+        // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
+        return new OrExpressionX(deptExpression, userExpression);
+    }
+
+    private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<String> 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),
+                new ExpressionList(deptIds.stream().map(StringValue::new).collect(Collectors.toList())));
+    }
+
+    private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, String 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, DataPermissionConstant.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, DataPermissionConstant.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
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/config/rule/dept/DeptDataPermissionRuleCustomizer.java

@@ -0,0 +1,20 @@
+package cn.tr.plugin.biz.data.permission.config.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);
+
+}

+ 24 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/constant/DataPermissionConstant.java

@@ -0,0 +1,24 @@
+package cn.tr.plugin.biz.data.permission.constant;
+
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.NullValue;
+
+/**
+ * @ClassName : DataPermissionConstant
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月03日
+ */
+
+public interface DataPermissionConstant {
+    /**
+     * 数据权限缓存key
+     */
+    String DATA_PERMISSION="dataPermission";
+
+    String DEPT_COLUMN_NAME = "dept_id";
+    String USER_COLUMN_NAME = "user_id";
+
+    Expression EXPRESSION_NULL = new NullValue();
+
+}

+ 62 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/main/java/cn/tr/plugin/biz/data/permission/context/DataPermissionContextHolder.java

@@ -0,0 +1,62 @@
+package cn.tr.plugin.biz.data.permission.context;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.core.context.SecurityContextHolder;
+import cn.tr.plugin.biz.data.permission.config.annotation.DataPermission;
+import cn.tr.plugin.biz.data.permission.constant.DataPermissionConstant;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * {@link DataPermission} 注解的 Context 上下文
+ *  使用 List 的原因,可能存在方法的嵌套调用
+ * @author tr
+ */
+public class DataPermissionContextHolder {
+
+    /**
+     * 获得当前的 DataPermission 注解
+     *
+     * @return DataPermission 注解
+     */
+    public static DataPermission get() {
+        return (DataPermission) CollectionUtil.getLast(SecurityContextHolder.get(DataPermissionConstant.DATA_PERMISSION, ArrayList.class));
+    }
+
+    /**
+     * 入栈 DataPermission 注解
+     *
+     * @param dataPermission DataPermission 注解
+     */
+    public static void add(DataPermission dataPermission) {
+        ArrayList<DataPermission> dataPermissions = Optional.ofNullable(SecurityContextHolder.get(DataPermissionConstant.DATA_PERMISSION, ArrayList.class)).orElse(new ArrayList());
+        dataPermissions.add(dataPermission);
+        SecurityContextHolder.set(DataPermissionConstant.DATA_PERMISSION,dataPermissions);
+    }
+
+    /**
+     * 出栈 DataPermission 注解
+     *
+     * @return DataPermission 注解
+     */
+    public static DataPermission remove() {
+        ArrayList dataPermissions = SecurityContextHolder.get(DataPermissionConstant.DATA_PERMISSION, ArrayList.class);
+        if(CollectionUtil.isEmpty(dataPermissions)){
+            return null;
+        }
+        DataPermission result = (DataPermission) dataPermissions.remove(CollectionUtil.size(dataPermissions) - 1);
+        return result;
+    }
+
+    /**
+     * 获得所有 DataPermission
+     *
+     * @return DataPermission 队列
+     */
+    public static List<DataPermission> getAll() {
+        return SecurityContextHolder.get(DataPermissionConstant.DATA_PERMISSION, ArrayList.class);
+    }
+
+}

+ 105 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/config/aop/DataPermissionAnnotationInterceptorTest.java

@@ -0,0 +1,105 @@
+package cn.tr.plugin.biz.data.permission.config.aop;
+
+import cn.tr.plugin.biz.data.permission.config.annotation.DataPermission;
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import org.aopalliance.intercept.MethodInvocation;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.springframework.core.MethodClassKey;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * @ClassName : DataPermissionAnnotationInterceptorTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+
+public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest {
+    @InjectMocks
+    private DataPermissionAnnotationInterceptor interceptor;
+
+    @Mock
+    private MethodInvocation methodInvocation;
+
+    /**
+     *  Method上有注解
+     */
+    @Test
+    public void testInvokeMethod() throws Throwable {
+        MethodClassKey methodClassKey = new MethodClassKey(TestMethod.class.getMethod("echo"), TestMethod.class);
+        mockMethodInvocation(TestMethod.class);
+        Object result = interceptor.invoke(methodInvocation);
+        Map<MethodClassKey, DataPermission> dataPermissionCache = interceptor.getDataPermissionCache();
+        Assertions.assertNotNull(dataPermissionCache.get(methodClassKey));
+        Assertions.assertEquals(result,"method");
+    }
+
+    /**
+     *  Class上有注解
+     */
+    @Test
+    public void testInvokeClass() throws Throwable {
+        MethodClassKey methodClassKey = new MethodClassKey(TestClass.class.getMethod("echo"), TestClass.class);
+        mockMethodInvocation(TestClass.class);
+        Object result = interceptor.invoke(methodInvocation);
+        Map<MethodClassKey, DataPermission> dataPermissionCache = interceptor.getDataPermissionCache();
+        Assertions.assertNotNull(dataPermissionCache.get(methodClassKey));
+        Assertions.assertEquals(result,"class");
+    }
+
+    /**
+     *  没有注解
+     */
+    @Test
+    public void testNone() throws Throwable {
+        MethodClassKey methodClassKey = new MethodClassKey(TestNone.class.getMethod("echo"), TestNone.class);
+        mockMethodInvocation(TestNone.class);
+        Object result = interceptor.invoke(methodInvocation);
+        Map<MethodClassKey, DataPermission> dataPermissionCache = interceptor.getDataPermissionCache();
+        Assertions.assertEquals(dataPermissionCache.get(methodClassKey),DataPermissionAnnotationInterceptor.DATA_PERMISSION_NULL);
+        Assertions.assertEquals(result,"none");
+    }
+
+    private void mockMethodInvocation(Class<?> clazz) throws Throwable {
+        Object targetObject = clazz.newInstance();
+        Method method = targetObject.getClass().getMethod("echo");
+        when(methodInvocation.getThis()).thenReturn(targetObject);
+        when(methodInvocation.getMethod()).thenReturn(method);
+        when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject));
+    }
+
+    static class TestMethod {
+
+        @DataPermission(enable = false)
+        public String echo() {
+            return "method";
+        }
+
+    }
+
+    @DataPermission(enable = false)
+    static class TestClass {
+
+        public String echo() {
+            return "class";
+        }
+
+    }
+
+    static class TestNone {
+
+        public String echo() {
+            return "none";
+        }
+
+    }
+
+}

+ 258 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/config/db/TrDataPermissionHandlerTest.java

@@ -0,0 +1,258 @@
+package cn.tr.plugin.biz.data.permission.config.db;
+
+import cn.hutool.core.util.ReflectUtil;
+import cn.tr.core.strategy.DeptDataPermissionStrategy;
+import cn.tr.core.strategy.ILoginUser;
+import cn.tr.core.strategy.LoginUserStrategy;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRuleFactory;
+import cn.tr.plugin.biz.data.permission.config.rule.dept.DeptDataPermissionRule;
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.*;
+import org.mockito.internal.util.collections.Sets;
+
+import java.util.Collections;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * @ClassName : TrDataPermissionHandlerTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月05日
+ */
+
+public class TrDataPermissionHandlerTest extends BaseMockitoUnitTest {
+    @InjectMocks
+    private TrDataPermissionHandler trDataPermissionHandler;
+
+
+    @Spy
+    private DataPermissionInterceptor interceptor;
+
+    @Spy
+    private DataPermissionRuleFactory ruleFactory;
+
+    MappedStatement mappedStatement;
+    BoundSql boundSql;
+
+
+    String sql="SELECT * FROM t_user WHERE id = 123";
+    @BeforeEach
+    public void setUp(){
+        interceptor.setDataPermissionHandler(trDataPermissionHandler);
+
+        mappedStatement = mock(MappedStatement.class);
+        Mockito.when(mappedStatement.getId()).thenReturn("mapper.test");
+        boundSql = mock(BoundSql.class);
+
+        Mockito.spy(ruleFactory.getDataPermissionRule(same("mapper.test")));
+
+        LoginUserStrategy.tr.anonymousLoginSupplier=()->false;
+        LoginUserStrategy.tr.loginUserSupplier=()->new ILoginUser() {
+            @Override
+            public String getUserId() {
+                return "user123";
+            }
+
+            @Override
+            public String getUsername() {
+                return "test";
+            }
+
+            @Override
+            public String getTenantId() {
+                return "tenantId123";
+            }
+        };
+
+        DeptDataPermissionStrategy.tr.allowAllDataSupplier=()->false;
+        DeptDataPermissionStrategy.tr.allowSelfDataSupplier=()->true;
+        DeptDataPermissionStrategy.tr.currentUserDeptIdsSupplier=()-> Sets.newSet("100");
+    }
+
+    /**
+     * 不存在数据规则
+     */
+    @Test
+    public void queryWithoutRule(){
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+            Mockito.when(mpBs.sql()).thenReturn(sql);
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+            verify(mpBs,times(1)).sql(sql);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+
+    /**
+     * 存在规则,查询全部
+     */
+    @Test
+    public void queryWithCondition1(){
+        DeptDataPermissionStrategy.tr.allowAllDataSupplier=()->true;
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+
+            //mock数据权限
+            DeptDataPermissionRule rule = new DeptDataPermissionRule();
+            Mockito.when(ruleFactory.getDataPermissionRule(same("mapper.test"))).thenReturn(Collections.singletonList(rule));
+            ReflectUtil.setFieldValue(rule,"TABLE_NAMES",Sets.newSet("t_user"));
+            rule.addDeptColumn("t_user","dept_id");
+            rule.addUserColumn("t_user","user_id");
+            //mock 方法
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+
+            //mock sql
+            Mockito.when(mpBs.sql()).thenReturn(sql);
+
+            //调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+
+            //断言
+            verify(mpBs,times(1)).sql("SELECT * FROM t_user WHERE id = 123");
+
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 即不能查看部门,又不能查看自己,则说明 100% 无权限
+     */
+    @Test
+    public void queryWithCondition2(){
+        DeptDataPermissionStrategy.tr.allowSelfDataSupplier=()->false;
+        DeptDataPermissionStrategy.tr.currentUserDeptIdsSupplier=()->null;
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+            //mock数据权限
+            DeptDataPermissionRule rule = new DeptDataPermissionRule();
+            Mockito.when(ruleFactory.getDataPermissionRule(same("mapper.test"))).thenReturn(Collections.singletonList(rule));
+            ReflectUtil.setFieldValue(rule,"TABLE_NAMES",Sets.newSet("t_user"));
+            rule.addDeptColumn("t_user","dept_id");
+            rule.addUserColumn("t_user","user_id");
+            //mock 方法
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+
+            //mock sql
+            Mockito.when(mpBs.sql()).thenReturn(sql);
+
+            //调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+
+            //断言
+            verify(mpBs,times(1)).sql("SELECT * FROM t_user WHERE id = 123 AND null = null");
+
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 存在规则,拼接dept
+     */
+    @Test
+    public void queryWithCondition3Dept(){
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+            //mock数据权限
+            DeptDataPermissionRule rule = new DeptDataPermissionRule();
+            ReflectUtil.setFieldValue(rule,"TABLE_NAMES",Sets.newSet("t_user"));
+            Mockito.when(ruleFactory.getDataPermissionRule(same("mapper.test"))).thenReturn(Collections.singletonList(rule));
+            rule.addDeptColumn("t_user","dept_id");
+            //mock 方法
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+
+            //mock sql
+            String sql="select * from t_user where id =123";
+            Mockito.when(mpBs.sql()).thenReturn(sql);
+
+            //调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+
+            //断言
+            verify(mpBs,times(1)).sql("SELECT * FROM t_user WHERE id = 123 AND t_user.dept_id IN ('100')");
+
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 存在规则,拼接 User 的条件
+     */
+    @Test
+    public void queryWithCondition3User(){
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+
+            //mock数据权限
+            DeptDataPermissionRule rule = new DeptDataPermissionRule();
+            ReflectUtil.setFieldValue(rule,"TABLE_NAMES",Sets.newSet("t_user"));
+            Mockito.when(ruleFactory.getDataPermissionRule("mapper.test")).thenReturn(Collections.singletonList(rule));
+            rule.addUserColumn("t_user","user_id");
+            //mock 方法
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+
+            //mock sql
+            String sql="select * from t_user where id =123";
+            Mockito.when(mpBs.sql()).thenReturn(sql);
+
+            //调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+
+            //断言
+            verify(mpBs,times(1)).sql("SELECT * FROM t_user WHERE id = 123 AND t_user.user_id = user123");
+
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 存在规则,拼接 Dept 和 User 的条件
+     */
+    @Test
+    public void queryWithCondition3DeptAndUser(){
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+            MappedStatement mappedStatement = mock(MappedStatement.class);
+            Mockito.when(mappedStatement.getId()).thenReturn("mapper.test");
+            BoundSql boundSql = mock(BoundSql.class);
+
+            //mock数据权限
+            DeptDataPermissionRule rule = new DeptDataPermissionRule();
+            ReflectUtil.setFieldValue(rule,"TABLE_NAMES",Sets.newSet("t_user"));
+            Mockito.when(ruleFactory.getDataPermissionRules()).thenReturn(Collections.singletonList(rule));
+            rule.addDeptColumn("t_user","dept_id");
+            rule.addUserColumn("t_user","user_id");
+            //mock 方法
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+
+            //mock sql
+            String sql="select * from t_user where id =123";
+            Mockito.when(mpBs.sql()).thenReturn(sql);
+
+            //调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+
+            //断言
+            verify(mpBs,times(1)).sql("SELECT * FROM t_user WHERE id = 123 AND (t_user.dept_id IN ('100') OR t_user.user_id = user123)");
+
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+
+
+}

+ 59 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/context/DataPermissionContextHolderTest.java

@@ -0,0 +1,59 @@
+package cn.tr.plugin.biz.data.permission.context;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.plugin.biz.data.permission.config.annotation.DataPermission;
+import cn.tr.plugin.biz.data.permission.config.aop.DataPermissionAnnotationInterceptor;
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+/**
+ * @ClassName : DataPermissionContextHolderTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月04日
+ */
+@DataPermission(enable = false)
+public class DataPermissionContextHolderTest extends BaseMockitoUnitTest {
+
+    private final DataPermission p1=DataPermissionContextHolderTest.class.getAnnotation(DataPermission.class);
+    private final DataPermission p2=DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
+    @BeforeEach
+    public void init(){
+        DataPermissionContextHolder.add(p1);
+
+        DataPermissionContextHolder.add(p2);
+    }
+
+    @Test
+    void getPermission(){
+        DataPermission dataPermission = DataPermissionContextHolder.get();
+        Assertions.assertNotEquals(dataPermission,p1);
+        Assertions.assertEquals(dataPermission,p2);
+
+        DataPermission dataPermission1 = DataPermissionContextHolder.get();
+        Assertions.assertEquals(dataPermission1,p2);
+        Assertions.assertNotEquals(dataPermission1,p1);
+    }
+
+    @Test
+    void removePermission(){
+        DataPermission dataPermission = DataPermissionContextHolder.remove();
+        Assertions.assertEquals(dataPermission,p2);
+        Assertions.assertNotEquals(dataPermission,p1);
+        DataPermission dataPermission1 = DataPermissionContextHolder.get();
+        Assertions.assertEquals(dataPermission1,p1);
+        Assertions.assertNotEquals(dataPermission1,p2);
+    }
+
+    @Test
+    void getAll(){
+        List<DataPermission> dataPermissions = DataPermissionContextHolder.getAll();
+        Assertions.assertTrue(dataPermissions.remove(p1));
+        Assertions.assertTrue(dataPermissions.remove(p2));
+        Assertions.assertEquals(CollectionUtil.size(dataPermissions),0);
+    }
+}

+ 110 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-data-permission/src/test/java/cn/tr/plugin/biz/data/permission/rule/DataPermissionRuleFactoryImplTest.java

@@ -0,0 +1,110 @@
+package cn.tr.plugin.biz.data.permission.rule;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.tr.plugin.biz.data.permission.config.annotation.DataPermission;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRule;
+import cn.tr.plugin.biz.data.permission.config.rule.DataPermissionRuleFactoryImpl;
+import cn.tr.plugin.biz.data.permission.config.rule.dept.DeptDataPermissionRule;
+import cn.tr.plugin.biz.data.permission.context.DataPermissionContextHolder;
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * @ClassName : DataPermissionRuleFactoryImplTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+
+public class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    DataPermissionRuleFactoryImpl ruleFactory;
+
+    @BeforeEach
+    public void init(){
+
+    }
+
+    /**
+     * 无数据权限
+     */
+    @Test
+    public void testNone() throws Throwable {
+        addDataPermissionAnnoation(TestNone.class);
+        ReflectUtil.setFieldValue(ruleFactory,"rules",null);
+        Assertions.assertEquals( ruleFactory.getDataPermissionRule("t_user"), Collections.emptyList());
+    }
+
+    /**
+     * 注解未配置,则默认开启
+     */
+    @Test
+    public void testDefault(){
+        addDataPermissionAnnoation(TestNone.class);
+        DeptDataPermissionRule rule = new DeptDataPermissionRule();
+        ReflectUtil.setFieldValue(ruleFactory,"rules", Arrays.asList(rule));
+        Assertions.assertEquals(CollectionUtil.size(ruleFactory.getDataPermissionRule("t_user")),1);
+    }
+
+    /**
+     * 已配置,但禁用
+     */
+    @Test
+    public void testClose(){
+        addDataPermissionAnnoation(TestClose.class);
+        DeptDataPermissionRule rule = new DeptDataPermissionRule();
+        ReflectUtil.setFieldValue(ruleFactory,"rules", Arrays.asList(rule));
+        Assertions.assertEquals( ruleFactory.getDataPermissionRule("t_user"), Collections.emptyList());
+    }
+
+    /**
+     * 已配置,但禁用
+     */
+    @Test
+    public void testInclude(){
+        addDataPermissionAnnoation(TestInclude.class);
+        DeptDataPermissionRule rule = new DeptDataPermissionRule();
+        ReflectUtil.setFieldValue(ruleFactory,"rules", Arrays.asList(rule, Mockito.mock(DataPermissionRule.class)));
+        Assertions.assertEquals( ruleFactory.getDataPermissionRule("t_user"), Collections.singletonList(rule));
+    }
+
+    /**
+     *  已配置,只排除部分规则
+     */
+    @Test
+    public void testexclude(){
+        addDataPermissionAnnoation(TestExclude.class);
+        DeptDataPermissionRule rule = new DeptDataPermissionRule();
+        DataPermissionRule mock = Mockito.mock(DataPermissionRule.class);
+        ReflectUtil.setFieldValue(ruleFactory,"rules", Arrays.asList(rule, mock));
+        Assertions.assertEquals( ruleFactory.getDataPermissionRule("t_user"), Collections.singletonList(mock));
+    }
+
+    private void addDataPermissionAnnoation(Class<?> aClass){
+        DataPermissionContextHolder.add(aClass.getAnnotation(DataPermission.class));
+    }
+
+    static class TestNone{
+    }
+
+    @DataPermission(enable = false)
+    static class  TestClose{
+    }
+
+    @DataPermission(includeRules = DeptDataPermissionRule.class)
+    static class  TestInclude{
+    }
+
+    @DataPermission(excludeRules = DeptDataPermissionRule.class)
+    static class  TestExclude{
+    }
+}

+ 59 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/pom.xml

@@ -0,0 +1,59 @@
+<?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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-spring-boot-starter-plugin-biz-excel</artifactId>
+
+
+    <description>excel的导出</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-test</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-file</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-dict</artifactId>
+        </dependency>
+
+        <!-- Apache Lang3 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <!-- excel工具 -->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>4.1.2</version>
+        </dependency>
+
+
+    </dependencies>
+</project>

+ 19 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/TrExcelAutoConfiguration.java

@@ -0,0 +1,19 @@
+package cn.tr.plugin.excel;
+
+import cn.tr.plugin.dict.config.cache.DictManager;
+import cn.tr.plugin.excel.config.ExcelHelperFactory;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * @ClassName : TrExcelAutoConfiguration
+ * @Description : 自动注册excel导出配置
+ * @Author : LF
+ * @Date: 2023年03月13日
+ */
+public class TrExcelAutoConfiguration {
+
+    @Bean
+    public ExcelHelperFactory excelHelperFactory(DictManager dictManager){
+        return new ExcelHelperFactory(dictManager);
+    }
+}

+ 180 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/annotation/Excel.java

@@ -0,0 +1,180 @@
+package cn.tr.plugin.excel.annotation;
+
+import cn.hutool.core.annotation.Alias;
+import cn.hutool.core.date.DatePattern;
+import cn.tr.plugin.excel.config.ExcelHandlerAdapter;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.math.BigDecimal;
+
+/**
+ * 自定义导出Excel数据注解
+ * 
+ * @author ruoyi
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Excel
+{
+    /**
+     * 导出时在excel中排序
+     */
+    public int sort() default Integer.MAX_VALUE;
+
+    /**
+     * 导出到Excel中的名字.
+     */
+    public String name() default "";
+
+    /**
+     * 日期格式, 如: yyyy-MM-dd
+     */
+    public String dateFormat() default "";
+
+    /**
+     * 字典码
+     */
+    public String dictCode() default "";
+
+    /**
+     * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
+     */
+    public int scale() default -1;
+
+    /**
+     * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
+     */
+    public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
+
+    /**
+     * 导出时在excel中每个列的高度 单位为字符
+     */
+    public double height() default 14;
+
+    /**
+     * 导出时在excel中每个列的宽 单位为字符
+     */
+    public double width() default 16;
+
+    /**
+     * 文字后缀,如% 90 变成90%
+     */
+    public String suffix() default "";
+
+    /**
+     * 当值为空时,字段的默认值
+     */
+    public String defaultValue() default "";
+
+    /**
+     * 提示信息
+     */
+    public String prompt() default "";
+
+    /**
+     * 设置只能选择不能输入的列内容.默认 与{@link Excel#dictCode()} 保持一致
+     */
+    public String[] combo() default {};
+
+    /**
+     * 是否需要纵向合并单元格,应对需求:含有list集合单元格)
+     */
+    public boolean needMerge() default false;
+
+    /**
+     * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写.
+     */
+    public boolean isExport() default true;
+
+    /**
+     * 另一个类中的属性名称,支持多级获取,以小数点隔开
+     */
+    public String targetAttr() default "";
+
+    /**
+     * 是否自动统计数据,在最后追加一行统计数据总和
+     */
+    public boolean isStatistics() default false;
+
+    /**
+     * 导出类型(0数字 1字符串)
+     */
+    public ColumnType cellType() default ColumnType.STRING;
+
+    /**
+     * 导出列头背景色
+     */
+    public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT;
+
+    /**
+     * 导出列头字体颜色
+     */
+    public IndexedColors headerColor() default IndexedColors.WHITE;
+
+    /**
+     * 导出单元格背景色
+     */
+    public IndexedColors backgroundColor() default IndexedColors.WHITE;
+
+    /**
+     * 导出单元格字体颜色
+     */
+    public IndexedColors color() default IndexedColors.BLACK;
+
+    /**
+     * 导出字段对齐方式
+     */
+    public HorizontalAlignment align() default HorizontalAlignment.CENTER;
+
+    /**
+     * 自定义数据处理器
+     */
+    public Class<?> handler() default ExcelHandlerAdapter.class;
+
+    /**
+     * 自定义数据处理器参数
+     */
+    public String[] args() default {};
+
+    /**
+     * 字段类型(0:导出导入;1:仅导出;2:仅导入)
+     */
+    Type type() default Type.ALL;
+
+    public enum Type
+    {
+        ALL(0), EXPORT(1), IMPORT(2);
+        private final int value;
+
+        Type(int value)
+        {
+            this.value = value;
+        }
+
+        public int value()
+        {
+            return this.value;
+        }
+    }
+
+    public enum ColumnType
+    {
+        NUMERIC(0), STRING(1), IMAGE(2),HYPERLINKS(3);
+        private final int value;
+
+        ColumnType(int value)
+        {
+            this.value = value;
+        }
+
+        public int value()
+        {
+            return this.value;
+        }
+    }
+}

+ 18 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/annotation/Excels.java

@@ -0,0 +1,18 @@
+package cn.tr.plugin.excel.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Excel注解集
+ * 
+ * @author ruoyi
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Excels
+{
+    Excel[] value();
+}

+ 19 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelHandlerAdapter.java

@@ -0,0 +1,19 @@
+package cn.tr.plugin.excel.config;
+
+/**
+ * Excel数据格式处理适配器
+ * 
+ * @author ruoyi
+ */
+public interface ExcelHandlerAdapter
+{
+    /**
+     * 格式化
+     * 
+     * @param value 单元格数据值
+     * @param args excel注解args参数组
+     *
+     * @return 处理后的值
+     */
+    Object format(Object value, String[] args);
+}

+ 1441 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelHelper.java

@@ -0,0 +1,1441 @@
+package cn.tr.plugin.excel.config;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.excel.annotation.Excel;
+import cn.tr.plugin.excel.annotation.Excel.*;
+import cn.tr.plugin.excel.annotation.Excels;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import cn.hutool.core.date.DateUtil;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Excel相关处理
+ *
+ * @author ruoyi
+ */
+public class ExcelHelper<T> {
+    private static ThreadPoolExecutor threadPoolExecutor;
+    static {
+        int processors = Runtime.getRuntime().availableProcessors();
+        threadPoolExecutor=new ThreadPoolExecutor(
+                processors*2,
+                processors*2,
+                0,
+                TimeUnit.HOURS,
+                new LinkedBlockingDeque<>(Integer.MAX_VALUE),
+                new ExcelThreadFactory()
+        );
+    }
+    private static final Logger log = LoggerFactory.getLogger(ExcelHelper.class);
+
+    private static final String FORMULA_REGEX_STR = "=|-|\\+|@";
+
+    private static final String[] FORMULA_STR = { "=", "-", "+", "@" };
+
+    private static final String[] parsePatterns = {
+            "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+            "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+            "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+    /**
+     * 根据dictCode和dictValue获取dictLabel
+     */
+    private final BiFunction<String,String,String> dictCacheOperator;
+
+    /**
+     * 根据dictCode获取所有 {@link cn.tr.plugin.dict.bo.DictBO}
+     */
+    private final Function<String,List<DictBO>> dictCacheAllOperator;
+    /**
+     * Excel sheet最大行数,默认65536
+     */
+    private static final int sheetSize = 200000;
+
+    private int sheetNo;
+    /**
+     * 工作表名称
+     */
+    private String sheetName;
+
+    /**
+     * 导出类型(EXPORT:导出数据;IMPORT:导入模板)
+     */
+    private Type type;
+
+    /**
+     * 工作薄对象
+     */
+    private Workbook wb;
+
+    /**
+     * 工作表对象
+     */
+    private List<Sheet> sheets=new ArrayList<>();
+
+    /**
+     * 与工作表相对应的数据集合
+     */
+    private Map<Sheet, List<T>> sheetDataMap=new HashMap<>();
+
+    /**
+     * 工作表对象对应的当前行号
+     */
+    private Map<Sheet,AtomicInteger> rowNumMap=new HashMap<>();
+
+    /**
+     * 样式列表
+     */
+    private Map<String, CellStyle> styles;
+
+
+    /**
+     * 注解列表
+     */
+    private List<Object[]> fields;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 最大高度
+     */
+    private short maxHeight;
+
+    /**
+     * 工作表合并后最后行数
+     */
+    private Map<Sheet, AtomicInteger> subMergedLastRowNumMap=new HashMap<>();
+
+    /**
+     * 工作表合并后开始行数
+     */
+    private Map<Sheet,AtomicInteger> subMergedFirstRowNumMap=new HashMap<>();
+
+    /**
+     * 对象的子列表方法
+     */
+    private Method subMethod;
+
+    /**
+     * 对象的子列表属性
+     */
+    private List<Field> subFields;
+
+    /**
+     * 统计列表
+     */
+    private Map<Integer, Double> statistics = new HashMap<Integer, Double>();
+
+    /**
+     * 数字格式
+     */
+    private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");
+
+    /**
+     * 实体对象
+     */
+    private Class<T> clazz;
+
+    /**
+     * 需要排除列属性
+     */
+    private String[] excludeFields;
+
+    protected ExcelHelper(Class<T> clazz,BiFunction<String,String,String> dictCacheOperator,Function<String,List<DictBO>> dictCacheAllOperator) {
+        this.clazz = clazz;
+        this.dictCacheAllOperator=dictCacheAllOperator;
+        this.dictCacheOperator=dictCacheOperator;
+    }
+
+    /**
+     * 隐藏Excel中列属性
+     *
+     * @param fields 列属性名 示例[单个"name"/多个"id","name"]
+     */
+    public void hideColumn(String... fields)
+    {
+        this.excludeFields = fields;
+    }
+
+    private void init(List<T> list, String sheetName, String title, Type type) {
+        if (list == null)
+        {
+            list = new ArrayList<T>();
+        }
+        List<List<T>> sheetDataList = CollectionUtil.split(list, sheetSize);
+        sheetNo =CollectionUtil.size(sheetDataList);
+        this.sheetName = sheetName;
+        this.type = type;
+        this.title = title;
+        createExcelField();
+        createWorkbook();
+        if(CollectionUtil.isNotEmpty(sheets)){
+            for (Sheet sheet : sheets) {
+                createTitle(sheet);
+                createSubHead(sheet);
+            }
+        }
+        for (int i = 0; i < sheetDataList.size(); i++) {
+            sheetDataMap.put(CollectionUtil.get(sheets,i),CollectionUtil.get(sheetDataList,i));
+        }
+    }
+
+    /**
+     * 创建excel第一行标题
+     */
+    private void createTitle(Sheet sheet) {
+        if (StringUtils.isNotEmpty(title)) {
+            incrementSheetIntValue(sheet,subMergedFirstRowNumMap);
+            incrementSheetIntValue(sheet,subMergedLastRowNumMap);
+            int titleLastCol = this.fields.size() - 1;
+            if (isSubList()) {
+                titleLastCol = titleLastCol + subFields.size() - 1;
+            }
+            Row titleRow = sheet.createRow(getAndIncrementSheetValue(sheet,rowNumMap));
+            titleRow.setHeightInPoints(30);
+            Cell titleCell = titleRow.createCell(0);
+            titleCell.setCellStyle(styles.get("title"));
+            titleCell.setCellValue(title);
+            sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol));
+        }
+    }
+
+    /**
+     * 创建对象的子列表名称
+     */
+    public void createSubHead(Sheet sheet)
+    {
+        if (isSubList())
+        {
+            incrementSheetIntValue(sheet,subMergedFirstRowNumMap);
+            incrementSheetIntValue(sheet,subMergedLastRowNumMap);
+            int rownum = getAndIncrementSheetValue(sheet,rowNumMap);
+            Row subRow = sheet.createRow(rownum);
+            int excelNum = 0;
+            for (Object[] objects : fields)
+            {
+                Excel attr = (Excel) objects[1];
+                Cell headCell1 = subRow.createCell(excelNum);
+                headCell1.setCellValue(attr.name());
+                headCell1.setCellStyle(styles.get(StrUtil.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+                excelNum++;
+            }
+            int headFirstRow = excelNum - 1;
+            int headLastRow = headFirstRow + subFields.size() - 1;
+            if (headLastRow > headFirstRow)
+            {
+                sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow));
+            }
+        }
+    }
+
+    /**
+     * 对excel表单默认第一个索引名转换成list
+     *
+     * @param is 输入流
+     * @return 转换后集合
+     */
+    public List<T> importExcel(InputStream is) throws Exception
+    {
+        return importExcel(is, 0);
+    }
+
+    /**
+     * 对excel表单默认第一个索引名转换成list
+     *
+     * @param is 输入流
+     * @param titleNum 标题占用行数
+     * @return 转换后集合
+     */
+    public List<T> importExcel(InputStream is, int titleNum) throws Exception
+    {
+        return importExcel(StringUtils.EMPTY, is, titleNum);
+    }
+
+    /**
+     * 对excel表单指定表格索引名转换成list
+     *
+     * @param sheetName 表格索引名
+     * @param titleNum 标题占用行数
+     * @param is 输入流
+     * @return 转换后集合
+     */
+    public List<T> importExcel(String sheetName, InputStream is, int titleNum) throws Exception
+    {
+        this.type = Type.IMPORT;
+        this.wb = WorkbookFactory.create(is);
+        List<T> list = new ArrayList<T>();
+        // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
+        Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
+        if (sheet == null)
+        {
+            throw new IOException("文件sheet不存在");
+        }
+
+        // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
+        int rows = sheet.getLastRowNum();
+
+        if (rows > 0)
+        {
+            // 定义一个map用于存放excel列的序号和field.
+            Map<String, Integer> cellMap = new HashMap<String, Integer>();
+            // 获取表头
+            Row heard = sheet.getRow(titleNum);
+            for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
+            {
+                Cell cell = heard.getCell(i);
+                if (ObjectUtil.isNotNull(cell))
+                {
+                    String value = this.getCellValue(heard, i).toString();
+                    cellMap.put(value, i);
+                }
+                else
+                {
+                    cellMap.put(null, i);
+                }
+            }
+            // 有数据时才处理 得到类的所有field.
+            List<Object[]> fields = this.getFields();
+            Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
+            for (Object[] objects : fields)
+            {
+                Excel attr = (Excel) objects[1];
+                Integer column = cellMap.get(attr.name());
+                if (column != null)
+                {
+                    fieldsMap.put(column, objects);
+                }
+            }
+            for (int i = titleNum + 1; i <= rows; i++)
+            {
+                // 从第2行开始取数据,默认第一行是表头.
+                Row row = sheet.getRow(i);
+                // 判断当前行是否是空行
+                if (isRowEmpty(row))
+                {
+                    continue;
+                }
+                T entity = null;
+                for (Map.Entry<Integer, Object[]> entry : fieldsMap.entrySet())
+                {
+                    Object val = this.getCellValue(row, entry.getKey());
+
+                    // 如果不存在实例则新建.
+                    entity = (entity == null ? clazz.newInstance() : entity);
+                    // 从map中得到对应列的field.
+                    Field field = (Field) entry.getValue()[0];
+                    Excel attr = (Excel) entry.getValue()[1];
+                    // 取得类型,并根据对象类型设置值.
+                    Class<?> fieldType = field.getType();
+                    if (String.class == fieldType)
+                    {
+                        String s = Convert.toStr(val);
+                        if (StringUtils.endsWith(s, ".0"))
+                        {
+                            val = StringUtils.substringBefore(s, ".0");
+                        }
+                        else
+                        {
+                            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
+                            if (StringUtils.isNotEmpty(dateFormat))
+                            {
+                                val = parseDateToStr(dateFormat, val);
+                            }
+                            else
+                            {
+                                val = Convert.toStr(val);
+                            }
+                        }
+                    }
+                    else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
+                    {
+                        val = Convert.toInt(val);
+                    }
+                    else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
+                    {
+                        val = Convert.toLong(val);
+                    }
+                    else if (Double.TYPE == fieldType || Double.class == fieldType)
+                    {
+                        val = Convert.toDouble(val);
+                    }
+                    else if (Float.TYPE == fieldType || Float.class == fieldType)
+                    {
+                        val = Convert.toFloat(val);
+                    }
+                    else if (BigDecimal.class == fieldType)
+                    {
+                        val = Convert.toBigDecimal(val);
+                    }
+                    else if (Date.class == fieldType)
+                    {
+                        if (val instanceof String)
+                        {
+                            val = DateUtils.parseDate(Convert.toStr(val),parsePatterns);
+                        }
+                        else if (val instanceof Double)
+                        {
+                            val = org.apache.poi.ss.usermodel.DateUtil.getJavaDate((Double) val);
+                        }
+                    }
+                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
+                    {
+                        val = Convert.toBool(val, false);
+                    }
+                    if (ObjectUtil.isNotNull(fieldType))
+                    {
+                        String propertyName = field.getName();
+                        if (StringUtils.isNotEmpty(attr.targetAttr()))
+                        {
+                            propertyName = field.getName() + "." + attr.targetAttr();
+                        }
+                        else if (StrUtil.isNotEmpty(attr.dictCode()))
+                        {
+                            val=dictCacheOperator.apply(attr.dictCode(),Convert.toStr(val));
+                        }
+                        else if (!attr.handler().equals(ExcelHandlerAdapter.class))
+                        {
+                            val = dataFormatHandlerAdapter(val, attr);
+                        }
+                        invokeSetter(entity, propertyName, val);
+                    }
+                }
+                list.add(entity);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param response 返回数据
+     * @param list 导出数据集合
+     * @param sheetName 工作表的名称
+     * @param fileName 文件名称
+     * @return 结果
+     */
+    public void exportExcel(HttpServletResponse response, List<T> list, String sheetName,String fileName) throws IOException {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        exportExcel(response.getOutputStream(),fileName, list, sheetName, StringUtils.EMPTY);
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param outputStream 数据输出流
+     * @param list 导出数据集合
+     * @param sheetName 工作表的名称
+     * @param title 标题
+     * @return 结果
+     */
+    public void exportExcel(OutputStream outputStream ,String fileName,List<T> list, String sheetName, String title) throws IOException {
+        this.init(list, sheetName, title, Type.EXPORT);
+        threadPoolExecutor.submit(()->{
+            sheets.parallelStream().forEach(sheet -> { exportExcel(sheet,sheetDataMap.get(sheet)); });
+            try {
+                wb.write(outputStream);
+                //表格写入成功回调 将相应的下载任务置为成功 todo
+            }catch (Exception e){
+                //表格写入失败通知 将相应的下载任务置为失败并重新进入下载队列 todo
+             }finally {
+                IOUtils.closeQuietly(wb);
+            }
+        });
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @return 结果
+     */
+    private void exportExcel(Sheet sheet,List<T> dataList) {
+        if(CollectionUtil.isEmpty(dataList)){
+            return;
+        }
+        try {
+            writeSheet(sheet,dataList);
+        }
+        catch (Exception e) {
+            e.printStackTrace();
+            log.error("导出Excel异常{}", e.getMessage());
+        }
+    }
+
+    /**
+     * 创建写入数据到Sheet
+     */
+    public void writeSheet(Sheet sheet,List<T> dataList) {
+        // 取出一共有多少个sheet.
+        // 产生一行
+        Row row = sheet.createRow(getAndIncrementSheetValue(sheet,rowNumMap));
+        int column = 0;
+        // 写入各个字段的列头名称
+        for (Object[] os : fields)
+        {
+            Field field = (Field) os[0];
+            Excel excel = (Excel) os[1];
+            if (Collection.class.isAssignableFrom(field.getType()))
+            {
+                for (Field subField : subFields)
+                {
+                    Excel subExcel = subField.getAnnotation(Excel.class);
+                    this.createHeadCell(sheet,subExcel, row, column++);
+                }
+            }
+            else
+            {
+                this.createHeadCell(sheet,excel, row, column++);
+            }
+        }
+        if (Type.EXPORT.equals(type))
+        {
+            fillExcelData(sheet,sheets.indexOf(sheet), row,dataList);
+            addStatisticsRow(sheet);
+        }
+    }
+
+    /**
+     * 向相应的工作表中填充excel数据
+     *
+     * @param index 序号
+     * @param row 单元格行
+     */
+    @SuppressWarnings("unchecked")
+    public void fillExcelData(Sheet sheet,int index, Row row,List<T> dataList)
+    {
+        int subMergedLastRowNum = getSheetIntValue(sheet,subMergedLastRowNumMap);
+        int subMergedFirstRowNum = getSheetIntValue(sheet,subMergedFirstRowNumMap);
+        AtomicInteger rowNo = getSheetValue(sheet,rowNumMap);
+        for (int i = 0; i <  CollectionUtil.size(dataList); i++)
+        {
+            if(i>1){
+                rowNo.incrementAndGet();
+            }else {
+                rowNo.addAndGet(i);
+            }
+            row = sheet.createRow(rowNo.get());
+            // 得到导出对象.
+            T vo = (T) dataList.get(i);
+            Collection<?> subList = null;
+            if (isSubList()) {
+                if (isSubListValue(vo))
+                {
+                    subList = getListCellValue(vo);
+                    subMergedLastRowNum = subMergedLastRowNum + subList.size();
+                }
+                else
+                {
+                    subMergedFirstRowNum++;
+                    subMergedLastRowNum++;
+                }
+            }
+            int column = 0;
+            for (Object[] os : fields) {
+                Field field = (Field) os[0];
+                Excel excel = (Excel) os[1];
+                if (Collection.class.isAssignableFrom(field.getType()) && CollectionUtil.isNotEmpty(subList))
+                {
+                    boolean subFirst = false;
+                    for (Object obj : subList) {
+                        if (subFirst)
+                        {
+                            rowNo.incrementAndGet();
+                            row = sheet.createRow(rowNo.intValue());
+                        }
+                        List<Field> subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class);
+                        int subIndex = 0;
+                        for (Field subField : subFields)
+                        {
+                            if (subField.isAnnotationPresent(Excel.class))
+                            {
+                                subField.setAccessible(true);
+                                Excel attr = subField.getAnnotation(Excel.class);
+                                this.addCell(sheet,attr, row, (T) obj, subField, column + subIndex);
+                            }
+                            subIndex++;
+                        }
+                        subFirst = true;
+                    }
+                    subMergedFirstRowNum = subMergedFirstRowNum + subList.size();
+                }
+                else
+                {
+                    this.addCell(sheet,excel, row, vo, field, column++);
+                }
+            }
+        }
+    }
+
+    /**
+     * 创建表格样式
+     *
+     * @param wb 工作薄对象
+     * @return 样式列表
+     */
+    private Map<String, CellStyle> createStyles(Workbook wb)
+    {
+        // 写入各条记录,每条记录对应excel表中的一行
+        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
+        CellStyle style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        Font titleFont = wb.createFont();
+        titleFont.setFontName("Arial");
+        titleFont.setFontHeightInPoints((short) 16);
+        titleFont.setBold(true);
+        style.setFont(titleFont);
+        styles.put("title", style);
+
+        style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderRight(BorderStyle.THIN);
+        style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderLeft(BorderStyle.THIN);
+        style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderTop(BorderStyle.THIN);
+        style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        Font dataFont = wb.createFont();
+        dataFont.setFontName("Arial");
+        dataFont.setFontHeightInPoints((short) 10);
+        style.setFont(dataFont);
+        styles.put("data", style);
+
+        style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        Font totalFont = wb.createFont();
+        totalFont.setFontName("Arial");
+        totalFont.setFontHeightInPoints((short) 10);
+        style.setFont(totalFont);
+        styles.put("total", style);
+
+        styles.putAll(annotationHeaderStyles(wb, styles));
+
+        styles.putAll(annotationDataStyles(wb));
+
+        return styles;
+    }
+
+    /**
+     * 根据Excel注解创建表格头样式
+     *
+     * @param wb 工作薄对象
+     * @return 自定义样式列表
+     */
+    private Map<String, CellStyle> annotationHeaderStyles(Workbook wb, Map<String, CellStyle> styles)
+    {
+        Map<String, CellStyle> headerStyles = new HashMap<String, CellStyle>();
+        for (Object[] os : fields)
+        {
+            Excel excel = (Excel) os[1];
+            String key = StrUtil.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor());
+            if (!headerStyles.containsKey(key))
+            {
+                CellStyle style = wb.createCellStyle();
+                style.cloneStyleFrom(styles.get("data"));
+                style.setAlignment(HorizontalAlignment.CENTER);
+                style.setVerticalAlignment(VerticalAlignment.CENTER);
+                style.setFillForegroundColor(excel.headerBackgroundColor().index);
+                style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+                Font headerFont = wb.createFont();
+                headerFont.setFontName("Arial");
+                headerFont.setFontHeightInPoints((short) 10);
+                headerFont.setBold(true);
+                headerFont.setColor(excel.headerColor().index);
+                style.setFont(headerFont);
+                headerStyles.put(key, style);
+            }
+        }
+        return headerStyles;
+    }
+
+    /**
+     * 根据Excel注解创建表格列样式
+     *
+     * @param wb 工作薄对象
+     * @return 自定义样式列表
+     */
+    private Map<String, CellStyle> annotationDataStyles(Workbook wb)
+    {
+        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
+        for (Object[] os : fields)
+        {
+            Excel excel = (Excel) os[1];
+            String key = StrUtil.format("data_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor());
+            if (!styles.containsKey(key))
+            {
+                CellStyle style = wb.createCellStyle();
+                style.setAlignment(excel.align());
+                style.setVerticalAlignment(VerticalAlignment.CENTER);
+                style.setBorderRight(BorderStyle.THIN);
+                style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+                style.setBorderLeft(BorderStyle.THIN);
+                style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+                style.setBorderTop(BorderStyle.THIN);
+                style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+                style.setBorderBottom(BorderStyle.THIN);
+                style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+                style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+                style.setFillForegroundColor(excel.backgroundColor().getIndex());
+                Font dataFont = wb.createFont();
+                dataFont.setFontName("Arial");
+                dataFont.setFontHeightInPoints((short) 10);
+                dataFont.setColor(excel.color().index);
+                style.setFont(dataFont);
+                styles.put(key, style);
+            }
+        }
+        return styles;
+    }
+
+    /**
+     * 创建单元格
+     */
+    public Cell createHeadCell(Sheet sheet,Excel attr, Row row, int column)
+    {
+        // 创建列
+        Cell cell = row.createCell(column);
+        // 写入列信息
+        cell.setCellValue(attr.name());
+        setDataValidation(sheet,attr, row, column);
+        cell.setCellStyle(styles.get(StrUtil.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+        if (isSubList())
+        {
+            // 填充默认样式,防止合并单元格样式失效
+            sheet.setDefaultColumnStyle(column, styles.get(StrUtil.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor())));
+            if (attr.needMerge())
+            {
+                int rownum = getSheetIntValue(sheet, rowNumMap);
+                sheet.addMergedRegion(new CellRangeAddress( rownum- 1, rownum, column, column));
+            }
+        }
+        return cell;
+    }
+
+    /**
+     * 设置单元格信息
+     *
+     * @param value 单元格值
+     * @param attr 注解相关
+     * @param cell 单元格信息
+     */
+    public void setCellVo(Object value, Excel attr, Cell cell)
+    {
+        if (ColumnType.STRING == attr.cellType())
+        {
+            String cellValue = Convert.toStr(value);
+            // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。
+            if (StringUtils.startsWithAny(cellValue, FORMULA_STR))
+            {
+                cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0");
+            }
+            cell.setCellValue(ObjectUtil.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix());
+        }
+        else if (ColumnType.NUMERIC == attr.cellType())
+        {
+            if (ObjectUtil.isNotNull(value))
+            {
+                cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value));
+            }
+        }
+        else if (ColumnType.IMAGE == attr.cellType())
+        {
+            ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1);
+            String imagePath = Convert.toStr(value);
+            if (StringUtils.isNotEmpty(imagePath))
+            {
+                //todo 获取文件
+//                byte[] data = ImageUtils.getImage(imagePath);
+//                getDrawingPatriarch(cell.getSheet()).createPicture(anchor,
+//                        cell.getSheet().getWorkbook().addPicture(data, getImageType(data)));
+            }
+        }
+    }
+
+    /**
+     * 获取画布
+     */
+    public static Drawing<?> getDrawingPatriarch(Sheet sheet)
+    {
+        if (sheet.getDrawingPatriarch() == null)
+        {
+            sheet.createDrawingPatriarch();
+        }
+        return sheet.getDrawingPatriarch();
+    }
+
+    /**
+     * todo
+     * 获取图片类型,设置图片插入类型
+     */
+    public int getImageType(byte[] value)
+    {
+//        String type = FileTypeUtils.getFileExtendName(value);
+//        if ("JPG".equalsIgnoreCase(type))
+//        {
+//            return Workbook.PICTURE_TYPE_JPEG;
+//        }
+//        else if ("PNG".equalsIgnoreCase(type))
+//        {
+//            return Workbook.PICTURE_TYPE_PNG;
+//        }
+        return Workbook.PICTURE_TYPE_JPEG;
+    }
+
+    /**
+     * 创建表格样式
+     */
+    public void setDataValidation(Sheet sheet,Excel attr, Row row, int column)
+    {
+        if (attr.name().indexOf("注:") >= 0)
+        {
+            sheet.setColumnWidth(column, 6000);
+        }
+        else
+        {
+            // 设置列宽
+            sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256));
+        }
+        if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0)
+        {
+            // 提示信息或只能选择不能输入的列内容.
+            setPromptOrValidation(sheet, attr.combo(), attr.prompt(), 1, 100, column, column);
+        }else if(StrUtil.isNotEmpty(attr.dictCode())){
+            List<DictBO> dicts = dictCacheAllOperator.apply(attr.dictCode());
+            if(CollectionUtil.isNotEmpty(dicts)){
+                setPromptOrValidation(sheet,
+                        dicts.stream().map(DictBO::getLabel).collect(Collectors.toList()).toArray(new String[dicts.size()]),
+                        attr.prompt(), 1, 100, column, column);
+            }
+
+        }
+    }
+
+    /**
+     * 添加单元格
+     */
+    public Cell addCell(Sheet sheet,Excel attr, Row row, T vo, Field field, int column)
+    {
+        Cell cell = null;
+        try
+        {
+            // 设置行高
+            row.setHeight(maxHeight);
+            // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
+            if (attr.isExport())
+            {
+                // 创建cell
+                cell = row.createCell(column);
+                if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge())
+                {
+                    CellRangeAddress cellAddress = new CellRangeAddress(getSheetIntValue(sheet,subMergedFirstRowNumMap), getSheetIntValue(sheet,subMergedLastRowNumMap), column, column);
+                    sheet.addMergedRegion(cellAddress);
+                }
+                cell.setCellStyle(styles.get(StrUtil.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor())));
+
+                // 用于读取对象中的属性
+                Object value = getTargetValue(vo, field, attr);
+                String dateFormat = attr.dateFormat();
+                String dictCode = attr.dictCode();
+                if (StringUtils.isNotEmpty(dateFormat) && ObjectUtil.isNotNull(value))
+                {
+                    cell.setCellValue(parseDateToStr(dateFormat, value));
+                }
+                else if (StringUtils.isNotEmpty(dictCode) && ObjectUtil.isNotNull(value))
+                {
+                    cell.setCellValue(dictCacheOperator.apply(attr.dictCode(),Convert.toStr(value)));
+                }
+                else if (value instanceof BigDecimal && -1 != attr.scale())
+                {
+                    cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue());
+                }
+                else if (!attr.handler().equals(ExcelHandlerAdapter.class))
+                {
+                    cell.setCellValue(dataFormatHandlerAdapter(value, attr));
+                }
+                else
+                {
+                    // 设置列类型
+                    setCellVo(value, attr, cell);
+                }
+                addStatisticsData(column, Convert.toStr(value), attr);
+            }
+        }
+        catch (Exception e)
+        {
+            log.error("导出Excel失败{}", e);
+        }
+        return cell;
+    }
+
+    /**
+     * 设置 POI XSSFSheet 单元格提示或选择框
+     *
+     * @param sheet 表单
+     * @param textlist 下拉框显示的内容
+     * @param promptContent 提示内容
+     * @param firstRow 开始行
+     * @param endRow 结束行
+     * @param firstCol 开始列
+     * @param endCol 结束列
+     */
+    public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow,
+                                      int firstCol, int endCol)
+    {
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1");
+        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+        DataValidation dataValidation = helper.createValidation(constraint, regions);
+        if (StringUtils.isNotEmpty(promptContent))
+        {
+            // 如果设置了提示信息则鼠标放上去提示
+            dataValidation.createPromptBox("", promptContent);
+            dataValidation.setShowPromptBox(true);
+        }
+        // 处理Excel兼容性问题
+        if (dataValidation instanceof XSSFDataValidation)
+        {
+            dataValidation.setSuppressDropDownArrow(true);
+            dataValidation.setShowErrorBox(true);
+        }
+        else
+        {
+            dataValidation.setSuppressDropDownArrow(false);
+        }
+        sheet.addValidationData(dataValidation);
+    }
+
+    /**
+     * 反向解析值 男=0,女=1,未知=2
+     *
+     * @param propertyValue 参数值
+     * @param converterExp 翻译注解
+     * @param separator 分隔符
+     * @return 解析后值
+     */
+    public static String reverseByExp(String propertyValue, String converterExp, String separator)
+    {
+        StringBuilder propertyString = new StringBuilder();
+        String[] convertSource = converterExp.split(",");
+        for (String item : convertSource)
+        {
+            String[] itemArray = item.split("=");
+            if (StringUtils.containsAny(propertyValue, separator))
+            {
+                for (String value : propertyValue.split(separator))
+                {
+                    if (itemArray[1].equals(value))
+                    {
+                        propertyString.append(itemArray[0] + separator);
+                        break;
+                    }
+                }
+            }
+            else
+            {
+                if (itemArray[1].equals(propertyValue))
+                {
+                    return itemArray[0];
+                }
+            }
+        }
+        return StringUtils.stripEnd(propertyString.toString(), separator);
+    }
+
+    /**
+     * 数据处理器
+     *
+     * @param value 数据值
+     * @param excel 数据注解
+     * @return
+     */
+    public String dataFormatHandlerAdapter(Object value, Excel excel)
+    {
+        try
+        {
+            Object instance = excel.handler().newInstance();
+            Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class });
+            value = formatMethod.invoke(instance, value, excel.args());
+        }
+        catch (Exception e)
+        {
+            log.error("不能格式化数据 " + excel.handler(), e.getMessage());
+        }
+        return Convert.toStr(value);
+    }
+
+    /**
+     * 合计统计信息
+     */
+    private void addStatisticsData(Integer index, String text, Excel entity)
+    {
+        if (entity != null && entity.isStatistics())
+        {
+            Double temp = 0D;
+            if (!statistics.containsKey(index))
+            {
+                statistics.put(index, temp);
+            }
+            try
+            {
+                temp = Double.valueOf(text);
+            }
+            catch (NumberFormatException e)
+            {
+            }
+            statistics.put(index, statistics.get(index) + temp);
+        }
+    }
+
+    /**
+     * 创建统计行
+     */
+    public void addStatisticsRow(Sheet sheet)
+    {
+        if (statistics.size() > 0)
+        {
+            Row row = sheet.createRow(sheet.getLastRowNum() + 1);
+            Set<Integer> keys = statistics.keySet();
+            Cell cell = row.createCell(0);
+            cell.setCellStyle(styles.get("total"));
+            cell.setCellValue("合计");
+
+            for (Integer key : keys)
+            {
+                cell = row.createCell(key);
+                cell.setCellStyle(styles.get("total"));
+                cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key)));
+            }
+            statistics.clear();
+        }
+    }
+
+    /**
+     * 获取bean中的属性值
+     *
+     * @param vo 实体对象
+     * @param field 字段
+     * @param excel 注解
+     * @return 最终的属性值
+     * @throws Exception
+     */
+    private Object getTargetValue(T vo, Field field, Excel excel) throws Exception
+    {
+        Object o = field.get(vo);
+        if (StringUtils.isNotEmpty(excel.targetAttr()))
+        {
+            String target = excel.targetAttr();
+            if (target.contains("."))
+            {
+                String[] targets = target.split("[.]");
+                for (String name : targets)
+                {
+                    o = getValue(o, name);
+                }
+            }
+            else
+            {
+                o = getValue(o, target);
+            }
+        }
+        return o;
+    }
+
+    /**
+     * 以类的属性的get方法方法形式获取值
+     *
+     * @param o
+     * @param name
+     * @return value
+     * @throws Exception
+     */
+    private Object getValue(Object o, String name) throws Exception
+    {
+        if (ObjectUtil.isNotNull(o) && StringUtils.isNotEmpty(name))
+        {
+            Class<?> clazz = o.getClass();
+            Field field = clazz.getDeclaredField(name);
+            field.setAccessible(true);
+            o = field.get(o);
+        }
+        return o;
+    }
+
+    /**
+     * 得到所有定义字段
+     */
+    private void createExcelField()
+    {
+        this.fields = getFields();
+        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
+        this.maxHeight = getRowHeight();
+    }
+
+    /**
+     * 获取字段注解信息
+     */
+    public List<Object[]> getFields()
+    {
+        List<Object[]> fields = new ArrayList<Object[]>();
+        List<Field> tempFields = new ArrayList<>();
+        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
+        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+        for (Field field : tempFields)
+        {
+            if (!ArrayUtils.contains(this.excludeFields, field.getName()))
+            {
+                // 单注解
+                if (field.isAnnotationPresent(Excel.class))
+                {
+                    Excel attr = field.getAnnotation(Excel.class);
+                    if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
+                    {
+                        field.setAccessible(true);
+                        fields.add(new Object[] { field, attr });
+                    }
+                    if (Collection.class.isAssignableFrom(field.getType()))
+                    {
+                        subMethod = getSubMethod(field.getName(), clazz);
+                        ParameterizedType pt = (ParameterizedType) field.getGenericType();
+                        Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
+                        this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
+                    }
+                }
+
+                // 多注解
+                if (field.isAnnotationPresent(Excels.class))
+                {
+                    Excels attrs = field.getAnnotation(Excels.class);
+                    Excel[] excels = attrs.value();
+                    for (Excel attr : excels)
+                    {
+                        if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
+                        {
+                            field.setAccessible(true);
+                            fields.add(new Object[] { field, attr });
+                        }
+                    }
+                }
+            }
+        }
+        return fields;
+    }
+
+    /**
+     * 根据注解获取最大行高
+     */
+    public short getRowHeight()
+    {
+        double maxHeight = 0;
+        for (Object[] os : this.fields)
+        {
+            Excel excel = (Excel) os[1];
+            maxHeight = Math.max(maxHeight, excel.height());
+        }
+        return (short) (maxHeight * 20);
+    }
+
+    /**
+     * 创建一个工作簿
+     */
+    public void createWorkbook()
+    {
+        this.wb = new SXSSFWorkbook(1000);
+        this.styles = createStyles(wb);
+        createSheet(sheetNo);
+    }
+
+    /**
+     * 创建工作表
+     *
+     * @param sheetCount sheet数量
+     */
+    private void createSheet(int sheetCount) {
+        // 设置工作表的名称.
+        for (int i = 0; i < sheetCount; i++) {
+            sheets.add( wb.createSheet(sheetName + i));
+        }
+
+    }
+
+    /**
+     * 获取单元格值
+     *
+     * @param row 获取的行
+     * @param column 获取单元格列号
+     * @return 单元格值
+     */
+    private Object getCellValue(Row row, int column)
+    {
+        if (row == null)
+        {
+            return row;
+        }
+        Object val = "";
+        try
+        {
+            Cell cell = row.getCell(column);
+            if (ObjectUtil.isNotNull(cell))
+            {
+                if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA)
+                {
+                    val = cell.getNumericCellValue();
+                    if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell))
+                    {
+                        val = org.apache.poi.ss.usermodel.DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
+                    }
+                    else
+                    {
+                        if ((Double) val % 1 != 0)
+                        {
+                            val = new BigDecimal(val.toString());
+                        }
+                        else
+                        {
+                            val = new DecimalFormat("0").format(val);
+                        }
+                    }
+                }
+                else if (cell.getCellType() == CellType.STRING)
+                {
+                    val = cell.getStringCellValue();
+                }
+                else if (cell.getCellType() == CellType.BOOLEAN)
+                {
+                    val = cell.getBooleanCellValue();
+                }
+                else if (cell.getCellType() == CellType.ERROR)
+                {
+                    val = cell.getErrorCellValue();
+                }
+
+            }
+        }
+        catch (Exception e)
+        {
+            return val;
+        }
+        return val;
+    }
+
+    /**
+     * 判断是否是空行
+     *
+     * @param row 判断的行
+     * @return
+     */
+    private boolean isRowEmpty(Row row)
+    {
+        if (row == null)
+        {
+            return true;
+        }
+        for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++)
+        {
+            Cell cell = row.getCell(i);
+            if (cell != null && cell.getCellType() != CellType.BLANK)
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 格式化不同类型的日期对象
+     *
+     * @param dateFormat 日期格式
+     * @param val 被格式化的日期对象
+     * @return 格式化后的日期字符
+     */
+    public String parseDateToStr(String dateFormat, Object val)
+    {
+        if (val == null)
+        {
+            return "";
+        }
+        String str;
+        if (val instanceof Date)
+        {
+            str=DateUtil.format((Date) val,dateFormat);
+        }
+        else if (val instanceof LocalDateTime)
+        {
+            str=DateUtil.format((LocalDateTime) val,dateFormat);
+        }
+        else if (val instanceof LocalDate)
+        {
+            str= ((LocalDate) val).format(DateTimeFormatter.ofPattern(dateFormat));
+        }
+        else
+        {
+            str = val.toString();
+        }
+        return str;
+    }
+
+    /**
+     * 是否有对象的子列表
+     */
+    public boolean isSubList()
+    {
+        return ObjectUtil.isNotNull(subFields) && subFields.size() > 0;
+    }
+
+    /**
+     * 是否有对象的子列表,集合不为空
+     */
+    public boolean isSubListValue(T vo)
+    {
+        return ObjectUtil.isNotNull(subFields) && subFields.size() > 0 && ObjectUtil.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0;
+    }
+
+    /**
+     * 获取集合的值
+     */
+    public Collection<?> getListCellValue(Object obj)
+    {
+        Object value;
+        try
+        {
+            value = subMethod.invoke(obj, new Object[] {});
+        }
+        catch (Exception e)
+        {
+            return new ArrayList<Object>();
+        }
+        return (Collection<?>) value;
+    }
+
+    /**
+     * 获取对象的子列表方法
+     *
+     * @param name 名称
+     * @param pojoClass 类对象
+     * @return 子列表方法
+     */
+    public Method getSubMethod(String name, Class<?> pojoClass)
+    {
+        StringBuffer getMethodName = new StringBuffer("get");
+        getMethodName.append(name.substring(0, 1).toUpperCase());
+        getMethodName.append(name.substring(1));
+        Method method = null;
+        try
+        {
+            method = pojoClass.getMethod(getMethodName.toString(), new Class[] {});
+        }
+        catch (Exception e)
+        {
+            log.error("获取对象异常{}", e.getMessage());
+        }
+        return method;
+    }
+
+    /**
+     * 调用Setter方法, 仅匹配方法名。
+     * 支持多级,如:对象名.对象名.方法
+     */
+    private static <E> void invokeSetter(Object obj, String propertyName, E value)
+    {
+        Object object = obj;
+        String[] names = StringUtils.split(propertyName, ".");
+        for (int i = 0; i < names.length; i++)
+        {
+            if (i < names.length - 1)
+            {
+                String getterMethodName = "get" + StringUtils.capitalize(names[i]);
+                object = ReflectUtil.invoke(object, getterMethodName, new Class[] {}, new Object[] {});
+            }
+            else
+            {
+                String setterMethodName = "set" + StringUtils.capitalize(names[i]);
+                ReflectUtil.invoke(object, setterMethodName, new Object[] { value });
+            }
+        }
+    }
+
+
+    private int incrementSheetIntValue(Sheet sheet, Map<Sheet,AtomicInteger> valueMap){
+        return valueMap.compute(sheet,(sh,count)->{
+            if(count==null){
+                return new AtomicInteger(0);
+            }else {
+                count.incrementAndGet();
+                return count;
+            }
+        }).get();
+    }
+
+    private int getAndIncrementSheetValue(Sheet sheet, Map<Sheet,AtomicInteger> valueMap){
+        return valueMap.compute(sheet,(sh,count)->{
+            if(count==null){
+                return new AtomicInteger(0);
+            }else {
+                return count;
+            }
+        }).getAndIncrement();
+    }
+
+    private AtomicInteger incrementSheetValue(Sheet sheet, Map<Sheet,AtomicInteger> valueMap){
+        return valueMap.compute(sheet,(sh,count)->{
+            if(count==null){
+                return new AtomicInteger(0);
+            }else {
+                count.incrementAndGet();
+                return count;
+            }
+        });
+    }
+
+    private int getSheetIntValue(Sheet sheet, Map<Sheet,AtomicInteger> valueMap){
+        return valueMap.compute(sheet,(sh,count)->count!=null?count:new AtomicInteger(0)).get();
+    }
+
+    private AtomicInteger getSheetValue(Sheet sheet, Map<Sheet,AtomicInteger> valueMap){
+        return valueMap.compute(sheet,(sh,count)->count!=null?count:new AtomicInteger(0));
+    }
+}

+ 36 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelHelperFactory.java

@@ -0,0 +1,36 @@
+package cn.tr.plugin.excel.config;
+
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.dict.config.cache.DictManager;
+import lombok.AllArgsConstructor;
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * @ClassName : ExcelHelperBuilder
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月13日
+ */
+@AllArgsConstructor
+public class ExcelHelperFactory {
+    private final DictManager dictManager;
+
+    public <T> ExcelHelper create(Class<T> tClass){
+        BiFunction<String, String, String> dictOperator = new BiFunction<String, String, String>() {
+            @Override
+            public String apply(String dictCode, String dictValue) {
+                return dictManager.lookByDictCode(dictCode,dictValue).getLabel();
+            }
+        };
+
+        Function<String, List<DictBO>> dictAllOperator = new Function<String, List<DictBO>>() {
+            @Override
+            public List<DictBO> apply(String dictCode) {
+                return dictManager.lookAllByDictCode(dictCode);
+            }
+        };
+        return new ExcelHelper(tClass,dictOperator,dictAllOperator);
+    }
+}

+ 46 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/java/cn/tr/plugin/excel/config/ExcelThreadFactory.java

@@ -0,0 +1,46 @@
+package cn.tr.plugin.excel.config;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @ClassName : ExcelThreadFactory
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月15日
+ */
+public class ExcelThreadFactory  implements ThreadFactory {
+    private static final AtomicInteger poolNumber = new AtomicInteger(1);
+    private final ThreadGroup group;
+    private final AtomicInteger threadNumber = new AtomicInteger(1);
+    private final String namePrefix;
+
+    ExcelThreadFactory() {
+
+        SecurityManager s = System.getSecurityManager();
+        group = (s != null) ? s.getThreadGroup() :
+                Thread.currentThread().getThreadGroup();
+        namePrefix = "excel-export-" +
+                poolNumber.getAndIncrement() +
+                "-thread-";
+    }
+
+
+    @Override
+    public Thread newThread(@NotNull Runnable r) {
+        Thread t = new Thread(group, r,
+                namePrefix + threadNumber.getAndIncrement(),
+                0);
+        if (t.isDaemon()){
+            t.setDaemon(false);
+        }
+        if (t.getPriority() != Thread.NORM_PRIORITY){
+            t.setPriority(Thread.NORM_PRIORITY);
+        }
+        return t;
+    }
+
+
+}

+ 1 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.tr.plugin.excel.TrExcelAutoConfiguration

+ 95 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-excel/src/test/java/cn/tr/plugin/excel/ExcelTest.java

@@ -0,0 +1,95 @@
+package cn.tr.plugin.excel;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.tr.core.enums.IEnum;
+import cn.tr.plugin.dict.annotation.Dict;
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.dict.config.cache.DictManager;
+import cn.tr.plugin.excel.annotation.Excel;
+import cn.tr.plugin.excel.config.ExcelHelper;
+import cn.tr.plugin.excel.config.ExcelHelperFactory;
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @ClassName : ExcelTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月13日
+ */
+
+public class ExcelTest extends BaseMockitoUnitTest {
+
+    private DictManager dictManager;
+
+    private ExcelHelperFactory excelHelperFactory;
+
+    private String pathPrefix="C:\\Users\\JR\\Desktop\\icon\\";
+    @BeforeEach
+    public void setUp(){
+        dictManager=new DictManager(new ConcurrentMapCacheManager());
+        excelHelperFactory=new ExcelHelperFactory(dictManager);
+        List<DictBO> dicts = Stream.of(Gender.values())
+                .map(gender -> {
+                    DictBO dict = new DictBO();
+                    dict.setValue(gender.getValue());
+                    dict.setLabel(gender.getLabel());
+                    return dict;
+                })
+                .collect(Collectors.toList());
+        dictManager.load("gender",dicts);
+    }
+
+
+    @Test
+    public void exportExcel() throws Exception {
+        File file = FileUtil.touch(pathPrefix + "111.xlsx");
+        ArrayList<User> users = new ArrayList<>();
+        for (int i = 0; i < 100; i++) {
+            users.add(User.of(i,"用户"+i, RandomUtil.randomString("123",1),RandomUtil.randomInt(10,40)));
+        }
+        long currentTimeMillis = System.currentTimeMillis();
+        ExcelHelper excelHelper = excelHelperFactory.create(User.class);
+        FileOutputStream fileOutputStream = new FileOutputStream(file);
+        excelHelper.exportExcel(fileOutputStream,users,"测试","用户表单");
+        System.out.println("耗时============"+(System.currentTimeMillis() - currentTimeMillis));
+    }
+
+    @Data
+    @AllArgsConstructor(staticName = "of")
+    private static class User{
+        @Excel(name = "序号")
+        private Integer sort;
+        @Excel(name = "姓名")
+        private String name;
+        @Excel(name = "性别",dictCode = "gender")
+        private String gender;
+        @Excel(name = "年龄",suffix = "岁")
+        private Integer age;
+    }
+
+    @Getter
+    @AllArgsConstructor
+    static enum Gender implements IEnum<String>{
+        man("1","男"),
+        woman("2","女"),
+        unkown("3","未知");
+        private String value;
+        private String label;
+
+    }
+}

+ 55 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/pom.xml

@@ -0,0 +1,55 @@
+<?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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <description>多租户插件</description>
+    <artifactId>tr-spring-boot-starter-plugin-biz-tenant</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-extension</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 96 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/TrTenantAutoConfiguration.java

@@ -0,0 +1,96 @@
+package cn.tr.plugin.biz.tenant;
+
+import cn.tr.core.enums.WebFilterOrderEnum;
+import cn.tr.plugin.biz.tenant.config.TenantStrategyConfig;
+import cn.tr.plugin.biz.tenant.config.aop.TenantIgnoreAspect;
+import cn.tr.plugin.biz.tenant.config.db.TenantDatabaseInterceptor;
+import cn.tr.plugin.biz.tenant.config.ignore.TenantIgnoreUrlConfig;
+import cn.tr.plugin.biz.tenant.config.secutiry.TenantSecurityWebFilter;
+import cn.tr.plugin.biz.tenant.config.service.TenantFrameworkService;
+import cn.tr.plugin.biz.tenant.config.web.TenantContextWebFilter;
+import cn.tr.plugin.biz.tenant.properties.TenantProperties;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+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.web.SpringServletContainerInitializer;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.util.*;
+
+/**
+ * @ClassName : TrTenantAutoConfiguration
+ * @Description : 允许使用 tr.tenant.enable=false 禁用多租户
+ * @Author : LF
+ * @Date: 2023年02月22日
+ */
+@ConditionalOnProperty(prefix = "tr.tenant", value = "enable", matchIfMissing = true)
+@EnableConfigurationProperties(TenantProperties.class)
+public class TrTenantAutoConfiguration {
+    @Bean
+    public TenantStrategyConfig tenantStrategyConfig(){
+        return new TenantStrategyConfig();
+    }
+
+    // ========== AOP ==========
+    @Bean
+    public TenantIgnoreAspect tenantIgnoreAspect() {
+        return new TenantIgnoreAspect();
+    }
+
+    // ========== DB ==========
+    @Bean
+    @ConditionalOnClass(MybatisPlusInterceptor.class)
+    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
+                                                                 MybatisPlusInterceptor interceptor) {
+        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
+        // 添加到 interceptor 中
+        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
+        addInterceptor(interceptor,inner,0);
+        return inner;
+    }
+
+    @Bean
+    @ConditionalOnClass(MybatisPlusInterceptor.class)
+    public MybatisPlusInterceptor mybatisPlusInterceptor(){
+        return new MybatisPlusInterceptor();
+    }
+
+    private void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) {
+        List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
+        inners.add(index, inner);
+        interceptor.setInterceptors(inners);
+    }
+
+    // ========== Security ==========
+    @Bean
+    @ConditionalOnClass(SpringServletContainerInitializer.class)
+    public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
+                                                                                   @Autowired(required = false) TenantFrameworkService tenantFrameworkService) {
+        FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties,
+                tenantFrameworkService));
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
+        return registrationBean;
+    }
+
+    // ========== WEB ==========
+    @Bean
+    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
+        FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantContextWebFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
+        return registrationBean;
+    }
+
+    @Bean
+    public TenantIgnoreUrlConfig ignoreConfig(RequestMappingHandlerMapping handlerMapping, TenantProperties tenantProperties){
+        return new TenantIgnoreUrlConfig(handlerMapping,tenantProperties);
+    }
+
+}

+ 17 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/TenantStrategyConfig.java

@@ -0,0 +1,17 @@
+package cn.tr.plugin.biz.tenant.config;
+
+import cn.tr.core.strategy.TenantStrategy;
+import cn.tr.core.utils.ServletUtils;
+
+/**
+ * @ClassName : TenantStrategyConfig
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年02月22日
+ */
+
+public class TenantStrategyConfig {
+    {
+        TenantStrategy.tr.tenantIdSupplier=request -> ServletUtils.getHeader("Tenant-Id");
+    }
+}

+ 18 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/aop/TenantIgnore.java

@@ -0,0 +1,18 @@
+package cn.tr.plugin.biz.tenant.config.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 {
+}

+ 33 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/aop/TenantIgnoreAspect.java

@@ -0,0 +1,33 @@
+package cn.tr.plugin.biz.tenant.config.aop;
+
+import cn.tr.plugin.biz.tenant.context.TenantContextHolder;
+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} 注解实现,用于一些全局的逻辑。
+ * 例如说,一个定时任务,读取所有数据,进行处理。
+ * 又例如说,读取所有数据,进行缓存。
+ *
+ *
+ * @author tr
+ */
+@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);
+        }
+    }
+
+}

+ 61 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/db/TenantDatabaseInterceptor.java

@@ -0,0 +1,61 @@
+package cn.tr.plugin.biz.tenant.config.db;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.strategy.TenantStrategy;
+import cn.tr.core.utils.ServletUtils;
+import cn.tr.plugin.biz.tenant.context.TenantContextHolder;
+import cn.tr.plugin.biz.tenant.properties.TenantProperties;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+
+import javax.servlet.http.HttpServletRequest;
+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() {
+        HttpServletRequest request = ServletUtils.getRequest();
+        Boolean ignore = TenantContextHolder.isIgnore();
+        if(ignore==null){
+            if(request==null){
+                TenantContextHolder.setIgnore(true);
+            }else {
+                TenantContextHolder.setIgnore(false);
+            }
+        }
+        return new StringValue(TenantStrategy.tr.getTenantId(request));
+    }
+
+    @Override
+    public boolean ignoreTable(String tableName) {
+        if(StrUtil.startWith(tableName,"`")){
+            tableName=tableName.replaceAll("`","");
+        }
+        // 情况一,全局忽略多租户
+        return Boolean.TRUE.equals(TenantContextHolder.isIgnore())
+                // 情况二,忽略多租户的表
+                || CollUtil.contains(ignoreTables, tableName);
+    }
+
+}

+ 62 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/ignore/TenantIgnoreUrlConfig.java

@@ -0,0 +1,62 @@
+package cn.tr.plugin.biz.tenant.config.ignore;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.plugin.biz.tenant.config.aop.TenantIgnore;
+import cn.tr.plugin.biz.tenant.properties.TenantProperties;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.lang.annotation.Annotation;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @ClassName : TenantIgnoreUrlConfig
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月01日
+ */
+@AllArgsConstructor
+@Slf4j
+public class TenantIgnoreUrlConfig implements CommandLineRunner {
+    private final RequestMappingHandlerMapping handlerMapping;
+    private final TenantProperties tenantProperties;
+    @Override
+    public void run(String... args) throws Exception {
+        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = handlerMapping.getHandlerMethods();
+        Set<String> controllerMappingUrls = new HashSet<>();
+        for (RequestMappingInfo requestMappingInfo : handlerMethodMap.keySet()) {
+            //1、获取控制器请求路径
+            PathPatternsRequestCondition pathPatternsCondition = requestMappingInfo.getPathPatternsCondition();
+            if(pathPatternsCondition==null){
+                continue;
+            }
+            //2、获取方法请求路径
+            HandlerMethod handlerMethod = handlerMethodMap.get(requestMappingInfo);
+            //获取方法所有注解
+            Annotation[] annotations = handlerMethod.getMethod().getAnnotations();
+            boolean ignoreTenant=false;
+            for (Annotation annotation : annotations) {
+                if(annotation instanceof TenantIgnore){
+                    ignoreTenant=true;
+                }
+            }
+            if(ignoreTenant){
+                String url= CollectionUtil.
+                        getFirst(pathPatternsCondition.getPatterns().iterator())
+                        .getPatternString();
+                controllerMappingUrls.add(url);
+                log.debug("controller 路径{}忽略租户",url);
+            }
+        }
+        Set<String> ignoreUrls =CollectionUtil.newHashSet(tenantProperties.getIgnoreUrls());
+        ignoreUrls.addAll(controllerMappingUrls);
+        tenantProperties.setIgnoreUrls(ignoreUrls);
+    }
+}

+ 119 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/secutiry/TenantSecurityWebFilter.java

@@ -0,0 +1,119 @@
+package cn.tr.plugin.biz.tenant.config.secutiry;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.exception.TRExcCode;
+import cn.tr.core.pojo.CommonResult;
+import cn.tr.core.strategy.ExceptionStrategy;
+import cn.tr.core.strategy.LoginUserStrategy;
+import cn.tr.core.utils.ServletUtils;
+import cn.tr.plugin.biz.tenant.config.service.TenantFrameworkService;
+import cn.tr.plugin.biz.tenant.context.TenantContextHolder;
+import cn.tr.plugin.biz.tenant.properties.TenantProperties;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * 多租户 Security Web 过滤器
+ * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
+ * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
+ * 3. 校验租户是合法,例如说被禁用、到期
+ *
+ * 校验用户访问的租户,是否是其所在的租户,
+ *
+ * @author tr
+ */
+@Slf4j
+public class TenantSecurityWebFilter  extends OncePerRequestFilter {
+
+    private final TenantProperties tenantProperties;
+
+    private final AntPathMatcher pathMatcher;
+    private final TenantFrameworkService tenantFrameworkService;
+
+    public TenantSecurityWebFilter(TenantProperties tenantProperties,
+                                   TenantFrameworkService tenantFrameworkService) {
+        this.tenantProperties = tenantProperties;
+        this.pathMatcher = new AntPathMatcher();
+        this.tenantFrameworkService = tenantFrameworkService;
+    }
+
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        String tenantId = TenantContextHolder.getTenantId();
+        if (LoginUserStrategy.tr.isAnonymous()) {
+            //匿名访问,不做任何校验
+            chain.doFilter(request, response);
+        }else {
+            // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
+            // 如果获取不到租户编号,则尝试使用登陆用户的租户编号
+            if (StrUtil.isEmpty(tenantId)) {
+                tenantId = LoginUserStrategy.tr.getTenantId();
+                TenantContextHolder.setTenantId(tenantId);
+                TenantContextHolder.setIgnore(false);
+                // 如果传递了租户编号,则进行比对租户编号,避免越权问题
+            } else if (!Objects.equals(tenantId,  LoginUserStrategy.tr.getTenantId())) {
+                log.error("[doFilterInternal][租户({}) User({}) 越权访问租户({}) URL({}/{})]",
+                        LoginUserStrategy.tr.getTenantId(), LoginUserStrategy.tr.getCurrentUserId(),
+                        TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
+                ServletUtils.writeJSON(response, CommonResult.error(TRExcCode.USER_ERROR_A0403,
+                        "您无权访问该租户的数据"));
+                return;
+            }
+
+            // 如果非允许忽略租户的 URL,则校验租户是否合法
+            if (!isIgnoreUrl(request) && !LoginUserStrategy.tr.isAnonymous()) {
+                // 2. 如果请求未带租户的编号,不允许访问。
+                if (StrUtil.isEmpty(tenantId)) {
+                    log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
+                    ServletUtils.writeJSON(response, CommonResult.error(TRExcCode.USER_ERROR_A0403,
+                            "未获取到租户编号,请进行排查"));
+                    return;
+                }
+                if(tenantFrameworkService!=null){
+                    // 3. 校验租户是合法,例如说被禁用、到期
+                    try {
+                        tenantFrameworkService.validTenant(tenantId);
+                    } catch (Throwable ex) {
+                        CommonResult<?> result = ExceptionStrategy.tr.exceptionHandle(request, ex);
+                        ServletUtils.writeJSON(response, result);
+                        return;
+                    }
+                }
+            } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
+                if (tenantId == null) {
+                    TenantContextHolder.setIgnore(true);
+                }
+            }
+
+            // 继续过滤
+            chain.doFilter(request, response);
+        }
+
+    }
+
+    private boolean isIgnoreUrl(HttpServletRequest request) {
+        // 快速匹配,保证性能
+        if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
+            return true;
+        }
+        // 逐个 Ant 路径匹配
+        for (String url : tenantProperties.getIgnoreUrls()) {
+            if (pathMatcher.match(url, request.getRequestURI())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

+ 26 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/service/TenantFrameworkService.java

@@ -0,0 +1,26 @@
+package cn.tr.plugin.biz.tenant.config.service;
+
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 接口,定义获取租户信息
+ *
+ * @author 芋道源码
+ */
+public interface TenantFrameworkService {
+
+    /**
+     * 获得所有租户
+     *
+     * @return 租户编号数组
+     */
+    List<String> getTenantIds();
+
+    /**
+     * 校验租户是否合法
+     *
+     * @param id 租户编号
+     */
+    void validTenant(String id);
+
+}

+ 34 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/config/web/TenantContextWebFilter.java

@@ -0,0 +1,34 @@
+package cn.tr.plugin.biz.tenant.config.web;
+
+import cn.tr.core.strategy.TenantStrategy;
+import cn.tr.plugin.biz.tenant.context.TenantContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 多租户 Context Web 过滤器
+ * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
+ *
+ * @author 芋道源码
+ */
+public class TenantContextWebFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 设置
+        String tenantId = TenantStrategy.tr.getTenantId(request);
+        tenantId ="1";
+        if (tenantId != null) {
+            TenantContextHolder.setTenantId(tenantId);
+            TenantContextHolder.setIgnore(false);
+        }
+        chain.doFilter(request, response);
+    }
+
+}

+ 26 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/constant/TenantConstant.java

@@ -0,0 +1,26 @@
+package cn.tr.plugin.biz.tenant.constant;
+
+/**
+ * @ClassName : TenantConstant
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public class TenantConstant {
+
+    /**
+     * 租户是否开启 默认值
+     */
+    public static final Boolean ENABLE_DEFAULT = true;
+
+    /**
+     * 租户id
+     */
+    public static final String TENANT_ID="tenant_id";
+
+    /**
+     * 是否忽略租户
+     */
+    public static final String TENANT_IGNORE="tenant_ignore";
+}

+ 52 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/context/TenantContextHolder.java

@@ -0,0 +1,52 @@
+package cn.tr.plugin.biz.tenant.context;
+
+import cn.tr.core.context.SecurityContextHolder;
+import cn.tr.plugin.biz.tenant.constant.TenantConstant;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author tr
+ */
+public class TenantContextHolder {
+
+    /**
+     * 获得租户编号。
+     *
+     * @return 租户编号
+     */
+    public static String getTenantId() {
+        return SecurityContextHolder.getStr(TenantConstant.TENANT_ID);
+    }
+
+    /**
+     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static String getRequiredTenantId() {
+        String tenantId = getTenantId();
+        if (tenantId == null) {
+            throw new NullPointerException("TenantContextHolder 不存在租户编号!");
+        }
+        return tenantId;
+    }
+
+    public static void setTenantId(String tenantId) {
+        SecurityContextHolder.set(TenantConstant.TENANT_ID,tenantId);
+    }
+
+    public static void setIgnore(boolean ignore) {
+        SecurityContextHolder.set(TenantConstant.TENANT_IGNORE,ignore);
+    }
+
+    /**
+     * 当前是否忽略租户
+     *
+     * @return 是否忽略
+     */
+    public static Boolean isIgnore() {
+        return SecurityContextHolder.get(TenantConstant.TENANT_ID,Boolean.class);
+    }
+
+}

+ 38 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/properties/TenantProperties.java

@@ -0,0 +1,38 @@
+package cn.tr.plugin.biz.tenant.properties;
+
+import cn.tr.plugin.biz.tenant.constant.TenantConstant;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 多租户配置
+ *
+ * @author lf
+ */
+@ConfigurationProperties(prefix = "tr.tenant")
+@Data
+public class TenantProperties {
+
+    /**
+     * 是否开启
+     */
+    private Boolean enable = TenantConstant.ENABLE_DEFAULT;
+
+    /**
+     * 需要忽略多租户的请求
+     *
+     * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
+     */
+    private Set<String> ignoreUrls = Collections.emptySet();
+
+    /**
+     * 需要忽略多租户的表
+     *
+     * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
+     */
+    private Set<String> ignoreTables = Collections.emptySet();
+
+}

+ 77 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/java/cn/tr/plugin/biz/tenant/utils/TenantUtils.java

@@ -0,0 +1,77 @@
+package cn.tr.plugin.biz.tenant.utils;
+
+
+import cn.tr.plugin.biz.tenant.context.TenantContextHolder;
+
+import java.util.function.Supplier;
+
+/**
+ * 多租户 Util
+ *
+ * @author tr
+ */
+public class TenantUtils {
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param runnable 逻辑
+     */
+    public static void execute(String tenantId, Runnable runnable) {
+        String oldTenantId = TenantContextHolder.getTenantId();
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setTenantId(tenantId);
+            TenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            TenantContextHolder.setTenantId(oldTenantId);
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 忽略租户,执行对应的逻辑
+     *
+     * @param runnable 逻辑
+     */
+    public static void executeIgnore(Runnable runnable) {
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param supplier 逻辑
+     */
+    public static <T> T execute(String tenantId, Supplier<T> supplier) {
+        String oldTenantId = TenantContextHolder.getTenantId();
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setTenantId(tenantId);
+            TenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            return supplier.get();
+        } finally {
+            TenantContextHolder.setTenantId(oldTenantId);
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+}

+ 2 - 0
tr-plugins/tr-spring-boot-starter-plugin-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,2 @@
+cn.tr.plugin.biz.tenant.TrTenantAutoConfiguration
+cn.tr.plugin.biz.tenant.config.aop.TenantIgnoreAspect

+ 51 - 0
tr-plugins/tr-spring-boot-starter-plugin-cache/pom.xml

@@ -0,0 +1,51 @@
+<?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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-spring-boot-starter-plugin-cache</artifactId>
+
+    <description>缓存</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 56 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/pom.xml

@@ -0,0 +1,56 @@
+<?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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-spring-boot-starter-plugin-dict</artifactId>
+
+    <description>字典插件</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-cache</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-test</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 24 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/TrDictAutoConfiguration.java

@@ -0,0 +1,24 @@
+package cn.tr.plugin.dict;
+
+import cn.tr.plugin.dict.config.cache.DictManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * @ClassName : TrDictAutoConfiguration
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月07日
+ */
+@Slf4j
+@EnableCaching
+public class TrDictAutoConfiguration {
+
+    @Bean
+    public DictManager dictManager(CacheManager cacheManager){
+        return  new DictManager(cacheManager);
+    }
+
+}

+ 43 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/annotation/Dict.java

@@ -0,0 +1,43 @@
+package cn.tr.plugin.dict.annotation;
+
+import cn.tr.plugin.dict.config.jackson.DictJacksonDeserializer;
+import cn.tr.plugin.dict.config.jackson.DictJacksonSerializer;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.springframework.core.annotation.AliasFor;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @@interface : Dict
+ * @Description : 字典编码,可通过此编码获取 字典值所对应的 {@link cn.tr.plugin.dict.bo.DictBO}
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+@Target({ElementType.FIELD,ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@JacksonAnnotationsInside
+@JsonDeserialize(using = DictJacksonDeserializer.class)
+@JsonSerialize(using = DictJacksonSerializer.class)
+public @interface Dict {
+    @AliasFor("dictCode")
+    String value() default "";
+    /**
+     * 字典编码
+     * @return
+     */
+    @AliasFor("value")
+    String dictCode()default "";
+
+    /**
+     * 字典描述
+     * @return
+     */
+    String dictDesc() default "";
+
+
+}

+ 61 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/bo/DictBO.java

@@ -0,0 +1,61 @@
+package cn.tr.plugin.dict.bo;
+
+import cn.tr.plugin.dict.constant.DictConstant;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @ClassName : DictBO
+ * @Description : 字典对象
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+@Data
+public class DictBO implements Serializable {
+    private static final long serialVersionUID = 5142288478671327671L;
+    /**
+     * value值
+     */
+    private String value;
+
+    /**
+     * 标签名称
+     */
+    private String label;
+
+    /**
+     * 标签颜色
+     */
+    private String color;
+
+    /**
+     * 顺序
+     */
+    private Integer sort;
+
+    public DictBO(String value, String label, String color, Integer sort) {
+        this.value = value;
+        this.label = label;
+        this.color = color;
+        this.sort = sort;
+    }
+
+    public DictBO(String value, String label, String color) {
+        this.value = value;
+        this.label = label;
+        this.color = color;
+        this.sort = DictConstant.DICT_SORT_DEFAULT;
+    }
+
+    public DictBO(String value, String label) {
+        this.value = value;
+        this.label = label;
+        this.color = color;
+        this.sort = DictConstant.DICT_SORT_DEFAULT;
+    }
+
+    public DictBO() {
+        this.sort = DictConstant.DICT_SORT_DEFAULT;
+    }
+}

+ 85 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/config/cache/DictManager.java

@@ -0,0 +1,85 @@
+package cn.tr.plugin.dict.config.cache;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.core.enums.IEnum;
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.dict.constant.DictConstant;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+
+import java.util.*;
+
+/**
+ * @ClassName : DictManager
+ * @Description : 字典管理器
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+@Slf4j
+@AllArgsConstructor
+public class DictManager {
+    private final CacheManager cacheManager;
+
+    public void load(String dictCode, List<DictBO> dictDetails){
+        Cache cache = getCache(dictCode);
+        Map<String, DictBO> result = Optional.ofNullable(getAllLabel(cache)).orElse(new HashMap<>());
+        dictDetails.forEach(detail->result.put(detail.getValue(),detail));
+        cache.put(DictConstant.DICT_KEY,result);
+    }
+
+    public void loadAll(Map<String,List<DictBO>> dictCodeMaps){
+        dictCodeMaps.forEach(this::load);
+    }
+
+    public List<DictBO> lookAllByDictCode(String dictCode){
+        Cache cache = getCache(dictCode);
+        Map<String, DictBO> result = getAllLabel(cache);
+        List<DictBO> dictBos = CollectionUtil.isEmpty(result) ? new ArrayList<>() : new ArrayList<>(result.values());
+        if (CollectionUtil.isNotEmpty(dictBos)) {
+            dictBos.sort((t1,t2)->t1.getSort().compareTo(t2.getSort()));
+        }
+        return dictBos;
+    }
+
+
+    public DictBO lookByDictCode(String dictCode,String value){
+        Cache cache = getCache(dictCode);
+        Map<String, DictBO> result = getAllLabel(cache);
+        return CollectionUtil.isEmpty(result)?new DictBO()
+                : Optional.ofNullable(result.get(value)).orElse(new DictBO());
+    }
+
+    public void evict(String dictCode,String value){
+        Cache cache = getCache(dictCode);
+        Map<String, DictBO> allLabel = getAllLabel(cache);
+        if(CollectionUtil.isNotEmpty(allLabel)){
+            allLabel.remove(value);
+            cache.put(DictConstant.DICT_KEY,allLabel);
+        }
+    }
+
+    public void clear(String dictCode){
+        getCache(dictCode)
+                .clear();
+    }
+
+    private Map<String, DictBO> getAllLabel(Cache cache){
+        return  cache.get(DictConstant.DICT_KEY, HashMap<String, DictBO>::new);
+    }
+
+    private Cache getCache(String key){
+        String cacheName = getCacheName(key);
+        Cache cache = cacheManager.getCache(cacheName);
+        if(cache==null){
+            log.error("cache:{} does not exist or could be not created",cacheName);
+            throw new NullPointerException("缓存异常,系统内部错误,请联系管理员");
+        }
+        return cache;
+    }
+
+    private String getCacheName(String key){
+        return DictConstant.DICT_CACHE+key;
+    }
+}

+ 45 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/config/jackson/DictJacksonDeserializer.java

@@ -0,0 +1,45 @@
+package cn.tr.plugin.dict.config.jackson;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.utils.JsonUtils;
+import cn.tr.plugin.dict.bo.DictBO;
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import lombok.NoArgsConstructor;
+
+import java.io.IOException;
+
+/**
+ * @ClassName : DictJacksonSerializer
+ * @Description : 字典序列化类
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+@NoArgsConstructor
+public class DictJacksonDeserializer extends JsonDeserializer<String>  {
+
+    @Override
+    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
+        TreeNode treeNode = p.getCodec().readTree(p);
+        if(ObjectUtil.isNull(treeNode)){
+            return p.getText();
+        }
+        try {
+            TreeNode result = treeNode.get("value");
+            if(result instanceof NullNode){
+                return null;
+            }
+            if(result instanceof TextNode){
+                return ((TextNode) result).asText();
+            }
+            return result.toString();
+        }catch (Exception e){
+            return p.getText();
+        }
+    }
+}

+ 57 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/config/jackson/DictJacksonSerializer.java

@@ -0,0 +1,57 @@
+package cn.tr.plugin.dict.config.jackson;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.tr.plugin.dict.annotation.Dict;
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.dict.config.cache.DictManager;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.io.IOException;
+
+/**
+ * @ClassName : DictJacksonSerializer
+ * @Description : 字典序列化类
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictJacksonSerializer extends JsonSerializer<String> implements ContextualSerializer {
+    private Dict dict;
+
+    @Override
+    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        DictManager dictManager = SpringUtil.getBean(DictManager.class);
+        DictBO dictBO = dictManager.lookByDictCode(dict.dictCode(), value);
+        gen.writeObject(dictBO);
+    }
+
+
+    @Override
+    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
+        if (beanProperty != null) {
+            // 为空直接跳过
+            if (ObjectUtil.equals(beanProperty.getType().getRawClass(), String.class)) {
+                // 非 String 类直接跳过
+                Dict dict = beanProperty.getAnnotation(Dict.class);
+                if (dict == null) {
+                    dict = beanProperty.getContextAnnotation(Dict.class);
+                }
+                // 如果能得到注解,就将注解的 value 传入 DictJacksonSerializer
+                if (dict != null) {
+                    return new DictJacksonSerializer(dict);
+                }
+            }
+            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
+        }
+        return serializerProvider.findNullValueSerializer(beanProperty);
+    }
+}

+ 17 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/java/cn/tr/plugin/dict/constant/DictConstant.java

@@ -0,0 +1,17 @@
+package cn.tr.plugin.dict.constant;
+
+/**
+ * @Interface : DictConstant
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+
+public interface DictConstant {
+    String DICT_CACHE="dict:";
+
+
+    String DICT_KEY="$key$";
+
+    Integer DICT_SORT_DEFAULT=9999;
+}

+ 1 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.tr.plugin.dict.TrDictAutoConfiguration

+ 17 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/main/resources/application-unit-test.yml

@@ -0,0 +1,17 @@
+spring:
+  redis:
+    host: 192.168.100.32
+    password: 6E6985E1F7CB40F24A\.
+    port: 9736
+    database: 7
+    timeout: 30s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 16
+        # 连接池中的最大空闲连接
+        max-idle: 16
+        # 连接池的最大数据库连接数
+        max-active: 16
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: -1ms

+ 74 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/test/java/cn/tr/plugin/dict/config/cache/CaffeineDictManagerTest.java

@@ -0,0 +1,74 @@
+package cn.tr.plugin.dict.config.cache;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.cache.caffeine.CaffeineCacheManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @ClassName : DictMananerTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月06日
+ */
+
+public class CaffeineDictManagerTest extends BaseMockitoUnitTest {
+    private DictManager dictManager;
+    @BeforeEach
+    public void setUp(){
+        dictManager=new DictManager(new CaffeineCacheManager());
+    }
+
+    @Test
+    public void testLoad(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"));
+        dictManager.load("test",dicts);
+        List<DictBO> test = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(test),1);
+        Assertions.assertEquals(test,dicts);
+    }
+
+    @Test
+    public void testLoadAll(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        List<DictBO> test = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(test),2);
+        Assertions.assertEquals(test,dicts);
+    }
+
+    @Test
+    public void testLookByDictCode(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        DictBO dictBO = dictManager.lookByDictCode("test", "t1");
+        Assertions.assertEquals(dictBO.getLabel(),"测试1");
+    }
+
+    @Test
+    public void testEvict(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        DictBO dictBO = dictManager.lookByDictCode("test", "t1");
+        Assertions.assertEquals(dictBO.getLabel(),"测试1");
+        dictManager.evict("test","t1");
+        dictBO = dictManager.lookByDictCode("test", "t1");
+        Assertions.assertNull(dictBO.getValue());
+    }
+    @Test
+    public void testClear(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        List<DictBO> result = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(result),2);
+        dictManager.clear("test");
+        result = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(result),0);
+    }
+}

+ 115 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/test/java/cn/tr/plugin/dict/config/cache/RedisDictManagerTest.java

@@ -0,0 +1,115 @@
+package cn.tr.plugin.dict.config.cache;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.test.ut.BaseRedisUnitTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.CacheManager;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @ClassName : RedisDictManangerTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月07日
+ */
+
+public class RedisDictManagerTest extends BaseRedisUnitTest {
+    private DictManager dictManager;
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @BeforeEach
+    public void init(){
+        dictManager=new DictManager(cacheManager(redisTemplate));
+        dictManager.clear("test");
+    }
+
+    public CacheManager cacheManager(RedisTemplate<String, Object> template) {
+
+        // 基本配置
+        RedisCacheConfiguration defaultCacheConfiguration =
+                RedisCacheConfiguration
+                        .defaultCacheConfig()
+                        // 设置key为String
+                        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(template.getStringSerializer()))
+                        // 设置value 为自动转Json的Object
+                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(template.getValueSerializer()))
+                        // 不缓存null
+                        .disableCachingNullValues()
+                        // 缓存数据保存1小时
+                        .entryTtl(Duration.ofHours(1));
+
+        // 够着一个redis缓存管理器
+        RedisCacheManager redisCacheManager =
+                RedisCacheManager.RedisCacheManagerBuilder
+                        // Redis 连接工厂
+                        .fromConnectionFactory(template.getConnectionFactory())
+                        // 缓存配置
+                        .cacheDefaults(defaultCacheConfiguration)
+                        // 配置同步修改或删除 put/evict
+                        .transactionAware()
+                        .build();
+
+        return redisCacheManager;
+    }
+
+    @Test
+    public void testLoad(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"));
+        dictManager.load("test",dicts);
+        List<DictBO> test = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(test),1);
+        Assertions.assertEquals(test,dicts);
+    }
+
+    @Test
+    public void testLoadAll(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        List<DictBO> test = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(test),2);
+        Assertions.assertEquals(test,dicts);
+    }
+
+    @Test
+    public void testLookByDictCode(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        DictBO dictBO = dictManager.lookByDictCode("test", "t1");
+        Assertions.assertEquals(dictBO.getLabel(),"测试1");
+    }
+
+    @Test
+    public void testEvict(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        DictBO dictBO = dictManager.lookByDictCode("test", "t1");
+        Assertions.assertEquals(dictBO.getLabel(),"测试1");
+        dictManager.evict("test","t1");
+        dictBO = dictManager.lookByDictCode("test", "t1");
+        Assertions.assertNull(dictBO.getValue());
+    }
+    @Test
+    public void testClear(){
+        List<DictBO> dicts = Arrays.asList(new DictBO("t1", "测试1"),new DictBO("t2", "测试2"));
+        dictManager.load("test",dicts);
+        List<DictBO> result = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(result),2);
+        dictManager.clear("test");
+        result = dictManager.lookAllByDictCode("test");
+        Assertions.assertEquals(CollectionUtil.size(result),0);
+    }
+
+
+}

+ 75 - 0
tr-plugins/tr-spring-boot-starter-plugin-dict/src/test/java/cn/tr/plugin/dict/config/jackson/DictJacksonSerializerTest.java

@@ -0,0 +1,75 @@
+package cn.tr.plugin.dict.config.jackson;
+
+import cn.hutool.extra.spring.SpringUtil;
+import cn.tr.plugin.dict.annotation.Dict;
+import cn.tr.plugin.dict.bo.DictBO;
+import cn.tr.plugin.dict.config.cache.DictManager;
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.*;
+import org.springframework.cache.caffeine.CaffeineCacheManager;
+import java.util.Arrays;
+
+/**
+ * @ClassName : DictJacksonSerializerTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月07日
+ */
+
+public class DictJacksonSerializerTest extends BaseMockitoUnitTest {
+
+    private ObjectMapper objectMapper=new ObjectMapper();
+
+    private DictManager dictManager;
+
+    @BeforeEach
+    public void setUp(){
+        dictManager=new DictManager(new CaffeineCacheManager());
+        MockedStatic<SpringUtil> mockedStatic = Mockito.mockStatic(SpringUtil.class);
+        mockedStatic.when(()->SpringUtil.getBean(Mockito.same(DictManager.class))).thenReturn(dictManager);
+
+    }
+
+
+    /**
+     * 序列化
+     * @throws JsonProcessingException
+     */
+    @Test
+    public void serializer() throws JsonProcessingException {
+        dictManager.load("gender", Arrays.asList(new DictBO("1","男"),new DictBO("2","女")));
+        User man = new User("1");
+        User woman = new User("2");
+        Assertions.assertEquals(objectMapper.writeValueAsString(man),"{\"gender\":{\"value\":\"1\",\"label\":\"男\",\"color\":null,\"sort\":9999}}");
+        Assertions.assertEquals(objectMapper.writeValueAsString(woman),"{\"gender\":{\"value\":\"2\",\"label\":\"女\",\"color\":null,\"sort\":9999}}");
+    }
+
+    /**
+     * 反序列化
+     */
+    @Test
+    public void deserializer() throws JsonProcessingException {
+        dictManager.load("gender", Arrays.asList(new DictBO("1","男"),new DictBO("2","女")));
+        User man = new User("1");
+        String json=objectMapper.writeValueAsString(man);
+        Assertions.assertEquals(objectMapper.writeValueAsString(man),"{\"gender\":{\"value\":\"1\",\"label\":\"男\",\"color\":null,\"sort\":9999}}");
+        User user = objectMapper.readValue(json, User.class);
+        Assertions.assertEquals(user.getGender(),"1");
+    }
+
+    @AllArgsConstructor
+    @Data
+    @NoArgsConstructor
+    static class User{
+        @Dict(dictCode = "gender")
+        String gender;
+    }
+}

+ 32 - 0
tr-plugins/tr-spring-boot-starter-plugin-eventbus/pom.xml

@@ -0,0 +1,32 @@
+<?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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-spring-boot-starter-plugin-eventbus</artifactId>
+
+    <description>消息总线</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-test</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.greenrobot</groupId>
+            <artifactId>eventbus-java</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 22 - 0
tr-plugins/tr-spring-boot-starter-plugin-eventbus/src/main/java/cn/tr/plugin/eventbus/TrEventBusAutoConfiguration.java

@@ -0,0 +1,22 @@
+package cn.tr.plugin.eventbus;
+
+import org.greenrobot.eventbus.EventBus;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * @ClassName : TrEventBusAutoConfiguration
+ * @Description : 消息总线自动装配
+ * @Author : LF
+ * @Date: 2023年03月15日
+ */
+
+public class TrEventBusAutoConfiguration {
+    @Bean
+    public EventBus eventBus(){
+        return EventBus.builder()
+                .logNoSubscriberMessages(false)
+                .sendNoSubscriberEvent(false)
+                .throwSubscriberException(true)
+                .build();
+    }
+}

+ 1 - 0
tr-plugins/tr-spring-boot-starter-plugin-eventbus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.tr.plugin.eventbus.TrEventBusAutoConfiguration

+ 40 - 0
tr-plugins/tr-spring-boot-starter-plugin-eventbus/src/test/java/cn/tr/plugin/eventbus/EventBusTest.java

@@ -0,0 +1,40 @@
+package cn.tr.plugin.eventbus;
+
+import cn.tr.plugin.test.ut.BaseMockitoUnitTest;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @ClassName : EventBusTest
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月15日
+ */
+
+public class EventBusTest extends BaseMockitoUnitTest {
+    private EventBus eventBus;
+
+    @BeforeEach
+    public void init(){
+        eventBus=EventBus.getDefault();
+        eventBus.register(new StringListener());
+    }
+
+    @Test
+    public void pub(){
+        for (int i = 0; i < 100; i++) {
+            eventBus.post(String.valueOf(i));
+        }
+
+    }
+
+
+    public static class StringListener{
+        @Subscribe
+        public void sub(String str){
+            System.out.println("订阅消息:"+str);
+        }
+    }
+}

+ 46 - 0
tr-plugins/tr-spring-boot-starter-plugin-file/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>tr-plugins</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>0.0.9</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-spring-boot-starter-plugin-file</artifactId>
+
+
+    <description>文件OSS插件</description>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-test</artifactId>
+        </dependency>
+
+        <!--ali OSS存储-->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+        </dependency>
+        <!-- minio -->
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+        </dependency>
+        <!--七牛云-->
+        <dependency>
+            <groupId>com.qiniu</groupId>
+            <artifactId>qiniu-java-sdk</artifactId>
+        </dependency>
+
+    </dependencies>
+</project>

+ 40 - 0
tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/bo/FileBO.java

@@ -0,0 +1,40 @@
+package cn.tr.plugin.file.bo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * @ClassName : FileBO
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月07日
+ */
+@Data
+@AllArgsConstructor(staticName = "of")
+public class FileBO {
+
+    /**
+     * 文件
+     */
+    private String clientId;
+
+    /**
+     * 文件路径(访问路径)
+     */
+    private String domainPath;
+
+    /**
+     * 文件业务路径
+     */
+    private String bizPath;
+
+    /**
+     * 文件大小
+     */
+    private int fileSize;
+
+    /**
+     * 文件名称
+     */
+    private String fileName;
+}

+ 60 - 0
tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/config/AbstractFileClient.java

@@ -0,0 +1,60 @@
+package cn.tr.plugin.file.config;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @ClassName : AbstractFileClient
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月07日
+ */
+@Slf4j
+public abstract class AbstractFileClient<Config extends FileClientConfig> implements FileClient {
+    @Getter
+    private String id;
+    @Getter
+    private Config config;
+
+    public AbstractFileClient(String id, Config config) {
+        this.id = id;
+        this.config = config;
+    }
+
+
+    public void init(){
+        doInit(this.config);
+        log.info("File Client[init][配置({}) 初始化完成],configClass:({})", this.config,this.getClass().getCanonicalName());
+    }
+
+    public abstract void doInit(Config config);
+
+    public void refresh(Config config){
+        // 判断是否更新
+        if (ObjectUtil.equal(config,this.config)) {
+            return;
+        }
+        log.info("File Client[refresh][配置({})发生变化,重新初始化]", config);
+        this.config = config;
+        // 初始化
+        this.init();
+    }
+
+    @Override
+    public String downUrl(String bizPath) throws Exception {
+        return previewUrl(bizPath);
+    }
+
+    /**
+     * 解析业务路径并返回
+     * @param bizPath
+     * @return
+     */
+    public String parseBizPath(String bizPath){
+        return bizPath.replaceAll("\\\\","#").replaceAll("/","-");
+    }
+}
+
+
+

+ 54 - 0
tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/config/FileClient.java

@@ -0,0 +1,54 @@
+package cn.tr.plugin.file.config;
+
+
+/**
+ * @ClassName : FileClient
+ * @Description : 文件操作客户端
+ * @Author : LF
+ * @Date: 2023年03月07日
+ */
+public interface FileClient {
+
+    /**
+     * 客户端id
+     * @return
+     */
+    String getId();
+
+    /**
+     * 上传文件至业务路径
+     *
+     * @param content
+     * @param bizPath   业务路径
+     * @return 返回bizPath
+     */
+    void upload(byte[] content,String bizPath) throws Exception;
+
+    /**
+     * 根据业务路径删除文件
+     * @param bizPath 业务路径
+     */
+    void delete(String bizPath);
+
+    /**
+     * 获取文件内容
+     * @param bizPath 业务路径
+     * @return 返回文件二进制流(不建议使用) 推荐使用{@link FileClient#downUrl(String)}
+     */
+    FileContent getContent(String bizPath) throws Exception;
+
+
+    /**
+     * 根据业务路径获取下载链接地址
+     * @param bizPath 业务路径
+     * @return 返回下载地址
+     */
+    String downUrl(String bizPath) throws Exception;
+
+    /**
+     * 根据业务路径获取预览连接地址
+     * @param bizPath 业务路径
+     * @return 返回预览地址
+     */
+    String previewUrl(String bizPath) throws Exception;
+}

+ 14 - 0
tr-plugins/tr-spring-boot-starter-plugin-file/src/main/java/cn/tr/plugin/file/config/FileClientConfig.java

@@ -0,0 +1,14 @@
+package cn.tr.plugin.file.config;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * @ClassName : FileClientConfig
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月07日
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+public interface FileClientConfig{
+
+}

Деякі файли не було показано, через те що забагато файлів було змінено