Przeglądaj źródła

!538 ♥️发布 5.2.0-BETA 公测版本
Merge pull request !538 from 疯狂的狮子Li/dev

疯狂的狮子Li 6 miesięcy temu
rodzic
commit
69e3afc770
100 zmienionych plików z 2464 dodań i 791 usunięć
  1. 1 1
      .run/ruoyi-monitor-admin.run.xml
  2. 1 1
      .run/ruoyi-server.run.xml
  3. 3 3
      .run/ruoyi-snailjob-server.run.xml
  4. 6 4
      README.md
  5. 64 31
      pom.xml
  6. 2 2
      ruoyi-admin/Dockerfile
  7. 21 0
      ruoyi-admin/pom.xml
  8. 7 3
      ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java
  9. 7 7
      ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java
  10. 9 0
      ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java
  11. 17 14
      ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java
  12. 12 2
      ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java
  13. 29 13
      ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java
  14. 2 4
      ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java
  15. 4 5
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java
  16. 4 5
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
  17. 4 5
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java
  18. 13 12
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
  19. 3 2
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java
  20. 51 42
      ruoyi-admin/src/main/resources/application-dev.yml
  21. 50 42
      ruoyi-admin/src/main/resources/application-prod.yml
  22. 28 4
      ruoyi-admin/src/main/resources/application.yml
  23. BIN
      ruoyi-admin/src/main/resources/ip2region.xdb
  24. 1 1
      ruoyi-common/ruoyi-common-bom/pom.xml
  25. 5 0
      ruoyi-common/ruoyi-common-core/pom.xml
  26. 2 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java
  27. 6 2
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java
  28. 5 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
  29. 54 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java
  30. 46 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/OssDTO.java
  31. 4 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/RoleDTO.java
  32. 73 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java
  33. 5 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java
  34. 1 12
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
  35. 52 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java
  36. 11 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/OssService.java
  37. 44 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java
  38. 6 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java
  39. 2 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
  40. 10 3
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java
  41. 30 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java
  42. 105 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java
  43. 12 5
      ruoyi-common/ruoyi-common-encrypt/pom.xml
  44. 11 3
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java
  45. 77 19
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java
  46. 24 30
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java
  47. 1 0
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/EncryptResponseBodyWrapper.java
  48. 4 0
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java
  49. 4 0
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java
  50. 5 0
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java
  51. 46 36
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
  52. 5 3
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
  53. 8 8
      ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/aspectj/RepeatSubmitAspect.java
  54. 5 11
      ruoyi-common/ruoyi-common-job/pom.xml
  55. 0 21
      ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/PowerJobConfig.java
  56. 37 0
      ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/SnailJobConfig.java
  57. 1 0
      ruoyi-common/ruoyi-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  58. 59 2
      ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/utils/JsonUtils.java
  59. 0 5
      ruoyi-common/ruoyi-common-log/pom.xml
  60. 15 16
      ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java
  61. 1 12
      ruoyi-common/ruoyi-common-mybatis/pom.xml
  62. 19 16
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java
  63. 12 2
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java
  64. 4 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java
  65. 15 13
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java
  66. 2 1
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java
  67. 59 24
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
  68. 24 30
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java
  69. 0 8
      ruoyi-common/ruoyi-common-mybatis/src/main/resources/spy.properties
  70. 38 2
      ruoyi-common/ruoyi-common-oss/pom.xml
  71. 459 132
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java
  72. 6 0
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java
  73. 12 6
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java
  74. 17 12
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java
  75. 27 43
      ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java
  76. 7 0
      ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/spel-extension.json
  77. 10 0
      ruoyi-common/ruoyi-common-redis/pom.xml
  78. 45 0
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/CacheConfig.java
  79. 23 11
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java
  80. 30 0
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/handler/RedisExceptionHandler.java
  81. 90 0
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java
  82. 2 2
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/PlusSpringCacheManager.java
  83. 11 1
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java
  84. 1 0
      ruoyi-common/ruoyi-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  85. 5 0
      ruoyi-common/ruoyi-common-satoken/pom.xml
  86. 9 0
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/config/SaTokenConfig.java
  87. 30 5
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java
  88. 52 0
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/handler/SaTokenExceptionHandler.java
  89. 57 39
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java
  90. 28 28
      ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java
  91. 0 1
      ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  92. 6 7
      ruoyi-common/ruoyi-common-sms/pom.xml
  93. 14 4
      ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java
  94. 72 0
      ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/dao/PlusSmsDao.java
  95. 7 0
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java
  96. 0 5
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java
  97. 100 0
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/topiam/AuthTopIamRequest.java
  98. 51 0
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/topiam/AuthTopiamSource.java
  99. 4 1
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java
  100. 1 5
      ruoyi-common/ruoyi-common-tenant/pom.xml

+ 1 - 1
.run/ruoyi-monitor-admin.run.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.1.2" />
+        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.2.0" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
       </settings>

+ 1 - 1
.run/ruoyi-server.run.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-server:5.1.2" />
+        <option name="imageTag" value="ruoyi/ruoyi-server:5.2.0" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
       </settings>

+ 3 - 3
.run/ruoyi-powerjob-server.run.xml → .run/ruoyi-snailjob-server.run.xml

@@ -1,10 +1,10 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="ruoyi-powerjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
+  <configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-powerjob-server:5.1.2" />
+        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.2.0" />
         <option name="buildOnly" value="true" />
-        <option name="sourceFilePath" value="ruoyi-extend/ruoyi-powerjob-server/Dockerfile" />
+        <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
       </settings>
     </deployment>
     <method v="2" />

+ 6 - 4
README.md

@@ -9,7 +9,7 @@
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
 [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 <br>
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.1.2-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
 [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.1-blue.svg)]()
 [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
 [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@@ -27,8 +27,10 @@
 
 ## 赞助商
 
-MaxKey - https://gitee.com/dromara/MaxKey <br>
-CCFlow - https://gitee.com/opencc/RuoYi-JFlow <br>
+MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey <br>
+CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
+数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br>
+引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
 [如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
 
 # 本框架与RuoYi的功能差异
@@ -62,7 +64,7 @@ CCFlow - https://gitee.com/opencc/RuoYi-JFlow <br>
 | 序列化         | 采用 Jackson Spring官方内置序列化 靠谱!!!                                                                                    | 采用 fastjson bugjson 远近闻名                                                           | 
 | 分布式幂等       | 参考美团GTIS防重系统简化实现(细节可看文档)                                                                                          | 手动编写注解基于aop实现                                                                      |
 | 分布式锁        | 采用 Lock4j 底层基于 Redisson                                                                                           | 无                                                                                  |
-| 分布式任务调度     | 采用 PowerJob 天生支持分布式 统一的管理中心                                                                                       | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造                                                   | 
+| 分布式任务调度     | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等                                                                 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造                                                   | 
 | 文件存储        | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储                                                     | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应                                                    |
 | 云存储         | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家                                                                          | 不支持                                                                                |
 | 短信          | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用                                                                 | 不支持                                                                                |

+ 64 - 31
pom.xml

@@ -13,40 +13,41 @@
     <description>RuoYi-Vue-Plus多租户管理系统</description>
 
     <properties>
-        <revision>5.1.2</revision>
-        <spring-boot.version>3.1.7</spring-boot.version>
+        <revision>5.2.0-BETA</revision>
+        <spring-boot.version>3.2.5</spring-boot.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>17</java.version>
-        <spring-boot.mybatis>3.0.3</spring-boot.mybatis>
-        <springdoc.version>2.2.0</springdoc.version>
+        <mybatis.version>3.5.16</mybatis.version>
+        <springdoc.version>2.5.0</springdoc.version>
         <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
         <poi.version>5.2.3</poi.version>
-        <easyexcel.version>3.3.3</easyexcel.version>
+        <easyexcel.version>3.3.4</easyexcel.version>
         <velocity.version>2.3</velocity.version>
-        <satoken.version>1.37.0</satoken.version>
-        <mybatis-plus.version>3.5.4</mybatis-plus.version>
+        <satoken.version>1.38.0</satoken.version>
+        <mybatis-plus.version>3.5.6</mybatis-plus.version>
         <p6spy.version>3.9.1</p6spy.version>
-        <hutool.version>5.8.22</hutool.version>
+        <hutool.version>5.8.27</hutool.version>
         <okhttp.version>4.10.0</okhttp.version>
-        <spring-boot-admin.version>3.1.8</spring-boot-admin.version>
-        <redisson.version>3.24.3</redisson.version>
-        <lock4j.version>2.2.5</lock4j.version>
-        <dynamic-ds.version>4.2.0</dynamic-ds.version>
+        <spring-boot-admin.version>3.2.3</spring-boot-admin.version>
+        <redisson.version>3.29.0</redisson.version>
+        <lock4j.version>2.2.7</lock4j.version>
+        <dynamic-ds.version>4.3.0</dynamic-ds.version>
         <alibaba-ttl.version>2.14.4</alibaba-ttl.version>
-        <powerjob.version>4.3.6</powerjob.version>
-        <mapstruct-plus.version>1.3.5</mapstruct-plus.version>
+        <snailjob.version>1.0.0-beta1</snailjob.version>
+        <mapstruct-plus.version>1.3.6</mapstruct-plus.version>
         <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
-        <lombok.version>1.18.30</lombok.version>
+        <lombok.version>1.18.32</lombok.version>
         <bouncycastle.version>1.76</bouncycastle.version>
         <justauth.version>1.16.6</justauth.version>
         <!-- 离线IP地址定位库 -->
         <ip2region.version>2.7.0</ip2region.version>
 
         <!-- OSS 配置 -->
-        <aws-java-sdk-s3.version>1.12.600</aws-java-sdk-s3.version>
+        <aws.sdk.version>2.25.15</aws.sdk.version>
+        <aws.crt.version>0.29.13</aws.crt.version>
         <!-- SMS 配置 -->
-        <sms4j.version>2.2.0</sms4j.version>
+        <sms4j.version>3.2.1</sms4j.version>
         <!-- 限制框架中的fastjson版本 -->
         <fastjson.version>1.2.83</fastjson.version>
 
@@ -56,6 +57,9 @@
         <maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
         <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
         <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
+
+        <!--工作流配置-->
+        <flowable.version>7.0.0</flowable.version>
     </properties>
 
     <profiles>
@@ -110,6 +114,14 @@
                 <scope>import</scope>
             </dependency>
 
+            <dependency>
+                <groupId>org.flowable</groupId>
+                <artifactId>flowable-bom</artifactId>
+                <version>${flowable.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
             <!-- JustAuth 的依赖配置-->
             <dependency>
                 <groupId>me.zhyd.oauth</groupId>
@@ -205,14 +217,14 @@
             </dependency>
 
             <dependency>
-                <groupId>org.mybatis.spring.boot</groupId>
-                <artifactId>mybatis-spring-boot-starter</artifactId>
-                <version>${spring-boot.mybatis}</version>
+                <groupId>org.mybatis</groupId>
+                <artifactId>mybatis</artifactId>
+                <version>${mybatis.version}</version>
             </dependency>
 
             <dependency>
                 <groupId>com.baomidou</groupId>
-                <artifactId>mybatis-plus-boot-starter</artifactId>
+                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                 <version>${mybatis-plus.version}</version>
             </dependency>
 
@@ -235,10 +247,23 @@
                 <version>${okhttp.version}</version>
             </dependency>
 
+            <!--  AWS SDK for Java 2.x  -->
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3</artifactId>
+                <version>${aws.sdk.version}</version>
+            </dependency>
+            <!-- 使用AWS基于 CRT 的 S3 客户端 -->
             <dependency>
-                <groupId>com.amazonaws</groupId>
-                <artifactId>aws-java-sdk-s3</artifactId>
-                <version>${aws-java-sdk-s3.version}</version>
+                <groupId>software.amazon.awssdk.crt</groupId>
+                <artifactId>aws-crt</artifactId>
+                <version>${aws.crt.version}</version>
+            </dependency>
+            <!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3-transfer-manager</artifactId>
+                <version>${aws.sdk.version}</version>
             </dependency>
             <!--短信sms4j-->
             <dependency>
@@ -271,16 +296,16 @@
                 <version>${lock4j.version}</version>
             </dependency>
 
-            <!-- PowerJob -->
+            <!-- SnailJob Client -->
             <dependency>
-                <groupId>tech.powerjob</groupId>
-                <artifactId>powerjob-worker-spring-boot-starter</artifactId>
-                <version>${powerjob.version}</version>
+                <groupId>com.aizuda</groupId>
+                <artifactId>snail-job-client-starter</artifactId>
+                <version>${snailjob.version}</version>
             </dependency>
             <dependency>
-                <groupId>tech.powerjob</groupId>
-                <artifactId>powerjob-official-processors</artifactId>
-                <version>${powerjob.version}</version>
+                <groupId>com.aizuda</groupId>
+                <artifactId>snail-job-client-job-core</artifactId>
+                <version>${snailjob.version}</version>
             </dependency>
 
             <dependency>
@@ -339,6 +364,13 @@
                 <version>${revision}</version>
             </dependency>
 
+            <!--  工作流模块  -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-workflow</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>
 
@@ -398,6 +430,7 @@
                 <artifactId>maven-surefire-plugin</artifactId>
                 <version>${maven-surefire-plugin.version}</version>
                 <configuration>
+                    <argLine>-Dfile.encoding=UTF-8</argLine>
                     <!-- 根据打包环境执行对应的@Tag测试方法 -->
                     <groups>${profiles.active}</groups>
                     <!-- 排除标签 -->

+ 2 - 2
ruoyi-admin/Dockerfile

@@ -19,6 +19,6 @@ ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_P
            # 应用名称 如果想区分集群节点监控 改成不同的名称即可
            #-Dskywalking.agent.service_name=ruoyi-server \
            #-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
-           -jar app.jar \
-           -XX:+HeapDumpOnOutOfMemoryError -Xlog:gc*,:time,tags,level -XX:+UseZGC ${JAVA_OPTS}
+           -XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
+           -jar app.jar
 

+ 21 - 0
ruoyi-admin/pom.xml

@@ -53,6 +53,11 @@
             <artifactId>ruoyi-common-ratelimiter</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-mail</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-system</artifactId>
@@ -75,6 +80,12 @@
             <artifactId>ruoyi-demo</artifactId>
         </dependency>
 
+        <!--  工作流模块  -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-workflow</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>de.codecentric</groupId>
             <artifactId>spring-boot-admin-starter-client</artifactId>
@@ -91,6 +102,16 @@
             <artifactId>JustAuth</artifactId>
         </dependency>
 
+        <!-- SnailJob client -->
+        <dependency>
+            <groupId>com.aizuda</groupId>
+            <artifactId>snail-job-client-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.aizuda</groupId>
+            <artifactId>snail-job-client-job-core</artifactId>
+        </dependency>
+
         <!-- skywalking 整合 logback -->
 <!--        <dependency>-->
 <!--            <groupId>org.apache.skywalking</groupId>-->

+ 7 - 3
ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java

@@ -23,9 +23,10 @@ import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
 import org.dromara.common.social.config.properties.SocialProperties;
 import org.dromara.common.social.utils.SocialUtils;
 import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.common.websocket.dto.WebSocketMessageDto;
 import org.dromara.common.websocket.utils.WebSocketUtils;
-import org.dromara.system.domain.SysClient;
 import org.dromara.system.domain.bo.SysTenantBo;
+import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysTenantVo;
 import org.dromara.system.service.ISysClientService;
 import org.dromara.system.service.ISysConfigService;
@@ -81,7 +82,7 @@ public class AuthController {
         // 授权类型和客户端id
         String clientId = loginBody.getClientId();
         String grantType = loginBody.getGrantType();
-        SysClient client = clientService.queryByClientId(clientId);
+        SysClientVo client = clientService.queryByClientId(clientId);
         // 查询不到 client 或 client 内不包含 grantType
         if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
             log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
@@ -96,7 +97,10 @@ public class AuthController {
 
         Long userId = LoginHelper.getUserId();
         scheduledExecutorService.schedule(() -> {
-            WebSocketUtils.sendMessage(userId, "欢迎登录RuoYi-Vue-Plus后台管理系统");
+            WebSocketMessageDto dto = new WebSocketMessageDto();
+            dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
+            dto.setSessionKeys(List.of(userId));
+            WebSocketUtils.publishMessage(dto);
         }, 3, TimeUnit.SECONDS);
         return R.ok(loginVo);
     }

+ 7 - 7
ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java

@@ -5,6 +5,9 @@ import cn.hutool.captcha.AbstractCaptcha;
 import cn.hutool.captcha.generator.CodeGenerator;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.RandomUtil;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.constant.GlobalConstants;
 import org.dromara.common.core.domain.R;
@@ -21,11 +24,7 @@ import org.dromara.common.web.enums.CaptchaType;
 import org.dromara.sms4j.api.SmsBlend;
 import org.dromara.sms4j.api.entity.SmsResponse;
 import org.dromara.sms4j.core.factory.SmsFactory;
-import org.dromara.sms4j.provider.enumerate.SupplierType;
 import org.dromara.web.domain.vo.CaptchaVo;
-import jakarta.validation.constraints.NotBlank;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -66,11 +65,11 @@ public class CaptchaController {
         String templateId = "";
         LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
         map.put("code", code);
-        SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
+        SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
         SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
-        if (!"OK".equals(smsResponse.getCode())) {
+        if (!smsResponse.isSuccess()) {
             log.error("验证码短信发送异常 => {}", smsResponse);
-            return R.fail(smsResponse.getMessage());
+            return R.fail(smsResponse.getData().toString());
         }
         return R.ok();
     }
@@ -121,6 +120,7 @@ public class CaptchaController {
         AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
         captcha.setGenerator(codeGenerator);
         captcha.createCode();
+        // 如果是数学验证码,使用SpEL表达式处理验证码结果
         String code = captcha.getCode();
         if (isMath) {
             ExpressionParser parser = new SpelExpressionParser();

+ 9 - 0
ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java

@@ -13,10 +13,19 @@ import lombok.Data;
 @AutoMapper(target = SysTenantVo.class)
 public class TenantListVo {
 
+    /**
+     * 租户编号
+     */
     private String tenantId;
 
+    /**
+     * 企业名称
+     */
     private String companyName;
 
+    /**
+     * 域名
+     */
     private String domain;
 
 }

+ 17 - 14
ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java

@@ -10,7 +10,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.CacheConstants;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.domain.dto.UserOnlineDTO;
-import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.utils.MessageUtils;
 import org.dromara.common.core.utils.ServletUtils;
 import org.dromara.common.core.utils.SpringUtils;
@@ -18,6 +17,7 @@ import org.dromara.common.core.utils.ip.AddressUtils;
 import org.dromara.common.log.event.LogininforEvent;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.web.service.SysLoginService;
 import org.springframework.stereotype.Component;
 
@@ -43,7 +43,6 @@ public class UserActionListener implements SaTokenListener {
     public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
         UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
         String ip = ServletUtils.getClientIP();
-        LoginUser user = LoginHelper.getLoginUser();
         UserOnlineDTO dto = new UserOnlineDTO();
         dto.setIpaddr(ip);
         dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
@@ -51,25 +50,29 @@ public class UserActionListener implements SaTokenListener {
         dto.setOs(userAgent.getOs().getName());
         dto.setLoginTime(System.currentTimeMillis());
         dto.setTokenId(tokenValue);
-        dto.setUserName(user.getUsername());
-        dto.setClientKey(user.getClientKey());
-        dto.setDeviceType(user.getDeviceType());
-        dto.setDeptName(user.getDeptName());
-        if(tokenConfig.getTimeout() == -1) {
-            RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
-        } else {
-            RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
-        }
+        String username = (String) loginModel.getExtra(LoginHelper.USER_NAME_KEY);
+        String tenantId = (String) loginModel.getExtra(LoginHelper.TENANT_KEY);
+        dto.setUserName(username);
+        dto.setClientKey((String) loginModel.getExtra(LoginHelper.CLIENT_KEY));
+        dto.setDeviceType(loginModel.getDevice());
+        dto.setDeptName((String) loginModel.getExtra(LoginHelper.DEPT_NAME_KEY));
+        TenantHelper.dynamic(tenantId, () -> {
+            if(tokenConfig.getTimeout() == -1) {
+                RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
+            } else {
+                RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
+            }
+        });
         // 记录登录日志
         LogininforEvent logininforEvent = new LogininforEvent();
-        logininforEvent.setTenantId(user.getTenantId());
-        logininforEvent.setUsername(user.getUsername());
+        logininforEvent.setTenantId(tenantId);
+        logininforEvent.setUsername(username);
         logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
         logininforEvent.setMessage(MessageUtils.message("user.login.success"));
         logininforEvent.setRequest(ServletUtils.getRequest());
         SpringUtils.context().publishEvent(logininforEvent);
         // 更新登录信息
-        loginService.recordLoginInfo(user.getUserId(), ip);
+        loginService.recordLoginInfo((Long) loginModel.getExtra(LoginHelper.USER_KEY), ip);
         log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
     }
 

+ 12 - 2
ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java

@@ -4,6 +4,7 @@ package org.dromara.web.service;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.system.domain.SysClient;
+import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.web.domain.vo.LoginVo;
 
 /**
@@ -17,8 +18,13 @@ public interface IAuthStrategy {
 
     /**
      * 登录
+     *
+     * @param body      登录对象
+     * @param client    授权管理视图对象
+     * @param grantType 授权类型
+     * @return 登录验证信息
      */
-    static LoginVo login(String body, SysClient client, String grantType) {
+    static LoginVo login(String body, SysClientVo client, String grantType) {
         // 授权类型和客户端id
         String beanName = grantType + BASE_NAME;
         if (!SpringUtils.containsBean(beanName)) {
@@ -30,7 +36,11 @@ public interface IAuthStrategy {
 
     /**
      * 登录
+     *
+     * @param body   登录对象
+     * @param client 授权管理视图对象
+     * @return 登录验证信息
      */
-    LoginVo login(String body, SysClient client);
+    LoginVo login(String body, SysClientVo client);
 
 }

+ 29 - 13
ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java

@@ -5,6 +5,7 @@ import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.lock.annotation.Lock4j;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.model.AuthUser;
@@ -15,6 +16,7 @@ import org.dromara.common.core.domain.dto.RoleDTO;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.enums.LoginType;
 import org.dromara.common.core.enums.TenantStatus;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.exception.user.UserException;
 import org.dromara.common.core.utils.*;
 import org.dromara.common.log.event.LogininforEvent;
@@ -25,13 +27,9 @@ import org.dromara.common.tenant.exception.TenantException;
 import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.system.domain.SysUser;
 import org.dromara.system.domain.bo.SysSocialBo;
-import org.dromara.system.domain.vo.SysSocialVo;
-import org.dromara.system.domain.vo.SysTenantVo;
-import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.system.domain.vo.*;
 import org.dromara.system.mapper.SysUserMapper;
-import org.dromara.system.service.ISysPermissionService;
-import org.dromara.system.service.ISysSocialService;
-import org.dromara.system.service.ISysTenantService;
+import org.dromara.system.service.*;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
@@ -59,6 +57,8 @@ public class SysLoginService {
     private final ISysTenantService tenantService;
     private final ISysPermissionService permissionService;
     private final ISysSocialService sysSocialService;
+    private final ISysRoleService roleService;
+    private final ISysDeptService deptService;
     private final SysUserMapper userMapper;
 
 
@@ -66,20 +66,28 @@ public class SysLoginService {
      * 绑定第三方用户
      *
      * @param authUserData 授权响应实体
-     * @return 统一响应实体
      */
+    @Lock4j
     public void socialRegister(AuthUser authUserData) {
         String authId = authUserData.getSource() + authUserData.getUuid();
         // 第三方用户信息
         SysSocialBo bo = BeanUtil.toBean(authUserData, SysSocialBo.class);
         BeanUtil.copyProperties(authUserData.getToken(), bo);
-        bo.setUserId(LoginHelper.getUserId());
+        Long userId = LoginHelper.getUserId();
+        bo.setUserId(userId);
         bo.setAuthId(authId);
         bo.setOpenId(authUserData.getUuid());
         bo.setUserName(authUserData.getUsername());
         bo.setNickName(authUserData.getNickname());
+        List<SysSocialVo> checkList = sysSocialService.selectByAuthId(authId);
+        if (CollUtil.isNotEmpty(checkList)) {
+            throw new ServiceException("此三方账号已经被绑定!");
+        }
         // 查询是否已经绑定用户
-        List<SysSocialVo> list = sysSocialService.selectByAuthId(authId);
+        SysSocialBo params = new SysSocialBo();
+        params.setUserId(userId);
+        params.setSource(bo.getSource());
+        List<SysSocialVo> list = sysSocialService.queryList(params);
         if (CollUtil.isEmpty(list)) {
             // 没有绑定用户, 新增用户信息
             sysSocialService.insertByBo(bo);
@@ -87,6 +95,8 @@ public class SysLoginService {
             // 更新用户信息
             bo.setId(list.get(0).getId());
             sysSocialService.updateByBo(bo);
+            // 如果要绑定的平台账号已经被绑定过了 是否抛异常自行决断
+            // throw new ServiceException("此平台账号已经被绑定!");
         }
     }
 
@@ -132,7 +142,6 @@ public class SysLoginService {
         SpringUtils.context().publishEvent(logininforEvent);
     }
 
-
     /**
      * 构建登录用户
      */
@@ -146,9 +155,16 @@ public class SysLoginService {
         loginUser.setUserType(user.getUserType());
         loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
         loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
-        loginUser.setDeptName(ObjectUtil.isNull(user.getDept()) ? "" : user.getDept().getDeptName());
-        List<RoleDTO> roles = BeanUtil.copyToList(user.getRoles(), RoleDTO.class);
-        loginUser.setRoles(roles);
+        TenantHelper.dynamic(user.getTenantId(), () -> {
+            SysDeptVo dept = null;
+            if (ObjectUtil.isNotNull(user.getDeptId())) {
+                dept = deptService.selectDeptById(user.getDeptId());
+            }
+            loginUser.setDeptName(ObjectUtil.isNull(dept) ? "" : dept.getDeptName());
+            loginUser.setDeptCategory(ObjectUtil.isNull(dept) ? "" : dept.getDeptCategory());
+            List<SysRoleVo> roles = roleService.selectRolesByUserId(user.getUserId());
+            loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
+        });
         return loginUser;
     }
 

+ 2 - 4
ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java

@@ -1,7 +1,6 @@
 package org.dromara.web.service;
 
 import cn.dev33.satoken.secure.BCrypt;
-import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.constant.Constants;
@@ -61,8 +60,7 @@ public class SysRegisterService {
 
         boolean exist = TenantHelper.dynamic(tenantId, () -> {
             return userMapper.exists(new LambdaQueryWrapper<SysUser>()
-                .eq(SysUser::getUserName, sysUser.getUserName())
-                .ne(ObjectUtil.isNotNull(sysUser.getUserId()), SysUser::getUserId, sysUser.getUserId()));
+                .eq(SysUser::getUserName, sysUser.getUserName()));
         });
         if (exist) {
             throw new UserException("user.register.save.error", username);
@@ -82,7 +80,7 @@ public class SysRegisterService {
      * @param uuid     唯一标识
      */
     public void validateCaptcha(String tenantId, String username, String code, String uuid) {
-        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
+        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
         String captcha = RedisUtils.getCacheObject(verifyKey);
         RedisUtils.deleteObject(verifyKey);
         if (captcha == null) {

+ 4 - 5
ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java

@@ -23,6 +23,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.system.domain.SysClient;
 import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysUserVo;
 import org.dromara.system.mapper.SysUserMapper;
 import org.dromara.web.domain.vo.LoginVo;
@@ -44,7 +45,7 @@ public class EmailAuthStrategy implements IAuthStrategy {
     private final SysUserMapper userMapper;
 
     @Override
-    public LoginVo login(String body, SysClient client) {
+    public LoginVo login(String body, SysClientVo client) {
         EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
         ValidatorUtils.validate(loginBody);
         String tenantId = loginBody.getTenantId();
@@ -90,9 +91,7 @@ public class EmailAuthStrategy implements IAuthStrategy {
 
     private SysUserVo loadUserByEmail(String tenantId, String email) {
         return TenantHelper.dynamic(tenantId, () -> {
-            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
-                .select(SysUser::getEmail, SysUser::getStatus)
-                .eq(SysUser::getEmail, email));
+            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
             if (ObjectUtil.isNull(user)) {
                 log.info("登录用户:{} 不存在.", email);
                 throw new UserException("user.not.exists", email);
@@ -100,7 +99,7 @@ public class EmailAuthStrategy implements IAuthStrategy {
                 log.info("登录用户:{} 已被停用.", email);
                 throw new UserException("user.blocked", email);
             }
-            return userMapper.selectUserByEmail(email);
+            return user;
         });
     }
 

+ 4 - 5
ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java

@@ -26,6 +26,7 @@ import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.common.web.config.properties.CaptchaProperties;
 import org.dromara.system.domain.SysClient;
 import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysUserVo;
 import org.dromara.system.mapper.SysUserMapper;
 import org.dromara.web.domain.vo.LoginVo;
@@ -48,7 +49,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
     private final SysUserMapper userMapper;
 
     @Override
-    public LoginVo login(String body, SysClient client) {
+    public LoginVo login(String body, SysClientVo client) {
         PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
         ValidatorUtils.validate(loginBody);
         String tenantId = loginBody.getTenantId();
@@ -109,9 +110,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
 
     private SysUserVo loadUserByUsername(String tenantId, String username) {
         return TenantHelper.dynamic(tenantId, () -> {
-            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
-                .select(SysUser::getUserName, SysUser::getStatus)
-                .eq(SysUser::getUserName, username));
+            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
             if (ObjectUtil.isNull(user)) {
                 log.info("登录用户:{} 不存在.", username);
                 throw new UserException("user.not.exists", username);
@@ -119,7 +118,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
                 log.info("登录用户:{} 已被停用.", username);
                 throw new UserException("user.blocked", username);
             }
-            return userMapper.selectUserByUserName(username);
+            return user;
         });
     }
 

+ 4 - 5
ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java

@@ -23,6 +23,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.system.domain.SysClient;
 import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysUserVo;
 import org.dromara.system.mapper.SysUserMapper;
 import org.dromara.web.domain.vo.LoginVo;
@@ -44,7 +45,7 @@ public class SmsAuthStrategy implements IAuthStrategy {
     private final SysUserMapper userMapper;
 
     @Override
-    public LoginVo login(String body, SysClient client) {
+    public LoginVo login(String body, SysClientVo client) {
         SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
         ValidatorUtils.validate(loginBody);
         String tenantId = loginBody.getTenantId();
@@ -90,9 +91,7 @@ public class SmsAuthStrategy implements IAuthStrategy {
 
     private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
         return TenantHelper.dynamic(tenantId, () -> {
-            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
-                .select(SysUser::getPhonenumber, SysUser::getStatus)
-                .eq(SysUser::getPhonenumber, phonenumber));
+            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
             if (ObjectUtil.isNull(user)) {
                 log.info("登录用户:{} 不存在.", phonenumber);
                 throw new UserException("user.not.exists", phonenumber);
@@ -100,7 +99,7 @@ public class SmsAuthStrategy implements IAuthStrategy {
                 log.info("登录用户:{} 已被停用.", phonenumber);
                 throw new UserException("user.blocked", phonenumber);
             }
-            return userMapper.selectUserByPhonenumber(phonenumber);
+            return user;
         });
     }
 

+ 13 - 12
ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java

@@ -7,7 +7,6 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.http.Method;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.model.AuthResponse;
@@ -23,8 +22,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.social.config.properties.SocialProperties;
 import org.dromara.common.social.utils.SocialUtils;
 import org.dromara.common.tenant.helper.TenantHelper;
-import org.dromara.system.domain.SysClient;
-import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysSocialVo;
 import org.dromara.system.domain.vo.SysUserVo;
 import org.dromara.system.mapper.SysUserMapper;
@@ -59,7 +57,7 @@ public class SocialAuthStrategy implements IAuthStrategy {
      * @param client   客户端信息
      */
     @Override
-    public LoginVo login(String body, SysClient client) {
+    public LoginVo login(String body, SysClientVo client) {
         SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
         ValidatorUtils.validate(loginBody);
         AuthResponse<AuthUser> response = SocialUtils.loginAuth(
@@ -83,11 +81,16 @@ public class SocialAuthStrategy implements IAuthStrategy {
         if (CollUtil.isEmpty(list)) {
             throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
         }
-        Optional<SysSocialVo> opt = list.stream().filter(x -> x.getTenantId().equals(loginBody.getTenantId())).findAny();
-        if (opt.isEmpty()) {
-            throw new ServiceException("对不起,你没有权限登录当前租户!");
+        SysSocialVo social;
+        if (TenantHelper.isEnable()) {
+            Optional<SysSocialVo> opt = list.stream().filter(x -> x.getTenantId().equals(loginBody.getTenantId())).findAny();
+            if (opt.isEmpty()) {
+                throw new ServiceException("对不起,你没有权限登录当前租户!");
+            }
+            social = opt.get();
+        } else {
+            social = list.get(0);
         }
-        SysSocialVo social = opt.get();
         // 查找用户
         SysUserVo user = loadUser(social.getTenantId(), social.getUserId());
 
@@ -114,9 +117,7 @@ public class SocialAuthStrategy implements IAuthStrategy {
 
     private SysUserVo loadUser(String tenantId, Long userId) {
         return TenantHelper.dynamic(tenantId, () -> {
-            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
-                .select(SysUser::getUserName, SysUser::getStatus)
-                .eq(SysUser::getUserId, userId));
+            SysUserVo user = userMapper.selectVoById(userId);
             if (ObjectUtil.isNull(user)) {
                 log.info("登录用户:{} 不存在.", "");
                 throw new UserException("user.not.exists", "");
@@ -124,7 +125,7 @@ public class SocialAuthStrategy implements IAuthStrategy {
                 log.info("登录用户:{} 已被停用.", "");
                 throw new UserException("user.blocked", "");
             }
-            return userMapper.selectUserByUserName(user.getUserName());
+            return user;
         });
     }
 

+ 3 - 2
ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java

@@ -12,6 +12,7 @@ import org.dromara.common.core.utils.ValidatorUtils;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.system.domain.SysClient;
+import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysUserVo;
 import org.dromara.web.domain.vo.LoginVo;
 import org.dromara.web.service.IAuthStrategy;
@@ -19,7 +20,7 @@ import org.dromara.web.service.SysLoginService;
 import org.springframework.stereotype.Service;
 
 /**
- * 邮件认证策略
+ * 小程序认证策略
  *
  * @author Michelle.Chung
  */
@@ -31,7 +32,7 @@ public class XcxAuthStrategy implements IAuthStrategy {
     private final SysLoginService loginService;
 
     @Override
-    public LoginVo login(String body, SysClient client) {
+    public LoginVo login(String body, SysClientVo client) {
         XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
         ValidatorUtils.validate(loginBody);
         // xcxCode 为 小程序调用 wx.login 授权后获取

+ 51 - 42
ruoyi-admin/src/main/resources/application-dev.yml

@@ -8,21 +8,19 @@ spring.boot.admin.client:
   username: ruoyi
   password: 123456
 
---- # powerjob 配置
-powerjob:
-  worker:
-    # 如何开启调度中心请查看文档教程
-    enabled: false
-    # 需要先在 powerjob 登录页执行应用注册后才能使用
-    app-name: ruoyi-worker
-    allow-lazy-connect-server: false
-    max-appended-wf-context-length: 4096
-    max-result-length: 4096
-    # 28080 端口 随着主应用端口飘逸 避免集群冲突
-    port: 2${server.port}
-    protocol: http
-    server-address: 127.0.0.1:7700
-    store-strategy: disk
+--- # snail-job 配置
+snail-job:
+  enabled: true
+  # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
+  group-name: "ruoyi_group"
+  # SnailJob 接入验证令牌 详见 script/sql/snail_job.sql `sj_group_config` 表
+  token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
+  server:
+    host: 127.0.0.1
+    port: 1788
+  # 详见 script/sql/snail_job.sql `sj_namespace` 表
+  namespace: ${spring.profiles.active}
+
 
 --- # 数据源配置
 spring:
@@ -43,7 +41,7 @@ spring:
           driverClassName: com.mysql.cj.jdbc.Driver
           # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
           # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username: root
           password: root
         # 从库数据源
@@ -51,7 +49,7 @@ spring:
           lazy: true
           type: ${spring.datasource.type}
           driverClassName: com.mysql.cj.jdbc.Driver
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username:
           password:
 #        oracle:
@@ -149,36 +147,40 @@ mail:
   connectionTimeout: 0
 
 --- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
-# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
+# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
 sms:
-  # 阿里云 dysmsapi.aliyuncs.com
-  alibaba:
-    #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
-    requestUrl: dysmsapi.aliyuncs.com
-    #阿里云的accessKey
-    accessKeyId: xxxxxxx
-    #阿里云的accessKeySecret
-    accessKeySecret: xxxxxxx
-    #短信签名
-    signature: 测试
-  tencent:
-    #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
-    requestUrl: sms.tencentcloudapi.com
-    #腾讯云的accessKey
-    accessKeyId: xxxxxxx
-    #腾讯云的accessKeySecret
-    accessKeySecret: xxxxxxx
-    #短信签名
-    signature: 测试
-    #短信sdkAppId
-    sdkAppId: appid
-    #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
-    territory: ap-guangzhou
+  # 配置源类型用于标定配置来源(interface,yaml)
+  config-type: yaml
+  # 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
+  restricted: true
+  # 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
+  minute-max: 1
+  # 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
+  account-max: 30
+  # 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
+  blends:
+    # 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
+    # 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
+    config1:
+      # 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: alibaba
+      # 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
+      access-key-id: 您的accessKey
+      # 称为accessSecret有些称之为apiSecret
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
+    config2:
+      # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: tencent
+      access-key-id: 您的accessKey
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
 
 
 --- # 三方授权
 justauth:
-  enabled: true
   # 前端外网访问地址
   address: http://localhost:80
   type:
@@ -189,6 +191,13 @@ justauth:
       client-id: 876892492581044224
       client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
       redirect-uri: ${justauth.address}/social-callback?source=maxkey
+    topiam:
+      # topiam 服务器地址
+      server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
+      client-id: 449c4*********937************759
+      client-secret: ac7***********1e0************28d
+      redirect-uri: ${justauth.address}/social-callback?source=topiam
+      scopes: [openid, email, phone, profile]
     qq:
       client-id: 10**********6
       client-secret: 1f7d08**********5b7**********29e

+ 50 - 42
ruoyi-admin/src/main/resources/application-prod.yml

@@ -11,21 +11,18 @@ spring.boot.admin.client:
   username: ruoyi
   password: 123456
 
---- # powerjob 配置
-powerjob:
-  worker:
-    # 如何开启调度中心请查看文档教程
-    enabled: false
-    # 需要先在 powerjob 登录页执行应用注册后才能使用
-    app-name: ruoyi-worker
-    allow-lazy-connect-server: false
-    max-appended-wf-context-length: 4096
-    max-result-length: 4096
-    # 28080 端口 随着主应用端口飘逸 避免集群冲突
-    port: 2${server.port}
-    protocol: http
-    server-address: 127.0.0.1:7700
-    store-strategy: disk
+--- # snail-job 配置
+snail-job:
+  enabled: false
+  # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
+  group-name: "ruoyi_group"
+  # SnailJob 接入验证令牌 详见 script/sql/snail_job.sql `sj_group_config` 表
+  token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
+  server:
+    host: 127.0.0.1
+    port: 1788
+  # 详见 script/sql/snail_job.sql `sj_namespace` 表
+  namespace: ${spring.profiles.active}
 
 --- # 数据源配置
 spring:
@@ -46,7 +43,7 @@ spring:
           driverClassName: com.mysql.cj.jdbc.Driver
           # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
           # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username: root
           password: root
         # 从库数据源
@@ -54,7 +51,7 @@ spring:
           lazy: true
           type: ${spring.datasource.type}
           driverClassName: com.mysql.cj.jdbc.Driver
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username:
           password:
 #        oracle:
@@ -152,35 +149,39 @@ mail:
   connectionTimeout: 0
 
 --- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
-# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
+# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
 sms:
-  # 阿里云 dysmsapi.aliyuncs.com
-  alibaba:
-    #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
-    requestUrl: dysmsapi.aliyuncs.com
-    #阿里云的accessKey
-    accessKeyId: xxxxxxx
-    #阿里云的accessKeySecret
-    accessKeySecret: xxxxxxx
-    #短信签名
-    signature: 测试
-  tencent:
-    #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
-    requestUrl: sms.tencentcloudapi.com
-    #腾讯云的accessKey
-    accessKeyId: xxxxxxx
-    #腾讯云的accessKeySecret
-    accessKeySecret: xxxxxxx
-    #短信签名
-    signature: 测试
-    #短信sdkAppId
-    sdkAppId: appid
-    #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
-    territory: ap-guangzhou
+  # 配置源类型用于标定配置来源(interface,yaml)
+  config-type: yaml
+  # 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
+  restricted: true
+  # 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
+  minute-max: 1
+  # 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
+  account-max: 30
+  # 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
+  blends:
+    # 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
+    # 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
+    config1:
+      # 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: alibaba
+      # 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
+      access-key-id: 您的accessKey
+      # 称为accessSecret有些称之为apiSecret
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
+    config2:
+      # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: tencent
+      access-key-id: 您的accessKey
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
 
 --- # 三方授权
 justauth:
-  enabled: true
   # 前端外网访问地址
   address: http://localhost:80
   type:
@@ -191,6 +192,13 @@ justauth:
       client-id: 876892492581044224
       client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
       redirect-uri: ${justauth.address}/social-callback?source=maxkey
+    topiam:
+      # topiam 服务器地址
+      server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
+      client-id: 449c4*********937************759
+      client-secret: ac7***********1e0************28d
+      redirect-uri: ${justauth.address}/social-callback?source=topiam
+      scopes: [ openid, email, phone, profile ]
     qq:
       client-id: 10**********6
       client-secret: 1f7d08**********5b7**********29e

+ 28 - 4
ruoyi-admin/src/main/resources/application.yml

@@ -5,7 +5,7 @@ ruoyi:
   # 版本
   version: ${revision}
   # 版权年份
-  copyrightYear: 2023
+  copyrightYear: 2024
 
 captcha:
   enable: true
@@ -46,7 +46,7 @@ logging:
   level:
     org.dromara: @logging.level@
     org.springframework: warn
-    tech.powerjob.worker.background: warn
+    org.mybatis.spring.mapper: error
   config: classpath:logback-plus.xml
 
 # 用户配置
@@ -61,6 +61,10 @@ user:
 spring:
   application:
     name: ${ruoyi.name}
+  threads:
+    # 开启虚拟线程 仅jdk21可用
+    virtual:
+      enabled: false
   # 资源信息
   messages:
     # 国际化资源文件路径
@@ -75,6 +79,8 @@ spring:
       # 设置总上传的文件大小
       max-request-size: 20MB
   mvc:
+    # 设置静态资源路径 防止所有请求都去查静态资源
+    static-path-pattern: /static/**
     format:
       date-time: yyyy-MM-dd HH:mm:ss
   jackson:
@@ -138,8 +144,7 @@ tenant:
 # MyBatisPlus配置
 # https://baomidou.com/config/
 mybatis-plus:
-  # 不支持多包, 如有需要可在注解配置 或 提升扫包等级
-  # 例如 com.**.**.mapper
+  # 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
   mapperPackage: org.dromara.**.mapper
   # 对应的 XML 文件位置
   mapperLocations: classpath*:mapper/**/*Mapper.xml
@@ -226,6 +231,7 @@ xss:
   urlPatterns: /system/*,/monitor/*,/tool/*
 
 # 全局线程池相关配置
+# 如使用JDK21请直接使用虚拟线程 不要开启此配置
 thread-pool:
   # 是否开启线程池
   enabled: false
@@ -261,3 +267,21 @@ websocket:
   path: /resource/websocket
   # 设置访问源地址
   allowedOrigins: '*'
+
+--- #flowable配置
+flowable:
+  async-executor-activate: false #关闭定时任务JOB
+  #  将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
+  database-schema-update: true
+  activity-font-name: 宋体
+  label-font-name: 宋体
+  annotation-font-name: 宋体
+  # 关闭各个模块生成表,目前只使用工作流基础表
+  idm:
+    enabled: false
+  cmmn:
+    enabled: false
+  dmn:
+    enabled: false
+  app:
+    enabled: false

BIN
ruoyi-admin/src/main/resources/ip2region.xdb


+ 1 - 1
ruoyi-common/ruoyi-common-bom/pom.xml

@@ -14,7 +14,7 @@
     </description>
 
     <properties>
-        <revision>5.1.2</revision>
+        <revision>5.2.0-BETA</revision>
     </properties>
 
     <dependencyManagement>

+ 5 - 0
ruoyi-common/ruoyi-common-core/pom.xml

@@ -94,6 +94,11 @@
             <artifactId>ip2region</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>transmittable-thread-local</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 2 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java

@@ -2,6 +2,7 @@ package org.dromara.common.core.config;
 
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableAsync;
 
 /**
  * 程序注解配置
@@ -11,6 +12,7 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
 @AutoConfiguration
 // 表示通过aop框架暴露该代理对象,AopContext能够访问
 @EnableAspectJAutoProxy(exposeProxy = true)
+@EnableAsync(proxyTargetClass = true)
 public class ApplicationConfig {
 
 }

+ 6 - 2
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java

@@ -5,18 +5,19 @@ import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.SpringUtils;
 import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.core.task.VirtualThreadTaskExecutor;
 import org.springframework.scheduling.annotation.AsyncConfigurer;
-import org.springframework.scheduling.annotation.EnableAsync;
 
 import java.util.Arrays;
 import java.util.concurrent.Executor;
 
 /**
  * 异步配置
+ * <p>
+ * 如果未使用虚拟线程则生效
  *
  * @author Lion Li
  */
-@EnableAsync(proxyTargetClass = true)
 @AutoConfiguration
 public class AsyncConfig implements AsyncConfigurer {
 
@@ -25,6 +26,9 @@ public class AsyncConfig implements AsyncConfigurer {
      */
     @Override
     public Executor getAsyncExecutor() {
+        if(SpringUtils.isVirtual()) {
+            return new VirtualThreadTaskExecutor("async-");
+        }
         return SpringUtils.getBean("scheduledExecutorService");
     }
 

+ 5 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java

@@ -35,6 +35,11 @@ public interface CacheNames {
      */
     String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
 
+    /**
+     * 客户端
+     */
+    String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
+
     /**
      * 用户账户
      */

+ 54 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java

@@ -0,0 +1,54 @@
+package org.dromara.common.core.constant;
+
+import cn.hutool.core.lang.RegexPool;
+
+/**
+ * 常用正则表达式字符串
+ * <p>
+ * 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/
+ *
+ * @author Feng
+ */
+public interface RegexConstants extends RegexPool {
+
+    /**
+     * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+     */
+    String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";
+
+    /**
+     * 权限标识必须符合 tool:build:list 格式,或者空字符串
+     */
+    String PERMISSION_STRING = "^(|^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+:[a-zA-Z0-9_]+)$";
+
+    /**
+     * 身份证号码(后6位)
+     */
+    String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
+
+    /**
+     * QQ号码
+     */
+    String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$";
+
+    /**
+     * 邮政编码
+     */
+    String POSTAL_CODE = "^[1-9]\\d{5}$";
+
+    /**
+     * 注册账号
+     */
+    String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$";
+
+    /**
+     * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+     */
+    String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
+
+    /**
+     * 通用状态(0表示正常,1表示停用)
+     */
+    String STATUS = "^[01]$";
+
+}

+ 46 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/OssDTO.java

@@ -0,0 +1,46 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * OSS对象
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class OssDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 对象存储主键
+     */
+    private Long ossId;
+
+    /**
+     * 文件名
+     */
+    private String fileName;
+
+    /**
+     * 原名
+     */
+    private String originalName;
+
+    /**
+     * 文件后缀名
+     */
+    private String fileSuffix;
+
+    /**
+     * URL地址
+     */
+    private String url;
+
+}

+ 4 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/RoleDTO.java

@@ -3,6 +3,7 @@ package org.dromara.common.core.domain.dto;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import java.io.Serial;
 import java.io.Serializable;
 
 /**
@@ -15,6 +16,9 @@ import java.io.Serializable;
 @NoArgsConstructor
 public class RoleDTO implements Serializable {
 
+    @Serial
+    private static final long serialVersionUID = 1L;
+
     /**
      * 角色ID
      */

+ 73 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java

@@ -0,0 +1,73 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 用户
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@NoArgsConstructor
+public class UserDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 用户账号
+     */
+    private String userName;
+
+    /**
+     * 用户昵称
+     */
+    private String nickName;
+
+    /**
+     * 用户类型(sys_user系统用户)
+     */
+    private String userType;
+
+    /**
+     * 用户邮箱
+     */
+    private String email;
+
+    /**
+     * 手机号码
+     */
+    private String phonenumber;
+
+    /**
+     * 用户性别(0男 1女 2未知)
+     */
+    private String sex;
+
+    /**
+     * 帐号状态(0正常 1停用)
+     */
+    private String status;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+}

+ 5 - 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java

@@ -14,7 +14,6 @@ import java.util.Set;
  *
  * @author Lion Li
  */
-
 @Data
 @NoArgsConstructor
 public class LoginUser implements Serializable {
@@ -37,6 +36,11 @@ public class LoginUser implements Serializable {
      */
     private Long deptId;
 
+    /**
+     * 部门类别编码
+     */
+    private String deptCategory;
+
     /**
      * 部门名
      */

+ 1 - 12
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java

@@ -1,9 +1,6 @@
 package org.dromara.common.core.exception;
 
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 import java.io.Serial;
 
@@ -45,19 +42,11 @@ public final class ServiceException extends RuntimeException {
         this.code = code;
     }
 
-    public String getDetailMessage() {
-        return detailMessage;
-    }
-
     @Override
     public String getMessage() {
         return message;
     }
 
-    public Integer getCode() {
-        return code;
-    }
-
     public ServiceException setMessage(String message) {
         this.message = message;
         return this;

+ 52 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java

@@ -0,0 +1,52 @@
+package org.dromara.common.core.factory;
+
+import cn.hutool.core.lang.PatternPool;
+import org.dromara.common.core.constant.RegexConstants;
+
+import java.util.regex.Pattern;
+
+/**
+ * 正则表达式模式池工厂
+ * <p>初始化的时候将正则表达式加入缓存池当中</p>
+ * <p>提高正则表达式的性能,避免重复编译相同的正则表达式</p>
+ *
+ * @author 21001
+ */
+public class RegexPatternPoolFactory extends PatternPool {
+
+    /**
+     * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+     */
+    public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);
+
+    /**
+     * 身份证号码(后6位)
+     */
+    public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6);
+
+    /**
+     * QQ号码
+     */
+    public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER);
+
+    /**
+     * 邮政编码
+     */
+    public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE);
+
+    /**
+     * 注册账号
+     */
+    public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT);
+
+    /**
+     * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+     */
+    public static final Pattern PASSWORD = get(RegexConstants.PASSWORD);
+
+    /**
+     * 通用状态(0表示正常,1表示停用)
+     */
+    public static final Pattern STATUS = get(RegexConstants.STATUS);
+
+}

+ 11 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/OssService.java

@@ -1,5 +1,9 @@
 package org.dromara.common.core.service;
 
+import org.dromara.common.core.domain.dto.OssDTO;
+
+import java.util.List;
+
 /**
  * 通用 OSS服务
  *
@@ -15,4 +19,11 @@ public interface OssService {
      */
     String selectUrlByIds(String ossIds);
 
+    /**
+     * 通过ossId查询列表
+     *
+     * @param ossIds ossId串逗号分隔
+     * @return 列表
+     */
+    List<OssDTO> selectByIds(String ossIds);
 }

+ 44 - 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java

@@ -1,5 +1,9 @@
 package org.dromara.common.core.service;
 
+import org.dromara.common.core.domain.dto.UserDTO;
+
+import java.util.List;
+
 /**
  * 通用 用户服务
  *
@@ -19,8 +23,47 @@ public interface UserService {
      * 通过用户ID查询用户账户
      *
      * @param userId 用户ID
-     * @return 用户账户
+     * @return 用户名称
      */
     String selectNicknameById(Long userId);
 
+    /**
+     * 通过用户ID查询用户账户
+     *
+     * @param userIds 用户ID 多个用逗号隔开
+     * @return 用户名称
+     */
+    String selectNicknameByIds(String userIds);
+
+    /**
+     * 通过用户ID查询用户手机号
+     *
+     * @param userId 用户id
+     * @return 用户手机号
+     */
+    String selectPhonenumberById(Long userId);
+
+    /**
+     * 通过用户ID查询用户邮箱
+     *
+     * @param userId 用户id
+     * @return 用户邮箱
+     */
+    String selectEmailById(Long userId);
+
+    /**
+     * 通过用户ID查询用户列表
+     *
+     * @param userIds 用户ids
+     * @return 用户列表
+     */
+    List<UserDTO> selectListByIds(List<Long> userIds);
+
+    /**
+     * 通过角色ID查询用户ID
+     *
+     * @param roleIds 角色ids
+     * @return 用户ids
+     */
+    List<Long> selectUserIdsByRoleIds(List<Long> roleIds);
 }

+ 6 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java

@@ -3,7 +3,9 @@ package org.dromara.common.core.utils;
 import cn.hutool.extra.spring.SpringUtil;
 import org.springframework.aop.framework.AopContext;
 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.boot.autoconfigure.thread.Threading;
 import org.springframework.context.ApplicationContext;
+import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Component;
 
 /**
@@ -59,4 +61,8 @@ public final class SpringUtils extends SpringUtil {
         return getApplicationContext();
     }
 
+    public static boolean isVirtual() {
+        return Threading.VIRTUAL.isActive(getBean(Environment.class));
+    }
+
 }

+ 2 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java

@@ -22,6 +22,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
 
     public static final String SEPARATOR = ",";
 
+    public static final String SLASH = "/";
+
     /**
      * 获取参数不为空值
      *

+ 10 - 3
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java

@@ -1,11 +1,11 @@
 package org.dromara.common.core.utils;
 
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
-
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.ConstraintViolationException;
 import jakarta.validation.Validator;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
 import java.util.Set;
 
 /**
@@ -18,6 +18,13 @@ public class ValidatorUtils {
 
     private static final Validator VALID = SpringUtils.getBean(Validator.class);
 
+    /**
+     * 对给定对象进行参数校验,并根据指定的校验组进行校验
+     *
+     * @param object 要进行校验的对象
+     * @param groups 校验组
+     * @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
+     */
     public static <T> void validate(T object, Class<?>... groups) {
         Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
         if (!validate.isEmpty()) {

+ 30 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java

@@ -0,0 +1,30 @@
+package org.dromara.common.core.utils.regex;
+
+
+import cn.hutool.core.util.ReUtil;
+import org.dromara.common.core.constant.RegexConstants;
+
+/**
+ * 正则相关工具类
+ *
+ * @author Feng
+ */
+public final class RegexUtils extends ReUtil {
+
+    /**
+     * 从输入字符串中提取匹配的部分,如果没有匹配则返回默认值
+     *
+     * @param input        要提取的输入字符串
+     * @param regex        用于匹配的正则表达式,可以使用 {@link RegexConstants} 中定义的常量
+     * @param defaultInput 如果没有匹配时返回的默认值
+     * @return 如果找到匹配的部分,则返回匹配的部分,否则返回默认值
+     */
+    public static String extractFromString(String input, String regex, String defaultInput) {
+        try {
+            return ReUtil.get(regex, input, 1);
+        } catch (Exception e) {
+            return defaultInput;
+        }
+    }
+
+}

+ 105 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java

@@ -0,0 +1,105 @@
+package org.dromara.common.core.utils.regex;
+
+import cn.hutool.core.exceptions.ValidateException;
+import cn.hutool.core.lang.Validator;
+import org.dromara.common.core.factory.RegexPatternPoolFactory;
+
+import java.util.regex.Pattern;
+
+/**
+ * 正则字段校验器
+ * 主要验证字段非空、是否为满足指定格式等
+ *
+ * @author Feng
+ */
+public class RegexValidator extends Validator {
+
+    /**
+     * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+     */
+    public static final Pattern DICTIONARY_TYPE = RegexPatternPoolFactory.DICTIONARY_TYPE;
+
+    /**
+     * 身份证号码(后6位)
+     */
+    public static final Pattern ID_CARD_LAST_6 = RegexPatternPoolFactory.ID_CARD_LAST_6;
+
+    /**
+     * QQ号码
+     */
+    public static final Pattern QQ_NUMBER = RegexPatternPoolFactory.QQ_NUMBER;
+
+    /**
+     * 邮政编码
+     */
+    public static final Pattern POSTAL_CODE = RegexPatternPoolFactory.POSTAL_CODE;
+
+    /**
+     * 注册账号
+     */
+    public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
+
+    /**
+     * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+     */
+    public static final Pattern PASSWORD = RegexPatternPoolFactory.PASSWORD;
+
+    /**
+     * 通用状态(0表示正常,1表示停用)
+     */
+    public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
+
+
+    /**
+     * 检查输入的账号是否匹配预定义的规则
+     *
+     * @param value 要验证的账号
+     * @return 如果账号符合规则,返回 true;否则,返回 false。
+     */
+    public static boolean isAccount(CharSequence value) {
+        return isMatchRegex(ACCOUNT, value);
+    }
+
+    /**
+     * 验证输入的账号是否符合规则,如果不符合,则抛出 ValidateException 异常
+     *
+     * @param value    要验证的账号
+     * @param errorMsg 验证失败时抛出的异常消息
+     * @param <T>      CharSequence 的子类型
+     * @return 如果验证通过,返回输入的账号
+     * @throws ValidateException 如果验证失败
+     */
+    public static <T extends CharSequence> T validateAccount(T value, String errorMsg) throws ValidateException {
+        if (!isAccount(value)) {
+            throw new ValidateException(errorMsg);
+        }
+        return value;
+    }
+
+    /**
+     * 检查输入的状态是否匹配预定义的规则
+     *
+     * @param value 要验证的状态
+     * @return 如果状态符合规则,返回 true;否则,返回 false。
+     */
+    public static boolean isStatus(CharSequence value) {
+        return isMatchRegex(STATUS, value);
+    }
+
+    /**
+     * 验证输入的状态是否符合规则,如果不符合,则抛出 ValidateException 异常
+     *
+     * @param value    要验证的状态
+     * @param errorMsg 验证失败时抛出的异常消息
+     * @param <T>      CharSequence 的子类型
+     * @return 如果验证通过,返回输入的状态
+     * @throws ValidateException 如果验证失败
+     */
+    public static <T extends CharSequence> T validateStatus(T value, String errorMsg) throws ValidateException {
+        if (!isStatus(value)) {
+            throw new ValidateException(errorMsg);
+        }
+        return value;
+    }
+
+}

+ 12 - 5
ruoyi-common/ruoyi-common-encrypt/pom.xml

@@ -22,11 +22,6 @@
             <artifactId>ruoyi-common-core</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>org.mybatis.spring.boot</groupId>
-            <artifactId>mybatis-spring-boot-starter</artifactId>
-        </dependency>
-
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk15to18</artifactId>
@@ -42,6 +37,18 @@
             <artifactId>spring-webmvc</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <optional>true</optional>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.mybatis</groupId>
+                    <artifactId>mybatis-spring</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 11 - 3
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/EncryptorAutoConfiguration.java

@@ -1,5 +1,8 @@
 package org.dromara.common.encrypt.config;
 
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.encrypt.core.EncryptorManager;
 import org.dromara.common.encrypt.interceptor.MybatisDecryptInterceptor;
 import org.dromara.common.encrypt.interceptor.MybatisEncryptInterceptor;
@@ -16,17 +19,18 @@ import org.springframework.context.annotation.Bean;
  * @author 老马
  * @version 4.6.0
  */
-@AutoConfiguration
+@AutoConfiguration(after = MybatisPlusAutoConfiguration.class)
 @EnableConfigurationProperties(EncryptorProperties.class)
 @ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true")
+@Slf4j
 public class EncryptorAutoConfiguration {
 
     @Autowired
     private EncryptorProperties properties;
 
     @Bean
-    public EncryptorManager encryptorManager() {
-        return new EncryptorManager();
+    public EncryptorManager encryptorManager(MybatisPlusProperties mybatisPlusProperties) {
+        return new EncryptorManager(mybatisPlusProperties.getTypeAliasesPackage());
     }
 
     @Bean
@@ -38,4 +42,8 @@ public class EncryptorAutoConfiguration {
     public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorManager encryptorManager) {
         return new MybatisDecryptInterceptor(encryptorManager, properties);
     }
+
 }
+
+
+

+ 77 - 19
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java

@@ -1,14 +1,23 @@
 package org.dromara.common.encrypt.core;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ReflectUtil;
+import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.io.Resources;
+import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.encrypt.annotation.EncryptField;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.ClassMetadata;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.util.ClassUtils;
 
 import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
@@ -19,6 +28,7 @@ import java.util.stream.Collectors;
  * @version 4.6.0
  */
 @Slf4j
+@NoArgsConstructor
 public class EncryptorManager {
 
     /**
@@ -31,25 +41,24 @@ public class EncryptorManager {
      */
     Map<Class<?>, Set<Field>> fieldCache = new ConcurrentHashMap<>();
 
+    /**
+     * 构造方法传入类加密字段缓存
+     *
+     * @param typeAliasesPackage 实体类包
+     */
+    public EncryptorManager(String typeAliasesPackage) {
+        scanEncryptClasses(typeAliasesPackage);
+    }
+
+
     /**
      * 获取类加密字段缓存
      */
     public Set<Field> getFieldCache(Class<?> sourceClazz) {
-        return fieldCache.computeIfAbsent(sourceClazz, clazz -> {
-            Set<Field> fieldSet = new HashSet<>();
-            while (clazz != null) {
-                Field[] fields = clazz.getDeclaredFields();
-                fieldSet.addAll(Arrays.asList(fields));
-                clazz = clazz.getSuperclass();
-            }
-            fieldSet = fieldSet.stream().filter(field ->
-                    field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class)
-                .collect(Collectors.toSet());
-            for (Field field : fieldSet) {
-                field.setAccessible(true);
-            }
-            return fieldSet;
-        });
+        if (ObjectUtil.isNotNull(fieldCache)) {
+            return fieldCache.get(sourceClazz);
+        }
+        return null;
     }
 
     /**
@@ -97,4 +106,53 @@ public class EncryptorManager {
         return encryptor.decrypt(value);
     }
 
+    /**
+     * 通过 typeAliasesPackage 设置的扫描包 扫描缓存实体
+     */
+    private void scanEncryptClasses(String typeAliasesPackage) {
+        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+        CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
+        String[] packagePatternArray = StringUtils.splitPreserveAllTokens(typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
+        String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
+        try {
+            for (String packagePattern : packagePatternArray) {
+                String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
+                Resource[] resources = resolver.getResources(classpath + path + "/*.class");
+                for (Resource resource : resources) {
+                    ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
+                    Class<?> clazz = Resources.classForName(classMetadata.getClassName());
+                    Set<Field> encryptFieldSet = getEncryptFieldSetFromClazz(clazz);
+                    if (CollUtil.isNotEmpty(encryptFieldSet)) {
+                        fieldCache.put(clazz, encryptFieldSet);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("初始化数据安全缓存时出错:{}", e.getMessage());
+        }
+    }
+
+    /**
+     * 获得一个类的加密字段集合
+     */
+    private Set<Field> getEncryptFieldSetFromClazz(Class<?> clazz) {
+        Set<Field> fieldSet = new HashSet<>();
+        // 判断clazz如果是接口,内部类,匿名类就直接返回
+        if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {
+            return fieldSet;
+        }
+        while (clazz != null) {
+            Field[] fields = clazz.getDeclaredFields();
+            fieldSet.addAll(Arrays.asList(fields));
+            clazz = clazz.getSuperclass();
+        }
+        fieldSet = fieldSet.stream().filter(field ->
+                field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class)
+            .collect(Collectors.toSet());
+        for (Field field : fieldSet) {
+            field.setAccessible(true);
+        }
+        return fieldSet;
+    }
+
 }

+ 24 - 30
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java

@@ -11,14 +11,12 @@ import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.encrypt.annotation.ApiEncrypt;
 import org.dromara.common.encrypt.properties.ApiDecryptProperties;
 import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
 import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.HandlerExceptionResolver;
 import org.springframework.web.servlet.HandlerExecutionChain;
 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
 
 import java.io.IOException;
-import java.io.PrintWriter;
 
 
 /**
@@ -37,42 +35,38 @@ public class CryptoFilter implements Filter {
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
         HttpServletRequest servletRequest = (HttpServletRequest) request;
         HttpServletResponse servletResponse = (HttpServletResponse) response;
-
-        boolean responseFlag = false;
+        // 获取加密注解
+        ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
+        boolean responseFlag = apiEncrypt != null && apiEncrypt.response();
         ServletRequest requestWrapper = null;
         ServletResponse responseWrapper = null;
         EncryptResponseBodyWrapper responseBodyWrapper = null;
 
-        // 是否为 json 请求
-        if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
-            // 是否为 put 或者 post 请求
-            if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
-                // 是否存在加密标头
-                String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
-                // 获取加密注解
-                ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
-                responseFlag = apiEncrypt != null && apiEncrypt.response();
-                if (StringUtils.isNotBlank(headerValue)) {
-                    // 请求解密
-                    requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
-                } else {
-                    // 是否有注解,有就报错,没有放行
-                    if (ObjectUtil.isNotNull(apiEncrypt)) {
-                        HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
-                        exceptionResolver.resolveException(
-                            servletRequest, servletResponse, null,
-                            new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
-                        return;
-                    }
-                }
-                // 判断是否响应加密
-                if (responseFlag) {
-                    responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
-                    responseWrapper = responseBodyWrapper;
+        // 是否为 put 或者 post 请求
+        if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
+            // 是否存在加密标头
+            String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
+            if (StringUtils.isNotBlank(headerValue)) {
+                // 请求解密
+                requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
+            } else {
+                // 是否有注解,有就报错,没有放行
+                if (ObjectUtil.isNotNull(apiEncrypt)) {
+                    HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
+                    exceptionResolver.resolveException(
+                        servletRequest, servletResponse, null,
+                        new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
+                    return;
                 }
             }
         }
 
+        // 判断是否响应加密
+        if (responseFlag) {
+            responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
+            responseWrapper = responseBodyWrapper;
+        }
+
         chain.doFilter(
             ObjectUtil.defaultIfNull(requestWrapper, request),
             ObjectUtil.defaultIfNull(responseWrapper, response));

+ 1 - 0
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/EncryptResponseBodyWrapper.java

@@ -76,6 +76,7 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
         String encryptPassword = EncryptUtils.encryptByRsa(encryptAes, publicKey);
 
         // 设置响应头
+        servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
         servletResponse.setHeader(headerFlag, encryptPassword);
         servletResponse.setHeader("Access-Control-Allow-Origin", "*");
         servletResponse.setHeader("Access-Control-Allow-Methods", "*");

+ 4 - 0
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisDecryptInterceptor.java

@@ -73,7 +73,11 @@ public class MybatisDecryptInterceptor implements Interceptor {
             list.forEach(this::decryptHandler);
             return;
         }
+        // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错)
         Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
+        if(ObjectUtil.isNull(fields)){
+            return;
+        }
         try {
             for (Field field : fields) {
                 field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field));

+ 4 - 0
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java

@@ -82,7 +82,11 @@ public class MybatisEncryptInterceptor implements Interceptor {
             list.forEach(this::encryptHandler);
             return;
         }
+        // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错)
         Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
+        if(ObjectUtil.isNull(fields)){
+            return;
+        }
         try {
             for (Field field : fields) {
                 field.set(sourceObject, this.encryptField(Convert.toStr(field.get(sourceObject)), field));

+ 5 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/CellMerge.java

@@ -21,4 +21,9 @@ public @interface CellMerge {
 	 */
 	int index() default -1;
 
+    /**
+     * 合并需要依赖的其他字段名称
+     */
+    String[] mergeBy() default {};
+
 }

+ 46 - 36
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java

@@ -1,8 +1,12 @@
 package org.dromara.common.excel.core;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
 import com.alibaba.excel.annotation.ExcelProperty;
 import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.write.handler.WorkbookWriteHandler;
+import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext;
 import com.alibaba.excel.write.merge.AbstractMergeStrategy;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -15,10 +19,7 @@ import org.dromara.common.core.utils.reflect.ReflectUtils;
 import org.dromara.common.excel.annotation.CellMerge;
 
 import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * 列值重复合并策略
@@ -26,7 +27,7 @@ import java.util.Map;
  * @author Lion Li
  */
 @Slf4j
-public class CellMergeStrategy extends AbstractMergeStrategy {
+public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
 
     private final List<CellRangeAddress> cellList;
     private final boolean hasTitle;
@@ -41,17 +42,28 @@ public class CellMergeStrategy extends AbstractMergeStrategy {
 
     @Override
     protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
-        // judge the list is not null
-        if (CollUtil.isNotEmpty(cellList)) {
-            // the judge is necessary
-            if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
-                for (CellRangeAddress item : cellList) {
-                    sheet.addMergedRegion(item);
+        //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
+        final int rowIndex = cell.getRowIndex();
+        if (CollUtil.isNotEmpty(cellList)){
+            for (CellRangeAddress cellAddresses : cellList) {
+                final int firstRow = cellAddresses.getFirstRow();
+                if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
+                    cell.setBlank();
                 }
             }
         }
     }
 
+    @Override
+    public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
+        //当前表格写完后,统一写入
+        if (CollUtil.isNotEmpty(cellList)){
+            for (CellRangeAddress item : cellList) {
+                context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
+            }
+        }
+    }
+
     @SneakyThrows
     private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
         List<CellRangeAddress> cellList = new ArrayList<>();
@@ -93,35 +105,15 @@ public class CellMergeStrategy extends AbstractMergeStrategy {
                         // 空值跳过不合并
                         continue;
                     }
+
                     if (!cellValue.equals(val)) {
-                        if (i - repeatCell.getCurrent() > 1) {
+                        if ((i - repeatCell.getCurrent() > 1) && isMerge(list, i, field)) {
                             cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
                         }
                         map.put(field, new RepeatCell(val, i));
-                    } else if (j == 0) {
-                        if (i == list.size() - 1) {
-                            if (i > repeatCell.getCurrent()) {
-                                cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
-                            }
-                        }
-                    } else {
-                        // 判断前面的是否合并了
-                        RepeatCell firstCell = map.get(mergeFields.get(0));
-                        if (repeatCell.getCurrent() != firstCell.getCurrent()) {
-                            if (i == list.size() - 1) {
-                                if (i > repeatCell.getCurrent()) {
-                                    cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
-                                }
-                            } else if (repeatCell.getCurrent() < firstCell.getCurrent()) {
-                                if (i - repeatCell.getCurrent() > 1) {
-                                    cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
-                                }
-                                map.put(field, new RepeatCell(val, i));
-                            }
-                        } else if (i == list.size() - 1) {
-                            if (i > repeatCell.getCurrent()) {
-                                cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
-                            }
+                    } else if (i == list.size() - 1) {
+                        if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
+                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
                         }
                     }
                 }
@@ -130,6 +122,24 @@ public class CellMergeStrategy extends AbstractMergeStrategy {
         return cellList;
     }
 
+    private boolean isMerge(List<?> list, int i, Field field) {
+        boolean isMerge = true;
+        CellMerge cm = field.getAnnotation(CellMerge.class);
+        final String[] mergeBy = cm.mergeBy();
+        if (StrUtil.isAllNotBlank(mergeBy)) {
+            //比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
+            for (String fieldName : mergeBy) {
+                final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
+                final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
+                if (!Objects.equals(valPre, valCurrent)) {
+                    //依赖字段如有任一不等值,则标记为不可合并
+                    isMerge = false;
+                }
+            }
+        }
+        return isMerge;
+    }
+
     @Data
     @AllArgsConstructor
     static class RepeatCell {

+ 5 - 3
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java

@@ -20,6 +20,7 @@ import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.service.DictService;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.excel.annotation.ExcelDictFormat;
 import org.dromara.common.excel.annotation.ExcelEnumFormat;
 
@@ -99,15 +100,16 @@ public class ExcelDownHandler implements SheetWriteHandler {
                 ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class);
                 String dictType = format.dictType();
                 String converterExp = format.readConverterExp();
-                if (StrUtil.isNotBlank(dictType)) {
+                if (StringUtils.isNotBlank(dictType)) {
                     // 如果传递了字典名,则依据字典建立下拉
                     Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
                         .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
                         .values();
                     options = new ArrayList<>(values);
-                } else if (StrUtil.isNotBlank(converterExp)) {
+                } else if (StringUtils.isNotBlank(converterExp)) {
                     // 如果指定了确切的值,则直接解析确切的值
-                    options = StrUtil.split(converterExp, format.separator(), true, true);
+                    List<String> strList = StringUtils.splitList(converterExp, format.separator());
+                    options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]);
                 }
             } else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
                 // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑

+ 8 - 8
ruoyi-common/ruoyi-common-idempotent/src/main/java/org/dromara/common/idempotent/aspectj/RepeatSubmitAspect.java

@@ -4,6 +4,13 @@ import cn.dev33.satoken.SaManager;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.crypto.SecureUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
 import org.dromara.common.core.constant.GlobalConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.exception.ServiceException;
@@ -13,13 +20,6 @@ import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.idempotent.annotation.RepeatSubmit;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.redis.utils.RedisUtils;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.aspectj.lang.JoinPoint;
-import org.aspectj.lang.annotation.AfterReturning;
-import org.aspectj.lang.annotation.AfterThrowing;
-import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Before;
 import org.springframework.validation.BindingResult;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -127,7 +127,7 @@ public class RepeatSubmitAspect {
     public boolean isFilterObject(final Object o) {
         Class<?> clazz = o.getClass();
         if (clazz.isArray()) {
-            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+            return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
         } else if (Collection.class.isAssignableFrom(clazz)) {
             Collection collection = (Collection) o;
             for (Object value : collection) {

+ 5 - 11
ruoyi-common/ruoyi-common-job/pom.xml

@@ -22,20 +22,14 @@
             <artifactId>spring-boot-autoconfigure</artifactId>
         </dependency>
 
-        <!--PowerJob-->
+        <!-- SnailJob client -->
         <dependency>
-            <groupId>tech.powerjob</groupId>
-            <artifactId>powerjob-worker-spring-boot-starter</artifactId>
-            <exclusions>
-                <exclusion>
-                    <artifactId>powerjob-remote-impl-akka</artifactId>
-                    <groupId>tech.powerjob</groupId>
-                </exclusion>
-            </exclusions>
+            <groupId>com.aizuda</groupId>
+            <artifactId>snail-job-client-starter</artifactId>
         </dependency>
         <dependency>
-            <groupId>tech.powerjob</groupId>
-            <artifactId>powerjob-official-processors</artifactId>
+            <groupId>com.aizuda</groupId>
+            <artifactId>snail-job-client-job-core</artifactId>
         </dependency>
 
         <dependency>

+ 0 - 21
ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/PowerJobConfig.java

@@ -1,21 +0,0 @@
-package org.dromara.common.job.config;
-
-import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.scheduling.annotation.EnableScheduling;
-import tech.powerjob.worker.PowerJobWorker;
-
-/**
- * 启动定时任务
- * @author yhan219
- * @since 2023/6/2
- */
-@Configuration
-@ConditionalOnBean(PowerJobWorker.class)
-@ConditionalOnProperty(prefix = "powerjob.worker", name = "enabled", havingValue = "true")
-@EnableScheduling
-public class PowerJobConfig {
-
-
-}

+ 37 - 0
ruoyi-common/ruoyi-common-job/src/main/java/org/dromara/common/job/config/SnailJobConfig.java

@@ -0,0 +1,37 @@
+package org.dromara.common.job.config;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import com.aizuda.snailjob.client.common.appender.SnailLogbackAppender;
+import com.aizuda.snailjob.client.common.event.SnailClientStartingEvent;
+import com.aizuda.snailjob.client.starter.EnableSnailJob;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启动定时任务
+ *
+ * @author opensnail
+ * @date 2024-05-17
+ */
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "snail-job", name = "enabled", havingValue = "true")
+@EnableScheduling
+@EnableSnailJob(group = "${snail-job.group-name}")
+public class SnailJobConfig {
+
+    @EventListener(SnailClientStartingEvent.class)
+    public void onStarting(SnailClientStartingEvent event) {
+        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+        SnailLogbackAppender<ILoggingEvent> ca = new SnailLogbackAppender<>();
+        ca.setName("snail_log_appender");
+        ca.start();
+        Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
+        rootLogger.addAppender(ca);
+    }
+
+}

+ 1 - 0
ruoyi-common/ruoyi-common-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+org.dromara.common.job.config.SnailJobConfig

+ 59 - 2
ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/utils/JsonUtils.java

@@ -7,10 +7,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.exc.MismatchedInputException;
-import org.dromara.common.core.utils.SpringUtils;
-import org.dromara.common.core.utils.StringUtils;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -30,6 +30,13 @@ public class JsonUtils {
         return OBJECT_MAPPER;
     }
 
+    /**
+     * 将对象转换为JSON格式的字符串
+     *
+     * @param object 要转换的对象
+     * @return JSON格式的字符串,如果对象为null,则返回null
+     * @throws RuntimeException 如果转换过程中发生JSON处理异常,则抛出运行时异常
+     */
     public static String toJsonString(Object object) {
         if (ObjectUtil.isNull(object)) {
             return null;
@@ -41,6 +48,15 @@ public class JsonUtils {
         }
     }
 
+    /**
+     * 将JSON格式的字符串转换为指定类型的对象
+     *
+     * @param text  JSON格式的字符串
+     * @param clazz 要转换的目标对象类型
+     * @param <T>   目标对象的泛型类型
+     * @return 转换后的对象,如果字符串为空则返回null
+     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
+     */
     public static <T> T parseObject(String text, Class<T> clazz) {
         if (StringUtils.isEmpty(text)) {
             return null;
@@ -52,6 +68,15 @@ public class JsonUtils {
         }
     }
 
+    /**
+     * 将字节数组转换为指定类型的对象
+     *
+     * @param bytes 字节数组
+     * @param clazz 要转换的目标对象类型
+     * @param <T>   目标对象的泛型类型
+     * @return 转换后的对象,如果字节数组为空则返回null
+     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
+     */
     public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
         if (ArrayUtil.isEmpty(bytes)) {
             return null;
@@ -63,6 +88,15 @@ public class JsonUtils {
         }
     }
 
+    /**
+     * 将JSON格式的字符串转换为指定类型的对象,支持复杂类型
+     *
+     * @param text          JSON格式的字符串
+     * @param typeReference 指定类型的TypeReference对象
+     * @param <T>           目标对象的泛型类型
+     * @return 转换后的对象,如果字符串为空则返回null
+     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
+     */
     public static <T> T parseObject(String text, TypeReference<T> typeReference) {
         if (StringUtils.isBlank(text)) {
             return null;
@@ -74,6 +108,13 @@ public class JsonUtils {
         }
     }
 
+    /**
+     * 将JSON格式的字符串转换为Dict对象
+     *
+     * @param text JSON格式的字符串
+     * @return 转换后的Dict对象,如果字符串为空或者不是JSON格式则返回null
+     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
+     */
     public static Dict parseMap(String text) {
         if (StringUtils.isBlank(text)) {
             return null;
@@ -88,6 +129,13 @@ public class JsonUtils {
         }
     }
 
+    /**
+     * 将JSON格式的字符串转换为Dict对象的列表
+     *
+     * @param text JSON格式的字符串
+     * @return 转换后的Dict对象的列表,如果字符串为空则返回null
+     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
+     */
     public static List<Dict> parseArrayMap(String text) {
         if (StringUtils.isBlank(text)) {
             return null;
@@ -99,6 +147,15 @@ public class JsonUtils {
         }
     }
 
+    /**
+     * 将JSON格式的字符串转换为指定类型对象的列表
+     *
+     * @param text  JSON格式的字符串
+     * @param clazz 要转换的目标对象类型
+     * @param <T>   目标对象的泛型类型
+     * @return 转换后的对象的列表,如果字符串为空则返回空列表
+     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
+     */
     public static <T> List<T> parseArray(String text, Class<T> clazz) {
         if (StringUtils.isEmpty(text)) {
             return new ArrayList<>();

+ 0 - 5
ruoyi-common/ruoyi-common-log/pom.xml

@@ -27,11 +27,6 @@
             <artifactId>ruoyi-common-json</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>com.alibaba</groupId>
-            <artifactId>transmittable-thread-local</artifactId>
-        </dependency>
-
     </dependencies>
 
 </project>

+ 15 - 16
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java

@@ -4,16 +4,6 @@ import cn.hutool.core.lang.Dict;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
-import com.alibaba.ttl.TransmittableThreadLocal;
-import org.dromara.common.core.domain.model.LoginUser;
-import org.dromara.common.core.utils.ServletUtils;
-import org.dromara.common.core.utils.SpringUtils;
-import org.dromara.common.core.utils.StringUtils;
-import org.dromara.common.json.utils.JsonUtils;
-import org.dromara.common.log.annotation.Log;
-import org.dromara.common.log.enums.BusinessStatus;
-import org.dromara.common.log.event.OperLogEvent;
-import org.dromara.common.satoken.utils.LoginHelper;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.extern.slf4j.Slf4j;
@@ -23,6 +13,15 @@ import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.AfterThrowing;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Before;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessStatus;
+import org.dromara.common.log.event.OperLogEvent;
+import org.dromara.common.satoken.utils.LoginHelper;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.http.HttpMethod;
 import org.springframework.validation.BindingResult;
@@ -49,9 +48,9 @@ public class LogAspect {
 
 
     /**
-     * 计算操作消耗时间
+     * 计时 key
      */
-    private static final ThreadLocal<StopWatch> TIME_THREADLOCAL = new TransmittableThreadLocal<>();
+    private static final ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
 
     /**
      * 处理请求前执行
@@ -59,7 +58,7 @@ public class LogAspect {
     @Before(value = "@annotation(controllerLog)")
     public void boBefore(JoinPoint joinPoint, Log controllerLog) {
         StopWatch stopWatch = new StopWatch();
-        TIME_THREADLOCAL.set(stopWatch);
+        KEY_CACHE.set(stopWatch);
         stopWatch.start();
     }
 
@@ -112,7 +111,7 @@ public class LogAspect {
             // 处理设置注解上的参数
             getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
             // 设置消耗时间
-            StopWatch stopWatch = TIME_THREADLOCAL.get();
+            StopWatch stopWatch = KEY_CACHE.get();
             stopWatch.stop();
             operLog.setCostTime(stopWatch.getTime());
             // 发布事件保存数据库
@@ -122,7 +121,7 @@ public class LogAspect {
             log.error("异常信息:{}", exp.getMessage());
             exp.printStackTrace();
         } finally {
-            TIME_THREADLOCAL.remove();
+            KEY_CACHE.remove();
         }
     }
 
@@ -204,7 +203,7 @@ public class LogAspect {
     public boolean isFilterObject(final Object o) {
         Class<?> clazz = o.getClass();
         if (clazz.isArray()) {
-            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+            return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
         } else if (Collection.class.isAssignableFrom(clazz)) {
             Collection collection = (Collection) o;
             for (Object value : collection) {

+ 1 - 12
ruoyi-common/ruoyi-common-mybatis/pom.xml

@@ -32,20 +32,9 @@
             <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>org.mybatis.spring.boot</groupId>
-            <artifactId>mybatis-spring-boot-starter</artifactId>
-        </dependency>
-
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-boot-starter</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.mybatis</groupId>
-                    <artifactId>mybatis-spring</artifactId>
-                </exclusion>
-            </exclusions>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
         </dependency>
 
         <!-- sql性能分析插件 -->

+ 19 - 16
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java

@@ -1,34 +1,30 @@
 package org.dromara.common.mybatis.config;
 
 import cn.hutool.core.net.NetUtil;
-import com.baomidou.mybatisplus.autoconfigure.DdlApplicationRunner;
-import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
 import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
 import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
-import com.baomidou.mybatisplus.extension.ddl.IDdl;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
 import org.dromara.common.core.factory.YmlPropertySourceFactory;
+import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
+import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
 import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
 import org.mybatis.spring.annotation.MapperScan;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.beans.BeansException;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.PropertySource;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
-import java.util.List;
-
 /**
  * mybatis-plus配置类(下方注释有插件介绍)
  *
  * @author Lion Li
  */
 @EnableTransactionManagement(proxyTargetClass = true)
-@AutoConfiguration(before = MybatisPlusAutoConfiguration.class)
 @MapperScan("${mybatis-plus.mapperPackage}")
 @PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
 public class MybatisPlusConfig {
@@ -36,6 +32,12 @@ public class MybatisPlusConfig {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        // 多租户插件 必须放到第一位
+        try {
+            TenantLineInnerInterceptor tenant = SpringUtils.getBean(TenantLineInnerInterceptor.class);
+            interceptor.addInnerInterceptor(tenant);
+        } catch (BeansException ignore) {
+        }
         // 数据权限处理
         interceptor.addInnerInterceptor(dataPermissionInterceptor());
         // 分页插件
@@ -49,7 +51,7 @@ public class MybatisPlusConfig {
      * 数据权限拦截器
      */
     public PlusDataPermissionInterceptor dataPermissionInterceptor() {
-        return new PlusDataPermissionInterceptor();
+        return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
     }
 
     /**
@@ -57,8 +59,6 @@ public class MybatisPlusConfig {
      */
     public PaginationInnerInterceptor paginationInnerInterceptor() {
         PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
-        // 设置最大单页限制数量,默认 500 条,-1 不受限制
-        paginationInnerInterceptor.setMaxLimit(-1L);
         // 分页合理化
         paginationInnerInterceptor.setOverflow(true);
         return paginationInnerInterceptor;
@@ -88,6 +88,14 @@ public class MybatisPlusConfig {
         return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
     }
 
+    /**
+     * 异常处理器
+     */
+    @Bean
+    public MybatisExceptionHandler mybatisExceptionHandler() {
+        return new MybatisExceptionHandler();
+    }
+
     /**
      * PaginationInnerInterceptor 分页插件,自动识别数据库类型
      * https://baomidou.com/pages/97710a/
@@ -108,9 +116,4 @@ public class MybatisPlusConfig {
      * https://baomidou.com/pages/2a45ff/
      */
 
-    @Bean
-    public DdlApplicationRunner ddlApplicationRunner(@Autowired(required = false) List<IDdl> ddlList) {
-        return new DdlApplicationRunner(ddlList);
-    }
-
 }

+ 12 - 2
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java

@@ -35,7 +35,6 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
     Log log = LogFactory.getLog(BaseMapperPlus.class);
 
     default Class<V> currentVoClass() {
-        GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class);
         return (Class<V>) GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class)[1];
     }
 
@@ -145,11 +144,22 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
         return selectVoOne(wrapper, this.currentVoClass());
     }
 
+    default V selectVoOne(Wrapper<T> wrapper, boolean throwEx) {
+        return selectVoOne(wrapper, this.currentVoClass(), throwEx);
+    }
+
     /**
      * 根据 entity 条件,查询一条记录
      */
     default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
-        T obj = this.selectOne(wrapper);
+        return selectVoOne(wrapper, voClass, true);
+    }
+
+    /**
+     * 根据 entity 条件,查询一条记录
+     */
+    default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass, boolean throwEx) {
+        T obj = this.selectOne(wrapper, throwEx);
         if (ObjectUtil.isNull(obj)) {
             return null;
         }

+ 4 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java

@@ -111,4 +111,8 @@ public class PageQuery implements Serializable {
         return list;
     }
 
+    public Integer getFirstNum() {
+        return (pageNum - 1) * pageSize;
+    }
+
 }

+ 15 - 13
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java

@@ -29,16 +29,17 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
                     ? baseEntity.getCreateTime() : new Date();
                 baseEntity.setCreateTime(current);
                 baseEntity.setUpdateTime(current);
-                LoginUser loginUser = getLoginUser();
-                if (ObjectUtil.isNotNull(loginUser)) {
-                    Long userId = ObjectUtil.isNotNull(baseEntity.getCreateBy())
-                        ? baseEntity.getCreateBy() : loginUser.getUserId();
-                    // 当前已登录 且 创建人为空 则填充
-                    baseEntity.setCreateBy(userId);
-                    // 当前已登录 且 更新人为空 则填充
-                    baseEntity.setUpdateBy(userId);
-                    baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept())
-                        ? baseEntity.getCreateDept() : loginUser.getDeptId());
+                if (ObjectUtil.isNull(baseEntity.getCreateBy())) {
+                    LoginUser loginUser = getLoginUser();
+                    if (ObjectUtil.isNotNull(loginUser)) {
+                        Long userId = loginUser.getUserId();
+                        // 当前已登录 且 创建人为空 则填充
+                        baseEntity.setCreateBy(userId);
+                        // 当前已登录 且 更新人为空 则填充
+                        baseEntity.setUpdateBy(userId);
+                        baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept())
+                            ? baseEntity.getCreateDept() : loginUser.getDeptId());
+                    }
                 }
             }
         } catch (Exception e) {
@@ -53,11 +54,12 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
                 Date current = new Date();
                 // 更新时间填充(不管为不为空)
                 baseEntity.setUpdateTime(current);
-                LoginUser loginUser = getLoginUser();
                 // 当前已登录 更新人填充(不管为不为空)
-                if (ObjectUtil.isNotNull(loginUser)) {
-                    baseEntity.setUpdateBy(loginUser.getUserId());
+                Long userId = LoginHelper.getUserId();
+                if (ObjectUtil.isNotNull(userId)) {
+                    baseEntity.setUpdateBy(userId);
                 }
+
             }
         } catch (Exception e) {
             throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);

+ 2 - 1
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java

@@ -2,6 +2,7 @@ package org.dromara.common.mybatis.handler;
 
 import org.dromara.common.core.domain.R;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.StringUtils;
 import org.mybatis.spring.MyBatisSystemException;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -35,7 +36,7 @@ public class MybatisExceptionHandler {
     public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
         String requestURI = request.getRequestURI();
         String message = e.getMessage();
-        if ("CannotFindDataSourceException".contains(message)) {
+        if (StringUtils.contains("CannotFindDataSourceException", message)) {
             log.error("请求地址'{}', 未找到数据源", requestURI);
             return R.fail("未找到数据源,请联系管理员确认");
         }

+ 59 - 24
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java

@@ -2,7 +2,6 @@ package org.dromara.common.mybatis.handler;
 
 import cn.hutool.core.annotation.AnnotationUtil;
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ClassUtil;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.extern.slf4j.Slf4j;
 import net.sf.jsqlparser.JSQLParserException;
@@ -10,6 +9,7 @@ import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.Parenthesis;
 import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
 import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import org.apache.ibatis.io.Resources;
 import org.dromara.common.core.domain.dto.RoleDTO;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.exception.ServiceException;
@@ -21,16 +21,26 @@ import org.dromara.common.mybatis.annotation.DataPermission;
 import org.dromara.common.mybatis.enums.DataScopeType;
 import org.dromara.common.mybatis.helper.DataPermissionHelper;
 import org.dromara.common.satoken.utils.LoginHelper;
+import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.expression.BeanFactoryResolver;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.ClassMetadata;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
 import org.springframework.expression.BeanResolver;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.ParserContext;
 import org.springframework.expression.common.TemplateParserContext;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.util.ClassUtils;
 
 import java.lang.reflect.Method;
-import java.util.*;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 
@@ -58,9 +68,13 @@ public class PlusDataPermissionHandler {
      */
     private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
 
+    public PlusDataPermissionHandler(String mapperPackage) {
+        scanMapperClasses(mapperPackage);
+    }
+
 
     public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
-        DataColumn[] dataColumns = findAnnotation(mappedStatementId);
+        DataPermission dataPermission = getDataPermission(mappedStatementId);
         LoginUser currentUser = DataPermissionHelper.getVariable("user");
         if (ObjectUtil.isNull(currentUser)) {
             currentUser = LoginHelper.getLoginUser();
@@ -70,7 +84,7 @@ public class PlusDataPermissionHandler {
         if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
             return where;
         }
-        String dataFilterSql = buildDataFilter(dataColumns, isSelect);
+        String dataFilterSql = buildDataFilter(dataPermission.value(), isSelect);
         if (StringUtils.isBlank(dataFilterSql)) {
             return where;
         }
@@ -144,43 +158,64 @@ public class PlusDataPermissionHandler {
         return "";
     }
 
-    public DataColumn[] findAnnotation(String mappedStatementId) {
-        StringBuilder sb = new StringBuilder(mappedStatementId);
-        int index = sb.lastIndexOf(".");
-        String clazzName = sb.substring(0, index);
-        String methodName = sb.substring(index + 1, sb.length());
-        Class<?> clazz;
+    /**
+     * 通过 mapperPackage 设置的扫描包 扫描缓存有注解的方法与类
+     */
+    private void scanMapperClasses(String mapperPackage) {
+        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+        CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
+        String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
+        String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
         try {
-            clazz = ClassUtil.loadClass(clazzName);
+            for (String packagePattern : packagePatternArray) {
+                String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
+                Resource[] resources = resolver.getResources(classpath + path + "/*.class");
+                for (Resource resource : resources) {
+                    ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
+                    Class<?> clazz = Resources.classForName(classMetadata.getClassName());
+                    findAnnotation(clazz);
+                }
+            }
         } catch (Exception e) {
-            return null;
+            log.error("初始化数据安全缓存时出错:{}", e.getMessage());
         }
-        List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
-            .filter(method -> method.getName().equals(methodName)).toList();
+    }
+
+    private void findAnnotation(Class<?> clazz) {
         DataPermission dataPermission;
         // 获取方法注解
-        for (Method method : methods) {
-            dataPermission = dataPermissionCacheMap.get(mappedStatementId);
-            if (ObjectUtil.isNotNull(dataPermission)) {
-                return dataPermission.value();
+        for (Method method : clazz.getMethods()) {
+            if (method.isDefault() || method.isVarArgs()) {
+                continue;
             }
+            String mappedStatementId = clazz.getName() + "." + method.getName();
             if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
                 dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
                 dataPermissionCacheMap.put(mappedStatementId, dataPermission);
-                return dataPermission.value();
             }
         }
-        dataPermission = dataPermissionCacheMap.get(clazz.getName());
-        if (ObjectUtil.isNotNull(dataPermission)) {
-            return dataPermission.value();
-        }
         // 获取类注解
         if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
             dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
             dataPermissionCacheMap.put(clazz.getName(), dataPermission);
-            return dataPermission.value();
+        }
+    }
+
+    public DataPermission getDataPermission(String mapperId) {
+        if (dataPermissionCacheMap.containsKey(mapperId)) {
+            return dataPermissionCacheMap.get(mapperId);
+        }
+        String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
+        if (dataPermissionCacheMap.containsKey(clazzName)) {
+            return dataPermissionCacheMap.get(clazzName);
         }
         return null;
     }
 
+    /**
+     * 是否无效
+     */
+    public boolean invalid(String mapperId) {
+        return getDataPermission(mapperId) == null;
+    }
 }

+ 24 - 30
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java

@@ -1,18 +1,16 @@
 package org.dromara.common.mybatis.interceptor;
 
-import cn.hutool.core.collection.ConcurrentHashSet;
-import cn.hutool.core.util.ArrayUtil;
 import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
 import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
-import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
+import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
-import org.dromara.common.mybatis.annotation.DataColumn;
-import org.dromara.common.mybatis.handler.PlusDataPermissionHandler;
+import lombok.extern.slf4j.Slf4j;
 import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.schema.Table;
 import net.sf.jsqlparser.statement.delete.Delete;
 import net.sf.jsqlparser.statement.select.PlainSelect;
 import net.sf.jsqlparser.statement.select.Select;
-import net.sf.jsqlparser.statement.select.SelectBody;
 import net.sf.jsqlparser.statement.select.SetOperationList;
 import net.sf.jsqlparser.statement.update.Update;
 import org.apache.ibatis.executor.Executor;
@@ -22,11 +20,11 @@ import org.apache.ibatis.mapping.MappedStatement;
 import org.apache.ibatis.mapping.SqlCommandType;
 import org.apache.ibatis.session.ResultHandler;
 import org.apache.ibatis.session.RowBounds;
+import org.dromara.common.mybatis.handler.PlusDataPermissionHandler;
 
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.List;
-import java.util.Set;
 
 /**
  * 数据权限拦截器
@@ -34,13 +32,14 @@ import java.util.Set;
  * @author Lion Li
  * @version 3.5.0
  */
-public class PlusDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
+@Slf4j
+public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
 
-    private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
-    /**
-     * 无效注解方法缓存用于快速返回
-     */
-    private final Set<String> invalidCacheSet = new ConcurrentHashSet<>();
+    private final PlusDataPermissionHandler dataPermissionHandler;
+
+    public PlusDataPermissionInterceptor(String mapperPackage) {
+        this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage);
+    }
 
     @Override
     public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
@@ -49,12 +48,7 @@ public class PlusDataPermissionInterceptor extends JsqlParserSupport implements
             return;
         }
         // 检查是否无效 无数据权限注解
-        if (invalidCacheSet.contains(ms.getId())) {
-            return;
-        }
-        DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId());
-        if (ArrayUtil.isEmpty(dataColumns)) {
-            invalidCacheSet.add(ms.getId());
+        if (dataPermissionHandler.invalid(ms.getId())) {
             return;
         }
         // 解析 sql 分配对应方法
@@ -72,12 +66,7 @@ public class PlusDataPermissionInterceptor extends JsqlParserSupport implements
                 return;
             }
             // 检查是否无效 无数据权限注解
-            if (invalidCacheSet.contains(ms.getId())) {
-                return;
-            }
-            DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId());
-            if (ArrayUtil.isEmpty(dataColumns)) {
-                invalidCacheSet.add(ms.getId());
+            if (dataPermissionHandler.invalid(ms.getId())) {
                 return;
             }
             PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
@@ -87,11 +76,10 @@ public class PlusDataPermissionInterceptor extends JsqlParserSupport implements
 
     @Override
     protected void processSelect(Select select, int index, String sql, Object obj) {
-        SelectBody selectBody = select.getSelectBody();
-        if (selectBody instanceof PlainSelect plainSelect) {
-            this.setWhere(plainSelect, (String) obj);
-        } else if (selectBody instanceof SetOperationList setOperationList) {
-            List<SelectBody> selectBodyList = setOperationList.getSelects();
+        if (select instanceof PlainSelect) {
+            this.setWhere((PlainSelect) select, (String) obj);
+        } else if (select instanceof SetOperationList setOperationList) {
+            List<Select> selectBodyList = setOperationList.getSelects();
             selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
         }
     }
@@ -125,5 +113,11 @@ public class PlusDataPermissionInterceptor extends JsqlParserSupport implements
         }
     }
 
+    @Override
+    public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
+        // 只有新版数据权限处理器才会执行到这里
+        final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
+        return handler.getSqlSegment(table, where, whereSegment);
+    }
 }
 

+ 0 - 8
ruoyi-admin/src/main/resources/spy.properties → ruoyi-common/ruoyi-common-mybatis/src/main/resources/spy.properties

@@ -6,8 +6,6 @@ logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
 # 使用日志系统记录 sql
 #appender=com.p6spy.engine.spy.appender.Slf4JLogger
-# 设置 p6spy driver 代理
-#deregisterdrivers=true
 # 取消JDBC URL前缀
 useprefix=true
 # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
@@ -16,12 +14,6 @@ excludecategories=info,debug,result,commit,resultset
 dateformat=yyyy-MM-dd HH:mm:ss
 # SQL语句打印时间格式
 databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
-# 实际驱动可多个
-#driverlist=org.h2.Driver
-# 是否开启慢SQL记录
-outagedetection=true
-# 慢SQL记录标准 2 秒
-outagedetectioninterval=2
 # 是否过滤 Log
 filter=true
 # 过滤 Log 时所排除的 sql 关键字,以逗号分隔

+ 38 - 2
ruoyi-common/ruoyi-common-oss/pom.xml

@@ -26,10 +26,46 @@
             <artifactId>ruoyi-common-redis</artifactId>
         </dependency>
 
+        <!--  AWS SDK for Java 2.x  -->
         <dependency>
-            <groupId>com.amazonaws</groupId>
-            <artifactId>aws-java-sdk-s3</artifactId>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+            <exclusions>
+                <!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>netty-nio-client</artifactId>
+                </exclusion>
+                <!-- 将基于 CRT 的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>aws-crt-client</artifactId>
+                </exclusion>
+                <!-- 将基于 Apache 的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>apache-client</artifactId>
+                </exclusion>
+                <!-- 将配置基于 URL 连接的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>url-connection-client</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
+
+        <!-- 使用AWS基于 CRT 的 S3 客户端 -->
+        <dependency>
+            <groupId>software.amazon.awssdk.crt</groupId>
+            <artifactId>aws-crt</artifactId>
+        </dependency>
+
+        <!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3-transfer-manager</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 459 - 132
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java

@@ -2,73 +2,117 @@ package org.dromara.common.oss.core;
 
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.util.IdUtil;
-import com.amazonaws.ClientConfiguration;
-import com.amazonaws.HttpMethod;
-import com.amazonaws.Protocol;
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.AWSStaticCredentialsProvider;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.AmazonS3Client;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.model.*;
+import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.file.FileUtils;
 import org.dromara.common.oss.constant.OssConstant;
 import org.dromara.common.oss.entity.UploadResult;
 import org.dromara.common.oss.enumd.AccessPolicyType;
 import org.dromara.common.oss.enumd.PolicyType;
 import org.dromara.common.oss.exception.OssException;
 import org.dromara.common.oss.properties.OssProperties;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.async.AsyncRequestBody;
+import software.amazon.awssdk.core.async.AsyncResponseTransformer;
+import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.transfer.s3.S3TransferManager;
+import software.amazon.awssdk.transfer.s3.model.*;
+import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
 
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.InputStream;
+import java.io.*;
+import java.net.URI;
 import java.net.URL;
-import java.util.Date;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
 
 /**
  * S3 存储协议 所有兼容S3协议的云厂商均支持
  * 阿里云 腾讯云 七牛云 minio
  *
- * @author Lion Li
+ * @author AprilWind
  */
 public class OssClient {
 
+    /**
+     * 服务商
+     */
     private final String configKey;
 
+    /**
+     * 配置属性
+     */
     private final OssProperties properties;
 
-    private final AmazonS3 client;
+    /**
+     * Amazon S3 异步客户端
+     */
+    private final S3AsyncClient client;
+
+    /**
+     * 用于管理 S3 数据传输的高级工具
+     */
+    private final S3TransferManager transferManager;
+
+    /**
+     * AWS S3 预签名 URL 的生成器
+     */
+    private final S3Presigner presigner;
 
+    /**
+     * 构造方法
+     *
+     * @param configKey     配置键
+     * @param ossProperties Oss配置属性
+     */
     public OssClient(String configKey, OssProperties ossProperties) {
         this.configKey = configKey;
         this.properties = ossProperties;
         try {
-            AwsClientBuilder.EndpointConfiguration endpointConfig =
-                new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
-
-            AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
-            AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
-            ClientConfiguration clientConfig = new ClientConfiguration();
-            if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
-                clientConfig.setProtocol(Protocol.HTTPS);
-            } else {
-                clientConfig.setProtocol(Protocol.HTTP);
-            }
-            AmazonS3ClientBuilder build = AmazonS3Client.builder()
-                .withEndpointConfiguration(endpointConfig)
-                .withClientConfiguration(clientConfig)
-                .withCredentials(credentialsProvider)
-                .disableChunkedEncoding();
-            if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
-                // minio 使用https限制使用域名访问 需要此配置 站点填域名
-                build.enablePathStyleAccess();
-            }
-            this.client = build.build();
+            // 创建 AWS 认证信息
+            StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
+                AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
+
+            //MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问
+            boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
+
+            //创建AWS基于 CRT 的 S3 客户端
+            this.client = S3AsyncClient.crtBuilder()
+                .credentialsProvider(credentialsProvider)
+                .endpointOverride(URI.create(getEndpoint()))
+                .region(of())
+                .targetThroughputInGbps(20.0)
+                .minimumPartSizeInBytes(10 * 1025 * 1024L)
+                .checksumValidationEnabled(false)
+                .forcePathStyle(isStyle)
+                .build();
 
+            //AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
+            this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
+
+            // 创建 S3 配置对象
+            S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false)
+                .pathStyleAccessEnabled(isStyle).build();
+
+            // 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL
+            this.presigner = S3Presigner.builder()
+                .region(of())
+                .credentialsProvider(credentialsProvider)
+                .endpointOverride(URI.create(getDomain()))
+                .serviceConfiguration(config)
+                .build();
+
+            // 创建存储桶
             createBucket();
         } catch (Exception e) {
             if (e instanceof OssException) {
@@ -78,141 +122,379 @@ public class OssClient {
         }
     }
 
+    /**
+     * 同步创建存储桶
+     * 如果存储桶不存在,会进行创建;如果存储桶存在,不执行任何操作
+     *
+     * @throws OssException 当创建存储桶时发生异常时抛出
+     */
     public void createBucket() {
+        String bucketName = properties.getBucketName();
         try {
-            String bucketName = properties.getBucketName();
-            if (client.doesBucketExistV2(bucketName)) {
-                return;
+            // 尝试获取存储桶的信息
+            client.headBucket(
+                    x -> x.bucket(bucketName)
+                        .build())
+                .join();
+        } catch (Exception ex) {
+            if (ex.getCause() instanceof NoSuchBucketException) {
+                try {
+                    // 存储桶不存在,尝试创建存储桶
+                    client.createBucket(
+                            x -> x.bucket(bucketName))
+                        .join();
+
+                    // 设置存储桶的访问策略(Bucket Policy)
+                    client.putBucketPolicy(
+                            x -> x.bucket(bucketName)
+                                .policy(getPolicy(bucketName, getAccessPolicy().getPolicyType())))
+                        .join();
+                } catch (S3Exception e) {
+                    // 存储桶创建或策略设置失败
+                    throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
+                }
+            } else {
+                throw new OssException("判断Bucket是否存在失败,请核对配置信息:[" + ex.getMessage() + "]");
             }
-            CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
-            AccessPolicyType accessPolicy = getAccessPolicy();
-            createBucketRequest.setCannedAcl(accessPolicy.getAcl());
-            client.createBucket(createBucketRequest);
-            client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
-        } catch (Exception e) {
-            throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
         }
     }
 
-    public UploadResult upload(byte[] data, String path, String contentType) {
-        return upload(new ByteArrayInputStream(data), path, contentType);
+    /**
+     * 上传文件到 Amazon S3,并返回上传结果
+     *
+     * @param filePath  本地文件路径
+     * @param key       在 Amazon S3 中的对象键
+     * @param md5Digest 本地文件的 MD5 哈希值(可选)
+     * @return UploadResult 包含上传后的文件信息
+     * @throws OssException 如果上传失败,抛出自定义异常
+     */
+    public UploadResult upload(Path filePath, String key, String md5Digest) {
+        try {
+            // 构建上传请求对象
+            FileUpload fileUpload = transferManager.uploadFile(
+                x -> x.putObjectRequest(
+                        y -> y.bucket(properties.getBucketName())
+                            .key(key)
+                            .contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
+                            .build())
+                    .addTransferListener(LoggingTransferListener.create())
+                    .source(filePath).build());
+
+            // 等待上传完成并获取上传结果
+            CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
+            String eTag = uploadResult.response().eTag();
+
+            // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象
+            return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
+        } catch (Exception e) {
+            // 捕获异常并抛出自定义异常
+            throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
+        } finally {
+            // 无论上传是否成功,最终都会删除临时文件
+            FileUtils.del(filePath);
+        }
     }
 
-    public UploadResult upload(InputStream inputStream, String path, String contentType) {
+    /**
+     * 上传 InputStream 到 Amazon S3
+     *
+     * @param inputStream 要上传的输入流
+     * @param key         在 Amazon S3 中的对象键
+     * @param length      输入流的长度
+     * @return UploadResult 包含上传后的文件信息
+     * @throws OssException 如果上传失败,抛出自定义异常
+     */
+    public UploadResult upload(InputStream inputStream, String key, Long length) {
+        // 如果输入流不是 ByteArrayInputStream,则将其读取为字节数组再创建 ByteArrayInputStream
         if (!(inputStream instanceof ByteArrayInputStream)) {
             inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
         }
         try {
-            ObjectMetadata metadata = new ObjectMetadata();
-            metadata.setContentType(contentType);
-            metadata.setContentLength(inputStream.available());
-            PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
-            // 设置上传对象的 Acl 为公共读
-            putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
-            client.putObject(putObjectRequest);
+            // 创建异步请求体(length如果为空会报错)
+            BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length);
+
+            // 使用 transferManager 进行上传
+            Upload upload = transferManager.upload(
+                x -> x.requestBody(body)
+                    .putObjectRequest(
+                        y -> y.bucket(properties.getBucketName())
+                            .key(key)
+                            .build())
+                    .build());
+
+            // 将输入流写入请求体
+            body.writeInputStream(inputStream);
+
+            // 等待文件上传操作完成
+            CompletedUpload uploadResult = upload.completionFuture().join();
+            String eTag = uploadResult.response().eTag();
+
+            // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象
+            return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
         } catch (Exception e) {
             throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
         }
-        return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
     }
 
-    public UploadResult upload(File file, String path) {
+    /**
+     * 下载文件从 Amazon S3 到临时目录
+     *
+     * @param path 文件在 Amazon S3 中的对象键
+     * @return 下载后的文件在本地的临时路径
+     * @throws OssException 如果下载失败,抛出自定义异常
+     */
+    public Path fileDownload(String path) {
+        // 构建临时文件
+        Path tempFilePath = FileUtils.createTempFile().toPath();
+        // 使用 S3TransferManager 下载文件
+        FileDownload downloadFile = transferManager.downloadFile(
+            x -> x.getObjectRequest(
+                    y -> y.bucket(properties.getBucketName())
+                        .key(removeBaseUrl(path))
+                        .build())
+                .addTransferListener(LoggingTransferListener.create())
+                .destination(tempFilePath)
+                .build());
+        // 等待文件下载操作完成
+        downloadFile.completionFuture().join();
+        return tempFilePath;
+    }
+
+    /**
+     * 下载文件从 Amazon S3 到 输出流
+     *
+     * @param key 文件在 Amazon S3 中的对象键
+     * @param out 输出流
+     * @return 输出流中写入的字节数(长度)
+     * @throws OssException 如果下载失败,抛出自定义异常
+     */
+    public long download(String key, OutputStream out) {
         try {
-            PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
-            // 设置上传对象的 Acl 为公共读
-            putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
-            client.putObject(putObjectRequest);
+            // 构建下载请求
+            DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
+                // 文件对象
+                .getObjectRequest(y -> y.bucket(properties.getBucketName())
+                    .key(key)
+                    .build())
+                .addTransferListener(LoggingTransferListener.create())
+                // 使用订阅转换器
+                .responseTransformer(AsyncResponseTransformer.toBlockingInputStream())
+                .build();
+            // 使用 S3TransferManager 下载文件
+            Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest);
+            // 输出到流中
+            try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream
+                return responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread
+            }
         } catch (Exception e) {
-            throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
+            throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
         }
-        return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
     }
 
+    /**
+     * 删除云存储服务中指定路径下文件
+     *
+     * @param path 指定路径
+     */
     public void delete(String path) {
-        path = path.replace(getUrl() + "/", "");
         try {
-            client.deleteObject(properties.getBucketName(), path);
+            client.deleteObject(
+                x -> x.bucket(properties.getBucketName())
+                    .key(removeBaseUrl(path))
+                    .build());
         } catch (Exception e) {
             throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
         }
     }
 
-    public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
-        return upload(data, getPath(properties.getPrefix(), suffix), contentType);
+    /**
+     * 获取私有URL链接
+     *
+     * @param objectKey 对象KEY
+     * @param second    授权时间
+     */
+    public String getPrivateUrl(String objectKey, Integer second) {
+        // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
+        URL url = presigner.presignGetObject(
+                x -> x.signatureDuration(Duration.ofSeconds(second))
+                    .getObjectRequest(
+                        y -> y.bucket(properties.getBucketName())
+                            .key(objectKey)
+                            .build())
+                    .build())
+            .url();
+        return url.toString();
     }
 
-    public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
-        return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
+    /**
+     * 上传 byte[] 数据到 Amazon S3,使用指定的后缀构造对象键。
+     *
+     * @param data   要上传的 byte[] 数据
+     * @param suffix 对象键的后缀
+     * @return UploadResult 包含上传后的文件信息
+     * @throws OssException 如果上传失败,抛出自定义异常
+     */
+    public UploadResult uploadSuffix(byte[] data, String suffix) {
+        return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length));
     }
 
+    /**
+     * 上传 InputStream 到 Amazon S3,使用指定的后缀构造对象键。
+     *
+     * @param inputStream 要上传的输入流
+     * @param suffix      对象键的后缀
+     * @param length      输入流的长度
+     * @return UploadResult 包含上传后的文件信息
+     * @throws OssException 如果上传失败,抛出自定义异常
+     */
+    public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length) {
+        return upload(inputStream, getPath(properties.getPrefix(), suffix), length);
+    }
+
+    /**
+     * 上传文件到 Amazon S3,使用指定的后缀构造对象键
+     *
+     * @param file   要上传的文件
+     * @param suffix 对象键的后缀
+     * @return UploadResult 包含上传后的文件信息
+     * @throws OssException 如果上传失败,抛出自定义异常
+     */
     public UploadResult uploadSuffix(File file, String suffix) {
-        return upload(file, getPath(properties.getPrefix(), suffix));
+        return upload(file.toPath(), getPath(properties.getPrefix(), suffix), null);
     }
 
     /**
-     * 获取文件元数据
+     * 获取文件输入流
      *
      * @param path 完整文件路径
+     * @return 输入流
+     */
+    public InputStream getObjectContent(String path) throws IOException {
+        // 下载文件到临时目录
+        Path tempFilePath = fileDownload(path);
+        // 创建输入流
+        InputStream inputStream = Files.newInputStream(tempFilePath);
+        // 删除临时文件
+        FileUtils.del(tempFilePath);
+        // 返回对象内容的输入流
+        return inputStream;
+    }
+
+    /**
+     * 获取 S3 客户端的终端点 URL
+     *
+     * @return 终端点 URL
      */
-    public ObjectMetadata getObjectMetadata(String path) {
-        path = path.replace(getUrl() + "/", "");
-        S3Object object = client.getObject(properties.getBucketName(), path);
-        return object.getObjectMetadata();
+    public String getEndpoint() {
+        // 根据配置文件中的是否使用 HTTPS,设置协议头部
+        String header = getIsHttps();
+        // 拼接协议头部和终端点,得到完整的终端点 URL
+        return header + properties.getEndpoint();
+    }
+
+    /**
+     * 获取 S3 客户端的终端点 URL(自定义域名)
+     *
+     * @return 终端点 URL
+     */
+    public String getDomain() {
+        // 从配置中获取域名、终端点、是否使用 HTTPS 等信息
+        String domain = properties.getDomain();
+        String endpoint = properties.getEndpoint();
+        String header = getIsHttps();
+
+        // 如果是云服务商,直接返回域名或终端点
+        if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
+            return StringUtils.isNotEmpty(domain) ? header + domain : header + endpoint;
+        }
+
+        // 如果是 MinIO,处理域名并返回
+        if (StringUtils.isNotEmpty(domain)) {
+            return domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP) ? domain : header + domain;
+        }
+
+        // 返回终端点
+        return header + endpoint;
     }
 
-    public InputStream getObjectContent(String path) {
-        path = path.replace(getUrl() + "/", "");
-        S3Object object = client.getObject(properties.getBucketName(), path);
-        return object.getObjectContent();
+    /**
+     * 根据传入的 region 参数返回相应的 AWS 区域
+     * 如果 region 参数非空,使用 Region.of 方法创建并返回对应的 AWS 区域对象
+     * 如果 region 参数为空,返回一个默认的 AWS 区域(例如,us-east-1),作为广泛支持的区域
+     *
+     * @return 对应的 AWS 区域对象,或者默认的广泛支持的区域(us-east-1)
+     */
+    public Region of() {
+        //AWS 区域字符串
+        String region = properties.getRegion();
+        // 如果 region 参数非空,使用 Region.of 方法创建对应的 AWS 区域对象,否则返回默认区域
+        return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1;
     }
 
+    /**
+     * 获取云存储服务的URL
+     *
+     * @return 文件路径
+     */
     public String getUrl() {
         String domain = properties.getDomain();
         String endpoint = properties.getEndpoint();
-        String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
+        String header = getIsHttps();
         // 云服务商直接返回
         if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
-            if (StringUtils.isNotBlank(domain)) {
-                return header + domain;
-            }
-            return header + properties.getBucketName() + "." + endpoint;
+            return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint);
         }
-        // minio 单独处理
-        if (StringUtils.isNotBlank(domain)) {
-            return header + domain + "/" + properties.getBucketName();
+        // MinIO 单独处理
+        if (StringUtils.isNotEmpty(domain)) {
+            // 如果 domain 以 "https://" 或 "http://" 开头
+            return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP)) ?
+                domain + StringUtils.SLASH + properties.getBucketName() : header + domain + StringUtils.SLASH + properties.getBucketName();
         }
-        return header + endpoint + "/" + properties.getBucketName();
+        return header + endpoint + StringUtils.SLASH + properties.getBucketName();
     }
 
+    /**
+     * 生成一个符合特定规则的、唯一的文件路径。通过使用日期、UUID、前缀和后缀等元素的组合,确保了文件路径的独一无二性
+     *
+     * @param prefix 前缀
+     * @param suffix 后缀
+     * @return 文件路径
+     */
     public String getPath(String prefix, String suffix) {
         // 生成uuid
         String uuid = IdUtil.fastSimpleUUID();
-        // 文件路径
-        String path = DateUtils.datePath() + "/" + uuid;
-        if (StringUtils.isNotBlank(prefix)) {
-            path = prefix + "/" + path;
-        }
+        // 生成日期路径
+        String datePath = DateUtils.datePath();
+        // 拼接路径
+        String path = StringUtils.isNotEmpty(prefix) ?
+            prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid;
         return path + suffix;
     }
 
+    /**
+     * 移除路径中的基础URL部分,得到相对路径
+     *
+     * @param path 完整的路径,包括基础URL和相对路径
+     * @return 去除基础URL后的相对路径
+     */
+    public String removeBaseUrl(String path) {
+        return path.replace(getUrl() + StringUtils.SLASH, "");
+    }
 
+    /**
+     * 服务商
+     */
     public String getConfigKey() {
         return configKey;
     }
 
     /**
-     * 获取私有URL链接
+     * 获取是否使用 HTTPS 的配置,并返回相应的协议头部。
      *
-     * @param objectKey 对象KEY
-     * @param second    授权时间
+     * @return 协议头部,根据是否使用 HTTPS 返回 "https://" 或 "http://"
      */
-    public String getPrivateUrl(String objectKey, Integer second) {
-        GeneratePresignedUrlRequest generatePresignedUrlRequest =
-            new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
-                .withMethod(HttpMethod.GET)
-                .withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
-        URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
-        return url.toString();
+    public String getIsHttps() {
+        return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP;
     }
 
     /**
@@ -231,32 +513,77 @@ public class OssClient {
         return AccessPolicyType.getByType(properties.getAccessPolicy());
     }
 
+    /**
+     * 生成 AWS S3 存储桶访问策略
+     *
+     * @param bucketName 存储桶
+     * @param policyType 桶策略类型
+     * @return 符合 AWS S3 存储桶访问策略格式的字符串
+     */
     private static String getPolicy(String bucketName, PolicyType policyType) {
-        StringBuilder builder = new StringBuilder();
-        builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
-        builder.append(switch (policyType) {
-            case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n";
-            case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n";
-            default -> "\"s3:GetBucketLocation\"\n";
-        });
-        builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
-        builder.append(bucketName);
-        builder.append("\"\n},\n");
-        if (policyType == PolicyType.READ) {
-            builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
-            builder.append(bucketName);
-            builder.append("\"\n},\n");
-        }
-        builder.append("{\n\"Action\": ");
-        builder.append(switch (policyType) {
-            case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
-            case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
-            default -> "\"s3:GetObject\",\n";
-        });
-        builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
-        builder.append(bucketName);
-        builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
-        return builder.toString();
+        String policy = switch (policyType) {
+            case WRITE -> """
+                {
+                  "Version": "2012-10-17",
+                  "Statement": []
+                }
+                """;
+            case READ_WRITE -> """
+                {
+                  "Version": "2012-10-17",
+                  "Statement": [
+                    {
+                      "Effect": "Allow",
+                      "Principal": "*",
+                      "Action": [
+                        "s3:GetBucketLocation",
+                        "s3:ListBucket",
+                        "s3:ListBucketMultipartUploads"
+                      ],
+                      "Resource": "arn:aws:s3:::bucketName"
+                    },
+                    {
+                      "Effect": "Allow",
+                      "Principal": "*",
+                      "Action": [
+                        "s3:AbortMultipartUpload",
+                        "s3:DeleteObject",
+                        "s3:GetObject",
+                        "s3:ListMultipartUploadParts",
+                        "s3:PutObject"
+                      ],
+                      "Resource": "arn:aws:s3:::bucketName/*"
+                    }
+                  ]
+                }
+                """;
+            case READ -> """
+                {
+                  "Version": "2012-10-17",
+                  "Statement": [
+                    {
+                      "Effect": "Allow",
+                      "Principal": "*",
+                      "Action": ["s3:GetBucketLocation"],
+                      "Resource": "arn:aws:s3:::bucketName"
+                    },
+                    {
+                      "Effect": "Deny",
+                      "Principal": "*",
+                      "Action": ["s3:ListBucket"],
+                      "Resource": "arn:aws:s3:::bucketName"
+                    },
+                    {
+                      "Effect": "Allow",
+                      "Principal": "*",
+                      "Action": "s3:GetObject",
+                      "Resource": "arn:aws:s3:::bucketName/*"
+                    }
+                  ]
+                }
+                """;
+        };
+        return policy.replaceAll("bucketName", bucketName);
     }
 
 }

+ 6 - 0
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java

@@ -21,4 +21,10 @@ public class UploadResult {
      * 文件名
      */
     private String filename;
+
+    /**
+     * 已上传对象的实体标记(用来校验文件)
+     */
+    private String eTag;
+
 }

+ 12 - 6
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java

@@ -1,8 +1,9 @@
 package org.dromara.common.oss.enumd;
 
-import com.amazonaws.services.s3.model.CannedAccessControlList;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
+import software.amazon.awssdk.services.s3.model.BucketCannedACL;
+import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
 
 /**
  * 桶访问策略配置
@@ -16,27 +17,32 @@ public enum AccessPolicyType {
     /**
      * private
      */
-    PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE),
+    PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE),
 
     /**
      * public
      */
-    PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ),
+    PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE),
 
     /**
      * custom
      */
-    CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ);
+    CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ);
 
     /**
-     * 桶 权限类型
+     * 桶 权限类型(数据库值)
      */
     private final String type;
 
+    /**
+     * 桶 权限类型
+     */
+    private final BucketCannedACL bucketCannedACL;
+
     /**
      * 文件对象 权限类型
      */
-    private final CannedAccessControlList acl;
+    private final ObjectCannedACL objectCannedACL;
 
     /**
      * 桶策略类型

+ 17 - 12
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java

@@ -1,5 +1,6 @@
 package org.dromara.common.oss.factory;
 
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.json.utils.JsonUtils;
@@ -9,10 +10,10 @@ import org.dromara.common.oss.exception.OssException;
 import org.dromara.common.oss.properties.OssProperties;
 import org.dromara.common.redis.utils.CacheUtils;
 import org.dromara.common.redis.utils.RedisUtils;
-import lombok.extern.slf4j.Slf4j;
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * 文件上传Factory
@@ -23,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
 public class OssFactory {
 
     private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
+    private static final ReentrantLock LOCK = new ReentrantLock();
 
     /**
      * 获取默认实例
@@ -39,7 +41,7 @@ public class OssFactory {
     /**
      * 根据类型获取实例
      */
-    public static synchronized OssClient instance(String configKey) {
+    public static OssClient instance(String configKey) {
         String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
         if (json == null) {
             throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
@@ -48,16 +50,19 @@ public class OssFactory {
         // 使用租户标识避免多个租户相同key实例覆盖
         String key = properties.getTenantId() + ":" + configKey;
         OssClient client = CLIENT_CACHE.get(key);
-        if (client == null) {
-            CLIENT_CACHE.put(key, new OssClient(configKey, properties));
-            log.info("创建OSS实例 key => {}", configKey);
-            return CLIENT_CACHE.get(key);
-        }
-        // 配置不相同则重新构建
-        if (!client.checkPropertiesSame(properties)) {
-            CLIENT_CACHE.put(key, new OssClient(configKey, properties));
-            log.info("重载OSS实例 key => {}", configKey);
-            return CLIENT_CACHE.get(key);
+        // 客户端不存在或配置不相同则重新构建
+        if (client == null || !client.checkPropertiesSame(properties)) {
+            LOCK.lock();
+            try {
+                client = CLIENT_CACHE.get(key);
+                if (client == null || !client.checkPropertiesSame(properties)) {
+                    CLIENT_CACHE.put(key, new OssClient(configKey, properties));
+                    log.info("创建OSS实例 key => {}", configKey);
+                    return CLIENT_CACHE.get(key);
+                }
+            } finally {
+                LOCK.unlock();
+            }
         }
         return client;
     }

+ 27 - 43
ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java

@@ -1,29 +1,29 @@
 package org.dromara.common.ratelimiter.aspectj;
 
-import cn.hutool.core.util.ArrayUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
 import org.dromara.common.core.constant.GlobalConstants;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MessageUtils;
 import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.ratelimiter.annotation.RateLimiter;
 import org.dromara.common.ratelimiter.enums.LimitType;
 import org.dromara.common.redis.utils.RedisUtils;
-import lombok.extern.slf4j.Slf4j;
-import org.aspectj.lang.JoinPoint;
-import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Before;
-import org.aspectj.lang.reflect.MethodSignature;
 import org.redisson.api.RateType;
+import org.springframework.context.expression.BeanFactoryResolver;
+import org.springframework.context.expression.MethodBasedEvaluationContext;
 import org.springframework.core.DefaultParameterNameDiscoverer;
 import org.springframework.core.ParameterNameDiscoverer;
-import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.ParserContext;
 import org.springframework.expression.common.TemplateParserContext;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.expression.spel.support.StandardEvaluationContext;
 
 import java.lang.reflect.Method;
 
@@ -44,21 +44,18 @@ public class RateLimiterAspect {
      * 定义spel解析模版
      */
     private final ParserContext parserContext = new TemplateParserContext();
-    /**
-     * 定义spel上下文对象进行解析
-     */
-    private final EvaluationContext context = new StandardEvaluationContext();
     /**
      * 方法参数解析器
      */
     private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
 
+
     @Before("@annotation(rateLimiter)")
-    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
+    public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
         int time = rateLimiter.time();
         int count = rateLimiter.count();
-        String combineKey = getCombineKey(rateLimiter, point);
         try {
+            String combineKey = getCombineKey(rateLimiter, point);
             RateType rateType = RateType.OVERALL;
             if (rateLimiter.limitType() == LimitType.CLUSTER) {
                 rateType = RateType.PER_CLIENT;
@@ -76,42 +73,29 @@ public class RateLimiterAspect {
             if (e instanceof ServiceException) {
                 throw e;
             } else {
-                throw new RuntimeException("服务器限流异常,请稍候再试");
+                throw new RuntimeException("服务器限流异常,请稍候再试", e);
             }
         }
     }
 
-    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
+    private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
         String key = rateLimiter.key();
-        // 获取方法(通过方法签名来获取)
-        MethodSignature signature = (MethodSignature) point.getSignature();
-        Method method = signature.getMethod();
-        Class<?> targetClass = method.getDeclaringClass();
-        // 判断是否是spel格式
-        if (StringUtils.containsAny(key, "#")) {
-            // 获取参数值
+        if (StringUtils.isNotBlank(key)) {
+            MethodSignature signature = (MethodSignature) point.getSignature();
+            Method targetMethod = signature.getMethod();
             Object[] args = point.getArgs();
-            // 获取方法上参数的名称
-            String[] parameterNames = pnd.getParameterNames(method);
-            if (ArrayUtil.isEmpty(parameterNames)) {
-                throw new ServiceException("限流key解析异常!请联系管理员!");
-            }
-            for (int i = 0; i < parameterNames.length; i++) {
-                context.setVariable(parameterNames[i], args[i]);
-            }
-            // 解析返回给key
-            try {
-                Expression expression;
-                if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
-                    && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
-                    expression = parser.parseExpression(key, parserContext);
-                } else {
-                    expression = parser.parseExpression(key);
-                }
-                key = expression.getValue(context, String.class) + ":";
-            } catch (Exception e) {
-                throw new ServiceException("限流key解析异常!请联系管理员!");
+            //noinspection DataFlowIssue
+            MethodBasedEvaluationContext context =
+                new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
+            context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));
+            Expression expression;
+            if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
+                && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
+                expression = parser.parseExpression(key, parserContext);
+            } else {
+                expression = parser.parseExpression(key);
             }
+            key = expression.getValue(context, String.class);
         }
         StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY);
         stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":");

+ 7 - 0
ruoyi-common/ruoyi-common-ratelimiter/src/main/resources/spel-extension.json

@@ -0,0 +1,7 @@
+{
+  "org.dromara.common.ratelimiter.annotation.RateLimiter@key": {
+    "method": {
+      "parameters": true
+    }
+  }
+}

+ 10 - 0
ruoyi-common/ruoyi-common-redis/pom.xml

@@ -32,6 +32,16 @@
             <groupId>com.baomidou</groupId>
             <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 45 - 0
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/CacheConfig.java

@@ -0,0 +1,45 @@
+package org.dromara.common.redis.config;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.dromara.common.redis.manager.PlusSpringCacheManager;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 缓存配置
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@EnableCaching
+public class CacheConfig {
+
+    /**
+     * caffeine 本地缓存处理器
+     */
+    @Bean
+    public Cache<Object, Object> caffeine() {
+        return Caffeine.newBuilder()
+            // 设置最后一次写入或访问后经过固定时间过期
+            .expireAfterWrite(30, TimeUnit.SECONDS)
+            // 初始的缓存空间大小
+            .initialCapacity(100)
+            // 缓存的最大条数
+            .maximumSize(1000)
+            .build();
+    }
+
+    /**
+     * 自定义缓存管理器 整合spring-cache
+     */
+    @Bean
+    public CacheManager cacheManager() {
+        return new PlusSpringCacheManager();
+    }
+
+}

+ 23 - 11
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java

@@ -5,10 +5,14 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.redis.config.properties.RedissonProperties;
 import org.dromara.common.redis.handler.KeyPrefixHandler;
-import org.dromara.common.redis.manager.PlusSpringCacheManager;
+import org.dromara.common.redis.handler.RedisExceptionHandler;
 import org.redisson.client.codec.StringCodec;
 import org.redisson.codec.CompositeCodec;
 import org.redisson.codec.TypedJsonJacksonCodec;
@@ -16,9 +20,12 @@ import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.cache.CacheManager;
-import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
+import org.springframework.core.task.VirtualThreadTaskExecutor;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.TimeZone;
 
 /**
  * redis配置
@@ -27,20 +34,22 @@ import org.springframework.context.annotation.Bean;
  */
 @Slf4j
 @AutoConfiguration
-@EnableCaching
 @EnableConfigurationProperties(RedissonProperties.class)
 public class RedisConfig {
 
     @Autowired
     private RedissonProperties redissonProperties;
 
-    @Autowired
-    private ObjectMapper objectMapper;
-
     @Bean
     public RedissonAutoConfigurationCustomizer redissonCustomizer() {
         return config -> {
-            ObjectMapper om = objectMapper.copy();
+            JavaTimeModule javaTimeModule = new JavaTimeModule();
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
+            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
+            ObjectMapper om = new ObjectMapper();
+            om.registerModule(javaTimeModule);
+            om.setTimeZone(TimeZone.getDefault());
             om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
             // 指定序列化输入的类型,类必须是非final修饰的。序列化时将对象全类名一起保存下来
             om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
@@ -52,6 +61,9 @@ public class RedisConfig {
                 // 缓存 Lua 脚本 减少网络传输(redisson 大部分的功能都是基于 Lua 脚本实现)
                 .setUseScriptCache(true)
                 .setCodec(codec);
+            if (SpringUtils.isVirtual()) {
+                config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-"));
+            }
             RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
             if (ObjectUtil.isNotNull(singleServerConfig)) {
                 // 使用单机模式
@@ -87,11 +99,11 @@ public class RedisConfig {
     }
 
     /**
-     * 自定义缓存管理器 整合spring-cache
+     * 异常处理器
      */
     @Bean
-    public CacheManager cacheManager() {
-        return new PlusSpringCacheManager();
+    public RedisExceptionHandler redisExceptionHandler() {
+        return new RedisExceptionHandler();
     }
 
     /**

+ 30 - 0
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/handler/RedisExceptionHandler.java

@@ -0,0 +1,30 @@
+package org.dromara.common.redis.handler;
+
+import cn.hutool.http.HttpStatus;
+import com.baomidou.lock.exception.LockFailureException;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * Redis异常处理器
+ *
+ * @author AprilWind
+ */
+@Slf4j
+@RestControllerAdvice
+public class RedisExceptionHandler {
+
+    /**
+     * 分布式锁Lock4j异常
+     */
+    @ExceptionHandler(LockFailureException.class)
+    public R<Void> handleLockFailureException(LockFailureException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("获取锁失败了'{}',发生Lock4j异常." + requestURI, e.getMessage());
+        return R.fail(HttpStatus.HTTP_UNAVAILABLE, "业务处理中,请稍后再试...");
+    }
+
+}

+ 90 - 0
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java

@@ -0,0 +1,90 @@
+package org.dromara.common.redis.manager;
+
+import org.dromara.common.core.utils.SpringUtils;
+import org.springframework.cache.Cache;
+
+import java.util.concurrent.Callable;
+
+/**
+ * Cache 装饰器模式(用于扩展 Caffeine 一级缓存)
+ *
+ * @author LionLi
+ */
+public class CaffeineCacheDecorator implements Cache {
+
+    private static final com.github.benmanes.caffeine.cache.Cache<Object, Object>
+        CAFFEINE = SpringUtils.getBean("caffeine");
+
+    private final Cache cache;
+
+    public CaffeineCacheDecorator(Cache cache) {
+        this.cache = cache;
+    }
+
+    @Override
+    public String getName() {
+        return cache.getName();
+    }
+
+    @Override
+    public Object getNativeCache() {
+        return cache.getNativeCache();
+    }
+
+    public String getUniqueKey(Object key) {
+        return cache.getName() + ":" + key;
+    }
+
+    @Override
+    public ValueWrapper get(Object key) {
+        Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key));
+        return (ValueWrapper) o;
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T get(Object key, Class<T> type) {
+        Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, type));
+        return (T) o;
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        CAFFEINE.invalidate(getUniqueKey(key));
+        cache.put(key, value);
+    }
+
+    public ValueWrapper putIfAbsent(Object key, Object value) {
+        CAFFEINE.invalidate(getUniqueKey(key));
+        return cache.putIfAbsent(key, value);
+    }
+
+    @Override
+    public void evict(Object key) {
+        evictIfPresent(key);
+    }
+
+    public boolean evictIfPresent(Object key) {
+        boolean b = cache.evictIfPresent(key);
+        if (b) {
+            CAFFEINE.invalidate(getUniqueKey(key));
+        }
+        return b;
+    }
+
+    @Override
+    public void clear() {
+        cache.clear();
+    }
+
+    public boolean invalidate() {
+        return cache.invalidate();
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T get(Object key, Callable<T> valueLoader) {
+        Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, valueLoader));
+        return (T) o;
+    }
+
+}

+ 2 - 2
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/PlusSpringCacheManager.java

@@ -156,7 +156,7 @@ public class PlusSpringCacheManager implements CacheManager {
     private Cache createMap(String name, CacheConfig config) {
         RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
 
-        Cache cache = new RedissonCache(map, allowNullValues);
+        Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, allowNullValues));
         if (transactionAware) {
             cache = new TransactionAwareCacheDecorator(cache);
         }
@@ -170,7 +170,7 @@ public class PlusSpringCacheManager implements CacheManager {
     private Cache createMapCache(String name, CacheConfig config) {
         RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
 
-        Cache cache = new RedissonCache(map, config, allowNullValues);
+        Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, config, allowNullValues));
         if (transactionAware) {
             cache = new TransactionAwareCacheDecorator(cache);
         }

+ 11 - 1
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java

@@ -65,6 +65,12 @@ public class RedisUtils {
         consumer.accept(msg);
     }
 
+    /**
+     * 发布消息到指定的频道
+     *
+     * @param channelKey 通道key
+     * @param msg        发送数据
+     */
     public static <T> void publish(String channelKey, T msg) {
         RTopic topic = CLIENT.getTopic(channelKey);
         topic.publish(msg);
@@ -107,7 +113,11 @@ public class RedisUtils {
                 bucket.setAndKeepTTL(value);
             } catch (Exception e) {
                 long timeToLive = bucket.remainTimeToLive();
-                setCacheObject(key, value, Duration.ofMillis(timeToLive));
+                if (timeToLive == -1) {
+                    setCacheObject(key, value);
+                } else {
+                    setCacheObject(key, value, Duration.ofMillis(timeToLive));
+                }
             }
         } else {
             bucket.set(value);

+ 1 - 0
ruoyi-common/ruoyi-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1 +1,2 @@
 org.dromara.common.redis.config.RedisConfig
+org.dromara.common.redis.config.CacheConfig

+ 5 - 0
ruoyi-common/ruoyi-common-satoken/pom.xml

@@ -36,6 +36,11 @@
             <artifactId>sa-token-jwt</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 9 - 0
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/config/SaTokenConfig.java

@@ -7,6 +7,7 @@ import cn.dev33.satoken.stp.StpLogic;
 import org.dromara.common.core.factory.YmlPropertySourceFactory;
 import org.dromara.common.satoken.core.dao.PlusSaTokenDao;
 import org.dromara.common.satoken.core.service.SaPermissionImpl;
+import org.dromara.common.satoken.handler.SaTokenExceptionHandler;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.PropertySource;
@@ -42,4 +43,12 @@ public class SaTokenConfig {
         return new PlusSaTokenDao();
     }
 
+    /**
+     * 异常处理器
+     */
+    @Bean
+    public SaTokenExceptionHandler saTokenExceptionHandler() {
+        return new SaTokenExceptionHandler();
+    }
+
 }

+ 30 - 5
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java

@@ -2,26 +2,42 @@ package org.dromara.common.satoken.core.dao;
 
 import cn.dev33.satoken.dao.SaTokenDao;
 import cn.dev33.satoken.util.SaFoxUtil;
+import cn.hutool.core.lang.Console;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
 import org.dromara.common.redis.utils.RedisUtils;
 
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Sa-Token持久层接口(使用框架自带RedisUtils实现 协议统一)
+ * <p>
+ * 采用 caffeine + redis 多级缓存 优化并发查询效率
  *
  * @author Lion Li
  */
 public class PlusSaTokenDao implements SaTokenDao {
 
+    private static final Cache<String, Object> CAFFEINE = Caffeine.newBuilder()
+        // 设置最后一次写入或访问后经过固定时间过期
+        .expireAfterWrite(5, TimeUnit.SECONDS)
+        // 初始的缓存空间大小
+        .initialCapacity(100)
+        // 缓存的最大条数
+        .maximumSize(1000)
+        .build();
+
     /**
      * 获取Value,如无返空
      */
     @Override
     public String get(String key) {
-        return RedisUtils.getCacheObject(key);
+        Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
+        return (String) o;
     }
 
     /**
@@ -38,6 +54,7 @@ public class PlusSaTokenDao implements SaTokenDao {
         } else {
             RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
         }
+        CAFFEINE.put(key, value);
     }
 
     /**
@@ -47,6 +64,7 @@ public class PlusSaTokenDao implements SaTokenDao {
     public void update(String key, String value) {
         if (RedisUtils.hasKey(key)) {
             RedisUtils.setCacheObject(key, value, true);
+            CAFFEINE.put(key, value);
         }
     }
 
@@ -81,7 +99,8 @@ public class PlusSaTokenDao implements SaTokenDao {
      */
     @Override
     public Object getObject(String key) {
-        return RedisUtils.getCacheObject(key);
+        Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
+        return o;
     }
 
     /**
@@ -98,6 +117,7 @@ public class PlusSaTokenDao implements SaTokenDao {
         } else {
             RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
         }
+        CAFFEINE.put(key, object);
     }
 
     /**
@@ -107,6 +127,7 @@ public class PlusSaTokenDao implements SaTokenDao {
     public void updateObject(String key, Object object) {
         if (RedisUtils.hasKey(key)) {
             RedisUtils.setCacheObject(key, object, true);
+            CAFFEINE.put(key, object);
         }
     }
 
@@ -139,10 +160,14 @@ public class PlusSaTokenDao implements SaTokenDao {
     /**
      * 搜索数据
      */
+    @SuppressWarnings("unchecked")
     @Override
     public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
-        Collection<String> keys = RedisUtils.keys(prefix + "*" + keyword + "*");
-        List<String> list = new ArrayList<>(keys);
-        return SaFoxUtil.searchList(list, start, size, sortType);
+        String keyStr = prefix + "*" + keyword + "*";
+        return (List<String>) CAFFEINE.get(keyStr, k -> {
+            Collection<String> keys = RedisUtils.keys(keyStr);
+            List<String> list = new ArrayList<>(keys);
+            return SaFoxUtil.searchList(list, start, size, sortType);
+        });
     }
 }

+ 52 - 0
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/handler/SaTokenExceptionHandler.java

@@ -0,0 +1,52 @@
+package org.dromara.common.satoken.handler;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.exception.NotRoleException;
+import cn.hutool.http.HttpStatus;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * SaToken异常处理器
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@RestControllerAdvice
+public class SaTokenExceptionHandler {
+
+    /**
+     * 权限码异常
+     */
+    @ExceptionHandler(NotPermissionException.class)
+    public R<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage());
+        return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
+    }
+
+    /**
+     * 角色权限异常
+     */
+    @ExceptionHandler(NotRoleException.class)
+    public R<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage());
+        return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
+    }
+
+    /**
+     * 认证失败
+     */
+    @ExceptionHandler(NotLoginException.class)
+    public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
+        return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
+    }
+
+}

+ 57 - 39
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java

@@ -1,7 +1,5 @@
 package org.dromara.common.satoken.utils;
 
-import cn.dev33.satoken.context.SaHolder;
-import cn.dev33.satoken.context.model.SaStorage;
 import cn.dev33.satoken.session.SaSession;
 import cn.dev33.satoken.stp.SaLoginModel;
 import cn.dev33.satoken.stp.StpUtil;
@@ -15,7 +13,6 @@ import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.enums.UserType;
 
 import java.util.Set;
-import java.util.function.Supplier;
 
 /**
  * 登录鉴权助手
@@ -35,9 +32,11 @@ public class LoginHelper {
     public static final String LOGIN_USER_KEY = "loginUser";
     public static final String TENANT_KEY = "tenantId";
     public static final String USER_KEY = "userId";
+    public static final String USER_NAME_KEY = "userName";
     public static final String DEPT_KEY = "deptId";
+    public static final String DEPT_NAME_KEY = "deptName";
+    public static final String DEPT_CATEGORY_KEY = "deptCategory";
     public static final String CLIENT_KEY = "clientid";
-    public static final String TENANT_ADMIN_KEY = "isTenantAdmin";
 
     /**
      * 登录系统 基于 设备类型
@@ -47,32 +46,27 @@ public class LoginHelper {
      * @param model     配置参数
      */
     public static void login(LoginUser loginUser, SaLoginModel model) {
-        SaStorage storage = SaHolder.getStorage();
-        storage.set(LOGIN_USER_KEY, loginUser);
-        storage.set(TENANT_KEY, loginUser.getTenantId());
-        storage.set(USER_KEY, loginUser.getUserId());
-        storage.set(DEPT_KEY, loginUser.getDeptId());
         model = ObjectUtil.defaultIfNull(model, new SaLoginModel());
         StpUtil.login(loginUser.getLoginId(),
             model.setExtra(TENANT_KEY, loginUser.getTenantId())
                 .setExtra(USER_KEY, loginUser.getUserId())
-                .setExtra(DEPT_KEY, loginUser.getDeptId()));
-        SaSession tokenSession = StpUtil.getTokenSession();
-        tokenSession.updateTimeout(model.getTimeout());
-        tokenSession.set(LOGIN_USER_KEY, loginUser);
+                .setExtra(USER_NAME_KEY, loginUser.getUsername())
+                .setExtra(DEPT_KEY, loginUser.getDeptId())
+                .setExtra(DEPT_NAME_KEY, loginUser.getDeptName())
+                .setExtra(DEPT_CATEGORY_KEY, loginUser.getDeptCategory())
+        );
+        StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
     }
 
     /**
      * 获取用户(多级缓存)
      */
     public static LoginUser getLoginUser() {
-        return (LoginUser) getStorageIfAbsentSet(LOGIN_USER_KEY, () -> {
-            SaSession session = StpUtil.getTokenSession();
-            if (ObjectUtil.isNull(session)) {
-                return null;
-            }
-            return session.get(LOGIN_USER_KEY);
-        });
+        SaSession session = StpUtil.getTokenSession();
+        if (ObjectUtil.isNull(session)) {
+            return null;
+        }
+        return (LoginUser) session.get(LOGIN_USER_KEY);
     }
 
     /**
@@ -90,7 +84,7 @@ public class LoginHelper {
      * 获取用户id
      */
     public static Long getUserId() {
-        return  Convert.toLong(getExtra(USER_KEY));
+        return Convert.toLong(getExtra(USER_KEY));
     }
 
     /**
@@ -107,8 +101,32 @@ public class LoginHelper {
         return Convert.toLong(getExtra(DEPT_KEY));
     }
 
+    /**
+     * 获取部门名
+     */
+    public static String getDeptName() {
+        return Convert.toStr(getExtra(DEPT_NAME_KEY));
+    }
+
+    /**
+     * 获取部门类别编码
+     */
+    public static String getDeptCategory() {
+        return Convert.toStr(getExtra(DEPT_CATEGORY_KEY));
+    }
+
+    /**
+     * 获取当前 Token 的扩展信息
+     *
+     * @param key 键值
+     * @return 对应的扩展数据
+     */
     private static Object getExtra(String key) {
-        return getStorageIfAbsentSet(key, () -> StpUtil.getExtra(key));
+        try {
+            return StpUtil.getExtra(key);
+        } catch (Exception e) {
+            return null;
+        }
     }
 
     /**
@@ -136,12 +154,17 @@ public class LoginHelper {
         return UserConstants.SUPER_ADMIN_ID.equals(userId);
     }
 
+    /**
+     * 是否为超级管理员
+     *
+     * @return 结果
+     */
     public static boolean isSuperAdmin() {
         return isSuperAdmin(getUserId());
     }
 
     /**
-     * 是否为超级管理员
+     * 是否为租户管理员
      *
      * @param rolePermission 角色权限标识组
      * @return 结果
@@ -150,27 +173,22 @@ public class LoginHelper {
         return rolePermission.contains(TenantConstants.TENANT_ADMIN_ROLE_KEY);
     }
 
+    /**
+     * 是否为租户管理员
+     *
+     * @return 结果
+     */
     public static boolean isTenantAdmin() {
-        Object value = getStorageIfAbsentSet(TENANT_ADMIN_KEY, () -> {
-            return isTenantAdmin(getLoginUser().getRolePermission());
-        });
-        return Convert.toBool(value);
+        return Convert.toBool(isTenantAdmin(getLoginUser().getRolePermission()));
     }
 
+    /**
+     * 检查当前用户是否已登录
+     *
+     * @return 结果
+     */
     public static boolean isLogin() {
         return getLoginUser() != null;
     }
 
-    public static Object getStorageIfAbsentSet(String key, Supplier<Object> handle) {
-        try {
-            Object obj = SaHolder.getStorage().get(key);
-            if (ObjectUtil.isNull(obj)) {
-                obj = handle.get();
-                SaHolder.getStorage().set(key, obj);
-            }
-            return obj;
-        } catch (Exception e) {
-            return null;
-        }
-    }
 }

+ 28 - 28
ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java

@@ -4,14 +4,14 @@ import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.interceptor.SaInterceptor;
 import cn.dev33.satoken.router.SaRouter;
 import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.utils.ServletUtils;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.security.config.properties.SecurityProperties;
 import org.dromara.common.security.handler.AllUrlHandler;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@@ -38,35 +38,35 @@ public class SecurityConfig implements WebMvcConfigurer {
     public void addInterceptors(InterceptorRegistry registry) {
         // 注册路由拦截器,自定义验证规则
         registry.addInterceptor(new SaInterceptor(handler -> {
-            AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
-            // 登录验证 -- 排除多个路径
-            SaRouter
-                // 获取所有的
-                .match(allUrlHandler.getUrls())
-                // 对未排除的路径进行检查
-                .check(() -> {
-                    // 检查是否登录 是否有token
-                    StpUtil.checkLogin();
+                AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
+                // 登录验证 -- 排除多个路径
+                SaRouter
+                    // 获取所有的
+                    .match(allUrlHandler.getUrls())
+                    // 对未排除的路径进行检查
+                    .check(() -> {
+                        // 检查是否登录 是否有token
+                        StpUtil.checkLogin();
 
-                    // 检查 header 与 param 里的 clientid 与 token 里的是否一致
-                    String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
-                    String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
-                    String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
-                    if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
-                        // token 无效
-                        throw NotLoginException.newInstance(StpUtil.getLoginType(),
-                            "-100", "客户端ID与Token不匹配",
-                            StpUtil.getTokenValue());
-                    }
+                        // 检查 header 与 param 里的 clientid 与 token 里的是否一致
+                        String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
+                        String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
+                        String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
+                        if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
+                            // token 无效
+                            throw NotLoginException.newInstance(StpUtil.getLoginType(),
+                                "-100", "客户端ID与Token不匹配",
+                                StpUtil.getTokenValue());
+                        }
 
-                    // 有效率影响 用于临时测试
-                    // if (log.isDebugEnabled()) {
-                    //     log.info("剩余有效时间: {}", StpUtil.getTokenTimeout());
-                    //     log.info("临时有效时间: {}", StpUtil.getTokenActivityTimeout());
-                    // }
+                        // 有效率影响 用于临时测试
+                        // if (log.isDebugEnabled()) {
+                        //     log.info("剩余有效时间: {}", StpUtil.getTokenTimeout());
+                        //     log.info("临时有效时间: {}", StpUtil.getTokenActivityTimeout());
+                        // }
 
-                });
-        })).addPathPatterns("/**")
+                    });
+            })).addPathPatterns("/**")
             // 排除不需要拦截的路径
             .excludePathPatterns(securityProperties.getExcludes());
     }

+ 0 - 1
ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1,3 +1,2 @@
-org.dromara.common.security.handler.GlobalExceptionHandler
 org.dromara.common.security.handler.AllUrlHandler
 org.dromara.common.security.config.SecurityConfig

+ 6 - 7
ruoyi-common/ruoyi-common-sms/pom.xml

@@ -20,13 +20,12 @@
         <dependency>
             <groupId>org.dromara.sms4j</groupId>
             <artifactId>sms4j-spring-boot-starter</artifactId>
-            <exclusions>
-                <!-- 排除京东短信内存在的fastjson等待作者后续修复 -->
-                <exclusion>
-                    <groupId>com.alibaba</groupId>
-                    <artifactId>fastjson</artifactId>
-                </exclusion>
-            </exclusions>
+        </dependency>
+
+        <!-- RuoYi Common Redis-->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-redis</artifactId>
         </dependency>
 
     </dependencies>

+ 14 - 4
ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/SmsAutoConfiguration.java

@@ -1,14 +1,24 @@
 package org.dromara.common.sms.config;
 
+import org.dromara.common.sms.core.dao.PlusSmsDao;
+import org.dromara.sms4j.api.dao.SmsDao;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
 
 /**
- * 短信配置类(暂时没用 预留扩展)
+ * 短信配置类
  *
- * @author Lion Li
- * @version 4.2.0
+ * @author Feng
  */
-@AutoConfiguration
+@AutoConfiguration(after = {RedisAutoConfiguration.class})
 public class SmsAutoConfiguration {
 
+    @Primary
+    @Bean
+    public SmsDao smsDao() {
+        return new PlusSmsDao();
+    }
+
 }

+ 72 - 0
ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/dao/PlusSmsDao.java

@@ -0,0 +1,72 @@
+package org.dromara.common.sms.core.dao;
+
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.sms4j.api.dao.SmsDao;
+
+import java.time.Duration;
+
+/**
+ * SmsDao缓存配置 (使用框架自带RedisUtils实现 协议统一)
+ * <p>主要用于短信重试和拦截的缓存
+ *
+ * @author Feng
+ */
+public class PlusSmsDao implements SmsDao {
+
+    /**
+     * 存储
+     *
+     * @param key       键
+     * @param value     值
+     * @param cacheTime 缓存时间(单位:秒)
+     */
+    @Override
+    public void set(String key, Object value, long cacheTime) {
+        RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key, value, Duration.ofSeconds(cacheTime));
+    }
+
+    /**
+     * 存储
+     *
+     * @param key   键
+     * @param value 值
+     */
+    @Override
+    public void set(String key, Object value) {
+        RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key, value, true);
+    }
+
+    /**
+     * 读取
+     *
+     * @param key 键
+     * @return 值
+     */
+    @Override
+    public Object get(String key) {
+        return RedisUtils.getCacheObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
+    }
+
+    /**
+     * remove
+     * <p> 根据key移除缓存
+     *
+     * @param key 缓存键
+     * @return 被删除的value
+     * @author :Wind
+     */
+    @Override
+    public Object remove(String key) {
+        return RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + key);
+    }
+
+    /**
+     * 清空
+     */
+    @Override
+    public void clean() {
+        RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + "sms:");
+    }
+
+}

+ 7 - 0
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java

@@ -2,6 +2,8 @@ package org.dromara.common.social.config.properties;
 
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * 社交登录配置
  *
@@ -65,4 +67,9 @@ public class SocialLoginConfigProperties {
      */
     private String serverUrl;
 
+    /**
+     * 请求范围
+     */
+    private List<String> scopes;
+
 }

+ 0 - 5
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java

@@ -16,11 +16,6 @@ import java.util.Map;
 @ConfigurationProperties(prefix = "justauth")
 public class SocialProperties {
 
-    /**
-     * 是否启用
-     */
-    private Boolean enabled;
-
     /**
      * 授权类型
      */

+ 100 - 0
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/topiam/AuthTopIamRequest.java

@@ -0,0 +1,100 @@
+package org.dromara.common.social.topiam;
+
+import cn.hutool.core.lang.Dict;
+import cn.hutool.core.util.StrUtil;
+import com.xkcoding.http.support.HttpHeader;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.cache.AuthStateCache;
+import me.zhyd.oauth.config.AuthConfig;
+import me.zhyd.oauth.exception.AuthException;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthToken;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthDefaultRequest;
+import me.zhyd.oauth.utils.HttpUtils;
+import me.zhyd.oauth.utils.UrlBuilder;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.json.utils.JsonUtils;
+
+import static org.dromara.common.social.topiam.AuthTopiamSource.TOPIAM;
+
+/**
+ * TopIAM 认证请求
+ *
+ * @author xlsea
+ * @since 2024-01-06
+ */
+@Slf4j
+public class AuthTopIamRequest extends AuthDefaultRequest {
+
+    public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.topiam.server-url");
+
+    /**
+     * 设定归属域
+     */
+    public AuthTopIamRequest(AuthConfig config) {
+        super(config, TOPIAM);
+    }
+
+    public AuthTopIamRequest(AuthConfig config, AuthStateCache authStateCache) {
+        super(config, TOPIAM, authStateCache);
+    }
+
+    @Override
+    protected AuthToken getAccessToken(AuthCallback authCallback) {
+        String body = doPostAuthorizationCode(authCallback.getCode());
+        Dict object = JsonUtils.parseMap(body);
+        checkResponse(object);
+        return AuthToken.builder()
+            .accessToken(object.getStr("access_token"))
+            .refreshToken(object.getStr("refresh_token"))
+            .idToken(object.getStr("id_token"))
+            .tokenType(object.getStr("token_type"))
+            .scope(object.getStr("scope"))
+            .build();
+    }
+
+    @Override
+    protected AuthUser getUserInfo(AuthToken authToken) {
+        String body = doGetUserInfo(authToken);
+        Dict object = JsonUtils.parseMap(body);
+        checkResponse(object);
+        return AuthUser.builder()
+            .uuid(object.getStr("sub"))
+            .username(object.getStr("preferred_username"))
+            .nickname(object.getStr("nickname"))
+            .avatar(object.getStr("picture"))
+            .email(object.getStr("email"))
+            .token(authToken)
+            .source(source.toString())
+            .build();
+    }
+
+
+    @Override
+    protected String doGetUserInfo(AuthToken authToken) {
+        return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader()
+            .add("Content-Type", "application/json")
+            .add("Authorization", "Bearer " + authToken.getAccessToken()), false).getBody();
+    }
+
+
+    @Override
+    public String authorize(String state) {
+        return UrlBuilder.fromBaseUrl(super.authorize(state))
+            .queryParam("scope", StrUtil.join("%20", config.getScopes()))
+            .build();
+    }
+
+    public static void checkResponse(Dict object) {
+        // oauth/token 验证异常
+        if (object.containsKey("error")) {
+            throw new AuthException(object.getStr("error_description"));
+        }
+        // user 验证异常
+        if (object.containsKey("message")) {
+            throw new AuthException(object.getStr("message"));
+        }
+    }
+
+}

+ 51 - 0
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/topiam/AuthTopiamSource.java

@@ -0,0 +1,51 @@
+package org.dromara.common.social.topiam;
+
+import me.zhyd.oauth.config.AuthSource;
+import me.zhyd.oauth.request.AuthDefaultRequest;
+
+/**
+ * Oauth2 默认接口说明
+ *
+ * @author xlsea
+ * @since 2024-01-06
+ */
+public enum AuthTopiamSource implements AuthSource {
+
+    /**
+     * 测试
+     */
+    TOPIAM {
+        /**
+         * 授权的api
+         */
+        @Override
+        public String authorize() {
+            return AuthTopIamRequest.SERVER_URL + "/oauth2/auth";
+        }
+
+        /**
+         * 获取accessToken的api
+         */
+        @Override
+        public String accessToken() {
+            return AuthTopIamRequest.SERVER_URL + "/oauth2/token";
+        }
+
+        /**
+         * 获取用户信息的api
+         */
+        @Override
+        public String userInfo() {
+            return AuthTopIamRequest.SERVER_URL + "/oauth2/userinfo";
+        }
+
+        /**
+         * 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
+         */
+        @Override
+        public Class<? extends AuthDefaultRequest> getTargetClass() {
+            return AuthTopIamRequest.class;
+        }
+
+    }
+}

+ 4 - 1
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java

@@ -11,6 +11,7 @@ import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
 import org.dromara.common.social.config.properties.SocialProperties;
 import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
+import org.dromara.common.social.topiam.AuthTopIamRequest;
 
 /**
  * 认证授权工具类
@@ -38,7 +39,8 @@ public class SocialUtils  {
         AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
             .clientId(obj.getClientId())
             .clientSecret(obj.getClientSecret())
-            .redirectUri(obj.getRedirectUri());
+            .redirectUri(obj.getRedirectUri())
+            .scopes(obj.getScopes());
         return switch (source.toLowerCase()) {
             case "dingtalk" -> new AuthDingTalkRequest(builder.build(), STATE_CACHE);
             case "baidu" -> new AuthBaiduRequest(builder.build(), STATE_CACHE);
@@ -63,6 +65,7 @@ public class SocialUtils  {
             case "wechat_mp" -> new AuthWeChatMpRequest(builder.build(), STATE_CACHE);
             case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE);
             case "maxkey" -> new AuthMaxKeyRequest(builder.build(), STATE_CACHE);
+            case "topiam" -> new AuthTopIamRequest(builder.build(), STATE_CACHE);
             default -> throw new AuthException("未获取到有效的Auth配置");
         };
     }

+ 1 - 5
ruoyi-common/ruoyi-common-tenant/pom.xml

@@ -19,6 +19,7 @@
         <dependency>
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-common-mybatis</artifactId>
+            <optional>true</optional>
         </dependency>
 
         <dependency>
@@ -26,11 +27,6 @@
             <artifactId>ruoyi-common-redis</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>com.alibaba</groupId>
-            <artifactId>transmittable-thread-local</artifactId>
-        </dependency>
-
     </dependencies>
 
 </project>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików