소스 검색

!766 发布 5.5.0 喜迎国庆
Merge pull request !766 from 疯狂的狮子Li/dev

疯狂的狮子Li 9 달 전
부모
커밋
b30ffa952f
100개의 변경된 파일1615개의 추가작업 그리고 891개의 파일을 삭제
  1. 1 1
      .run/ruoyi-monitor-admin.run.xml
  2. 1 1
      .run/ruoyi-server.run.xml
  3. 1 1
      .run/ruoyi-snailjob-server.run.xml
  4. 6 3
      README.md
  5. 32 32
      pom.xml
  6. 2 2
      ruoyi-admin/Dockerfile
  7. 3 0
      ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java
  8. 7 4
      ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java
  9. 1 1
      ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java
  10. 1 1
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
  11. 0 2
      ruoyi-admin/src/main/resources/application-dev.yml
  12. 0 2
      ruoyi-admin/src/main/resources/application-prod.yml
  13. 13 26
      ruoyi-admin/src/main/resources/application.yml
  14. 1 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  15. 1 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  16. 1 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  17. BIN
      ruoyi-admin/src/main/resources/ip2region.xdb
  18. 2 2
      ruoyi-admin/src/main/resources/logback-plus.xml
  19. 1 1
      ruoyi-common/ruoyi-common-bom/pom.xml
  20. 0 52
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java
  21. 0 14
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java
  22. 5 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
  23. 5 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java
  24. 5 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java
  25. 4 4
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java
  26. 5 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java
  27. 11 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java
  28. 1 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
  29. 4 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java
  30. 10 2
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
  31. 9 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java
  32. 11 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java
  33. 11 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java
  34. 0 24
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java
  35. 10 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java
  36. 1 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
  37. 1 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java
  38. 89 44
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java
  39. 21 2
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
  40. 0 1
      ruoyi-common/ruoyi-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  41. 9 8
      ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java
  42. 10 8
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java
  43. 11 2
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java
  44. 200 0
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java
  45. 17 121
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
  46. 2 1
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java
  47. 3 2
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
  48. 39 0
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java
  49. 127 0
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelWriterWrapper.java
  50. 16 11
      ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java
  51. 7 1
      ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/CustomDateDeserializer.java
  52. 21 9
      ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java
  53. 54 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAdvice.java
  54. 0 50
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAspect.java
  55. 39 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcut.java
  56. 33 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcutAdvisor.java
  57. 8 4
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java
  58. 32 3
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java
  59. 6 102
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
  60. 30 29
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java
  61. 6 15
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java
  62. 50 16
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java
  63. 15 0
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/WriteOutSubscriber.java
  64. 4 7
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java
  65. 216 45
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java
  66. 8 12
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java
  67. 13 4
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java
  68. 2 1
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java
  69. 2 1
      ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java
  70. 4 1
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java
  71. 29 29
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java
  72. 0 5
      ruoyi-common/ruoyi-common-web/pom.xml
  73. 12 15
      ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java
  74. 5 3
      ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java
  75. 2 2
      ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java
  76. 0 88
      ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/utils/UnsignedMathGenerator.java
  77. 2 2
      ruoyi-extend/ruoyi-monitor-admin/Dockerfile
  78. 2 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/MonitorAdminApplication.java
  79. 0 31
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/AdminServerConfig.java
  80. 4 4
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/SecurityConfig.java
  81. 3 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml
  82. 2 2
      ruoyi-extend/ruoyi-snailjob-server/Dockerfile
  83. 146 0
      ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/common/register/ServerRegister.java
  84. 18 7
      ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml
  85. 18 7
      ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml
  86. 1 0
      ruoyi-extend/ruoyi-snailjob-server/src/main/resources/logback-plus.xml
  87. 1 1
      ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailSendController.java
  88. 12 0
      ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java
  89. 9 0
      ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java
  90. 61 0
      ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java
  91. 5 0
      ruoyi-modules/ruoyi-generator/pom.xml
  92. 6 0
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/controller/GenController.java
  93. 1 1
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java
  94. 5 3
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java
  95. 4 7
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm
  96. 1 1
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm
  97. 1 1
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm
  98. 2 3
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm
  99. 1 1
      ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/AlipayBillTask.java
  100. 1 1
      ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/SummaryBillTask.java

+ 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">
   <configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
     <deployment type="dockerfile">
       <settings>
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.4.1" />
+        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.5.0" />
         <option name="buildOnly" value="true" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
       </settings>
       </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">
   <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
     <deployment type="dockerfile">
       <settings>
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-server:5.4.1" />
+        <option name="imageTag" value="ruoyi/ruoyi-server:5.5.0" />
         <option name="buildOnly" value="true" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
       </settings>
       </settings>

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

@@ -2,7 +2,7 @@
   <configuration default="false" name="ruoyi-snailjob-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">
     <deployment type="dockerfile">
       <settings>
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.4.1" />
+        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.5.0" />
         <option name="buildOnly" value="true" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
       </settings>
       </settings>

+ 6 - 3
README.md

@@ -10,7 +10,7 @@
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
 [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 <br>
 <br>
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.4.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
 [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]()
 [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]()
 [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
 [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
 [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
 [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@@ -27,7 +27,7 @@
 > 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
 > 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
 > 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>
 > 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>
 
 
-> 文档地址: [plus-doc](https://plus-doc.dromara.org) 文档在华为云上如果打不开大概率是DNS问题 可以尝试切换网络等方式(或者科学上网)
+> 文档地址: [plus-doc](https://plus-doc.dromara.org) 国内加速: [plus-doc.top](https://plus-doc.top)
 
 
 ## 赞助商
 ## 赞助商
 
 
@@ -37,7 +37,10 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
 引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
 <font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
 <font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
 Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br>
 Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br>
-[如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
+aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br>
+Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
+
+[如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group)
 
 
 # 本框架与RuoYi的功能差异
 # 本框架与RuoYi的功能差异
 
 

+ 32 - 32
pom.xml

@@ -13,28 +13,28 @@
     <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
     <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
 
 
     <properties>
     <properties>
-        <revision>5.4.1</revision>
-        <spring-boot.version>3.4.7</spring-boot.version>
+        <revision>5.5.0</revision>
+        <spring-boot.version>3.5.6</spring-boot.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>17</java.version>
         <java.version>17</java.version>
         <mybatis.version>3.5.16</mybatis.version>
         <mybatis.version>3.5.16</mybatis.version>
-        <springdoc.version>2.8.8</springdoc.version>
+        <springdoc.version>2.8.13</springdoc.version>
         <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
         <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
-        <fastexcel.version>1.2.0</fastexcel.version>
+        <fastexcel.version>1.3.0</fastexcel.version>
         <velocity.version>2.3</velocity.version>
         <velocity.version>2.3</velocity.version>
         <satoken.version>1.44.0</satoken.version>
         <satoken.version>1.44.0</satoken.version>
-        <mybatis-plus.version>3.5.12</mybatis-plus.version>
+        <mybatis-plus.version>3.5.14</mybatis-plus.version>
         <p6spy.version>3.9.1</p6spy.version>
         <p6spy.version>3.9.1</p6spy.version>
-        <hutool.version>5.8.38</hutool.version>
-        <spring-boot-admin.version>3.4.7</spring-boot-admin.version>
-        <redisson.version>3.50.0</redisson.version>
+        <hutool.version>5.8.40</hutool.version>
+        <spring-boot-admin.version>3.5.3</spring-boot-admin.version>
+        <redisson.version>3.51.0</redisson.version>
         <lock4j.version>2.2.7</lock4j.version>
         <lock4j.version>2.2.7</lock4j.version>
         <dynamic-ds.version>4.3.1</dynamic-ds.version>
         <dynamic-ds.version>4.3.1</dynamic-ds.version>
-        <snailjob.version>1.5.0</snailjob.version>
-        <mapstruct-plus.version>1.4.8</mapstruct-plus.version>
+        <snailjob.version>1.8.0</snailjob.version>
+        <mapstruct-plus.version>1.5.0</mapstruct-plus.version>
         <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
         <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
-        <lombok.version>1.18.36</lombok.version>
+        <lombok.version>1.18.40</lombok.version>
         <bouncycastle.version>1.80</bouncycastle.version>
         <bouncycastle.version>1.80</bouncycastle.version>
         <justauth.version>1.16.7</justauth.version>
         <justauth.version>1.16.7</justauth.version>
         <!-- 离线IP地址定位库 -->
         <!-- 离线IP地址定位库 -->
@@ -42,13 +42,13 @@
         <!-- OSS 配置 -->
         <!-- OSS 配置 -->
         <aws.sdk.version>2.28.22</aws.sdk.version>
         <aws.sdk.version>2.28.22</aws.sdk.version>
         <!-- SMS 配置 -->
         <!-- SMS 配置 -->
-        <sms4j.version>3.3.4</sms4j.version>
+        <sms4j.version>3.3.5</sms4j.version>
         <!-- 限制框架中的fastjson版本 -->
         <!-- 限制框架中的fastjson版本 -->
         <fastjson.version>1.2.83</fastjson.version>
         <fastjson.version>1.2.83</fastjson.version>
         <!-- 面向运行时的D-ORM依赖 -->
         <!-- 面向运行时的D-ORM依赖 -->
         <anyline.version>8.7.2-20250603</anyline.version>
         <anyline.version>8.7.2-20250603</anyline.version>
         <!-- 工作流配置 -->
         <!-- 工作流配置 -->
-        <warm-flow.version>1.7.4</warm-flow.version>
+        <warm-flow.version>1.8.1</warm-flow.version>
 
 
         <!-- 插件版本 -->
         <!-- 插件版本 -->
         <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
         <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
@@ -118,25 +118,6 @@
                 <scope>import</scope>
                 <scope>import</scope>
             </dependency>
             </dependency>
 
 
-            <!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ -->
-            <dependency>
-                <groupId>org.dromara.warm</groupId>
-                <artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
-                <version>${warm-flow.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.dromara.warm</groupId>
-                <artifactId>warm-flow-plugin-ui-sb-web</artifactId>
-                <version>${warm-flow.version}</version>
-            </dependency>
-
-            <!-- JustAuth 的依赖配置-->
-            <dependency>
-                <groupId>me.zhyd.oauth</groupId>
-                <artifactId>JustAuth</artifactId>
-                <version>${justauth.version}</version>
-            </dependency>
-
             <!-- common 的依赖配置-->
             <!-- common 的依赖配置-->
             <dependency>
             <dependency>
                 <groupId>org.dromara</groupId>
                 <groupId>org.dromara</groupId>
@@ -313,6 +294,25 @@
                 <version>${mapstruct-plus.version}</version>
                 <version>${mapstruct-plus.version}</version>
             </dependency>
             </dependency>
 
 
+            <!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ -->
+            <dependency>
+                <groupId>org.dromara.warm</groupId>
+                <artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
+                <version>${warm-flow.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.dromara.warm</groupId>
+                <artifactId>warm-flow-plugin-ui-sb-web</artifactId>
+                <version>${warm-flow.version}</version>
+            </dependency>
+
+            <!-- JustAuth 的依赖配置-->
+            <dependency>
+                <groupId>me.zhyd.oauth</groupId>
+                <artifactId>JustAuth</artifactId>
+                <version>${justauth.version}</version>
+            </dependency>
+
             <!-- 离线IP地址定位库 ip2region -->
             <!-- 离线IP地址定位库 ip2region -->
             <dependency>
             <dependency>
                 <groupId>org.lionsoul</groupId>
                 <groupId>org.lionsoul</groupId>

+ 2 - 2
ruoyi-admin/Dockerfile

@@ -1,6 +1,6 @@
 # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
 # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
-FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
-#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
+FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
+#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
 #FROM findepi/graalvm:java17-native
 #FROM findepi/graalvm:java17-native
 
 
 LABEL maintainer="Lion Li"
 LABEL maintainer="Lion Li"

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

@@ -21,6 +21,8 @@ import org.dromara.common.core.domain.model.SocialLoginBody;
 import org.dromara.common.core.utils.*;
 import org.dromara.common.core.utils.*;
 import org.dromara.common.encrypt.annotation.ApiEncrypt;
 import org.dromara.common.encrypt.annotation.ApiEncrypt;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.ratelimiter.annotation.RateLimiter;
+import org.dromara.common.ratelimiter.enums.LimitType;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
 import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
 import org.dromara.common.social.config.properties.SocialProperties;
 import org.dromara.common.social.config.properties.SocialProperties;
@@ -198,6 +200,7 @@ public class AuthController {
      *
      *
      * @return 租户列表
      * @return 租户列表
      */
      */
+    @RateLimiter(time = 60, count = 20, limitType = LimitType.IP)
     @GetMapping("/tenant/list")
     @GetMapping("/tenant/list")
     public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
     public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
         // 返回对象
         // 返回对象

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

@@ -131,15 +131,18 @@ public class CaptchaController {
         String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
         String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
         // 生成验证码
         // 生成验证码
         CaptchaType captchaType = captchaProperties.getType();
         CaptchaType captchaType = captchaProperties.getType();
-        boolean isMath = CaptchaType.MATH == captchaType;
-        Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
-        CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
+        CodeGenerator codeGenerator;
+        if (CaptchaType.MATH == captchaType) {
+            codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false);
+        } else {
+            codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength());
+        }
         AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
         AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
         captcha.setGenerator(codeGenerator);
         captcha.setGenerator(codeGenerator);
         captcha.createCode();
         captcha.createCode();
         // 如果是数学验证码,使用SpEL表达式处理验证码结果
         // 如果是数学验证码,使用SpEL表达式处理验证码结果
         String code = captcha.getCode();
         String code = captcha.getCode();
-        if (isMath) {
+        if (CaptchaType.MATH == captchaType) {
             ExpressionParser parser = new SpelExpressionParser();
             ExpressionParser parser = new SpelExpressionParser();
             Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
             Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
             code = exp.getValue(String.class);
             code = exp.getValue(String.class);

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

@@ -87,7 +87,7 @@ public class SysRegisterService {
             recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
             recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
             throw new CaptchaExpireException();
             throw new CaptchaExpireException();
         }
         }
-        if (!code.equalsIgnoreCase(captcha)) {
+        if (!StringUtils.equalsIgnoreCase(code, captcha)) {
             recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
             recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
             throw new CaptchaException();
             throw new CaptchaException();
         }
         }

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

@@ -102,7 +102,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
             loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
             loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
             throw new CaptchaExpireException();
             throw new CaptchaExpireException();
         }
         }
-        if (!code.equalsIgnoreCase(captcha)) {
+        if (!StringUtils.equalsIgnoreCase(code, captcha)) {
             loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
             loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
             throw new CaptchaException();
             throw new CaptchaException();
         }
         }

+ 0 - 2
ruoyi-admin/src/main/resources/application-dev.yml

@@ -27,8 +27,6 @@ snail-job:
   port: 2${server.port}
   port: 2${server.port}
   # 客户端ip指定
   # 客户端ip指定
   host:
   host:
-  # RPC类型: netty, grpc
-  rpc-type: grpc
 
 
 --- # 数据源配置
 --- # 数据源配置
 spring:
 spring:

+ 0 - 2
ruoyi-admin/src/main/resources/application-prod.yml

@@ -30,8 +30,6 @@ snail-job:
   port: 2${server.port}
   port: 2${server.port}
   # 客户端ip指定
   # 客户端ip指定
   host:
   host:
-  # RPC类型: netty, grpc
-  rpc-type: grpc
 
 
 --- # 数据源配置
 --- # 数据源配置
 spring:
 spring:

+ 13 - 26
ruoyi-admin/src/main/resources/application.yml

@@ -57,6 +57,13 @@ spring:
     # 开启虚拟线程 仅jdk21可用
     # 开启虚拟线程 仅jdk21可用
     virtual:
     virtual:
       enabled: false
       enabled: false
+  task:
+    execution:
+      # 从 springboot 3.5 开始 spring自带线程池
+      # 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用
+      thread-name-prefix: async-
+      # 由spring自己初始化线程池
+      mode: force
   # 资源信息
   # 资源信息
   messages:
   messages:
     # 国际化资源文件路径
     # 国际化资源文件路径
@@ -127,6 +134,7 @@ tenant:
     - sys_user_role
     - sys_user_role
     - sys_client
     - sys_client
     - sys_oss_config
     - sys_oss_config
+    - flow_spel
 
 
 # MyBatisPlus配置
 # MyBatisPlus配置
 # https://baomidou.com/config/
 # https://baomidou.com/config/
@@ -189,13 +197,6 @@ springdoc:
       name: Lion Li
       name: Lion Li
       email: crazylionli@163.com
       email: crazylionli@163.com
       url: https://gitee.com/dromara/RuoYi-Vue-Plus
       url: https://gitee.com/dromara/RuoYi-Vue-Plus
-  components:
-    # 鉴权方式配置
-    security-schemes:
-      apiKey:
-        type: APIKEY
-        in: HEADER
-        name: ${sa-token.token-name}
   #这里定义了两个分组,可定义多个,也可以不定义
   #这里定义了两个分组,可定义多个,也可以不定义
   group-configs:
   group-configs:
     - group: 1.演示模块
     - group: 1.演示模块
@@ -213,20 +214,10 @@ springdoc:
 xss:
 xss:
   # 过滤开关
   # 过滤开关
   enabled: true
   enabled: true
-  # 排除链接(多个用逗号分隔)
+  # 排除链接
   excludeUrls:
   excludeUrls:
     - /system/notice
     - /system/notice
 
 
-# 全局线程池相关配置
-# 如使用JDK21请直接使用虚拟线程 不要开启此配置
-thread-pool:
-  # 是否开启线程池
-  enabled: false
-  # 队列最大长度
-  queueCapacity: 128
-  # 线程池维护线程所允许的空闲时间
-  keepAliveSeconds: 300
-
 --- # 分布式锁 lock4j 全局配置
 --- # 分布式锁 lock4j 全局配置
 lock4j:
 lock4j:
   # 获取分布式锁超时时间,默认为 3000 毫秒
   # 获取分布式锁超时时间,默认为 3000 毫秒
@@ -266,13 +257,9 @@ warm-flow:
   enabled: true
   enabled: true
   # 是否开启设计器ui
   # 是否开启设计器ui
   ui: true
   ui: true
+  # 是否显示流程图顶部文字
+  top-text-show: true
+  # 是否渲染节点悬浮提示,默认true
+  node-tooltip: true
   # 默认Authorization,如果有多个token,用逗号分隔
   # 默认Authorization,如果有多个token,用逗号分隔
   token-name: ${sa-token.token-name},clientid
   token-name: ${sa-token.token-name},clientid
-  # 流程状态对应的三元色
-  chart-status-color:
-    ## 未办理
-    - 62,62,62
-    ## 待办理
-    - 255,205,23
-    ## 已办理
-    - 157,255,0

+ 1 - 0
ruoyi-admin/src/main/resources/i18n/messages.properties

@@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
 user.password.not.blank=用户密码不能为空
 user.password.not.blank=用户密码不能为空
 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 user.password.not.valid=* 5-50个字符
 user.password.not.valid=* 5-50个字符
+user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符
 user.email.not.valid=邮箱格式错误
 user.email.not.valid=邮箱格式错误
 user.email.not.blank=邮箱不能为空
 user.email.not.blank=邮箱不能为空
 user.phonenumber.not.blank=用户手机号不能为空
 user.phonenumber.not.blank=用户手机号不能为空

+ 1 - 0
ruoyi-admin/src/main/resources/i18n/messages_en_US.properties

@@ -17,6 +17,7 @@ user.username.length.valid=Account length must be between {min} and {max} charac
 user.password.not.blank=Password cannot be empty
 user.password.not.blank=Password cannot be empty
 user.password.length.valid=Password length must be between {min} and {max} characters
 user.password.length.valid=Password length must be between {min} and {max} characters
 user.password.not.valid=* 5-50 characters
 user.password.not.valid=* 5-50 characters
+user.password.format.valid=Password must contain uppercase, lowercase, digit, and special character
 user.email.not.valid=Mailbox format error
 user.email.not.valid=Mailbox format error
 user.email.not.blank=Mailbox cannot be blank
 user.email.not.blank=Mailbox cannot be blank
 user.phonenumber.not.blank=Phone number cannot be blank
 user.phonenumber.not.blank=Phone number cannot be blank

+ 1 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties

@@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
 user.password.not.blank=用户密码不能为空
 user.password.not.blank=用户密码不能为空
 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 user.password.not.valid=* 5-50个字符
 user.password.not.valid=* 5-50个字符
+user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符
 user.email.not.valid=邮箱格式错误
 user.email.not.valid=邮箱格式错误
 user.email.not.blank=邮箱不能为空
 user.email.not.blank=邮箱不能为空
 user.phonenumber.not.blank=用户手机号不能为空
 user.phonenumber.not.blank=用户手机号不能为空

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


+ 2 - 2
ruoyi-admin/src/main/resources/logback-plus.xml

@@ -38,7 +38,7 @@
         <!-- 循环政策:基于时间创建日志文件 -->
         <!-- 循环政策:基于时间创建日志文件 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <!-- 日志文件名格式 -->
             <!-- 日志文件名格式 -->
-            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
             <!-- 日志最大的历史 60天 -->
             <!-- 日志最大的历史 60天 -->
             <maxHistory>60</maxHistory>
             <maxHistory>60</maxHistory>
         </rollingPolicy>
         </rollingPolicy>
@@ -60,7 +60,7 @@
         <!-- 循环政策:基于时间创建日志文件 -->
         <!-- 循环政策:基于时间创建日志文件 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <!-- 日志文件名格式 -->
             <!-- 日志文件名格式 -->
-            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
             <!-- 日志最大的历史 60天 -->
             <!-- 日志最大的历史 60天 -->
             <maxHistory>60</maxHistory>
             <maxHistory>60</maxHistory>
         </rollingPolicy>
         </rollingPolicy>

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

@@ -14,7 +14,7 @@
     </description>
     </description>
 
 
     <properties>
     <properties>
-        <revision>5.4.1</revision>
+        <revision>5.5.0</revision>
     </properties>
     </properties>
 
 
     <dependencyManagement>
     <dependencyManagement>

+ 0 - 52
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java

@@ -1,52 +0,0 @@
-package org.dromara.common.core.config;
-
-import cn.hutool.core.util.ArrayUtil;
-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 java.util.Arrays;
-import java.util.concurrent.Executor;
-
-/**
- * 异步配置
- * <p>
- * 如果未使用虚拟线程则生效
- *
- * @author Lion Li
- */
-@AutoConfiguration
-public class AsyncConfig implements AsyncConfigurer {
-
-    /**
-     * 自定义 @Async 注解使用系统线程池
-     */
-    @Override
-    public Executor getAsyncExecutor() {
-        if(SpringUtils.isVirtual()) {
-            return new VirtualThreadTaskExecutor("async-");
-        }
-        return SpringUtils.getBean("scheduledExecutorService");
-    }
-
-    /**
-     * 异步执行异常处理
-     */
-    @Override
-    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
-        return (throwable, method, objects) -> {
-            throwable.printStackTrace();
-            StringBuilder sb = new StringBuilder();
-            sb.append("Exception message - ").append(throwable.getMessage())
-                .append(", Method name - ").append(method.getName());
-            if (ArrayUtil.isNotEmpty(objects)) {
-                sb.append(", Parameter value - ").append(Arrays.toString(objects));
-            }
-            throw new ServiceException(sb.toString());
-        };
-    }
-
-}

+ 0 - 14
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java

@@ -7,11 +7,9 @@ import org.dromara.common.core.config.properties.ThreadPoolProperties;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.Threads;
 import org.dromara.common.core.utils.Threads;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.core.task.VirtualThreadTaskExecutor;
 import org.springframework.core.task.VirtualThreadTaskExecutor;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
 
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -34,18 +32,6 @@ public class ThreadPoolConfig {
 
 
     private ScheduledExecutorService scheduledExecutorService;
     private ScheduledExecutorService scheduledExecutorService;
 
 
-    @Bean(name = "threadPoolTaskExecutor")
-    @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
-    public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
-        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
-        executor.setCorePoolSize(core);
-        executor.setMaxPoolSize(core * 2);
-        executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
-        executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
-        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
-        return executor;
-    }
-
     /**
     /**
      * 执行周期性或定时任务
      * 执行周期性或定时任务
      */
      */

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

@@ -72,5 +72,10 @@ public interface Constants {
      */
      */
     Long TOP_PARENT_ID = 0L;
     Long TOP_PARENT_ID = 0L;
 
 
+    /**
+     * 加密头
+     */
+    String ENCRYPT_HEADER = "ENC_";
+
 }
 }
 
 

+ 5 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java

@@ -50,6 +50,11 @@ public class CompleteTaskDTO implements Serializable {
      */
      */
     private String notice;
     private String notice;
 
 
+    /**
+     * 办理人(可不填 用于覆盖当前节点办理人)
+     */
+    private String handler;
+
     /**
     /**
      * 流程变量
      * 流程变量
      */
      */

+ 5 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java

@@ -30,6 +30,11 @@ public class StartProcessDTO implements Serializable {
      */
      */
     private String flowCode;
     private String flowCode;
 
 
+    /**
+     * 办理人(可不填 用于覆盖当前节点办理人)
+     */
+    private String handler;
+
     /**
     /**
      * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
      * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
      */
      */

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

@@ -52,17 +52,17 @@ public class TaskAssigneeDTO implements Serializable {
      */
      */
     public static <T> List<TaskHandler> convertToHandlerList(
     public static <T> List<TaskHandler> convertToHandlerList(
         List<T> sourceList,
         List<T> sourceList,
-        Function<T, Long> storageId,
+        Function<T, String> storageId,
         Function<T, String> handlerCode,
         Function<T, String> handlerCode,
         Function<T, String> handlerName,
         Function<T, String> handlerName,
-        Function<T, Long> groupName,
+        Function<T, String> groupName,
         Function<T, Date> createTimeMapper) {
         Function<T, Date> createTimeMapper) {
         return sourceList.stream()
         return sourceList.stream()
             .map(item -> new TaskHandler(
             .map(item -> new TaskHandler(
-                String.valueOf(storageId.apply(item)),
+                storageId.apply(item),
                 handlerCode.apply(item),
                 handlerCode.apply(item),
                 handlerName.apply(item),
                 handlerName.apply(item),
-                groupName != null ? String.valueOf(groupName.apply(item)) : null,
+                groupName.apply(item),
                 createTimeMapper.apply(item)
                 createTimeMapper.apply(item)
             )).collect(Collectors.toList());
             )).collect(Collectors.toList());
     }
     }

+ 5 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java

@@ -27,6 +27,11 @@ public class ProcessEvent implements Serializable {
      */
      */
     private String flowCode;
     private String flowCode;
 
 
+    /**
+     * 实例id
+     */
+    private Long instanceId;
+
     /**
     /**
      * 业务id
      * 业务id
      */
      */

+ 11 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java

@@ -4,6 +4,7 @@ import lombok.Data;
 
 
 import java.io.Serial;
 import java.io.Serial;
 import java.io.Serializable;
 import java.io.Serializable;
+import java.util.Map;
 
 
 /**
 /**
  * 流程任务监听
  * 流程任务监听
@@ -46,6 +47,11 @@ public class ProcessTaskEvent implements Serializable {
      */
      */
     private Long taskId;
     private Long taskId;
 
 
+    /**
+     * 实例id
+     */
+    private Long instanceId;
+
     /**
     /**
      * 业务id
      * 业务id
      */
      */
@@ -56,4 +62,9 @@ public class ProcessTaskEvent implements Serializable {
      */
      */
     private String status;
     private String status;
 
 
+    /**
+     * 办理参数
+     */
+    private Map<String, Object> params;
+
 }
 }

+ 1 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java

@@ -26,6 +26,7 @@ public class PasswordLoginBody extends LoginBody {
      */
      */
     @NotBlank(message = "{user.password.not.blank}")
     @NotBlank(message = "{user.password.not.blank}")
     @Length(min = 5, max = 30, message = "{user.password.length.valid}")
     @Length(min = 5, max = 30, message = "{user.password.length.valid}")
+//    @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
     private String password;
     private String password;
 
 
 }
 }

+ 4 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java

@@ -26,8 +26,12 @@ public class RegisterBody extends LoginBody {
      */
      */
     @NotBlank(message = "{user.password.not.blank}")
     @NotBlank(message = "{user.password.not.blank}")
     @Length(min = 5, max = 30, message = "{user.password.length.valid}")
     @Length(min = 5, max = 30, message = "{user.password.length.valid}")
+//    @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
     private String password;
     private String password;
 
 
+    /**
+     * 用户类型
+     */
     private String userType;
     private String userType;
 
 
 }
 }

+ 10 - 2
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java

@@ -1,11 +1,15 @@
 package org.dromara.common.core.exception;
 package org.dromara.common.core.exception;
 
 
-import lombok.*;
+import cn.hutool.core.text.StrFormatter;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
 
 
 import java.io.Serial;
 import java.io.Serial;
 
 
 /**
 /**
- * 业务异常
+ * 业务异常(支持占位符 {} )
  *
  *
  * @author ruoyi
  * @author ruoyi
  */
  */
@@ -42,6 +46,10 @@ public final class ServiceException extends RuntimeException {
         this.code = code;
         this.code = code;
     }
     }
 
 
+    public ServiceException(String message, Object... args) {
+        this.message = StrFormatter.format(message, args);
+    }
+
     @Override
     @Override
     public String getMessage() {
     public String getMessage() {
         return message;
         return message;

+ 9 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java

@@ -3,6 +3,7 @@ package org.dromara.common.core.service;
 import org.dromara.common.core.domain.dto.DeptDTO;
 import org.dromara.common.core.domain.dto.DeptDTO;
 
 
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
 /**
 /**
  * 通用 部门服务
  * 通用 部门服务
@@ -34,4 +35,12 @@ public interface DeptService {
      */
      */
     List<DeptDTO> selectDeptsByList();
     List<DeptDTO> selectDeptsByList();
 
 
+    /**
+     * 根据部门 ID 列表查询部门名称映射关系
+     *
+     * @param deptIds 部门 ID 列表
+     * @return Map,其中 key 为部门 ID,value 为对应的部门名称
+     */
+    Map<Long, String> selectDeptNamesByIds(List<Long> deptIds);
+
 }
 }

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

@@ -1,5 +1,8 @@
 package org.dromara.common.core.service;
 package org.dromara.common.core.service;
 
 
+import java.util.List;
+import java.util.Map;
+
 /**
 /**
  * 通用 岗位服务
  * 通用 岗位服务
  *
  *
@@ -7,4 +10,12 @@ package org.dromara.common.core.service;
  */
  */
 public interface PostService {
 public interface PostService {
 
 
+    /**
+     * 根据岗位 ID 列表查询岗位名称映射关系
+     *
+     * @param postIds 岗位 ID 列表
+     * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称
+     */
+    Map<Long, String> selectPostNamesByIds(List<Long> postIds);
+
 }
 }

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

@@ -1,5 +1,8 @@
 package org.dromara.common.core.service;
 package org.dromara.common.core.service;
 
 
+import java.util.List;
+import java.util.Map;
+
 /**
 /**
  * 通用 角色服务
  * 通用 角色服务
  *
  *
@@ -7,4 +10,12 @@ package org.dromara.common.core.service;
  */
  */
 public interface RoleService {
 public interface RoleService {
 
 
+    /**
+     * 根据角色 ID 列表查询角色名称映射关系
+     *
+     * @param roleIds 角色 ID 列表
+     * @return Map,其中 key 为角色 ID,value 为对应的角色名称
+     */
+    Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
+
 }
 }

+ 0 - 24
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java

@@ -100,28 +100,4 @@ public interface UserService {
      */
      */
     Map<Long, String> selectUserNamesByIds(List<Long> userIds);
     Map<Long, String> selectUserNamesByIds(List<Long> userIds);
 
 
-    /**
-     * 根据角色 ID 列表查询角色名称映射关系
-     *
-     * @param roleIds 角色 ID 列表
-     * @return Map,其中 key 为角色 ID,value 为对应的角色名称
-     */
-    Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
-
-    /**
-     * 根据部门 ID 列表查询部门名称映射关系
-     *
-     * @param deptIds 部门 ID 列表
-     * @return Map,其中 key 为部门 ID,value 为对应的部门名称
-     */
-    Map<Long, String> selectDeptNamesByIds(List<Long> deptIds);
-
-    /**
-     * 根据岗位 ID 列表查询岗位名称映射关系
-     *
-     * @param postIds 岗位 ID 列表
-     * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称
-     */
-    Map<Long, String> selectPostNamesByIds(List<Long> postIds);
-
 }
 }

+ 10 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java

@@ -82,6 +82,7 @@ public interface WorkflowService {
      * completeTask.getVariables().put("ignore", true);
      * completeTask.getVariables().put("ignore", true);
      *
      *
      * @param completeTask 参数
      * @param completeTask 参数
+     * @return 结果
      */
      */
     boolean completeTask(CompleteTaskDTO completeTask);
     boolean completeTask(CompleteTaskDTO completeTask);
 
 
@@ -90,6 +91,15 @@ public interface WorkflowService {
      *
      *
      * @param taskId  任务ID
      * @param taskId  任务ID
      * @param message 办理意见
      * @param message 办理意见
+     * @return 结果
      */
      */
     boolean completeTask(Long taskId, String message);
     boolean completeTask(Long taskId, String message);
+
+    /**
+     * 启动流程并办理第一个任务
+     *
+     * @param startProcess 参数
+     * @return 结果
+     */
+    boolean startCompleteTask(StartProcessDTO startProcess);
 }
 }

+ 1 - 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java

@@ -293,7 +293,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
 
 
         // 校验时间跨度不超过最大限制
         // 校验时间跨度不超过最大限制
         if (diff > maxValue) {
         if (diff > maxValue) {
-            throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase());
+            throw new ServiceException("最大时间跨度为 {} {}", maxValue, unit.toString().toLowerCase());
         }
         }
     }
     }
 
 

+ 1 - 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java

@@ -115,7 +115,7 @@ public class ServletUtils extends JakartaServletUtil {
     public static Map<String, String> getParamMap(ServletRequest request) {
     public static Map<String, String> getParamMap(ServletRequest request) {
         Map<String, String> params = new HashMap<>();
         Map<String, String> params = new HashMap<>();
         for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
         for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
-            params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR));
+            params.put(entry.getKey(), StringUtils.joinComma(entry.getValue()));
         }
         }
         return params;
         return params;
     }
     }

+ 89 - 44
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java

@@ -7,7 +7,6 @@ import lombok.NoArgsConstructor;
 
 
 import java.util.*;
 import java.util.*;
 import java.util.function.BiFunction;
 import java.util.function.BiFunction;
-import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
@@ -31,8 +30,10 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return CollUtil.newArrayList();
             return CollUtil.newArrayList();
         }
         }
-        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
-        return collection.stream().filter(function).collect(Collectors.toList());
+        return collection.stream()
+            .filter(function)
+            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+            .collect(Collectors.toList());
     }
     }
 
 
     /**
     /**
@@ -40,13 +41,26 @@ public class StreamUtils {
      *
      *
      * @param collection 需要查询的集合
      * @param collection 需要查询的集合
      * @param function   过滤方法
      * @param function   过滤方法
-     * @return 找到符合条件的第一个元素,没有则返回null
+     * @return 找到符合条件的第一个元素,没有则返回 Optional.empty()
      */
      */
-    public static <E> E findFirst(Collection<E> collection, Predicate<E> function) {
+    public static <E> Optional<E> findFirst(Collection<E> collection, Predicate<E> function) {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
-            return null;
+            return Optional.empty();
         }
         }
-        return collection.stream().filter(function).findFirst().orElse(null);
+        return collection.stream()
+            .filter(function)
+            .findFirst();
+    }
+
+    /**
+     * 找到流中满足条件的第一个元素值
+     *
+     * @param collection 需要查询的集合
+     * @param function   过滤方法
+     * @return 找到符合条件的第一个元素,没有则返回 null
+     */
+    public static <E> E findFirstValue(Collection<E> collection, Predicate<E> function) {
+        return findFirst(collection,function).orElse(null);
     }
     }
 
 
     /**
     /**
@@ -54,13 +68,26 @@ public class StreamUtils {
      *
      *
      * @param collection 需要查询的集合
      * @param collection 需要查询的集合
      * @param function   过滤方法
      * @param function   过滤方法
-     * @return 找到符合条件的任意一个元素,没有则返回null
+     * @return 找到符合条件的任意一个元素,没有则返回 Optional.empty()
      */
      */
     public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
     public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return Optional.empty();
             return Optional.empty();
         }
         }
-        return collection.stream().filter(function).findAny();
+        return collection.stream()
+            .filter(function)
+            .findAny();
+    }
+
+    /**
+     * 找到流中任意一个满足条件的元素值
+     *
+     * @param collection 需要查询的集合
+     * @param function   过滤方法
+     * @return 找到符合条件的任意一个元素,没有则返回null
+     */
+    public static <E> E findAnyValue(Collection<E> collection, Predicate<E> function) {
+        return findAny(collection,function).orElse(null);
     }
     }
 
 
     /**
     /**
@@ -86,7 +113,10 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return StringUtils.EMPTY;
             return StringUtils.EMPTY;
         }
         }
-        return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
+        return collection.stream()
+            .map(function)
+            .filter(Objects::nonNull)
+            .collect(Collectors.joining(delimiter));
     }
     }
 
 
     /**
     /**
@@ -100,8 +130,11 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return CollUtil.newArrayList();
             return CollUtil.newArrayList();
         }
         }
-        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
-        return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
+        return collection.stream()
+            .filter(Objects::nonNull)
+            .sorted(comparing)
+            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+            .collect(Collectors.toList());
     }
     }
 
 
     /**
     /**
@@ -118,7 +151,9 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return MapUtil.newHashMap();
             return MapUtil.newHashMap();
         }
         }
-        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+        return collection.stream()
+            .filter(Objects::nonNull)
+            .collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
     }
     }
 
 
     /**
     /**
@@ -137,7 +172,25 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return MapUtil.newHashMap();
             return MapUtil.newHashMap();
         }
         }
-        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
+        return collection.stream()
+            .filter(Objects::nonNull)
+            .collect(Collectors.toMap(key, value, (l, r) -> l));
+    }
+
+    /**
+     * 获取 map 中的数据作为新 Map 的 value ,key 不变
+     * @param map 需要处理的map
+     * @param take 取值函数
+     * @param <K> map中的key类型
+     * @param <E> map中的value类型
+     * @param <V> 新map中的value类型
+     * @return 新的map
+     */
+    public static <K, E, V> Map<K, V> toMap(Map<K, E> map, BiFunction<K, E, V> take) {
+        if (CollUtil.isEmpty(map)) {
+            return MapUtil.newHashMap();
+        }
+        return toMap(map.entrySet(), Map.Entry::getKey, entry -> take.apply(entry.getKey(), entry.getValue()));
     }
     }
 
 
     /**
     /**
@@ -154,8 +207,8 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return MapUtil.newHashMap();
             return MapUtil.newHashMap();
         }
         }
-        return collection
-            .stream().filter(Objects::nonNull)
+        return collection.stream()
+            .filter(Objects::nonNull)
             .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
             .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
     }
     }
 
 
@@ -175,8 +228,8 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return MapUtil.newHashMap();
             return MapUtil.newHashMap();
         }
         }
-        return collection
-            .stream().filter(Objects::nonNull)
+        return collection.stream()
+            .filter(Objects::nonNull)
             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
     }
     }
 
 
@@ -193,11 +246,11 @@ public class StreamUtils {
      * @return 分类后的map
      * @return 分类后的map
      */
      */
     public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
     public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
-        if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
+        if (CollUtil.isEmpty(collection)) {
             return MapUtil.newHashMap();
             return MapUtil.newHashMap();
         }
         }
-        return collection
-            .stream().filter(Objects::nonNull)
+        return collection.stream()
+            .filter(Objects::nonNull)
             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
     }
     }
 
 
@@ -215,8 +268,7 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
         if (CollUtil.isEmpty(collection)) {
             return CollUtil.newArrayList();
             return CollUtil.newArrayList();
         }
         }
-        return collection
-            .stream()
+        return collection.stream()
             .map(function)
             .map(function)
             .filter(Objects::nonNull)
             .filter(Objects::nonNull)
             // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
             // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
@@ -234,11 +286,10 @@ public class StreamUtils {
      * @return 转化后的Set
      * @return 转化后的Set
      */
      */
     public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
     public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
-        if (CollUtil.isEmpty(collection) || function == null) {
+        if (CollUtil.isEmpty(collection)) {
             return CollUtil.newHashSet();
             return CollUtil.newHashSet();
         }
         }
-        return collection
-            .stream()
+        return collection.stream()
             .map(function)
             .map(function)
             .filter(Objects::nonNull)
             .filter(Objects::nonNull)
             .collect(Collectors.toSet());
             .collect(Collectors.toSet());
@@ -258,26 +309,20 @@ public class StreamUtils {
      * @return 合并后的map
      * @return 合并后的map
      */
      */
     public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
     public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
-        if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
+        if (CollUtil.isEmpty(map1) && CollUtil.isEmpty(map2)) {
+            // 如果两个 map 都为空,则直接返回空的 map
             return MapUtil.newHashMap();
             return MapUtil.newHashMap();
-        } else if (MapUtil.isEmpty(map1)) {
-            map1 = MapUtil.newHashMap();
-        } else if (MapUtil.isEmpty(map2)) {
-            map2 = MapUtil.newHashMap();
-        }
-        Set<K> key = new HashSet<>();
-        key.addAll(map1.keySet());
-        key.addAll(map2.keySet());
-        Map<K, V> map = new HashMap<>();
-        for (K t : key) {
-            X x = map1.get(t);
-            Y y = map2.get(t);
-            V z = merge.apply(x, y);
-            if (z != null) {
-                map.put(t, z);
-            }
+        } else if (CollUtil.isEmpty(map1)) {
+            // 如果 map1 为空,则直接处理返回 map2
+            return toMap(map2.entrySet(), Map.Entry::getKey, entry -> merge.apply(null, entry.getValue()));
+        } else if (CollUtil.isEmpty(map2)) {
+            // 如果 map2 为空,则直接处理返回 map1
+            return toMap(map1.entrySet(), Map.Entry::getKey, entry -> merge.apply(entry.getValue(), null));
         }
         }
-        return map;
+        Set<K> keySet = new HashSet<>();
+        keySet.addAll(map1.keySet());
+        keySet.addAll(map2.keySet());
+        return toMap(keySet, key -> key, key -> merge.apply(map1.get(key), map2.get(key)));
     }
     }
 
 
 }
 }

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

@@ -260,13 +260,13 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
         if (s != null) {
         if (s != null) {
             final int len = s.length();
             final int len = s.length();
             if (s.length() <= size) {
             if (s.length() <= size) {
-                sb.append(String.valueOf(c).repeat(size - len));
+                sb.append(Convert.toStr(c).repeat(size - len));
                 sb.append(s);
                 sb.append(s);
             } else {
             } else {
                 return s.substring(len - size, len);
                 return s.substring(len - size, len);
             }
             }
         } else {
         } else {
-            sb.append(String.valueOf(c).repeat(Math.max(0, size)));
+            sb.append(Convert.toStr(c).repeat(Math.max(0, size)));
         }
         }
         return sb.toString();
         return sb.toString();
     }
     }
@@ -361,5 +361,24 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
             return input;
             return input;
         }
         }
     }
     }
+    /**
+     * 将可迭代对象中的元素使用逗号拼接成字符串
+     *
+     * @param iterable 可迭代对象,如 List、Set 等
+     * @return 拼接后的字符串
+     */
+    public static String joinComma(Iterable<?> iterable) {
+        return StringUtils.join(iterable, SEPARATOR);
+    }
+
+    /**
+     * 将数组中的元素使用逗号拼接成字符串
+     *
+     * @param array 任意类型的数组
+     * @return 拼接后的字符串
+     */
+    public static String joinComma(Object[] array) {
+        return StringUtils.join(array, SEPARATOR);
+    }
 
 
 }
 }

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

@@ -1,5 +1,4 @@
 org.dromara.common.core.config.ApplicationConfig
 org.dromara.common.core.config.ApplicationConfig
-org.dromara.common.core.config.AsyncConfig
 org.dromara.common.core.config.ThreadPoolConfig
 org.dromara.common.core.config.ThreadPoolConfig
 org.dromara.common.core.config.ValidatorConfig
 org.dromara.common.core.config.ValidatorConfig
 org.dromara.common.core.utils.SpringUtils
 org.dromara.common.core.utils.SpringUtils

+ 9 - 8
ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java

@@ -54,14 +54,15 @@ public class SpringDocConfig {
         openApi.externalDocs(properties.getExternalDocs());
         openApi.externalDocs(properties.getExternalDocs());
         openApi.tags(properties.getTags());
         openApi.tags(properties.getTags());
         openApi.paths(properties.getPaths());
         openApi.paths(properties.getPaths());
-        openApi.components(properties.getComponents());
-        Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
-        List<SecurityRequirement> list = new ArrayList<>();
-        SecurityRequirement securityRequirement = new SecurityRequirement();
-        keySet.forEach(securityRequirement::addList);
-        list.add(securityRequirement);
-        openApi.security(list);
-
+        if (properties.getComponents() != null) {
+            openApi.components(properties.getComponents());
+            Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
+            List<SecurityRequirement> list = new ArrayList<>();
+            SecurityRequirement securityRequirement = new SecurityRequirement();
+            keySet.forEach(securityRequirement::addList);
+            list.add(securityRequirement);
+            openApi.security(list);
+        }
         return openApi;
         return openApi;
     }
     }
 
 

+ 10 - 8
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/config/ApiDecryptAutoConfiguration.java

@@ -6,6 +6,7 @@ import org.dromara.common.encrypt.properties.ApiDecryptProperties;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistration;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 
 
@@ -20,13 +21,14 @@ import org.springframework.context.annotation.Bean;
 public class ApiDecryptAutoConfiguration {
 public class ApiDecryptAutoConfiguration {
 
 
     @Bean
     @Bean
-    public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
-        FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
-        registration.setDispatcherTypes(DispatcherType.REQUEST);
-        registration.setFilter(new CryptoFilter(properties));
-        registration.addUrlPatterns("/*");
-        registration.setName("cryptoFilter");
-        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
-        return registration;
+    @FilterRegistration(
+        name = "cryptoFilter",
+        urlPatterns = "/*",
+        order = FilterRegistrationBean.HIGHEST_PRECEDENCE,
+        dispatcherTypes = DispatcherType.REQUEST
+    )
+    public CryptoFilter cryptoFilter(ApiDecryptProperties properties) {
+        return new CryptoFilter(properties);
     }
     }
+
 }
 }

+ 11 - 2
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ReflectUtil;
 import lombok.NoArgsConstructor;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.io.Resources;
 import org.apache.ibatis.io.Resources;
+import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.utils.ObjectUtils;
 import org.dromara.common.core.utils.ObjectUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.encrypt.annotation.EncryptField;
 import org.dromara.common.encrypt.annotation.EncryptField;
@@ -92,8 +93,12 @@ public class EncryptorManager {
      * @param encryptContext 加密相关的配置信息
      * @param encryptContext 加密相关的配置信息
      */
      */
     public String encrypt(String value, EncryptContext encryptContext) {
     public String encrypt(String value, EncryptContext encryptContext) {
+        if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
+            return value;
+        }
         IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
         IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
-        return encryptor.encrypt(value, encryptContext.getEncode());
+        String encrypt = encryptor.encrypt(value, encryptContext.getEncode());
+        return Constants.ENCRYPT_HEADER + encrypt;
     }
     }
 
 
     /**
     /**
@@ -103,8 +108,12 @@ public class EncryptorManager {
      * @param encryptContext 加密相关的配置信息
      * @param encryptContext 加密相关的配置信息
      */
      */
     public String decrypt(String value, EncryptContext encryptContext) {
     public String decrypt(String value, EncryptContext encryptContext) {
+        if (!StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
+            return value;
+        }
         IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
         IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
-        return encryptor.decrypt(value);
+        String str = StringUtils.removeStart(value, Constants.ENCRYPT_HEADER);
+        return encryptor.decrypt(str);
     }
     }
 
 
     /**
     /**

+ 200 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java

@@ -0,0 +1,200 @@
+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 cn.idev.excel.annotation.ExcelIgnore;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import lombok.SneakyThrows;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.excel.annotation.CellMerge;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * 单元格合并处理器
+ *
+ * @author Lion Li
+ */
+public class CellMergeHandler {
+
+    private final boolean hasTitle;
+    private int rowIndex;
+
+    private CellMergeHandler(final boolean hasTitle) {
+        this.hasTitle = hasTitle;
+        // 行合并开始下标
+        this.rowIndex = hasTitle ? 1 : 0;
+    }
+
+    @SneakyThrows
+    public List<CellRangeAddress> handle(List<?> rows) {
+        // 如果入参为空集合则返回空集
+        if (CollUtil.isEmpty(rows)) {
+            return Collections.emptyList();
+        }
+
+        // 获取有合并注解的字段
+        Map<Field, FieldColumnIndex> mergeFields = getFieldColumnIndexMap(rows.get(0).getClass());
+        // 如果没有需要合并的字段则返回空集
+        if (CollUtil.isEmpty(mergeFields)) {
+            return Collections.emptyList();
+        }
+
+        // 结果集
+        List<CellRangeAddress> result = new ArrayList<>();
+
+        // 生成两两合并单元格
+        Map<Field, RepeatCell> rowRepeatCellMap = new HashMap<>();
+        for (Map.Entry<Field, FieldColumnIndex> item : mergeFields.entrySet()) {
+            Field field = item.getKey();
+            FieldColumnIndex itemValue = item.getValue();
+            int colNum = itemValue.colIndex();
+            CellMerge cellMerge = itemValue.cellMerge();
+
+            for (int i = 0; i < rows.size(); i++) {
+                // 当前行数据
+                Object currentRowObj = rows.get(i);
+                // 当前行数据字段值
+                Object currentRowObjFieldVal = ReflectUtils.invokeGetter(currentRowObj, field.getName());
+
+                // 空值跳过不处理
+                if (currentRowObjFieldVal == null || "".equals(currentRowObjFieldVal)) {
+                    continue;
+                }
+
+                // 单元格合并Map是否存在数据,如果不存在则添加当前行的字段值
+                if (!rowRepeatCellMap.containsKey(field)) {
+                    rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
+                    continue;
+                }
+
+                // 获取 单元格合并Map 中字段值
+                RepeatCell repeatCell = rowRepeatCellMap.get(field);
+                Object cellValue = repeatCell.value();
+                int current = repeatCell.current();
+
+                // 检查是否满足合并条件
+                // currentRowObj 当前行数据
+                // rows.get(i - 1) 上一行数据 注:由于 if (!rowRepeatCellMap.containsKey(field)) 条件的存在,所以该 i 必不可能小于1
+                // cellMerge 当前行字段合并注解
+                boolean merge = isMerge(currentRowObj, rows.get(i - 1), cellMerge);
+
+                // 是否添加到结果集
+                boolean isAddResult = false;
+                // 最新行
+                int lastRow = i + rowIndex - 1;
+
+                // 如果当前行字段值和缓存中的字段值不相等,或不满足合并条件,则替换
+                if (!currentRowObjFieldVal.equals(cellValue) || !merge) {
+                    rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
+                    isAddResult = true;
+                }
+
+                // 如果最后一行不能合并,检查之前的数据是否需要合并;如果最后一行可以合并,则直接合并到最后
+                if (i == rows.size() - 1) {
+                    isAddResult = true;
+                    if (i > current) {
+                        lastRow = i + rowIndex;
+                    }
+                }
+
+                if (isAddResult && i > current) {
+                    result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum));
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 获取带有合并注解的字段列索引和合并注解信息Map集
+     */
+    private Map<Field, FieldColumnIndex> getFieldColumnIndexMap(Class<?> clazz) {
+        boolean annotationPresent = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
+        Field[] fields = ReflectUtils.getFields(clazz, field -> {
+            if ("serialVersionUID".equals(field.getName())) {
+                return false;
+            }
+            if (field.isAnnotationPresent(ExcelIgnore.class)) {
+                return false;
+            }
+            return !annotationPresent || field.isAnnotationPresent(ExcelProperty.class);
+        });
+
+        // 有注解的字段
+        Map<Field, FieldColumnIndex> mergeFields = new HashMap<>();
+        for (int i = 0; i < fields.length; i++) {
+            Field field = fields[i];
+            if (!field.isAnnotationPresent(CellMerge.class)) {
+                continue;
+            }
+            CellMerge cm = field.getAnnotation(CellMerge.class);
+            int index = cm.index() == -1 ? i : cm.index();
+            mergeFields.put(field, FieldColumnIndex.of(index, cm));
+
+            if (hasTitle) {
+                ExcelProperty property = field.getAnnotation(ExcelProperty.class);
+                rowIndex = Math.max(rowIndex, property.value().length);
+            }
+        }
+        return mergeFields;
+    }
+
+    private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
+        final String[] mergeBy = cellMerge.mergeBy();
+        if (StrUtil.isAllNotBlank(mergeBy)) {
+            //比对当前行和上一行的各个属性值一一比对 如果全为真 则为真
+            for (String fieldName : mergeBy) {
+                final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName);
+                final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName);
+                if (!Objects.equals(valPre, valCurrent)) {
+                    //依赖字段如有任一不等值,则标记为不可合并
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 单元格合并
+     */
+    record RepeatCell(Object value, int current) {
+        static RepeatCell of(Object value, int current) {
+            return new RepeatCell(value, current);
+        }
+    }
+
+    /**
+     * 字段列索引和合并注解信息
+     */
+    record FieldColumnIndex(int colIndex, CellMerge cellMerge) {
+        static FieldColumnIndex of(int colIndex, CellMerge cellMerge) {
+            return new FieldColumnIndex(colIndex, cellMerge);
+        }
+    }
+
+    /**
+     * 创建一个单元格合并处理器实例
+     *
+     * @param hasTitle 是否合并标题
+     * @return 单元格合并处理器
+     */
+    public static CellMergeHandler of(final boolean hasTitle) {
+        return new CellMergeHandler(hasTitle);
+    }
+
+    /**
+     * 创建一个单元格合并处理器实例(默认不合并标题)
+     *
+     * @return 单元格合并处理器
+     */
+    public static CellMergeHandler of() {
+        return new CellMergeHandler(false);
+    }
+
+}

+ 17 - 121
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java

@@ -1,24 +1,15 @@
 package org.dromara.common.excel.core;
 package org.dromara.common.excel.core;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.idev.excel.annotation.ExcelProperty;
 import cn.idev.excel.metadata.Head;
 import cn.idev.excel.metadata.Head;
 import cn.idev.excel.write.handler.WorkbookWriteHandler;
 import cn.idev.excel.write.handler.WorkbookWriteHandler;
 import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext;
 import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext;
 import cn.idev.excel.write.merge.AbstractMergeStrategy;
 import cn.idev.excel.write.merge.AbstractMergeStrategy;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.util.CellRangeAddress;
 import org.apache.poi.ss.util.CellRangeAddress;
-import org.dromara.common.core.utils.reflect.ReflectUtils;
-import org.dromara.common.excel.annotation.CellMerge;
 
 
-import java.lang.reflect.Field;
 import java.util.*;
 import java.util.*;
 
 
 /**
 /**
@@ -30,134 +21,39 @@ import java.util.*;
 public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
 public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
 
 
     private final List<CellRangeAddress> cellList;
     private final List<CellRangeAddress> cellList;
-    private final boolean hasTitle;
-    private int rowIndex;
+
+    public CellMergeStrategy(List<CellRangeAddress> cellList) {
+        this.cellList = cellList;
+    }
 
 
     public CellMergeStrategy(List<?> list, boolean hasTitle) {
     public CellMergeStrategy(List<?> list, boolean hasTitle) {
-        this.hasTitle = hasTitle;
-        // 行合并开始下标
-        this.rowIndex = hasTitle ? 1 : 0;
-        this.cellList = handle(list, hasTitle);
+        this.cellList = CellMergeHandler.of(hasTitle).handle(list);
     }
     }
 
 
     @Override
     @Override
     protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
     protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
+        if (CollUtil.isEmpty(cellList)){
+            return;
+        }
         //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
         //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
         final int rowIndex = cell.getRowIndex();
         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();
-                }
+        for (CellRangeAddress cellAddresses : cellList) {
+            final int firstRow = cellAddresses.getFirstRow();
+            if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
+                cell.setBlank();
             }
             }
         }
         }
     }
     }
 
 
     @Override
     @Override
     public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
     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<>();
-        if (CollUtil.isEmpty(list)) {
-            return cellList;
-        }
-        Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
-
-        // 有注解的字段
-        List<Field> mergeFields = new ArrayList<>();
-        List<Integer> mergeFieldsIndex = new ArrayList<>();
-        for (int i = 0; i < fields.length; i++) {
-            Field field = fields[i];
-            if (field.isAnnotationPresent(CellMerge.class)) {
-                CellMerge cm = field.getAnnotation(CellMerge.class);
-                mergeFields.add(field);
-                mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
-                if (hasTitle) {
-                    ExcelProperty property = field.getAnnotation(ExcelProperty.class);
-                    rowIndex = Math.max(rowIndex, property.value().length);
-                }
-            }
-        }
-
-        Map<Field, RepeatCell> map = new HashMap<>();
-        // 生成两两合并单元格
-        for (int i = 0; i < list.size(); i++) {
-            for (int j = 0; j < mergeFields.size(); j++) {
-                Field field = mergeFields.get(j);
-                Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
-
-                int colNum = mergeFieldsIndex.get(j);
-                if (!map.containsKey(field)) {
-                    map.put(field, new RepeatCell(val, i));
-                } else {
-                    RepeatCell repeatCell = map.get(field);
-                    Object cellValue = repeatCell.getValue();
-                    if (cellValue == null || "".equals(cellValue)) {
-                        // 空值跳过不合并
-                        continue;
-                    }
-
-                    if (!cellValue.equals(val)) {
-                        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 (!isMerge(list, i, field)) {
-                            // 如果最后一行不能合并,检查之前的数据是否需要合并
-                            if (i - repeatCell.getCurrent() > 1) {
-                                cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
-                            }
-                        } else if (i > repeatCell.getCurrent()) {
-                            // 如果最后一行可以合并,则直接合并到最后
-                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
-                        }
-                    } else if (!isMerge(list, i, field)) {
-                        if ((i - repeatCell.getCurrent() > 1)) {
-                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
-                        }
-                        map.put(field, new RepeatCell(val, i));
-                    }
-                }
-            }
+        if (CollUtil.isEmpty(cellList)){
+            return;
         }
         }
-        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;
-                }
-            }
+        //当前表格写完后,统一写入
+        for (CellRangeAddress item : cellList) {
+            context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
         }
         }
-        return isMerge;
     }
     }
 
 
-    @Data
-    @AllArgsConstructor
-    static class RepeatCell {
-
-        private Object value;
-
-        private int current;
-
-    }
 }
 }

+ 2 - 1
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java

@@ -1,5 +1,6 @@
 package org.dromara.common.excel.core;
 package org.dromara.common.excel.core;
 
 
+import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.Data;
@@ -65,7 +66,7 @@ public class DropDownOptions {
         StringBuilder stringBuffer = new StringBuilder();
         StringBuilder stringBuffer = new StringBuilder();
         String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
         String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
         for (int i = 0; i < vars.length; i++) {
         for (int i = 0; i < vars.length; i++) {
-            String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
+            String var = StrUtil.trimToEmpty(Convert.toStr(vars[i]));
             if (!var.matches(regex)) {
             if (!var.matches(regex)) {
                 throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
                 throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
             }
             }

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

@@ -1,6 +1,7 @@
 package org.dromara.common.excel.core;
 package org.dromara.common.excel.core;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.EnumUtil;
 import cn.hutool.core.util.EnumUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ObjectUtil;
@@ -103,7 +104,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
                 if (StringUtils.isNotBlank(dictType)) {
                 if (StringUtils.isNotBlank(dictType)) {
                     // 如果传递了字典名,则依据字典建立下拉
                     // 如果传递了字典名,则依据字典建立下拉
                     Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
                     Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
-                        .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
+                        .orElseThrow(() -> new ServiceException("字典 {} 不存在", dictType))
                         .values();
                         .values();
                     options = new ArrayList<>(values);
                     options = new ArrayList<>(values);
                 } else if (StringUtils.isNotBlank(converterExp)) {
                 } else if (StringUtils.isNotBlank(converterExp)) {
@@ -115,7 +116,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
                 // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
                 // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
                 ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
                 ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
                 List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
                 List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
-                options = StreamUtils.toList(values, String::valueOf);
+                options = StreamUtils.toList(values, Convert::toStr);
             }
             }
             if (ObjectUtil.isNotEmpty(options)) {
             if (ObjectUtil.isNotEmpty(options)) {
                 // 仅当下拉可选项不为空时执行
                 // 仅当下拉可选项不为空时执行

+ 39 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java

@@ -27,6 +27,7 @@ import java.io.UnsupportedEncodingException;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.function.Consumer;
 
 
 /**
 /**
  * Excel相关处理
  * Excel相关处理
@@ -203,6 +204,44 @@ public class ExcelUtil {
         builder.doWrite(list);
         builder.doWrite(list);
     }
     }
 
 
+    /**
+     * 导出excel
+     *
+     * @param headType 带Excel注解的类型
+     * @param os       输出流
+     * @param options  Excel下拉可选项
+     * @param consumer 导出助手消费函数
+     */
+    public static <T> void exportExcel(Class<T> headType, OutputStream os, List<DropDownOptions> options, Consumer<ExcelWriterWrapper<T>> consumer) {
+        try (ExcelWriter writer = FastExcel.write(os, headType)
+            .autoCloseStream(false)
+            // 自动适配
+            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+            // 大数值自动转换 防止失真
+            .registerConverter(new ExcelBigNumberConvert())
+            // 批注必填项处理
+            .registerWriteHandler(new DataWriteHandler(headType))
+            // 添加下拉框操作
+            .registerWriteHandler(new ExcelDownHandler(options))
+            .build()) {
+            // 执行消费函数
+            consumer.accept(ExcelWriterWrapper.of(writer));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param headType 带Excel注解的类型
+     * @param os       输出流
+     * @param consumer 导出助手消费函数
+     */
+    public static <T> void exportExcel(Class<T> headType, OutputStream os, Consumer<ExcelWriterWrapper<T>> consumer) {
+        exportExcel(headType, os, null, consumer);
+    }
+
     /**
     /**
      * 单表多数据模板导出 模板格式为 {.属性}
      * 单表多数据模板导出 模板格式为 {.属性}
      *
      *

+ 127 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelWriterWrapper.java

@@ -0,0 +1,127 @@
+package org.dromara.common.excel.utils;
+
+import cn.idev.excel.ExcelWriter;
+import cn.idev.excel.FastExcel;
+import cn.idev.excel.context.WriteContext;
+import cn.idev.excel.write.builder.ExcelWriterSheetBuilder;
+import cn.idev.excel.write.builder.ExcelWriterTableBuilder;
+import cn.idev.excel.write.metadata.WriteSheet;
+import cn.idev.excel.write.metadata.WriteTable;
+import cn.idev.excel.write.metadata.fill.FillConfig;
+
+import java.util.Collection;
+import java.util.function.Supplier;
+
+/**
+ * ExcelWriterWrapper Excel写出包装器
+ * <br>
+ * 提供了一组与 ExcelWriter 一一对应的写出方法,避免直接提供 ExcelWriter 而导致的一些不可控问题(比如提前关闭了IO流等)
+ *
+ * @author 秋辞未寒
+ * @see ExcelWriter
+ */
+public record ExcelWriterWrapper<T>(ExcelWriter excelWriter) {
+
+    public void write(Collection<T> data, WriteSheet writeSheet) {
+        excelWriter.write(data, writeSheet);
+    }
+
+    public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet) {
+        excelWriter.write(supplier.get(), writeSheet);
+    }
+
+    public void write(Collection<T> data, WriteSheet writeSheet, WriteTable writeTable) {
+        excelWriter.write(data, writeSheet, writeTable);
+    }
+
+    public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet, WriteTable writeTable) {
+        excelWriter.write(supplier.get(), writeSheet, writeTable);
+    }
+
+    public void fill(Object data, WriteSheet writeSheet) {
+        excelWriter.fill(data, writeSheet);
+    }
+
+    public void fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) {
+        excelWriter.fill(data, fillConfig, writeSheet);
+    }
+
+    public void fill(Supplier<Object> supplier, WriteSheet writeSheet) {
+        excelWriter.fill(supplier, writeSheet);
+    }
+
+    public void fill(Supplier<Object> supplier, FillConfig fillConfig, WriteSheet writeSheet) {
+        excelWriter.fill(supplier, fillConfig, writeSheet);
+    }
+
+    public WriteContext writeContext() {
+        return excelWriter.writeContext();
+    }
+
+    /**
+     * 创建一个 ExcelWriterWrapper
+     *
+     * @param excelWriter ExcelWriter
+     * @return ExcelWriterWrapper
+     */
+    public static  <T> ExcelWriterWrapper<T> of(ExcelWriter excelWriter) {
+        return new ExcelWriterWrapper<>(excelWriter);
+    }
+
+    // -------------------------------- sheet start
+
+    public static WriteSheet buildSheet(Integer sheetNo, String sheetName) {
+        return sheetBuilder(sheetNo, sheetName).build();
+    }
+
+    public static WriteSheet buildSheet(Integer sheetNo) {
+        return sheetBuilder(sheetNo).build();
+    }
+
+    public static WriteSheet buildSheet(String sheetName) {
+        return sheetBuilder(sheetName).build();
+    }
+
+    public static WriteSheet buildSheet() {
+        return sheetBuilder().build();
+    }
+
+    public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo, String sheetName) {
+        return FastExcel.writerSheet(sheetNo, sheetName);
+    }
+
+    public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo) {
+        return FastExcel.writerSheet(sheetNo);
+    }
+
+    public static ExcelWriterSheetBuilder sheetBuilder(String sheetName) {
+        return FastExcel.writerSheet(sheetName);
+    }
+
+    public static ExcelWriterSheetBuilder sheetBuilder() {
+        return FastExcel.writerSheet();
+    }
+
+    // -------------------------------- sheet end
+
+    // -------------------------------- table start
+
+    public static WriteTable buildTable(Integer tableNo) {
+        return tableBuilder(tableNo).build();
+    }
+
+    public static WriteTable buildTable() {
+        return tableBuilder().build();
+    }
+
+    public static ExcelWriterTableBuilder tableBuilder(Integer tableNo) {
+        return FastExcel.writerTable(tableNo);
+    }
+
+    public static ExcelWriterTableBuilder tableBuilder() {
+        return FastExcel.writerTable();
+    }
+
+    // -------------------------------- table end
+
+}

+ 16 - 11
ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java

@@ -1,5 +1,6 @@
 package org.dromara.common.json.config;
 package org.dromara.common.json.config;
 
 
+import com.fasterxml.jackson.databind.Module;
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
 import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
@@ -28,20 +29,24 @@ import java.util.TimeZone;
 @AutoConfiguration(before = JacksonAutoConfiguration.class)
 @AutoConfiguration(before = JacksonAutoConfiguration.class)
 public class JacksonConfig {
 public class JacksonConfig {
 
 
+    @Bean
+    public Module registerJavaTimeModule() {
+        // 全局配置序列化返回 JSON 处理
+        JavaTimeModule javaTimeModule = new JavaTimeModule();
+        javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
+        javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
+        javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
+        javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
+        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
+        javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
+        return javaTimeModule;
+    }
+
     @Bean
     @Bean
     public Jackson2ObjectMapperBuilderCustomizer customizer() {
     public Jackson2ObjectMapperBuilderCustomizer customizer() {
         return builder -> {
         return builder -> {
-            // 全局配置序列化返回 JSON 处理
-            JavaTimeModule javaTimeModule = new JavaTimeModule();
-            javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
-            javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
-            javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
-            javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
-            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
-            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
-            javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
-            builder.modules(javaTimeModule);
             builder.timeZone(TimeZone.getDefault());
             builder.timeZone(TimeZone.getDefault());
             log.info("初始化 jackson 配置");
             log.info("初始化 jackson 配置");
         };
         };

+ 7 - 1
ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/CustomDateDeserializer.java

@@ -1,9 +1,11 @@
 package org.dromara.common.json.handler;
 package org.dromara.common.json.handler;
 
 
+import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.date.DateUtil;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.JsonDeserializer;
 import com.fasterxml.jackson.databind.JsonDeserializer;
+import org.dromara.common.core.utils.ObjectUtils;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.util.Date;
 import java.util.Date;
@@ -25,7 +27,11 @@ public class CustomDateDeserializer extends JsonDeserializer<Date> {
      */
      */
     @Override
     @Override
     public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
     public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
-        return DateUtil.parse(p.getText());
+        DateTime parse = DateUtil.parse(p.getText());
+        if (ObjectUtils.isNull(parse)) {
+            return null;
+        }
+        return parse.toJdkDate();
     }
     }
 
 
 }
 }

+ 21 - 9
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java

@@ -27,9 +27,7 @@ import org.springframework.http.HttpMethod;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.BindingResult;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartFile;
 
 
-import java.util.Collection;
-import java.util.Map;
-import java.util.StringJoiner;
+import java.util.*;
 
 
 /**
 /**
  * 操作日志记录处理
  * 操作日志记录处理
@@ -176,14 +174,28 @@ public class LogAspect {
         if (ArrayUtil.isEmpty(paramsArray)) {
         if (ArrayUtil.isEmpty(paramsArray)) {
             return params.toString();
             return params.toString();
         }
         }
+        String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES);
         for (Object o : paramsArray) {
         for (Object o : paramsArray) {
             if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
             if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
-                String str = JsonUtils.toJsonString(o);
-                Dict dict = JsonUtils.parseMap(str);
-                if (MapUtil.isNotEmpty(dict)) {
-                    MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
-                    MapUtil.removeAny(dict, excludeParamNames);
-                    str = JsonUtils.toJsonString(dict);
+                String str = "";
+                if (o instanceof List<?> list) {
+                    List<Dict> list1 = new ArrayList<>();
+                    for (Object obj : list) {
+                        String str1 = JsonUtils.toJsonString(obj);
+                        Dict dict = JsonUtils.parseMap(str1);
+                        if (MapUtil.isNotEmpty(dict)) {
+                            MapUtil.removeAny(dict, exclude);
+                            list1.add(dict);
+                        }
+                    }
+                    str = JsonUtils.toJsonString(list1);
+                } else {
+                    str = JsonUtils.toJsonString(o);
+                    Dict dict = JsonUtils.parseMap(str);
+                    if (MapUtil.isNotEmpty(dict)) {
+                        MapUtil.removeAny(dict, exclude);
+                        str = JsonUtils.toJsonString(dict);
+                    }
                 }
                 }
                 params.add(str);
                 params.add(str);
             }
             }

+ 54 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAdvice.java

@@ -0,0 +1,54 @@
+package org.dromara.common.mybatis.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.dromara.common.mybatis.annotation.DataPermission;
+import org.dromara.common.mybatis.helper.DataPermissionHelper;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * 数据权限注解Advice
+ *
+ * @author 秋辞未寒
+ */
+@Slf4j
+public class DataPermissionAdvice implements MethodInterceptor {
+
+    @Override
+    public Object invoke(MethodInvocation invocation) throws Throwable {
+        Object target = invocation.getThis();
+        Method method = invocation.getMethod();
+        Object[] args = invocation.getArguments();
+        // 设置权限注解
+        DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method, args));
+        try {
+            // 执行代理方法
+            return invocation.proceed();
+        } finally {
+            // 清除权限注解
+            DataPermissionHelper.removePermission();
+        }
+    }
+
+    /**
+     * 获取数据权限注解
+     */
+    private DataPermission getDataPermissionAnnotation(Object target, Method method,Object[] args){
+        DataPermission dataPermission = method.getAnnotation(DataPermission.class);
+        // 优先获取方法上的注解
+        if (dataPermission != null) {
+            return dataPermission;
+        }
+        // 方法上没有注解,则获取类上的注解
+        Class<?> targetClass = target.getClass();
+        // 如果是 JDK 动态代理,则获取真实的Class实例
+        if (Proxy.isProxyClass(targetClass)) {
+            targetClass = targetClass.getInterfaces()[0];
+        }
+        dataPermission = targetClass.getAnnotation(DataPermission.class);
+        return dataPermission;
+    }
+}

+ 0 - 50
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAspect.java

@@ -1,50 +0,0 @@
-package org.dromara.common.mybatis.aspect;
-
-import lombok.extern.slf4j.Slf4j;
-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.mybatis.annotation.DataPermission;
-import org.dromara.common.mybatis.helper.DataPermissionHelper;
-
-/**
- * 数据权限处理
- *
- * @author Lion Li
- */
-@Slf4j
-@Aspect
-public class DataPermissionAspect {
-
-    /**
-     * 处理请求前执行
-     */
-    @Before(value = "@annotation(dataPermission)")
-    public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) {
-        DataPermissionHelper.setPermission(dataPermission);
-    }
-
-    /**
-     * 处理完请求后执行
-     *
-     * @param joinPoint 切点
-     */
-    @AfterReturning(pointcut = "@annotation(dataPermission)")
-    public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) {
-        DataPermissionHelper.removePermission();
-    }
-
-    /**
-     * 拦截异常操作
-     *
-     * @param joinPoint 切点
-     * @param e         异常
-     */
-    @AfterThrowing(value = "@annotation(dataPermission)", throwing = "e")
-    public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) {
-        DataPermissionHelper.removePermission();
-    }
-
-}

+ 39 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcut.java

@@ -0,0 +1,39 @@
+package org.dromara.common.mybatis.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.mybatis.annotation.DataPermission;
+import org.springframework.aop.support.StaticMethodMatcherPointcut;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * 数据权限匹配切点
+ *
+ * @author 秋辞未寒
+ */
+@Slf4j
+@SuppressWarnings("all")
+public class DataPermissionPointcut extends StaticMethodMatcherPointcut {
+
+    @Override
+    public boolean matches(Method method, Class<?> targetClass) {
+        // 优先匹配方法
+        // 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口
+        if (method.isAnnotationPresent(DataPermission.class)) {
+            return true;
+        }
+
+        // MyBatis 的 Mapper 就是通过 JDK 动态代理实现的,所以这里需要检查是否匹配 JDK 的动态代理
+        Class<?> targetClassRef = targetClass;
+        if (Proxy.isProxyClass(targetClassRef)) {
+            // 数据权限注解不对继承生效,但由于 SpringIOC 容器拿到的实际上是 MyBatis 代理过后的 Mapper,而 targetClass.isAnnotationPresent 实际匹配的是 Proxy 类的注解,不会查找代理类。
+            // 所以这里不能用 targetClass.isAnnotationPresent,只能用 AnnotatedElementUtils.hasAnnotation 或 targetClass.getInterfaces()[0].isAnnotationPresent 去做匹配,以检查被代理的 MapperClass 是否具有注解
+            // 原理:JDK 动态代理本质上就是对接口进行实现然后对具体的接口实现做代理,所以直接通过接口可以拿到实际的 MapperClass
+            targetClassRef = targetClass.getInterfaces()[0];
+
+        }
+        return targetClassRef.isAnnotationPresent(DataPermission.class);
+    }
+
+}

+ 33 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionPointcutAdvisor.java

@@ -0,0 +1,33 @@
+package org.dromara.common.mybatis.aspect;
+
+import org.aopalliance.aop.Advice;
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.AbstractPointcutAdvisor;
+
+/**
+ * 数据权限注解切面定义
+ *
+ * @author 秋辞未寒
+ */
+@SuppressWarnings("all")
+public class DataPermissionPointcutAdvisor extends AbstractPointcutAdvisor {
+
+    private final Advice advice;
+    private final Pointcut pointcut;
+
+    public DataPermissionPointcutAdvisor() {
+        this.advice = new DataPermissionAdvice();
+        this.pointcut =  new DataPermissionPointcut();
+    }
+
+    @Override
+    public Pointcut getPointcut() {
+        return this.pointcut;
+    }
+
+    @Override
+    public Advice getAdvice() {
+        return this.advice;
+    }
+
+}

+ 8 - 4
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java

@@ -11,15 +11,17 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
 import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
 import org.dromara.common.core.factory.YmlPropertySourceFactory;
 import org.dromara.common.core.factory.YmlPropertySourceFactory;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.SpringUtils;
-import org.dromara.common.mybatis.aspect.DataPermissionAspect;
+import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor;
 import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
 import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
 import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
 import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
 import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
 import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
 import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
 import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
 import org.mybatis.spring.annotation.MapperScan;
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.PropertySource;
 import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.Role;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 
 /**
 /**
@@ -27,6 +29,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
  *
  *
  * @author Lion Li
  * @author Lion Li
  */
  */
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 @EnableTransactionManagement(proxyTargetClass = true)
 @EnableTransactionManagement(proxyTargetClass = true)
 @MapperScan("${mybatis-plus.mapperPackage}")
 @MapperScan("${mybatis-plus.mapperPackage}")
 @PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
 @PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
@@ -54,15 +57,16 @@ public class MybatisPlusConfig {
      * 数据权限拦截器
      * 数据权限拦截器
      */
      */
     public PlusDataPermissionInterceptor dataPermissionInterceptor() {
     public PlusDataPermissionInterceptor dataPermissionInterceptor() {
-        return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
+        return new PlusDataPermissionInterceptor();
     }
     }
 
 
     /**
     /**
      * 数据权限切面处理器
      * 数据权限切面处理器
      */
      */
     @Bean
     @Bean
-    public DataPermissionAspect dataPermissionAspect() {
-        return new DataPermissionAspect();
+    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+    public DataPermissionPointcutAdvisor dataPermissionPointcutAdvisor() {
+        return new DataPermissionPointcutAdvisor();
     }
     }
 
 
     /**
     /**

+ 32 - 3
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataBaseType.java

@@ -42,17 +42,46 @@ public enum DataBaseType {
      * 根据数据库产品名称查找对应的数据库类型
      * 根据数据库产品名称查找对应的数据库类型
      *
      *
      * @param databaseProductName 数据库产品名称
      * @param databaseProductName 数据库产品名称
-     * @return 对应的数据库类型枚举值,如果未找到则返回 null
+     * @return 对应的数据库类型枚举值
      */
      */
     public static DataBaseType find(String databaseProductName) {
     public static DataBaseType find(String databaseProductName) {
         if (StringUtils.isBlank(databaseProductName)) {
         if (StringUtils.isBlank(databaseProductName)) {
-            return null;
+            return MY_SQL;
         }
         }
         for (DataBaseType type : values()) {
         for (DataBaseType type : values()) {
             if (type.getType().equals(databaseProductName)) {
             if (type.getType().equals(databaseProductName)) {
                 return type;
                 return type;
             }
             }
         }
         }
-        return null;
+        return MY_SQL;
     }
     }
+
+    /**
+     * 判断是否为 MySQL 类型
+     */
+    public boolean isMySql() {
+        return this == MY_SQL;
+    }
+
+    /**
+     * 判断是否为 Oracle 类型
+     */
+    public boolean isOracle() {
+        return this == ORACLE;
+    }
+
+    /**
+     * 判断是否为 PostgreSQL 类型
+     */
+    public boolean isPostgreSql() {
+        return this == POSTGRE_SQL;
+    }
+
+    /**
+     * 判断是否为 SQL Server 类型
+     */
+    public boolean isSqlServer() {
+        return this == SQL_SERVER;
+    }
+
 }
 }

+ 6 - 102
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java

@@ -1,6 +1,5 @@
 package org.dromara.common.mybatis.handler;
 package org.dromara.common.mybatis.handler;
 
 
-import cn.hutool.core.annotation.AnnotationUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
@@ -10,7 +9,6 @@ import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
 import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
 import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
 import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
 import net.sf.jsqlparser.parser.CCJSqlParserUtil;
 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.dto.RoleDTO;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.exception.ServiceException;
@@ -22,22 +20,13 @@ import org.dromara.common.mybatis.annotation.DataPermission;
 import org.dromara.common.mybatis.enums.DataScopeType;
 import org.dromara.common.mybatis.enums.DataScopeType;
 import org.dromara.common.mybatis.helper.DataPermissionHelper;
 import org.dromara.common.mybatis.helper.DataPermissionHelper;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.satoken.utils.LoginHelper;
-import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.expression.BeanFactoryResolver;
 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.*;
 import org.springframework.expression.*;
 import org.springframework.expression.common.TemplateParserContext;
 import org.springframework.expression.common.TemplateParserContext;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
-import org.springframework.util.ClassUtils;
 
 
-import java.lang.reflect.Method;
 import java.util.*;
 import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 import java.util.function.Function;
 
 
 /**
 /**
@@ -49,11 +38,6 @@ import java.util.function.Function;
 @Slf4j
 @Slf4j
 public class PlusDataPermissionHandler {
 public class PlusDataPermissionHandler {
 
 
-    /**
-     * 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行)
-     */
-    private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
-
     /**
     /**
      * spel 解析器
      * spel 解析器
      */
      */
@@ -64,27 +48,17 @@ public class PlusDataPermissionHandler {
      */
      */
     private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
     private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
 
 
-    /**
-     * 构造方法,扫描指定包下的 Mapper 类并初始化缓存
-     *
-     * @param mapperPackage Mapper 类所在的包路径
-     */
-    public PlusDataPermissionHandler(String mapperPackage) {
-        scanMapperClasses(mapperPackage);
-    }
-
     /**
     /**
      * 获取数据过滤条件的 SQL 片段
      * 获取数据过滤条件的 SQL 片段
      *
      *
      * @param where             原始的查询条件表达式
      * @param where             原始的查询条件表达式
-     * @param mappedStatementId Mapper 方法的 ID
      * @param isSelect          是否为查询语句
      * @param isSelect          是否为查询语句
      * @return 数据过滤条件的 SQL 片段
      * @return 数据过滤条件的 SQL 片段
      */
      */
-    public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
+    public Expression getSqlSegment(Expression where, boolean isSelect) {
         try {
         try {
             // 获取数据权限配置
             // 获取数据权限配置
-            DataPermission dataPermission = getDataPermission(mappedStatementId);
+            DataPermission dataPermission = getDataPermission();
             // 获取当前登录用户信息
             // 获取当前登录用户信息
             LoginUser currentUser = DataPermissionHelper.getVariable("user");
             LoginUser currentUser = DataPermissionHelper.getVariable("user");
             if (ObjectUtil.isNull(currentUser)) {
             if (ObjectUtil.isNull(currentUser)) {
@@ -206,92 +180,22 @@ public class PlusDataPermissionHandler {
         return StringUtils.EMPTY;
         return StringUtils.EMPTY;
     }
     }
 
 
-    /**
-     * 扫描指定包下的 Mapper 类,并查找其中带有特定注解的方法或类
-     *
-     * @param mapperPackage Mapper 类所在的包路径
-     */
-    private void scanMapperClasses(String mapperPackage) {
-        // 创建资源解析器和元数据读取工厂
-        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
-        CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
-        // 将 Mapper 包路径按分隔符拆分为数组
-        String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
-        String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
-        try {
-            for (String packagePattern : packagePatternArray) {
-                // 将包路径转换为资源路径
-                String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
-                // 获取指定路径下的所有 .class 文件资源
-                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) {
-            log.error("初始化数据安全缓存时出错:{}", e.getMessage());
-        }
-    }
-
-    /**
-     * 在指定的类中查找特定的注解 DataPermission,并将带有这个注解的方法或类存储到 dataPermissionCacheMap 中
-     *
-     * @param clazz 要查找的类
-     */
-    private void findAnnotation(Class<?> clazz) {
-        DataPermission dataPermission;
-        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);
-            }
-        }
-        if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
-            dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
-            dataPermissionCacheMap.put(clazz.getName(), dataPermission);
-        }
-    }
-
     /**
     /**
      * 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
      * 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
      *
      *
-     * @param mapperId 映射语句 ID
      * @return DataPermission 注解对象,如果不存在则返回 null
      * @return DataPermission 注解对象,如果不存在则返回 null
      */
      */
-    public DataPermission getDataPermission(String mapperId) {
-        // 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象
-        if (DataPermissionHelper.getPermission() != null) {
-            return DataPermissionHelper.getPermission();
-        }
-        // 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象
-        if (dataPermissionCacheMap.containsKey(mapperId)) {
-            return dataPermissionCacheMap.get(mapperId);
-        }
-        // 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找
-        String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
-        if (dataPermissionCacheMap.containsKey(clazzName)) {
-            return dataPermissionCacheMap.get(clazzName);
-        }
-        return null;
+    public DataPermission getDataPermission() {
+        return DataPermissionHelper.getPermission();
     }
     }
 
 
     /**
     /**
      * 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象
      * 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象
      *
      *
-     * @param mapperId 映射语句 ID
      * @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true
      * @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true
      */
      */
-    public boolean invalid(String mapperId) {
-        return getDataPermission(mapperId) == null;
+    public boolean invalid() {
+        return getDataPermission() == null;
     }
     }
 
 
     /**
     /**

+ 30 - 29
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataBaseHelper.java

@@ -26,7 +26,14 @@ public class DataBaseHelper {
     private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
     private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
 
 
     /**
     /**
-     * 获取当前数据库类型
+     * 获取当前数据源对应的数据库类型
+     * <p>
+     * 通过 DynamicRoutingDataSource 获取当前线程绑定的数据源,
+     * 然后从数据源获取数据库连接,利用连接的元数据获取数据库产品名称,
+     * 最后调用 DataBaseType.find 方法将数据库名称转换为对应的枚举类型
+     *
+     * @return 当前数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL
+     * @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常
      */
      */
     public static DataBaseType getDataBaseType() {
     public static DataBaseType getDataBaseType() {
         DataSource dataSource = DS.determineDataSource();
         DataSource dataSource = DS.determineDataSource();
@@ -39,37 +46,31 @@ public class DataBaseHelper {
         }
         }
     }
     }
 
 
-    public static boolean isMySql() {
-        return DataBaseType.MY_SQL == getDataBaseType();
-    }
-
-    public static boolean isOracle() {
-        return DataBaseType.ORACLE == getDataBaseType();
-    }
-
-    public static boolean isPostgerSql() {
-        return DataBaseType.POSTGRE_SQL == getDataBaseType();
-    }
-
-    public static boolean isSqlServer() {
-        return DataBaseType.SQL_SERVER == getDataBaseType();
-    }
-
+    /**
+     * 根据当前数据库类型,生成兼容的 FIND_IN_SET 语句片段
+     * <p>
+     * 用于判断指定值是否存在于逗号分隔的字符串列中,SQL写法根据不同数据库方言自动切换:
+     * - Oracle 使用 instr 函数
+     * - PostgreSQL 使用 strpos 函数
+     * - SQL Server 使用 charindex 函数
+     * - 其他默认使用 MySQL 的 find_in_set 函数
+     *
+     * @param var1 要查找的值(支持任意类型,内部会转换成字符串)
+     * @param var2 存储逗号分隔值的数据库列名
+     * @return 适用于当前数据库的 SQL 条件字符串,通常用于 where 或 apply 中拼接
+     */
     public static String findInSet(Object var1, String var2) {
     public static String findInSet(Object var1, String var2) {
-        DataBaseType dataBasyType = getDataBaseType();
         String var = Convert.toStr(var1);
         String var = Convert.toStr(var1);
-        if (dataBasyType == DataBaseType.SQL_SERVER) {
-            // charindex(',100,' , ',0,100,101,') <> 0
-            return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
-        } else if (dataBasyType == DataBaseType.POSTGRE_SQL) {
-            // (select strpos(',0,100,101,' , ',100,')) <> 0
-            return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
-        } else if (dataBasyType == DataBaseType.ORACLE) {
+        return switch (getDataBaseType()) {
             // instr(',0,100,101,' , ',100,') <> 0
             // instr(',0,100,101,' , ',100,') <> 0
-            return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
-        }
-        // find_in_set(100 , '0,100,101')
-        return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
+            case ORACLE -> "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
+            // (select strpos(',0,100,101,' , ',100,')) <> 0
+            case POSTGRE_SQL -> "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
+            // charindex(',100,' , ',0,100,101,') <> 0
+            case SQL_SERVER -> "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
+            // find_in_set(100 , '0,100,101')
+            default -> "find_in_set('%s' , %s) <> 0".formatted(var, var2);
+        };
     }
     }
 
 
     /**
     /**

+ 6 - 15
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlusDataPermissionInterceptor.java

@@ -35,16 +35,7 @@ import java.util.List;
 @Slf4j
 @Slf4j
 public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
 public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
 
 
-    private final PlusDataPermissionHandler dataPermissionHandler;
-
-    /**
-     * 构造函数,初始化 PlusDataPermissionHandler 实例
-     *
-     * @param mapperPackage 扫描的映射器包
-     */
-    public PlusDataPermissionInterceptor(String mapperPackage) {
-        this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage);
-    }
+    private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
 
 
     /**
     /**
      * 在执行查询之前,检查并处理数据权限相关逻辑
      * 在执行查询之前,检查并处理数据权限相关逻辑
@@ -64,7 +55,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
             return;
             return;
         }
         }
         // 检查是否缺少有效的数据权限注解
         // 检查是否缺少有效的数据权限注解
-        if (dataPermissionHandler.invalid(ms.getId())) {
+        if (dataPermissionHandler.invalid()) {
             return;
             return;
         }
         }
         // 解析 sql 分配对应方法
         // 解析 sql 分配对应方法
@@ -92,7 +83,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
                 return;
                 return;
             }
             }
             // 检查是否缺少有效的数据权限注解
             // 检查是否缺少有效的数据权限注解
-            if (dataPermissionHandler.invalid(ms.getId())) {
+            if (dataPermissionHandler.invalid()) {
                 return;
                 return;
             }
             }
             PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
             PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
@@ -128,7 +119,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
      */
      */
     @Override
     @Override
     protected void processUpdate(Update update, int index, String sql, Object obj) {
     protected void processUpdate(Update update, int index, String sql, Object obj) {
-        Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
+        Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), false);
         if (null != sqlSegment) {
         if (null != sqlSegment) {
             update.setWhere(sqlSegment);
             update.setWhere(sqlSegment);
         }
         }
@@ -144,7 +135,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
      */
      */
     @Override
     @Override
     protected void processDelete(Delete delete, int index, String sql, Object obj) {
     protected void processDelete(Delete delete, int index, String sql, Object obj) {
-        Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
+        Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), false);
         if (null != sqlSegment) {
         if (null != sqlSegment) {
             delete.setWhere(sqlSegment);
             delete.setWhere(sqlSegment);
         }
         }
@@ -157,7 +148,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
      * @param mappedStatementId 映射语句的 ID
      * @param mappedStatementId 映射语句的 ID
      */
      */
     protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
     protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
-        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
+        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), true);
         if (null != sqlSegment) {
         if (null != sqlSegment) {
             plainSelect.setWhere(sqlSegment);
             plainSelect.setWhere(sqlSegment);
         }
         }

+ 50 - 16
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java

@@ -2,6 +2,7 @@ package org.dromara.common.oss.core;
 
 
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.IdUtil;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.core.utils.StringUtils;
@@ -13,9 +14,7 @@ import org.dromara.common.oss.exception.OssException;
 import org.dromara.common.oss.properties.OssProperties;
 import org.dromara.common.oss.properties.OssProperties;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
 import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import software.amazon.awssdk.core.ResponseInputStream;
-import software.amazon.awssdk.core.async.AsyncResponseTransformer;
-import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
+import software.amazon.awssdk.core.async.*;
 import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
 import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
 import software.amazon.awssdk.regions.Region;
 import software.amazon.awssdk.regions.Region;
 import software.amazon.awssdk.services.s3.S3AsyncClient;
 import software.amazon.awssdk.services.s3.S3AsyncClient;
@@ -29,9 +28,12 @@ import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
 import java.io.*;
 import java.io.*;
 import java.net.URI;
 import java.net.URI;
 import java.net.URL;
 import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
 import java.nio.file.Files;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Path;
 import java.time.Duration;
 import java.time.Duration;
+import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
 
 
 /**
 /**
@@ -40,6 +42,7 @@ import java.util.function.Consumer;
  *
  *
  * @author AprilWind
  * @author AprilWind
  */
  */
+@Slf4j
 public class OssClient {
 public class OssClient {
 
 
     /**
     /**
@@ -177,12 +180,12 @@ public class OssClient {
             // 创建异步请求体(length如果为空会报错)
             // 创建异步请求体(length如果为空会报错)
             BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
             BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
                 .contentLength(length)
                 .contentLength(length)
-                .subscribeTimeout(Duration.ofSeconds(30))
+                .subscribeTimeout(Duration.ofSeconds(120))
                 .build();
                 .build();
 
 
             // 使用 transferManager 进行上传
             // 使用 transferManager 进行上传
             Upload upload = transferManager.upload(
             Upload upload = transferManager.upload(
-                x -> x.requestBody(body)
+                x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create())
                     .putObjectRequest(
                     .putObjectRequest(
                         y -> y.bucket(properties.getBucketName())
                         y -> y.bucket(properties.getBucketName())
                             .key(key)
                             .key(key)
@@ -237,30 +240,61 @@ public class OssClient {
      * @param key 文件在 Amazon S3 中的对象键
      * @param key 文件在 Amazon S3 中的对象键
      * @param out 输出流
      * @param out 输出流
      * @param consumer 自定义处理逻辑
      * @param consumer 自定义处理逻辑
-     * @return 输出流中写入的字节数(长度)
      * @throws OssException 如果下载失败,抛出自定义异常
      * @throws OssException 如果下载失败,抛出自定义异常
      */
      */
     public void download(String key, OutputStream out, Consumer<Long> consumer) {
     public void download(String key, OutputStream out, Consumer<Long> consumer) {
+        try {
+            this.download(key, consumer).writeTo(out);
+        } catch (Exception e) {
+            throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
+        }
+    }
+
+    /**
+     * 下载文件从 Amazon S3 到 输出流
+     *
+     * @param key 文件在 Amazon S3 中的对象键
+     * @param contentLengthConsumer 文件大小消费者函数
+     * @return 写出订阅器
+     * @throws OssException 如果下载失败,抛出自定义异常
+     */
+    public WriteOutSubscriber<OutputStream> download(String key, Consumer<Long> contentLengthConsumer) {
         try {
         try {
             // 构建下载请求
             // 构建下载请求
-            DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
+            DownloadRequest<ResponsePublisher<GetObjectResponse>> publisherDownloadRequest = DownloadRequest.builder()
                 // 文件对象
                 // 文件对象
                 .getObjectRequest(y -> y.bucket(properties.getBucketName())
                 .getObjectRequest(y -> y.bucket(properties.getBucketName())
                     .key(key)
                     .key(key)
                     .build())
                     .build())
                 .addTransferListener(LoggingTransferListener.create())
                 .addTransferListener(LoggingTransferListener.create())
-                // 使用订阅转换器
-                .responseTransformer(AsyncResponseTransformer.toBlockingInputStream())
+                // 使用发布订阅转换器
+                .responseTransformer(AsyncResponseTransformer.toPublisher())
                 .build();
                 .build();
+
             // 使用 S3TransferManager 下载文件
             // 使用 S3TransferManager 下载文件
-            Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest);
-            // 输出到流中
-            try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream
-                if (consumer != null) {
-                    consumer.accept(responseStream.response().contentLength());
+            Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(publisherDownloadRequest);
+            // 获取下载发布订阅转换器
+            ResponsePublisher<GetObjectResponse> publisher = publisherDownload.completionFuture().join().result();
+            // 执行文件大小消费者函数
+            Optional.ofNullable(contentLengthConsumer)
+                .ifPresent(lengthConsumer -> lengthConsumer.accept(publisher.response().contentLength()));
+
+            // 构建写出订阅器对象
+            return out -> {
+                // 创建可写入的字节通道
+                try(WritableByteChannel channel = Channels.newChannel(out)){
+                    // 订阅数据
+                    publisher.subscribe(byteBuffer -> {
+                        while (byteBuffer.hasRemaining()) {
+                            try {
+                                channel.write(byteBuffer);
+                            } catch (IOException e) {
+                                throw new RuntimeException(e);
+                            }
+                        }
+                    }).join();
                 }
                 }
-                responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread
-            }
+            };
         } catch (Exception e) {
         } catch (Exception e) {
             throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
             throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
         }
         }

+ 15 - 0
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/WriteOutSubscriber.java

@@ -0,0 +1,15 @@
+package org.dromara.common.oss.core;
+
+import java.io.IOException;
+
+/**
+ * 写出订阅器
+ *
+ * @author 秋辞未寒
+ */
+@FunctionalInterface
+public interface WriteOutSubscriber<T> {
+
+    void writeTo(T out) throws IOException;
+
+}

+ 4 - 7
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java

@@ -129,9 +129,9 @@ public class RedisUtils {
             } catch (Exception e) {
             } catch (Exception e) {
                 long timeToLive = bucket.remainTimeToLive();
                 long timeToLive = bucket.remainTimeToLive();
                 if (timeToLive == -1) {
                 if (timeToLive == -1) {
-                    setCacheObject(key, value);
+                    bucket.set(value);
                 } else {
                 } else {
-                    setCacheObject(key, value, Duration.ofMillis(timeToLive));
+                    bucket.set(value, Duration.ofMillis(timeToLive));
                 }
                 }
             }
             }
         } else {
         } else {
@@ -147,11 +147,8 @@ public class RedisUtils {
      * @param duration 时间
      * @param duration 时间
      */
      */
     public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
     public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
-        RBatch batch = CLIENT.createBatch();
-        RBucketAsync<T> bucket = batch.getBucket(key);
-        bucket.setAsync(value);
-        bucket.expireAsync(duration);
-        batch.execute();
+        RBucket<T> bucket = CLIENT.getBucket(key);
+        bucket.set(value, duration);
     }
     }
 
 
     /**
     /**

+ 216 - 45
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java

@@ -1,7 +1,7 @@
 package org.dromara.common.redis.utils;
 package org.dromara.common.redis.utils;
 
 
+import cn.hutool.core.convert.Convert;
 import cn.hutool.core.date.DatePattern;
 import cn.hutool.core.date.DatePattern;
-import cn.hutool.core.date.DateUtil;
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.NoArgsConstructor;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.SpringUtils;
@@ -10,6 +10,10 @@ import org.redisson.api.RIdGenerator;
 import org.redisson.api.RedissonClient;
 import org.redisson.api.RedissonClient;
 
 
 import java.time.Duration;
 import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
 
 
 /**
 /**
  * 发号器工具类
  * 发号器工具类
@@ -23,12 +27,12 @@ public class SequenceUtils {
     /**
     /**
      * 默认初始值
      * 默认初始值
      */
      */
-    public static final Long DEFAULT_INIT_VALUE = 1L;
+    public static final long DEFAULT_INIT_VALUE = 1L;
 
 
     /**
     /**
      * 默认步长
      * 默认步长
      */
      */
-    public static final Long DEFAULT_STEP_VALUE = 1L;
+    public static final long DEFAULT_STEP_VALUE = 1L;
 
 
     /**
     /**
      * 默认过期时间-天
      * 默认过期时间-天
@@ -40,6 +44,11 @@ public class SequenceUtils {
      */
      */
     public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1);
     public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1);
 
 
+    /**
+     * 默认最小ID容量位数 - 6位数(即至少可以生成的ID为999999个)
+     */
+    public static final int DEFAULT_MIN_ID_CAPACITY_BITS = 6;
+
     /**
     /**
      * 获取Redisson客户端实例
      * 获取Redisson客户端实例
      */
      */
@@ -54,14 +63,11 @@ public class SequenceUtils {
      * @param stepValue  ID步长
      * @param stepValue  ID步长
      * @return ID生成器
      * @return ID生成器
      */
      */
-    private static RIdGenerator getIdGenerator(String key, Duration expireTime, Long initValue, Long stepValue) {
-        if (initValue == null || initValue <= 0) {
-            initValue = DEFAULT_INIT_VALUE;
-        }
-        if (stepValue == null || stepValue <= 0) {
-            stepValue = DEFAULT_STEP_VALUE;
-        }
+    public static RIdGenerator getIdGenerator(String key, Duration expireTime, long initValue, long stepValue) {
         RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key);
         RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key);
+        // 初始值和步长不能小于等于0
+        initValue = initValue <= 0 ? DEFAULT_INIT_VALUE : initValue;
+        stepValue = stepValue <= 0 ? DEFAULT_STEP_VALUE : stepValue;
         // 设置初始值和步长
         // 设置初始值和步长
         idGenerator.tryInit(initValue, stepValue);
         idGenerator.tryInit(initValue, stepValue);
         // 设置过期时间
         // 设置过期时间
@@ -69,6 +75,17 @@ public class SequenceUtils {
         return idGenerator;
         return idGenerator;
     }
     }
 
 
+    /**
+     * 获取ID生成器
+     *
+     * @param key        业务key
+     * @param expireTime 过期时间
+     * @return ID生成器
+     */
+    public static RIdGenerator getIdGenerator(String key, Duration expireTime) {
+        return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
+    }
+
     /**
     /**
      * 获取指定业务key的唯一id
      * 获取指定业务key的唯一id
      *
      *
@@ -78,32 +95,32 @@ public class SequenceUtils {
      * @param stepValue  ID步长
      * @param stepValue  ID步长
      * @return 唯一id
      * @return 唯一id
      */
      */
-    public static long nextId(String key, Duration expireTime, Long initValue, Long stepValue) {
+    public static long getNextId(String key, Duration expireTime, long initValue, long stepValue) {
         return getIdGenerator(key, expireTime, initValue, stepValue).nextId();
         return getIdGenerator(key, expireTime, initValue, stepValue).nextId();
     }
     }
 
 
     /**
     /**
-     * 获取指定业务key的唯一id字符串
+     * 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
      *
      *
      * @param key        业务key
      * @param key        业务key
      * @param expireTime 过期时间
      * @param expireTime 过期时间
-     * @param initValue  ID初始值
-     * @param stepValue  ID步长
      * @return 唯一id
      * @return 唯一id
      */
      */
-    public static String nextIdStr(String key, Duration expireTime, Long initValue, Long stepValue) {
-        return String.valueOf(nextId(key, expireTime, initValue, stepValue));
+    public static long getNextId(String key, Duration expireTime) {
+        return getIdGenerator(key, expireTime).nextId();
     }
     }
 
 
     /**
     /**
-     * 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
+     * 获取指定业务key的唯一id字符串
      *
      *
      * @param key        业务key
      * @param key        业务key
      * @param expireTime 过期时间
      * @param expireTime 过期时间
+     * @param initValue  ID初始值
+     * @param stepValue  ID步长
      * @return 唯一id
      * @return 唯一id
      */
      */
-    public static long nextId(String key, Duration expireTime) {
-        return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
+    public static String getNextIdString(String key, Duration expireTime, long initValue, long stepValue) {
+        return Convert.toStr(getNextId(key, expireTime, initValue, stepValue));
     }
     }
 
 
     /**
     /**
@@ -113,8 +130,8 @@ public class SequenceUtils {
      * @param expireTime 过期时间
      * @param expireTime 过期时间
      * @return 唯一id
      * @return 唯一id
      */
      */
-    public static String nextIdStr(String key, Duration expireTime) {
-        return String.valueOf(nextId(key, expireTime));
+    public static String getNextIdString(String key, Duration expireTime) {
+        return Convert.toStr(getNextId(key, expireTime));
     }
     }
 
 
     /**
     /**
@@ -125,56 +142,210 @@ public class SequenceUtils {
      * @param width      位数,不足左补0
      * @param width      位数,不足左补0
      * @return 补零后的唯一id字符串
      * @return 补零后的唯一id字符串
      */
      */
-    public static String nextPaddedIdStr(String key, Duration expireTime, Integer width) {
-        return StringUtils.leftPad(nextIdStr(key, expireTime), width, '0');
+    public static String getPaddedNextIdString(String key, Duration expireTime, Integer width) {
+        return StringUtils.leftPad(getNextIdString(key, expireTime), width, '0');
     }
     }
 
 
     /**
     /**
-     * 获取 yyyyMMdd 开头的唯一id
+     * 获取 yyyyMMdd 格式的唯一id
      *
      *
      * @return 唯一id
      * @return 唯一id
+     * @deprecated 请使用 {@link #getDateId(String)} 或 {@link #getDateId(String, boolean)}、{@link #getDateId(String, boolean, int)},确保不同业务的ID连续性
      */
      */
-    public static String nextIdDate() {
-        return nextIdDate("");
+    @Deprecated
+    public static String getDateId() {
+        return getDateId("");
     }
     }
 
 
     /**
     /**
-     * 获取 prefix + yyyyMMdd 开头的唯一id
+     * 获取 prefix + yyyyMMdd 格式的唯一id
      *
      *
      * @param prefix 业务前缀
      * @param prefix 业务前缀
      * @return 唯一id
      * @return 唯一id
      */
      */
-    public static String nextIdDate(String prefix) {
-        // 前缀+日期 构建 prefixKey
-        String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMATTER));
-        // 获取下一个id
-        long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_DAY, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
-        // 返回完整id
-        return StringUtils.format("{}{}", prefixKey, nextId);
+    public static String getDateId(String prefix) {
+        return getDateId(prefix, true);
+    }
+
+    /**
+     * 获取 prefix + yyyyMMdd 格式的唯一id
+     *
+     * @param prefix       业务前缀
+     * @param isWithPrefix id是否携带业务前缀
+     * @return 唯一id
+     */
+    public static String getDateId(String prefix, boolean isWithPrefix) {
+        return getDateId(prefix, isWithPrefix, -1);
+    }
+
+    /**
+     * 获取 prefix + yyyyMMdd 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})})
+     *
+     * @param prefix       业务前缀
+     * @param isWithPrefix id是否携带业务前缀
+     * @return 唯一id
+     */
+    public static String getPaddedDateId(String prefix, boolean isWithPrefix) {
+        return getDateId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
+    }
+
+    /**
+     * 获取 prefix + yyyyMMdd 格式的唯一id
+     *
+     * @param prefix            业务前缀
+     * @param isWithPrefix      id是否携带业务前缀
+     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
+     * @return 唯一id
+     */
+    public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
+        return getDateId(prefix, isWithPrefix, minIdCapacityBits, LocalDate.now());
+    }
+
+    /**
+     * 获取 prefix + yyyyMMdd 格式的唯一id
+     *
+     * @param prefix            业务前缀
+     * @param isWithPrefix      id是否携带业务前缀
+     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
+     * @param time              时间
+     * @return 唯一id
+     */
+    public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time) {
+        return getDateId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
     }
     }
 
 
     /**
     /**
-     * 获取 yyyyMMddHHmmss 开头的唯一id
+     * 获取 prefix + yyyyMMdd 格式的唯一id
      *
      *
+     * @param prefix            业务前缀
+     * @param isWithPrefix      id是否携带业务前缀
+     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
+     * @param time              时间
+     * @param initValue         ID初始值
+     * @param stepValue         ID步长
      * @return 唯一id
      * @return 唯一id
      */
      */
-    public static String nextIdDateTime() {
-        return nextIdDateTime("");
+    public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time, long initValue, long stepValue) {
+        return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATE_FORMATTER, DEFAULT_EXPIRE_TIME_DAY, initValue, stepValue);
     }
     }
 
 
     /**
     /**
-     * 获取 prefix + yyyyMMddHHmmss 开头的唯一id
+     * 获取 yyyyMMddHHmmss 格式的唯一id
+     *
+     * @return 唯一id
+     * @deprecated 请使用 {@link #getDateTimeId(String)} 或 {@link #getDateTimeId(String, boolean)}、{@link #getDateTimeId(String, boolean, int)},确保不同业务的ID连续性
+     */
+    @Deprecated
+    public static String getDateTimeId() {
+        return getDateTimeId("", false);
+    }
+
+    /**
+     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
      *
      *
      * @param prefix 业务前缀
      * @param prefix 业务前缀
      * @return 唯一id
      * @return 唯一id
      */
      */
-    public static String nextIdDateTime(String prefix) {
-        // 前缀+日期时间 构建 prefixKey
-        String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMATTER));
-        // 获取下一个id
-        long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_MINUTE, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
-        // 返回完整id
-        return StringUtils.format("{}{}", prefixKey, nextId);
+    public static String getDateTimeId(String prefix) {
+        return getDateTimeId(prefix, true);
+    }
+
+    /**
+     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
+     *
+     * @param prefix       业务前缀
+     * @param isWithPrefix id是否携带业务前缀
+     * @return 唯一id
+     */
+    public static String getDateTimeId(String prefix, boolean isWithPrefix) {
+        return getDateTimeId(prefix, isWithPrefix, -1);
+    }
+
+    /**
+     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})})
+     *
+     * @param prefix       业务前缀
+     * @param isWithPrefix id是否携带业务前缀
+     * @return 唯一id
+     */
+    public static String getPaddedDateTimeId(String prefix, boolean isWithPrefix) {
+        return getDateTimeId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
+    }
+
+    /**
+     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
+     *
+     * @param prefix            业务前缀
+     * @param isWithPrefix      id是否携带业务前缀
+     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
+     * @return 唯一id
+     */
+    public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
+        return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, LocalDateTime.now());
+    }
+
+    /**
+     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
+     *
+     * @param prefix            业务前缀
+     * @param isWithPrefix      id是否携带业务前缀
+     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
+     * @param time              时间
+     * @return 唯一id
+     */
+    public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time) {
+        return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
     }
     }
 
 
+    /**
+     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
+     *
+     * @param prefix            业务前缀
+     * @param isWithPrefix      id是否携带业务前缀
+     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
+     * @param initValue         ID初始值
+     * @param stepValue         ID步长
+     * @return 唯一id
+     */
+    public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time, long initValue, long stepValue) {
+        return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATETIME_FORMATTER, DEFAULT_EXPIRE_TIME_MINUTE, initValue, stepValue);
+    }
+
+    /**
+     * 获取指定业务key的指定时间格式的ID
+     *
+     * @param prefix            业务前缀
+     * @param isWithPrefix      id是否携带业务前缀
+     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
+     * @param temporalAccessor  时间访问器
+     * @param timeFormatter     时间格式
+     * @param expireTime        过期时间
+     * @param initValue         ID初始值
+     * @param stepValue         ID步长
+     * @return 唯一id
+     */
+    private static String getDatePatternId(String prefix, boolean isWithPrefix, int minIdCapacityBits, TemporalAccessor temporalAccessor, DateTimeFormatter timeFormatter, Duration expireTime, long initValue, long stepValue) {
+        // 时间前缀
+        String timePrefix = timeFormatter.format(temporalAccessor);
+        // 业务前缀 + 时间前缀 构建 prefixKey
+        String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), timePrefix);
+
+        // 获取id,例 -> 1
+        String nextId = getNextIdString(prefixKey, expireTime, initValue, stepValue);
+
+        // minIdCapacityBits 大于0,且 nextId 的长度小于 minIdCapacityBits,则左补0
+        if (minIdCapacityBits > 0 && nextId.length() < minIdCapacityBits) {
+            nextId = StringUtils.leftPad(nextId, minIdCapacityBits, '0');
+        }
+
+        // 是否携带业务前缀
+        if (isWithPrefix) {
+            // 例 -> P202507031
+            // 其中 P 为业务前缀,202507031 为 yyyyMMdd 格式时间, 1 为nextId
+            return StringUtils.format("{}{}", prefixKey, nextId);
+        }
+        // 例 -> 202507031
+        // 其中 202507031 为 yyyyMMdd 格式时间, 1 为nextId
+        return StringUtils.format("{}{}", timePrefix, nextId);
+    }
 }
 }

+ 8 - 12
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java

@@ -53,11 +53,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
         if (timeout == NEVER_EXPIRE) {
         if (timeout == NEVER_EXPIRE) {
             RedisUtils.setCacheObject(key, value);
             RedisUtils.setCacheObject(key, value);
         } else {
         } else {
-            if (RedisUtils.hasKey(key)) {
-                RedisUtils.setCacheObject(key, value, true);
-            } else {
-                RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
-            }
+            RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
         }
         }
         CAFFEINE.invalidate(key);
         CAFFEINE.invalidate(key);
     }
     }
@@ -78,7 +74,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
      */
      */
     @Override
     @Override
     public void delete(String key) {
     public void delete(String key) {
-        RedisUtils.deleteObject(key);
+        if (RedisUtils.deleteObject(key)) {
+            CAFFEINE.invalidate(key);
+        }
     }
     }
 
 
     /**
     /**
@@ -134,11 +132,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
         if (timeout == NEVER_EXPIRE) {
         if (timeout == NEVER_EXPIRE) {
             RedisUtils.setCacheObject(key, object);
             RedisUtils.setCacheObject(key, object);
         } else {
         } else {
-            if (RedisUtils.hasKey(key)) {
-                RedisUtils.setCacheObject(key, object, true);
-            } else {
-                RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
-            }
+            RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
         }
         }
         CAFFEINE.invalidate(key);
         CAFFEINE.invalidate(key);
     }
     }
@@ -159,7 +153,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
      */
      */
     @Override
     @Override
     public void deleteObject(String key) {
     public void deleteObject(String key) {
-        RedisUtils.deleteObject(key);
+        if (RedisUtils.deleteObject(key)) {
+            CAFFEINE.invalidate(key);
+        }
     }
     }
 
 
     /**
     /**

+ 13 - 4
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/service/SaPermissionImpl.java

@@ -1,6 +1,7 @@
 package org.dromara.common.satoken.core.service;
 package org.dromara.common.satoken.core.service;
 
 
 import cn.dev33.satoken.stp.StpInterface;
 import cn.dev33.satoken.stp.StpInterface;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ObjectUtil;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.enums.UserType;
 import org.dromara.common.core.enums.UserType;
@@ -39,8 +40,12 @@ public class SaPermissionImpl implements StpInterface {
         if (userType == UserType.APP_USER) {
         if (userType == UserType.APP_USER) {
             // 其他端 自行根据业务编写
             // 其他端 自行根据业务编写
         }
         }
-        // SYS_USER 默认返回权限
-        return new ArrayList<>(loginUser.getMenuPermission());
+        if (CollUtil.isNotEmpty(loginUser.getMenuPermission())) {
+            // SYS_USER 默认返回权限
+            return new ArrayList<>(loginUser.getMenuPermission());
+        } else {
+            return new ArrayList<>();
+        }
     }
     }
 
 
     /**
     /**
@@ -62,8 +67,12 @@ public class SaPermissionImpl implements StpInterface {
         if (userType == UserType.APP_USER) {
         if (userType == UserType.APP_USER) {
             // 其他端 自行根据业务编写
             // 其他端 自行根据业务编写
         }
         }
-        // SYS_USER 默认返回权限
-        return new ArrayList<>(loginUser.getRolePermission());
+        if (CollUtil.isNotEmpty(loginUser.getRolePermission())) {
+            // SYS_USER 默认返回权限
+            return new ArrayList<>(loginUser.getRolePermission());
+        } else {
+            return new ArrayList<>();
+        }
     }
     }
 
 
     private PermissionService getPermissionService() {
     private PermissionService getPermissionService() {

+ 2 - 1
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java

@@ -207,7 +207,8 @@ public class LoginHelper {
      */
      */
     public static boolean isLogin() {
     public static boolean isLogin() {
         try {
         try {
-            return getLoginUser() != null;
+            StpUtil.checkLogin();
+            return true;
         } catch (Exception e) {
         } catch (Exception e) {
             return false;
             return false;
         }
         }

+ 2 - 1
ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java

@@ -1,5 +1,6 @@
 package org.dromara.common.sensitive.core;
 package org.dromara.common.sensitive.core;
 
 
+import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.DesensitizedUtil;
 import cn.hutool.core.util.DesensitizedUtil;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
 
 
@@ -52,7 +53,7 @@ public enum SensitiveStrategy {
     /**
     /**
      * 用户ID
      * 用户ID
      */
      */
-    USER_ID(s -> String.valueOf(DesensitizedUtil.userId())),
+    USER_ID(s -> Convert.toStr(DesensitizedUtil.userId())),
 
 
     /**
     /**
      * 密码
      * 密码

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

@@ -14,6 +14,9 @@ import org.dromara.common.social.gitea.AuthGiteaRequest;
 import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
 import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
 import org.dromara.common.social.topiam.AuthTopIamRequest;
 import org.dromara.common.social.topiam.AuthTopIamRequest;
 
 
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
 /**
 /**
  * 认证授权工具类
  * 认证授权工具类
  *
  *
@@ -40,7 +43,7 @@ public class SocialUtils  {
         AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
         AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
             .clientId(obj.getClientId())
             .clientId(obj.getClientId())
             .clientSecret(obj.getClientSecret())
             .clientSecret(obj.getClientSecret())
-            .redirectUri(obj.getRedirectUri())
+            .redirectUri(URLEncoder.encode(obj.getRedirectUri(), StandardCharsets.UTF_8))
             .scopes(obj.getScopes());
             .scopes(obj.getScopes());
         return switch (source.toLowerCase()) {
         return switch (source.toLowerCase()) {
             case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);
             case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);

+ 29 - 29
ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java

@@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.sse.core.SseEmitterManager;
 import org.dromara.common.sse.core.SseEmitterManager;
-import org.dromara.common.sse.dto.SseMessageDto;
 import org.springframework.beans.factory.DisposableBean;
 import org.springframework.beans.factory.DisposableBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.http.MediaType;
 import org.springframework.http.MediaType;
@@ -14,8 +13,6 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 
-import java.util.List;
-
 /**
 /**
  * SSE 控制器
  * SSE 控制器
  *
  *
@@ -33,7 +30,9 @@ public class SseController implements DisposableBean {
      */
      */
     @GetMapping(value = "${sse.path}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
     @GetMapping(value = "${sse.path}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
     public SseEmitter connect() {
     public SseEmitter connect() {
-        StpUtil.checkLogin();
+        if (!StpUtil.isLogin()) {
+            return null;
+        }
         String tokenValue = StpUtil.getTokenValue();
         String tokenValue = StpUtil.getTokenValue();
         Long userId = LoginHelper.getUserId();
         Long userId = LoginHelper.getUserId();
         return sseEmitterManager.connect(userId, tokenValue);
         return sseEmitterManager.connect(userId, tokenValue);
@@ -51,31 +50,32 @@ public class SseController implements DisposableBean {
         return R.ok();
         return R.ok();
     }
     }
 
 
-    /**
-     * 向特定用户发送消息
-     *
-     * @param userId 目标用户的 ID
-     * @param msg    要发送的消息内容
-     */
-    @GetMapping(value = "${sse.path}/send")
-    public R<Void> send(Long userId, String msg) {
-        SseMessageDto dto = new SseMessageDto();
-        dto.setUserIds(List.of(userId));
-        dto.setMessage(msg);
-        sseEmitterManager.publishMessage(dto);
-        return R.ok();
-    }
-
-    /**
-     * 向所有用户发送消息
-     *
-     * @param msg 要发送的消息内容
-     */
-    @GetMapping(value = "${sse.path}/sendAll")
-    public R<Void> send(String msg) {
-        sseEmitterManager.publishAll(msg);
-        return R.ok();
-    }
+    // 以下为demo仅供参考 禁止使用 请在业务逻辑中使用工具发送而不是用接口发送
+//    /**
+//     * 向特定用户发送消息
+//     *
+//     * @param userId 目标用户的 ID
+//     * @param msg    要发送的消息内容
+//     */
+//    @GetMapping(value = "${sse.path}/send")
+//    public R<Void> send(Long userId, String msg) {
+//        SseMessageDto dto = new SseMessageDto();
+//        dto.setUserIds(List.of(userId));
+//        dto.setMessage(msg);
+//        sseEmitterManager.publishMessage(dto);
+//        return R.ok();
+//    }
+//
+//    /**
+//     * 向所有用户发送消息
+//     *
+//     * @param msg 要发送的消息内容
+//     */
+//    @GetMapping(value = "${sse.path}/sendAll")
+//    public R<Void> send(String msg) {
+//        sseEmitterManager.publishAll(msg);
+//        return R.ok();
+//    }
 
 
     /**
     /**
      * 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误
      * 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误

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

@@ -21,11 +21,6 @@
             <artifactId>ruoyi-common-json</artifactId>
             <artifactId>ruoyi-common-json</artifactId>
         </dependency>
         </dependency>
 
 
-        <dependency>
-            <groupId>org.dromara</groupId>
-            <artifactId>ruoyi-common-redis</artifactId>
-        </dependency>
-
         <!-- SpringBoot Web容器 -->
         <!-- SpringBoot Web容器 -->
         <dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <groupId>org.springframework.boot</groupId>

+ 12 - 15
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java

@@ -7,6 +7,7 @@ import org.dromara.common.web.filter.XssFilter;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistration;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 
 
@@ -21,24 +22,20 @@ public class FilterConfig {
 
 
     @Bean
     @Bean
     @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
     @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
-    public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
-        FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
-        registration.setDispatcherTypes(DispatcherType.REQUEST);
-        registration.setFilter(new XssFilter());
-        registration.addUrlPatterns("/*");
-        registration.setName("xssFilter");
-        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1);
-        return registration;
+    @FilterRegistration(
+        name = "xssFilter",
+        urlPatterns = "/*",
+        order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1,
+        dispatcherTypes = DispatcherType.REQUEST
+    )
+    public XssFilter xssFilter() {
+        return new XssFilter();
     }
     }
 
 
     @Bean
     @Bean
-    public FilterRegistrationBean<RepeatableFilter> someFilterRegistration() {
-        FilterRegistrationBean<RepeatableFilter> registration = new FilterRegistrationBean<>();
-        registration.setFilter(new RepeatableFilter());
-        registration.addUrlPatterns("/*");
-        registration.setName("repeatableFilter");
-        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
-        return registration;
+    @FilterRegistration(name = "repeatableFilter", urlPatterns = "/*")
+    public RepeatableFilter repeatableFilter() {
+        return new RepeatableFilter();
     }
     }
 
 
 }
 }

+ 5 - 3
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java

@@ -1,7 +1,8 @@
 package org.dromara.common.web.config;
 package org.dromara.common.web.config;
 
 
+import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.date.DateUtil;
-import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ObjectUtils;
 import org.dromara.common.web.handler.GlobalExceptionHandler;
 import org.dromara.common.web.handler.GlobalExceptionHandler;
 import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor;
 import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -34,10 +35,11 @@ public class ResourcesConfig implements WebMvcConfigurer {
     public void addFormatters(FormatterRegistry registry) {
     public void addFormatters(FormatterRegistry registry) {
         // 全局日期格式转换配置
         // 全局日期格式转换配置
         registry.addConverter(String.class, Date.class, source -> {
         registry.addConverter(String.class, Date.class, source -> {
-            if (StringUtils.isBlank(source)) {
+            DateTime parse = DateUtil.parse(source);
+            if (ObjectUtils.isNull(parse)) {
                 return null;
                 return null;
             }
             }
-            return DateUtil.parse(source);
+            return parse.toJdkDate();
         });
         });
     }
     }
 
 

+ 2 - 2
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java

@@ -1,8 +1,8 @@
 package org.dromara.common.web.enums;
 package org.dromara.common.web.enums;
 
 
 import cn.hutool.captcha.generator.CodeGenerator;
 import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.captcha.generator.MathGenerator;
 import cn.hutool.captcha.generator.RandomGenerator;
 import cn.hutool.captcha.generator.RandomGenerator;
-import org.dromara.common.web.utils.UnsignedMathGenerator;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Getter;
 
 
@@ -18,7 +18,7 @@ public enum CaptchaType {
     /**
     /**
      * 数字
      * 数字
      */
      */
-    MATH(UnsignedMathGenerator.class),
+    MATH(MathGenerator.class),
 
 
     /**
     /**
      * 字符
      * 字符

+ 0 - 88
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/utils/UnsignedMathGenerator.java

@@ -1,88 +0,0 @@
-package org.dromara.common.web.utils;
-
-import cn.hutool.captcha.generator.CodeGenerator;
-import cn.hutool.core.math.Calculator;
-import cn.hutool.core.util.CharUtil;
-import cn.hutool.core.util.RandomUtil;
-import org.dromara.common.core.utils.StringUtils;
-
-import java.io.Serial;
-
-/**
- * 无符号计算生成器
- *
- * @author Lion Li
- */
-public class UnsignedMathGenerator implements CodeGenerator {
-
-    @Serial
-    private static final long serialVersionUID = -5514819971774091076L;
-
-    private static final String OPERATORS = "+-*";
-
-    /**
-     * 参与计算数字最大长度
-     */
-    private final int numberLength;
-
-    /**
-     * 构造
-     */
-    public UnsignedMathGenerator() {
-        this(2);
-    }
-
-    /**
-     * 构造
-     *
-     * @param numberLength 参与计算最大数字位数
-     */
-    public UnsignedMathGenerator(int numberLength) {
-        this.numberLength = numberLength;
-    }
-
-    @Override
-    public String generate() {
-        final int limit = getLimit();
-        int a = RandomUtil.randomInt(limit);
-        int b = RandomUtil.randomInt(limit);
-        String max = Integer.toString(Math.max(a,b));
-        String min = Integer.toString(Math.min(a,b));
-        max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
-        min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
-
-        return max + RandomUtil.randomChar(OPERATORS) + min + '=';
-    }
-
-    @Override
-    public boolean verify(String code, String userInputCode) {
-        int result;
-        try {
-            result = Integer.parseInt(userInputCode);
-        } catch (NumberFormatException e) {
-            // 用户输入非数字
-            return false;
-        }
-
-        final int calculateResult = (int) Calculator.conversion(code);
-        return result == calculateResult;
-    }
-
-    /**
-     * 获取验证码长度
-     *
-     * @return 验证码长度
-     */
-    public int getLength() {
-        return this.numberLength * 2 + 2;
-    }
-
-    /**
-     * 根据长度获取参与计算数字最大值
-     *
-     * @return 最大值
-     */
-    private int getLimit() {
-        return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
-    }
-}

+ 2 - 2
ruoyi-extend/ruoyi-monitor-admin/Dockerfile

@@ -1,6 +1,6 @@
 # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
 # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
-FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
-#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
+FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
+#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
 #FROM findepi/graalvm:java17-native
 #FROM findepi/graalvm:java17-native
 
 
 LABEL maintainer="Lion Li"
 LABEL maintainer="Lion Li"

+ 2 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/MonitorAdminApplication.java

@@ -1,5 +1,6 @@
 package org.dromara.monitor.admin;
 package org.dromara.monitor.admin;
 
 
+import de.codecentric.boot.admin.server.config.EnableAdminServer;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 
@@ -8,6 +9,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
  *
  *
  * @author Lion Li
  * @author Lion Li
  */
  */
+@EnableAdminServer
 @SpringBootApplication
 @SpringBootApplication
 public class MonitorAdminApplication {
 public class MonitorAdminApplication {
 
 

+ 0 - 31
ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/AdminServerConfig.java

@@ -1,31 +0,0 @@
-package org.dromara.monitor.admin.config;
-
-import de.codecentric.boot.admin.server.config.EnableAdminServer;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
-import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
-
-import java.util.concurrent.Executor;
-
-/**
- * springboot-admin server配置类
- *
- * @author Lion Li
- */
-@Configuration
-@EnableAdminServer
-public class AdminServerConfig {
-
-    @Lazy
-    @Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
-    @ConditionalOnMissingBean(Executor.class)
-    public ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder builder) {
-        return builder.build();
-    }
-
-
-}

+ 4 - 4
ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/SecurityConfig.java

@@ -10,7 +10,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
 import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
 import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
-import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
 
 
 /**
 /**
  * admin 监控 安全配置
  * admin 监控 安全配置
@@ -32,14 +32,14 @@ public class SecurityConfig {
         SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
         SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
         successHandler.setTargetUrlParameter("redirectTo");
         successHandler.setTargetUrlParameter("redirectTo");
         successHandler.setDefaultTargetUrl(adminContextPath + "/");
         successHandler.setDefaultTargetUrl(adminContextPath + "/");
-
+        PathPatternRequestMatcher.Builder mvc = PathPatternRequestMatcher.withDefaults();
         return httpSecurity
         return httpSecurity
             .headers((header) ->
             .headers((header) ->
                 header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                 header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
             .authorizeHttpRequests((authorize) ->
             .authorizeHttpRequests((authorize) ->
                 authorize.requestMatchers(
                 authorize.requestMatchers(
-                        new AntPathRequestMatcher(adminContextPath + "/assets/**"),
-                        new AntPathRequestMatcher(adminContextPath + "/login")
+                        mvc.matcher(adminContextPath + "/assets/**"),
+                        mvc.matcher(adminContextPath + "/login")
                     ).permitAll()
                     ).permitAll()
                     .anyRequest().authenticated())
                     .anyRequest().authenticated())
             .formLogin((formLogin) ->
             .formLogin((formLogin) ->

+ 3 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml

@@ -20,6 +20,9 @@ spring:
       ui:
       ui:
         title: RuoYi-Vue-Plus服务监控中心
         title: RuoYi-Vue-Plus服务监控中心
       context-path: /admin
       context-path: /admin
+  # 忽略无用警告
+  thymeleaf:
+    check-template-location: false
 
 
 --- # Actuator 监控端点的配置项
 --- # Actuator 监控端点的配置项
 management:
 management:

+ 2 - 2
ruoyi-extend/ruoyi-snailjob-server/Dockerfile

@@ -1,6 +1,6 @@
 # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
 # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
-FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
-#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
+FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
+#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
 #FROM findepi/graalvm:java17-native
 #FROM findepi/graalvm:java17-native
 
 
 LABEL maintainer="Lion Li"
 LABEL maintainer="Lion Li"

+ 146 - 0
ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/common/register/ServerRegister.java

@@ -0,0 +1,146 @@
+package com.aizuda.snailjob.server.common.register;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.aizuda.snailjob.common.core.enums.NodeTypeEnum;
+import com.aizuda.snailjob.common.core.util.JsonUtil;
+import com.aizuda.snailjob.common.core.util.NetUtil;
+import com.aizuda.snailjob.common.core.util.SnailJobVersion;
+import com.aizuda.snailjob.common.core.util.StreamUtils;
+import com.aizuda.snailjob.common.log.SnailJobLog;
+import com.aizuda.snailjob.server.common.cache.CacheConsumerGroup;
+import com.aizuda.snailjob.server.common.config.SystemProperties;
+import com.aizuda.snailjob.server.common.convert.RegisterNodeInfoConverter;
+import com.aizuda.snailjob.server.common.dto.ServerNodeExtAttrs;
+import com.aizuda.snailjob.server.common.handler.InstanceManager;
+import com.aizuda.snailjob.template.datasource.persistence.po.ServerNode;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.google.common.collect.Lists;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 服务端注册
+ *
+ * @author opensnail
+ * @date 2023-06-07
+ * @since 1.6.0
+ */
+@Component(ServerRegister.BEAN_NAME)
+@RequiredArgsConstructor
+public class ServerRegister extends AbstractRegister {
+    public static final String BEAN_NAME = "serverRegister";
+    private final ScheduledExecutorService serverRegisterNode = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "server-register-node"));
+    public static final int DELAY_TIME = 30;
+    public static final String CURRENT_CID;
+    public static final String GROUP_NAME = "DEFAULT_SERVER";
+    public static final String NAMESPACE_ID = "DEFAULT_SERVER_NAMESPACE_ID";
+    private final InstanceManager instanceManager;
+    private final SystemProperties systemProperties;
+    private final ServerProperties serverProperties;
+
+    static {
+        CURRENT_CID = IdUtil.getSnowflakeNextIdStr();
+    }
+
+    @Override
+    public boolean supports(int type) {
+        return getNodeType().equals(type);
+    }
+
+    @Override
+    protected void beforeProcessor(RegisterContext context) {
+        // 新增扩展参数
+        ServerNodeExtAttrs serverNodeExtAttrs = new ServerNodeExtAttrs();
+        serverNodeExtAttrs.setWebPort(serverProperties.getPort());
+        serverNodeExtAttrs.setSystemVersion(SnailJobVersion.getVersion());
+
+        context.setGroupName(GROUP_NAME);
+        context.setHostId(CURRENT_CID);
+        String serverHost = systemProperties.getServerHost();
+        if (StrUtil.isEmptyIfStr(serverHost)) {
+            serverHost = NetUtil.getLocalIpStr();
+        }
+        context.setHostIp(serverHost);
+        context.setHostPort(systemProperties.getServerPort());
+        context.setContextPath(Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse(StrUtil.EMPTY));
+        context.setNamespaceId(NAMESPACE_ID);
+        context.setExtAttrs(JsonUtil.toJsonString(serverNodeExtAttrs));
+    }
+
+    @Override
+    protected LocalDateTime getExpireAt() {
+        return LocalDateTime.now().plusSeconds(DELAY_TIME);
+    }
+
+    @Override
+    protected boolean doRegister(RegisterContext context, ServerNode serverNode) {
+        refreshExpireAt(Lists.newArrayList(serverNode));
+        return Boolean.TRUE;
+    }
+
+
+    @Override
+    protected void afterProcessor(final ServerNode serverNode) {
+        try {
+            // 同步当前POD消费的组的节点信息
+            // netty的client只会注册到一个服务端,若组分配的和client连接的不是一个POD则会导致当前POD没有其他客户端的注册信息
+            ConcurrentMap<String /*groupName*/, Set<String>/*namespaceId*/> allConsumerGroupName = CacheConsumerGroup.getAllConsumerGroupName();
+            if (CollUtil.isNotEmpty(allConsumerGroupName)) {
+                Set<String> namespaceIdSets = StreamUtils.toSetByFlatMap(allConsumerGroupName.values(), Set::stream);
+                if (CollUtil.isEmpty(namespaceIdSets)) {
+                    return;
+                }
+
+                List<ServerNode> serverNodes = serverNodeMapper.selectList(
+                        new LambdaQueryWrapper<ServerNode>()
+                                .eq(ServerNode::getNodeType, NodeTypeEnum.CLIENT.getType())
+                                .in(ServerNode::getNamespaceId, namespaceIdSets)
+                                .in(ServerNode::getGroupName, allConsumerGroupName.keySet()));
+                for (final ServerNode node : serverNodes) {
+                    // 刷新全量本地缓存
+                    instanceManager.registerOrUpdate(RegisterNodeInfoConverter.INSTANCE.toRegisterNodeInfo(node));
+                    // 刷新过期时间
+                    CacheConsumerGroup.addOrUpdate(node.getGroupName(), node.getNamespaceId());
+                }
+            }
+        } catch (Exception e) {
+            SnailJobLog.LOCAL.error("Client refresh failed", e);
+        }
+    }
+
+    @Override
+    protected Integer getNodeType() {
+        return NodeTypeEnum.SERVER.getType();
+    }
+
+    @Override
+    public void start() {
+        SnailJobLog.LOCAL.info("ServerRegister start");
+
+        serverRegisterNode.scheduleAtFixedRate(() -> {
+            try {
+                this.register(new RegisterContext());
+            } catch (Exception e) {
+                SnailJobLog.LOCAL.error("Server-side registration failed", e);
+            }
+        }, 0, DELAY_TIME * 2 / 3, TimeUnit.SECONDS);
+
+    }
+
+    @Override
+    public void close() {
+        SnailJobLog.LOCAL.info("ServerRegister close");
+    }
+}

+ 18 - 7
ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml

@@ -16,15 +16,26 @@ spring:
 
 
 --- # snail-job 服务端配置
 --- # snail-job 服务端配置
 snail-job:
 snail-job:
-  # 拉取重试数据的每批次的大小
-  retry-pull-page-size: 1000
-  # 拉取重试数据的每批次的大小
-  job-pull-page-size: 1000
-  # 服务器端口
+  # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
+  server-host:
+  # 服务端端口号
   server-port: 17888
   server-port: 17888
-  # 日志保存时间(单位: day)
+  # 合并日志默认保存天数
+  merge-Log-days: 1
+  # 合并日志默认的条数
+  merge-Log-num: 500
+  # 配置每批次拉取重试数据的大小
+  retry-pull-page-size: 100
+  # 配置日志保存时间(单位:天)
   log-storage: 7
   log-storage: 7
-  rpc-type: grpc
+  # bucket的总数量
+  bucket-total: 128
+  # Dashboard 任务容错天数
+  summary-day: 7
+  # 配置负载均衡周期时间
+  load-balance-cycle-time: 10
+  # 重试任务拉取的并行度
+  retry-max-pull-parallel: 2
 
 
 --- # 监控中心配置
 --- # 监控中心配置
 spring.boot.admin.client:
 spring.boot.admin.client:

+ 18 - 7
ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml

@@ -16,15 +16,26 @@ spring:
 
 
 --- # snail-job 服务端配置
 --- # snail-job 服务端配置
 snail-job:
 snail-job:
-  # 拉取重试数据的每批次的大小
-  retry-pull-page-size: 1000
-  # 拉取重试数据的每批次的大小
-  job-pull-page-size: 1000
-  # 服务器端口
+  # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
+  server-host:
+  # 服务端端口号
   server-port: 17888
   server-port: 17888
-  # 日志保存时间(单位: day)
+  # 合并日志默认保存天数
+  merge-Log-days: 1
+  # 合并日志默认的条数
+  merge-Log-num: 500
+  # 配置每批次拉取重试数据的大小
+  retry-pull-page-size: 100
+  # 配置日志保存时间(单位:天)
   log-storage: 7
   log-storage: 7
-  rpc-type: grpc
+  # bucket的总数量
+  bucket-total: 128
+  # Dashboard 任务容错天数
+  summary-day: 7
+  # 配置负载均衡周期时间
+  load-balance-cycle-time: 10
+  # 重试任务拉取的并行度
+  retry-max-pull-parallel: 2
 
 
 --- # 监控中心配置
 --- # 监控中心配置
 spring.boot.admin.client:
 spring.boot.admin.client:

+ 1 - 0
ruoyi-extend/ruoyi-snailjob-server/src/main/resources/logback-plus.xml

@@ -85,6 +85,7 @@
     <!-- 控制台输出日志级别 -->
     <!-- 控制台输出日志级别 -->
     <root level="info">
     <root level="info">
         <appender-ref ref="console" />
         <appender-ref ref="console" />
+        <appender-ref ref="file_console" />
         <appender-ref ref="async_info" />
         <appender-ref ref="async_info" />
         <appender-ref ref="async_error" />
         <appender-ref ref="async_error" />
         <appender-ref ref="snail_log_server_appender" />
         <appender-ref ref="snail_log_server_appender" />

+ 1 - 1
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailController.java → ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/MailSendController.java

@@ -21,7 +21,7 @@ import java.util.Arrays;
 @RequiredArgsConstructor
 @RequiredArgsConstructor
 @RestController
 @RestController
 @RequestMapping("/demo/mail")
 @RequestMapping("/demo/mail")
-public class MailController {
+public class MailSendController {
 
 
     /**
     /**
      * 发送邮件
      * 发送邮件

+ 12 - 0
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java

@@ -1,5 +1,6 @@
 package org.dromara.demo.controller;
 package org.dromara.demo.controller;
 
 
+import cn.dev33.satoken.annotation.SaIgnore;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
@@ -14,6 +15,7 @@ import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartFile;
 
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
@@ -94,6 +96,16 @@ public class TestExcelController {
         exportExcelService.exportWithOptions(response);
         exportExcelService.exportWithOptions(response);
     }
     }
 
 
+    /**
+     * 自定义导出
+     *
+     * @param response /
+     */
+    @GetMapping("/customExport")
+    public void customExport(HttpServletResponse response) throws IOException {
+        exportExcelService.customExport(response);
+    }
+
     /**
     /**
      * 多个sheet导出
      * 多个sheet导出
      */
      */

+ 9 - 0
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java

@@ -2,6 +2,8 @@ package org.dromara.demo.service;
 
 
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponse;
 
 
+import java.io.IOException;
+
 /**
 /**
  * 导出下拉框Excel示例
  * 导出下拉框Excel示例
  *
  *
@@ -15,4 +17,11 @@ public interface IExportExcelService {
      * @param response /
      * @param response /
      */
      */
     void exportWithOptions(HttpServletResponse response);
     void exportWithOptions(HttpServletResponse response);
+
+    /**
+     * 自定义导出
+     *
+     * @param response /
+     */
+    void customExport(HttpServletResponse response) throws IOException;
 }
 }

+ 61 - 0
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java

@@ -2,17 +2,21 @@ package org.dromara.demo.service.impl;
 
 
 import cn.hutool.core.util.RandomUtil;
 import cn.hutool.core.util.RandomUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.idev.excel.write.metadata.WriteSheet;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.Data;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.utils.StreamUtils;
 import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.file.FileUtils;
 import org.dromara.common.excel.core.DropDownOptions;
 import org.dromara.common.excel.core.DropDownOptions;
 import org.dromara.common.excel.utils.ExcelUtil;
 import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.excel.utils.ExcelWriterWrapper;
 import org.dromara.demo.domain.vo.ExportDemoVo;
 import org.dromara.demo.domain.vo.ExportDemoVo;
 import org.dromara.demo.service.IExportExcelService;
 import org.dromara.demo.service.IExportExcelService;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
@@ -233,4 +237,61 @@ public class ExportExcelServiceImpl implements IExportExcelService {
             this.name = name;
             this.name = name;
         }
         }
     }
     }
+
+
+    @Override
+    public void customExport(HttpServletResponse response) throws IOException {
+        String filename = ExcelUtil.encodingFilename("自定义导出");
+        FileUtils.setAttachmentResponseHeader(response, filename);
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
+
+        ExcelUtil.exportExcel(ExportDemoVo.class, response.getOutputStream(), wrapper -> {
+            // 创建表格数据,业务中一般通过数据库查询
+            List<ExportDemoVo> excelDataList = new ArrayList<>();
+            for (int i = 0; i < 30; i++) {
+                // 模拟数据库中的一条数据
+                ExportDemoVo everyRowData = new ExportDemoVo();
+                everyRowData.setNickName("用户-" + i);
+                everyRowData.setUserStatus(SystemConstants.NORMAL);
+                everyRowData.setGender("1");
+                everyRowData.setPhoneNumber(String.format("175%08d", i));
+                everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
+                everyRowData.setProvinceId(i);
+                everyRowData.setCityId(i);
+                everyRowData.setAreaId(i);
+                excelDataList.add(everyRowData);
+            }
+
+            // 创建表格
+            WriteSheet sheet = ExcelWriterWrapper.sheetBuilder("自定义导出demo")
+                // 合并单元格
+                // .registerWriteHandler(new CellMergeStrategy(excelDataList, true))
+                .build();
+
+
+            wrapper.write(excelDataList, sheet);
+
+            List<ExportDemoVo> excelDataList2 = new ArrayList<>();
+            for (int i = 0; i < 20; i++) {
+                int index = 1000 + i;
+                // 模拟数据库中的一条数据
+                ExportDemoVo everyRowData = new ExportDemoVo();
+                everyRowData.setNickName("用户-" + index);
+                everyRowData.setUserStatus(SystemConstants.NORMAL);
+                everyRowData.setGender("1");
+                everyRowData.setPhoneNumber(String.format("175%08d", index));
+                everyRowData.setEmail(String.format("175%08d", index) + "@163.com");
+                everyRowData.setProvinceId(index);
+                everyRowData.setCityId(index);
+                everyRowData.setAreaId(index);
+                excelDataList2.add(everyRowData);
+            }
+
+            wrapper.write(excelDataList2, sheet);
+
+            // 或者在同一个excel中创建多个表格
+            // WriteSheet sheet2 = ExcelWriterWrapper.sheetBuilder("自定义导出demo2").build();
+            // wrapper.write(excelDataList2, sheet2);
+        });
+    }
 }
 }

+ 5 - 0
ruoyi-modules/ruoyi-generator/pom.xml

@@ -42,6 +42,11 @@
             <artifactId>ruoyi-common-log</artifactId>
             <artifactId>ruoyi-common-log</artifactId>
         </dependency>
         </dependency>
 
 
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-idempotent</artifactId>
+        </dependency>
+
         <!--velocity代码生成使用模板 -->
         <!--velocity代码生成使用模板 -->
         <dependency>
         <dependency>
             <groupId>org.apache.velocity</groupId>
             <groupId>org.apache.velocity</groupId>

+ 6 - 0
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/controller/GenController.java

@@ -3,9 +3,11 @@ package org.dromara.generator.controller;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.io.IoUtil;
+import com.baomidou.lock.annotation.Lock4j;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.R;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
 import org.dromara.common.log.annotation.Log;
 import org.dromara.common.log.annotation.Log;
 import org.dromara.common.log.enums.BusinessType;
 import org.dromara.common.log.enums.BusinessType;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.PageQuery;
@@ -50,6 +52,7 @@ public class GenController extends BaseController {
      *
      *
      * @param tableId 表ID
      * @param tableId 表ID
      */
      */
+    @RepeatSubmit()
     @SaCheckPermission("tool:gen:query")
     @SaCheckPermission("tool:gen:query")
     @GetMapping(value = "/{tableId}")
     @GetMapping(value = "/{tableId}")
     public R<Map<String, Object>> getInfo(@PathVariable Long tableId) {
     public R<Map<String, Object>> getInfo(@PathVariable Long tableId) {
@@ -91,6 +94,7 @@ public class GenController extends BaseController {
      */
      */
     @SaCheckPermission("tool:gen:import")
     @SaCheckPermission("tool:gen:import")
     @Log(title = "代码生成", businessType = BusinessType.IMPORT)
     @Log(title = "代码生成", businessType = BusinessType.IMPORT)
+    @RepeatSubmit()
     @PostMapping("/importTable")
     @PostMapping("/importTable")
     public R<Void> importTableSave(String tables, String dataName) {
     public R<Void> importTableSave(String tables, String dataName) {
         String[] tableNames = Convert.toStrArray(tables);
         String[] tableNames = Convert.toStrArray(tables);
@@ -105,6 +109,7 @@ public class GenController extends BaseController {
      */
      */
     @SaCheckPermission("tool:gen:edit")
     @SaCheckPermission("tool:gen:edit")
     @Log(title = "代码生成", businessType = BusinessType.UPDATE)
     @Log(title = "代码生成", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
     @PutMapping
     @PutMapping
     public R<Void> editSave(@Validated @RequestBody GenTable genTable) {
     public R<Void> editSave(@Validated @RequestBody GenTable genTable) {
         genTableService.validateEdit(genTable);
         genTableService.validateEdit(genTable);
@@ -170,6 +175,7 @@ public class GenController extends BaseController {
      */
      */
     @SaCheckPermission("tool:gen:edit")
     @SaCheckPermission("tool:gen:edit")
     @Log(title = "代码生成", businessType = BusinessType.UPDATE)
     @Log(title = "代码生成", businessType = BusinessType.UPDATE)
+    @Lock4j
     @GetMapping("/synchDb/{tableId}")
     @GetMapping("/synchDb/{tableId}")
     public R<Void> synchDb(@PathVariable("tableId") Long tableId) {
     public R<Void> synchDb(@PathVariable("tableId") Long tableId) {
         genTableService.synchDb(tableId);
         genTableService.synchDb(tableId);

+ 1 - 1
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java

@@ -302,7 +302,7 @@ public class GenTableServiceImpl implements IGenTableService {
             tableColumn.setColumnComment(column.getComment());
             tableColumn.setColumnComment(column.getComment());
             tableColumn.setColumnType(column.getOriginType().toLowerCase());
             tableColumn.setColumnType(column.getOriginType().toLowerCase());
             tableColumn.setSort(column.getPosition());
             tableColumn.setSort(column.getPosition());
-            tableColumn.setIsRequired(column.isNullable() ? "1" : "0");
+            tableColumn.setIsRequired(column.isNullable() ? "0" : "1");
             tableColumn.setIsIncrement(column.isAutoIncrement() ? "1" : "0");
             tableColumn.setIsIncrement(column.isAutoIncrement() ? "1" : "0");
             tableColumns.add(tableColumn);
             tableColumns.add(tableColumn);
         });
         });

+ 5 - 3
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java

@@ -3,6 +3,7 @@ package org.dromara.generator.util;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.lang.Dict;
 import cn.hutool.core.lang.Dict;
+import org.dromara.common.mybatis.enums.DataBaseType;
 import org.dromara.generator.constant.GenConstants;
 import org.dromara.generator.constant.GenConstants;
 import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.core.utils.StringUtils;
@@ -118,11 +119,12 @@ public class VelocityUtils {
         templates.add("vm/java/serviceImpl.java.vm");
         templates.add("vm/java/serviceImpl.java.vm");
         templates.add("vm/java/controller.java.vm");
         templates.add("vm/java/controller.java.vm");
         templates.add("vm/xml/mapper.xml.vm");
         templates.add("vm/xml/mapper.xml.vm");
-        if (DataBaseHelper.isOracle()) {
+        DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
+        if (dataBaseType.isOracle()) {
             templates.add("vm/sql/oracle/sql.vm");
             templates.add("vm/sql/oracle/sql.vm");
-        } else if (DataBaseHelper.isPostgerSql()) {
+        } else if (dataBaseType.isPostgreSql()) {
             templates.add("vm/sql/postgres/sql.vm");
             templates.add("vm/sql/postgres/sql.vm");
-        } else if (DataBaseHelper.isSqlServer()) {
+        } else if (dataBaseType.isSqlServer()) {
             templates.add("vm/sql/sqlserver/sql.vm");
             templates.add("vm/sql/sqlserver/sql.vm");
         } else {
         } else {
             templates.add("vm/sql/sql.vm");
             templates.add("vm/sql/sql.vm");

+ 4 - 7
ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm

@@ -54,11 +54,8 @@ export interface ${BusinessName}Query #if(!${treeCode})extends PageQuery #end{
                     #end
                     #end
 #end
 #end
 #end
 #end
-    /**
-     * 日期范围参数
-     */
-    params?: any;
+  /**
+   * 日期范围参数
+   */
+  params?: any;
 }
 }
-
-
-

+ 1 - 1
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm

@@ -123,7 +123,7 @@
 #end
 #end
 #end
 #end
 #end
 #end
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
           <template #default="scope">
           <template #default="scope">
             <el-tooltip content="修改" placement="top">
             <el-tooltip content="修改" placement="top">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']" />
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']" />

+ 1 - 1
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm

@@ -120,7 +120,7 @@
         <el-table-column label="${comment}" align="center" prop="${javaField}" />
         <el-table-column label="${comment}" align="center" prop="${javaField}" />
 #end
 #end
 #end
 #end
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <el-table-column label="操作" align="center" fixed="right"  class-name="small-padding fixed-width">
           <template #default="scope">
           <template #default="scope">
             <el-tooltip content="修改" placement="top">
             <el-tooltip content="修改" placement="top">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']"></el-button>
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']"></el-button>

+ 2 - 3
ruoyi-modules/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm

@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE mapper
 <!DOCTYPE mapper
-PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="${packageName}.mapper.${ClassName}Mapper">
 <mapper namespace="${packageName}.mapper.${ClassName}Mapper">
-
 </mapper>
 </mapper>

+ 1 - 1
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/AlipayBillTask.java

@@ -4,8 +4,8 @@ import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
 import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
 import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
 import com.aizuda.snailjob.client.job.core.dto.JobArgs;
 import com.aizuda.snailjob.client.job.core.dto.JobArgs;
-import com.aizuda.snailjob.client.model.ExecuteResult;
 import com.aizuda.snailjob.common.log.SnailJobLog;
 import com.aizuda.snailjob.common.log.SnailJobLog;
+import com.aizuda.snailjob.model.dto.ExecuteResult;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.job.entity.BillDto;
 import org.dromara.job.entity.BillDto;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;

+ 1 - 1
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/SummaryBillTask.java

@@ -3,8 +3,8 @@ package org.dromara.job.snailjob;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
 import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
 import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
 import com.aizuda.snailjob.client.job.core.dto.JobArgs;
 import com.aizuda.snailjob.client.job.core.dto.JobArgs;
-import com.aizuda.snailjob.client.model.ExecuteResult;
 import com.aizuda.snailjob.common.log.SnailJobLog;
 import com.aizuda.snailjob.common.log.SnailJobLog;
+import com.aizuda.snailjob.model.dto.ExecuteResult;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.job.entity.BillDto;
 import org.dromara.job.entity.BillDto;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.