Просмотр исходного кода

!639 发布 5.3.0-BETA 公测版本
Merge pull request !639 from 疯狂的狮子Li/dev

疯狂的狮子Li 1 год назад
Родитель
Сommit
3c8d864b5f
100 измененных файлов с 2366 добавлено и 813 удалено
  1. 1 1
      .run/ruoyi-monitor-admin.run.xml
  2. 2 2
      .run/ruoyi-server.run.xml
  3. 1 1
      .run/ruoyi-snailjob-server.run.xml
  4. 10 6
      README.md
  5. 31 21
      pom.xml
  6. 5 1
      ruoyi-admin/Dockerfile
  7. 11 6
      ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java
  8. 14 9
      ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java
  9. 2 2
      ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java
  10. 2 2
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java
  11. 2 2
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
  12. 2 2
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java
  13. 2 2
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
  14. 25 6
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java
  15. 15 13
      ruoyi-admin/src/main/resources/application-dev.yml
  16. 14 12
      ruoyi-admin/src/main/resources/application-prod.yml
  17. 14 25
      ruoyi-admin/src/main/resources/application.yml
  18. 1 1
      ruoyi-admin/src/main/resources/logback-plus.xml
  19. 1 0
      ruoyi-admin/zhFonts/.uuid
  20. BIN
      ruoyi-admin/zhFonts/SIMSUN.TTC
  21. 4 0
      ruoyi-admin/zhFonts/fonts.dir
  22. 4 0
      ruoyi-admin/zhFonts/fonts.scale
  23. 1 1
      ruoyi-common/ruoyi-common-bom/pom.xml
  24. 10 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java
  25. 10 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
  26. 1 6
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
  27. 75 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/SystemConstants.java
  28. 0 10
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java
  29. 0 152
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
  30. 71 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/CompleteTaskDTO.java
  31. 37 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DeptDTO.java
  32. 30 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java
  33. 46 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/PostDTO.java
  34. 45 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java
  35. 101 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/TaskAssigneeDTO.java
  36. 34 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessDeleteEvent.java
  37. 8 5
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java
  38. 11 7
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java
  39. 7 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java
  40. 2 4
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
  41. 2 4
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java
  42. 56 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/TaskAssigneeBody.java
  43. 71 8
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/BusinessStatusEnum.java
  44. 146 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/FormatsType.java
  45. 0 30
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java
  46. 19 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java
  47. 10 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/PostService.java
  48. 10 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/RoleService.java
  49. 45 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/TaskAssigneeService.java
  50. 9 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java
  51. 28 26
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java
  52. 189 70
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
  53. 60 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ObjectUtils.java
  54. 80 19
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java
  55. 21 3
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
  56. 0 12
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java
  57. 17 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java
  58. 1 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
  59. 48 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPattern.java
  60. 37 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPatternValidator.java
  61. 2 5
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/core/EncryptorManager.java
  62. 6 4
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/utils/EncryptUtils.java
  63. 24 0
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelNotation.java
  64. 26 0
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelRequired.java
  65. 67 41
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
  66. 135 0
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/DataWriteHandler.java
  67. 15 12
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java
  68. 1 1
      ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/BigNumberSerializer.java
  69. 5 6
      ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java
  70. 7 1
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/MailProperties.java
  71. 5 0
      ruoyi-common/ruoyi-common-mybatis/pom.xml
  72. 50 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/aspect/DataPermissionAspect.java
  73. 19 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/MybatisPlusConfig.java
  74. 7 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java
  75. 2 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/TableDataInfo.java
  76. 6 1
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java
  77. 3 4
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java
  78. 130 72
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
  79. 27 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusPostInitTableInfoHandler.java
  80. 28 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java
  81. 2 2
      ruoyi-common/ruoyi-common-mybatis/src/main/resources/common-mybatis.yml
  82. 11 121
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java
  83. 3 8
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java
  84. 0 35
      ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/PolicyType.java
  85. 6 0
      ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/annotation/RateLimiter.java
  86. 2 1
      ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java
  87. 12 0
      ruoyi-common/ruoyi-common-redis/pom.xml
  88. 3 0
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java
  89. 3 3
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/QueueUtils.java
  90. 40 4
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java
  91. 165 0
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java
  92. 10 2
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java
  93. 10 2
      ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java
  94. 8 2
      ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/annotation/Sensitive.java
  95. 1 1
      ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveService.java
  96. 2 2
      ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java
  97. 2 2
      ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java
  98. 1 1
      ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/dao/PlusSmsDao.java
  99. 2 2
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeyRequest.java
  100. 17 4
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/topiam/AuthTopIamRequest.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">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.2.3" />
+        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.3.0-BETA" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
       </settings>

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

@@ -1,8 +1,8 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="演示机">
+  <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-server:5.2.3" />
+        <option name="imageTag" value="ruoyi/ruoyi-server:5.3.0-BETA" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
       </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">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.2.3" />
+        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.3.0-BETA" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
       </settings>

+ 10 - 6
README.md

@@ -6,22 +6,25 @@
 
 [![码云Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Vue-Plus)
 [![GitHub](https://img.shields.io/github/stars/dromara/RuoYi-Vue-Plus.svg?style=social&label=Stars)](https://github.com/dromara/RuoYi-Vue-Plus)
+[![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus)
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
 [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 <br>
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.3-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
-[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2-blue.svg)]()
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.3.0--BETA-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[![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-21](https://img.shields.io/badge/JDK-21-green.svg)]()
 
-> RuoYi-Vue-Plus 是重写 RuoYi-Vue 针对 `分布式集群与多租户` 场景全方位升级(不兼容原框架)
+> Dromara RuoYi-Vue-Plus 是重写 RuoYi-Vue 针对 `分布式集群与多租户` 场景全方位升级(不兼容原框架)
 
 > 项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可<br>
 活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源
 
 > 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
 
-> 前端项目地址: [plus-ui](https://gitee.com/JavaLionLi/plus-ui)
+> 官方前端项目地址: [plus-ui](https://gitee.com/JavaLionLi/plus-ui)<br>
+> 成员前端项目地址: 基于vben [ruoyi-plus-vben](https://gitee.com/dapppp/ruoyi-plus-vben)<br>
+> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
 
 > 文档地址: [plus-doc](https://plus-doc.dromara.org)
 
@@ -31,6 +34,7 @@ MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey <br>
 CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br>
 引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
+<font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
 [如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
 
 # 本框架与RuoYi的功能差异
@@ -165,8 +169,8 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 | ![输入图片说明](https://foruda.gitee.com/images/1680078779416197879/75e3ed02_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078802329118061/77e10915_1766278.png "屏幕截图") |
 | ![输入图片说明](https://foruda.gitee.com/images/1680078893627848351/34a1c342_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078928175016986/f126ec4a_1766278.png "屏幕截图") |
 | ![输入图片说明](https://foruda.gitee.com/images/1680078941718318363/b68a0f72_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078963175518631/3bb769a1_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078982294090567/b31c343d_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079000642440444/77ca82a9_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680079020995074177/03b7d52e_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079039367822173/76811806_1766278.png "屏幕截图") |
+| ![输入图片说明](https://foruda.gitee.com/images/1735829153637063344/3c21fd4c_1419627.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1735829181303499815/4522cefa_1419627.png "屏幕截图") |
+| ![输入图片说明](https://foruda.gitee.com/images/1735829377205259767/76a705d7_1419627.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1722959592856812900/e2d0d342_1419627.png "屏幕截图") |
 | ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png "屏幕截图") |
 
 

+ 31 - 21
pom.xml

@@ -10,33 +10,33 @@
 
     <name>RuoYi-Vue-Plus</name>
     <url>https://gitee.com/dromara/RuoYi-Vue-Plus</url>
-    <description>RuoYi-Vue-Plus多租户管理系统</description>
+    <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
 
     <properties>
-        <revision>5.2.3</revision>
-        <spring-boot.version>3.2.11</spring-boot.version>
+        <revision>5.3.0-BETA</revision>
+        <spring-boot.version>3.4.1</spring-boot.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>17</java.version>
         <mybatis.version>3.5.16</mybatis.version>
-        <springdoc.version>2.6.0</springdoc.version>
+        <springdoc.version>2.8.3</springdoc.version>
         <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
         <easyexcel.version>4.0.3</easyexcel.version>
         <velocity.version>2.3</velocity.version>
         <satoken.version>1.39.0</satoken.version>
-        <mybatis-plus.version>3.5.8</mybatis-plus.version>
+        <mybatis-plus.version>3.5.10</mybatis-plus.version>
         <p6spy.version>3.9.1</p6spy.version>
-        <hutool.version>5.8.31</hutool.version>
-        <spring-boot-admin.version>3.2.3</spring-boot-admin.version>
-        <redisson.version>3.37.0</redisson.version>
+        <hutool.version>5.8.35</hutool.version>
+        <spring-boot-admin.version>3.4.1</spring-boot-admin.version>
+        <redisson.version>3.43.0</redisson.version>
         <lock4j.version>2.2.7</lock4j.version>
         <dynamic-ds.version>4.3.1</dynamic-ds.version>
-        <snailjob.version>1.1.2</snailjob.version>
-        <mapstruct-plus.version>1.4.5</mapstruct-plus.version>
+        <snailjob.version>1.3.0-beta1.1</snailjob.version>
+        <mapstruct-plus.version>1.4.6</mapstruct-plus.version>
         <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
-        <lombok.version>1.18.34</lombok.version>
+        <lombok.version>1.18.36</lombok.version>
         <bouncycastle.version>1.76</bouncycastle.version>
-        <justauth.version>1.16.6</justauth.version>
+        <justauth.version>1.16.7</justauth.version>
         <!-- 离线IP地址定位库 -->
         <ip2region.version>2.7.0</ip2region.version>
 
@@ -48,14 +48,14 @@
         <!-- 限制框架中的fastjson版本 -->
         <fastjson.version>1.2.83</fastjson.version>
         <!-- 面向运行时的D-ORM依赖 -->
-        <anyline.version>8.7.2-20241022</anyline.version>
+        <anyline.version>8.7.2-20250101</anyline.version>
         <!--工作流配置-->
-        <flowable.version>7.0.1</flowable.version>
+        <warm-flow.version>1.6.0-m5</warm-flow.version>
 
         <!-- 插件版本 -->
         <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
         <maven-war-plugin.version>3.2.2</maven-war-plugin.version>
-        <maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
+        <maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
         <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
         <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
     </properties>
@@ -118,12 +118,16 @@
                 <scope>import</scope>
             </dependency>
 
+            <!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ -->
             <dependency>
-                <groupId>org.flowable</groupId>
-                <artifactId>flowable-bom</artifactId>
-                <version>${flowable.version}</version>
-                <type>pom</type>
-                <scope>import</scope>
+                <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 的依赖配置-->
@@ -216,6 +220,12 @@
                 <version>${mybatis-plus.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-jsqlparser</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>com.baomidou</groupId>
                 <artifactId>mybatis-plus-annotation</artifactId>
@@ -369,7 +379,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>${maven-compiler-plugin.verison}</version>
+                <version>${maven-compiler-plugin.version}</version>
                 <configuration>
                     <source>${java.version}</source>
                     <target>${java.version}</target>

+ 5 - 1
ruoyi-admin/Dockerfile

@@ -1,6 +1,6 @@
 # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
 FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
-#FROM bellsoft/liberica-openjdk-debian:21.0.3-cds
+#FROM bellsoft/liberica-openjdk-debian:21.0.5-cds
 #FROM findepi/graalvm:java17-native
 
 LABEL maintainer="Lion Li"
@@ -16,6 +16,10 @@ ENV SERVER_PORT=8080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
 EXPOSE ${SERVER_PORT}
 
 ADD ./target/ruoyi-admin.jar ./app.jar
+# 工作流字体文件
+ADD ./zhFonts/ /usr/share/fonts/zhFonts/
+
+SHELL ["/bin/bash", "-c"]
 
 ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
            # 应用名称 如果想区分集群节点监控 改成不同的名称即可

+ 11 - 6
ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java

@@ -2,6 +2,7 @@ package org.dromara.web.controller;
 
 import cn.dev33.satoken.annotation.SaIgnore;
 import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.codec.Base64;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
@@ -12,7 +13,7 @@ import me.zhyd.oauth.model.AuthResponse;
 import me.zhyd.oauth.model.AuthUser;
 import me.zhyd.oauth.request.AuthRequest;
 import me.zhyd.oauth.utils.AuthStateUtils;
-import org.dromara.common.core.constant.UserConstants;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.LoginBody;
 import org.dromara.common.core.domain.model.RegisterBody;
@@ -92,7 +93,7 @@ public class AuthController {
         if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
             log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
             return R.fail(MessageUtils.message("auth.grant.type.error"));
-        } else if (!UserConstants.NORMAL.equals(client.getStatus())) {
+        } else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
             return R.fail(MessageUtils.message("auth.grant.type.blocked"));
         }
         // 校验租户
@@ -111,7 +112,7 @@ public class AuthController {
     }
 
     /**
-     * 第三方登录请求
+     * 获取跳转URL
      *
      * @param source 登录来源
      * @return 结果
@@ -133,13 +134,15 @@ public class AuthController {
     }
 
     /**
-     * 第三方登录回调业务处理 绑定授权
+     * 前端回调绑定授权(需要token)
      *
      * @param loginBody 请求体
      * @return 结果
      */
     @PostMapping("/social/callback")
     public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
+        // 校验token
+        StpUtil.checkLogin();
         // 获取第三方登录信息
         AuthResponse<AuthUser> response = SocialUtils.loginAuth(
                 loginBody.getSource(), loginBody.getSocialCode(),
@@ -155,12 +158,14 @@ public class AuthController {
 
 
     /**
-     * 取消授权
+     * 取消授权(需要token)
      *
      * @param socialId socialId
      */
     @DeleteMapping(value = "/unlock/{socialId}")
     public R<Void> unlockSocial(@PathVariable Long socialId) {
+        // 校验token
+        StpUtil.checkLogin();
         Boolean rows = socialUserService.deleteWithValidById(socialId);
         return rows ? R.ok() : R.fail("取消授权失败");
     }
@@ -226,7 +231,7 @@ public class AuthController {
         }
         // 根据域名进行筛选
         List<TenantListVo> list = StreamUtils.filter(voList, vo ->
-                StringUtils.equals(vo.getDomain(), host));
+            StringUtils.equalsIgnoreCase(vo.getDomain(), host));
         result.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
         return R.ok(result);
     }

+ 14 - 9
ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java

@@ -12,11 +12,12 @@ import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.model.AuthUser;
 import org.dromara.common.core.constant.CacheConstants;
 import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.constant.TenantConstants;
+import org.dromara.common.core.domain.dto.PostDTO;
 import org.dromara.common.core.domain.dto.RoleDTO;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.enums.LoginType;
-import org.dromara.common.core.enums.TenantStatus;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.exception.user.UserException;
 import org.dromara.common.core.utils.*;
@@ -60,6 +61,7 @@ public class SysLoginService {
     private final ISysSocialService sysSocialService;
     private final ISysRoleService roleService;
     private final ISysDeptService deptService;
+    private final ISysPostService postService;
     private final SysUserMapper userMapper;
 
 
@@ -148,21 +150,24 @@ public class SysLoginService {
      */
     public LoginUser buildLoginUser(SysUserVo user) {
         LoginUser loginUser = new LoginUser();
+        Long userId = user.getUserId();
         loginUser.setTenantId(user.getTenantId());
-        loginUser.setUserId(user.getUserId());
+        loginUser.setUserId(userId);
         loginUser.setDeptId(user.getDeptId());
         loginUser.setUsername(user.getUserName());
         loginUser.setNickname(user.getNickName());
         loginUser.setUserType(user.getUserType());
-        loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
-        loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
+        loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
+        loginUser.setRolePermission(permissionService.getRolePermission(userId));
         if (ObjectUtil.isNotNull(user.getDeptId())) {
             Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
             loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
             loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
         }
-        List<SysRoleVo> roles = roleService.selectRolesByUserId(user.getUserId());
+        List<SysRoleVo> roles = roleService.selectRolesByUserId(userId);
+        List<SysPostVo> posts = postService.selectPostsByUserId(userId);
         loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
+        loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
         return loginUser;
     }
 
@@ -223,17 +228,17 @@ public class SysLoginService {
         if (!TenantHelper.isEnable()) {
             return;
         }
-        if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
-            return;
-        }
         if (StringUtils.isBlank(tenantId)) {
             throw new TenantException("tenant.number.not.blank");
         }
+        if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
+            return;
+        }
         SysTenantVo tenant = tenantService.queryByTenantId(tenantId);
         if (ObjectUtil.isNull(tenant)) {
             log.info("登录租户:{} 不存在.", tenantId);
             throw new TenantException("tenant.not.exists");
-        } else if (TenantStatus.DISABLE.getCode().equals(tenant.getStatus())) {
+        } else if (SystemConstants.DISABLE.equals(tenant.getStatus())) {
             log.info("登录租户:{} 已被停用.", tenantId);
             throw new TenantException("tenant.blocked");
         } else if (ObjectUtil.isNotNull(tenant.getExpireTime())

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

@@ -84,11 +84,11 @@ public class SysRegisterService {
         String captcha = RedisUtils.getCacheObject(verifyKey);
         RedisUtils.deleteObject(verifyKey);
         if (captcha == null) {
-            recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.expire"));
+            recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
             throw new CaptchaExpireException();
         }
         if (!code.equalsIgnoreCase(captcha)) {
-            recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error"));
+            recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
             throw new CaptchaException();
         }
     }

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

@@ -8,10 +8,10 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.domain.model.EmailLoginBody;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.enums.LoginType;
-import org.dromara.common.core.enums.UserStatus;
 import org.dromara.common.core.exception.user.CaptchaExpireException;
 import org.dromara.common.core.exception.user.UserException;
 import org.dromara.common.core.utils.MessageUtils;
@@ -92,7 +92,7 @@ public class EmailAuthStrategy implements IAuthStrategy {
         if (ObjectUtil.isNull(user)) {
             log.info("登录用户:{} 不存在.", email);
             throw new UserException("user.not.exists", email);
-        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+        } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
             log.info("登录用户:{} 已被停用.", email);
             throw new UserException("user.blocked", email);
         }

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

@@ -9,10 +9,10 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.domain.model.PasswordLoginBody;
 import org.dromara.common.core.enums.LoginType;
-import org.dromara.common.core.enums.UserStatus;
 import org.dromara.common.core.exception.user.CaptchaException;
 import org.dromara.common.core.exception.user.CaptchaExpireException;
 import org.dromara.common.core.exception.user.UserException;
@@ -113,7 +113,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
         if (ObjectUtil.isNull(user)) {
             log.info("登录用户:{} 不存在.", username);
             throw new UserException("user.not.exists", username);
-        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+        } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
             log.info("登录用户:{} 已被停用.", username);
             throw new UserException("user.blocked", username);
         }

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

@@ -8,10 +8,10 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.domain.model.SmsLoginBody;
 import org.dromara.common.core.enums.LoginType;
-import org.dromara.common.core.enums.UserStatus;
 import org.dromara.common.core.exception.user.CaptchaExpireException;
 import org.dromara.common.core.exception.user.UserException;
 import org.dromara.common.core.utils.MessageUtils;
@@ -92,7 +92,7 @@ public class SmsAuthStrategy implements IAuthStrategy {
         if (ObjectUtil.isNull(user)) {
             log.info("登录用户:{} 不存在.", phonenumber);
             throw new UserException("user.not.exists", phonenumber);
-        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+        } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
             log.info("登录用户:{} 已被停用.", phonenumber);
             throw new UserException("user.blocked", phonenumber);
         }

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

@@ -11,9 +11,9 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.model.AuthResponse;
 import me.zhyd.oauth.model.AuthUser;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.domain.model.SocialLoginBody;
-import org.dromara.common.core.enums.UserStatus;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.exception.user.UserException;
 import org.dromara.common.core.utils.StreamUtils;
@@ -121,7 +121,7 @@ public class SocialAuthStrategy implements IAuthStrategy {
         if (ObjectUtil.isNull(user)) {
             log.info("登录用户:{} 不存在.", "");
             throw new UserException("user.not.exists", "");
-        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+        } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
             log.info("登录用户:{} 已被停用.", "");
             throw new UserException("user.blocked", "");
         }

+ 25 - 6
ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java

@@ -5,13 +5,20 @@ import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.config.AuthConfig;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthToken;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.request.AuthWechatMiniProgramRequest;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.domain.model.XcxLoginBody;
 import org.dromara.common.core.domain.model.XcxLoginUser;
-import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.ValidatorUtils;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
-import org.dromara.system.domain.SysClient;
 import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysUserVo;
 import org.dromara.web.domain.vo.LoginVo;
@@ -40,12 +47,24 @@ public class XcxAuthStrategy implements IAuthStrategy {
         // 多个小程序识别使用
         String appid = loginBody.getAppid();
 
-        // todo 以下自行实现
         // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
-        String openid = "";
+        AuthRequest authRequest = new AuthWechatMiniProgramRequest(AuthConfig.builder()
+            .clientId(appid).clientSecret("自行填写密钥 可根据不同appid填入不同密钥")
+            .ignoreCheckRedirectUri(true).ignoreCheckState(true).build());
+        AuthCallback authCallback = new AuthCallback();
+        authCallback.setCode(xcxCode);
+        AuthResponse<AuthUser> resp = authRequest.login(authCallback);
+        String openid, unionId;
+        if (resp.ok()) {
+            AuthToken token = resp.getData().getToken();
+            openid = token.getOpenId();
+            // 微信小程序只有关联到微信开放平台下之后才能获取到 unionId,因此unionId不一定能返回。
+            unionId = token.getUnionId();
+        } else {
+            throw new ServiceException(resp.getMsg());
+        }
         // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
         SysUserVo user = loadUserByOpenid(openid);
-
         // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
         XcxLoginUser loginUser = new XcxLoginUser();
         loginUser.setTenantId(user.getTenantId());
@@ -82,7 +101,7 @@ public class XcxAuthStrategy implements IAuthStrategy {
         if (ObjectUtil.isNull(user)) {
             log.info("登录用户:{} 不存在.", openid);
             // todo 用户不存在 业务逻辑自行实现
-        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+        } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
             log.info("登录用户:{} 已被停用.", openid);
             // todo 用户已被停用 业务逻辑自行实现
         }

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

@@ -2,7 +2,7 @@
 spring.boot.admin.client:
   # 增加客户端开关
   enabled: true
-  url: http://localhost:9090/admin
+  url: http://localhost:9090
   instance:
     service-host-type: IP
     metadata:
@@ -16,17 +16,19 @@ snail-job:
   enabled: true
   # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
   group: "ruoyi_group"
-  # SnailJob 接入验证令牌 详见 script/sql/snail_job.sql `sj_group_config` 表
+  # SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表
   token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
   server:
     host: 127.0.0.1
     port: 17888
-  # 详见 script/sql/snail_job.sql `sj_namespace` 表
+  # 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
   namespace: ${spring.profiles.active}
-  # 随主应用端口飘逸
+  # 随主应用端口漂移
   port: 2${server.port}
   # 客户端ip指定
   host:
+  # RPC类型: netty, grpc
+  rpc-type: grpc
 
 --- # 数据源配置
 spring:
@@ -50,14 +52,14 @@ spring:
           url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username: root
           password: root
-        # 从库数据源
-        slave:
-          lazy: true
-          type: ${spring.datasource.type}
-          driverClassName: com.mysql.cj.jdbc.Driver
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
-          username:
-          password:
+#        # 从库数据源
+#        slave:
+#          lazy: true
+#          type: ${spring.datasource.type}
+#          driverClassName: com.mysql.cj.jdbc.Driver
+#          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+#          username:
+#          password:
 #        oracle:
 #          type: ${spring.datasource.type}
 #          driverClassName: oracle.jdbc.OracleDriver
@@ -200,7 +202,7 @@ justauth:
       redirect-uri: ${justauth.address}/social-callback?source=maxkey
     topiam:
       # topiam 服务器地址
-      server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
+      server-url: http://127.0.0.1:1898/api/v1/authorize/y0q************spq***********8ol
       client-id: 449c4*********937************759
       client-secret: ac7***********1e0************28d
       redirect-uri: ${justauth.address}/social-callback?source=topiam

+ 14 - 12
ruoyi-admin/src/main/resources/application-prod.yml

@@ -5,7 +5,7 @@ spring.servlet.multipart.location: /ruoyi/server/temp
 spring.boot.admin.client:
   # 增加客户端开关
   enabled: true
-  url: http://localhost:9090/admin
+  url: http://localhost:9090
   instance:
     service-host-type: IP
     metadata:
@@ -19,17 +19,19 @@ snail-job:
   enabled: true
   # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
   group: "ruoyi_group"
-  # SnailJob 接入验证令牌 详见 script/sql/snail_job.sql `sj_group_config` 
+  # SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config`
   token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
   server:
     host: 127.0.0.1
     port: 17888
-  # 详见 script/sql/snail_job.sql `sj_namespace` 表
+  # 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
   namespace: ${spring.profiles.active}
-  # 随主应用端口飘逸
+  # 随主应用端口漂移
   port: 2${server.port}
   # 客户端ip指定
   host:
+  # RPC类型: netty, grpc
+  rpc-type: grpc
 
 --- # 数据源配置
 spring:
@@ -53,14 +55,14 @@ spring:
           url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username: root
           password: root
-        # 从库数据源
-        slave:
-          lazy: true
-          type: ${spring.datasource.type}
-          driverClassName: com.mysql.cj.jdbc.Driver
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
-          username:
-          password:
+#        # 从库数据源
+#        slave:
+#          lazy: true
+#          type: ${spring.datasource.type}
+#          driverClassName: com.mysql.cj.jdbc.Driver
+#          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+#          username:
+#          password:
 #        oracle:
 #          type: ${spring.datasource.type}
 #          driverClassName: oracle.jdbc.OracleDriver

+ 14 - 25
ruoyi-admin/src/main/resources/application.yml

@@ -47,6 +47,7 @@ logging:
     org.dromara: @logging.level@
     org.springframework: warn
     org.mybatis.spring.mapper: error
+    org.apache.fury: warn
   config: classpath:logback-plus.xml
 
 # 用户配置
@@ -110,17 +111,15 @@ sa-token:
 security:
   # 排除路径
   excludes:
-    # 静态资源
     - /*.html
     - /**/*.html
     - /**/*.css
     - /**/*.js
-    # 公共路径
     - /favicon.ico
     - /error
-    # swagger 文档配置
     - /*/api-docs
     - /*/api-docs/**
+    - /warm-flow-ui/token-name
 
 # 多租户配置
 tenant:
@@ -141,6 +140,8 @@ tenant:
 # MyBatisPlus配置
 # https://baomidou.com/config/
 mybatis-plus:
+  # 自定义配置 是否全局开启逻辑删除 关闭后 所有逻辑删除功能将失效
+  enableLogicDelete: true
   # 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
   mapperPackage: org.dromara.**.mapper
   # 对应的 XML 文件位置
@@ -217,6 +218,8 @@ springdoc:
       packages-to-scan: org.dromara.system
     - group: 4.代码生成模块
       packages-to-scan: org.dromara.generator
+    - group: 5.工作流模块
+      packages-to-scan: org.dromara.workflow
 
 # 防止XSS攻击
 xss:
@@ -225,8 +228,7 @@ xss:
   # 排除链接(多个用逗号分隔)
   excludeUrls:
     - /system/notice
-    - /workflow/model/save
-    - /workflow/model/editModelXml
+    - /warm-flow/save-xml
 
 # 全局线程池相关配置
 # 如使用JDK21请直接使用虚拟线程 不要开启此配置
@@ -271,24 +273,11 @@ websocket:
   # 设置访问源地址
   allowedOrigins: '*'
 
---- #flowable配置
-flowable:
-  # 开关 用于动/停用工作流
+--- # warm-flow工作流配置
+warm-flow:
+  # 是否开启工作流,默认true
   enabled: true
-  process.enabled: ${flowable.enabled}
-  eventregistry.enabled: ${flowable.enabled}
-  async-executor-activate: false #关闭定时任务JOB
-  #  将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
-  database-schema-update: true
-  activity-font-name: 宋体
-  label-font-name: 宋体
-  annotation-font-name: 宋体
-  # 关闭各个模块生成表,目前只使用工作流基础表
-  idm:
-    enabled: false
-  cmmn:
-    enabled: false
-  dmn:
-    enabled: false
-  app:
-    enabled: false
+  # 是否开启设计器ui
+  ui: true
+  # 默认Authorization,如果有多个token,用逗号分隔
+  token-name: ${sa-token.token-name},clientid

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

@@ -2,7 +2,7 @@
 <configuration>
     <property name="log.path" value="./logs"/>
     <property name="console.log.pattern"
-              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
+              value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
     <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
 
     <!-- 控制台输出 -->

+ 1 - 0
ruoyi-admin/zhFonts/.uuid

@@ -0,0 +1 @@
+3f2ee348-0303-40ca-bf03-03f48d2d2141

BIN
ruoyi-admin/zhFonts/SIMSUN.TTC


+ 4 - 0
ruoyi-admin/zhFonts/fonts.dir

@@ -0,0 +1,4 @@
+3
+SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
+SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
+SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r

+ 4 - 0
ruoyi-admin/zhFonts/fonts.scale

@@ -0,0 +1,4 @@
+3
+SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
+SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
+SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r

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

@@ -14,7 +14,7 @@
     </description>
 
     <properties>
-        <revision>5.2.3</revision>
+        <revision>5.3.0-BETA</revision>
     </properties>
 
     <dependencyManagement>

+ 10 - 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java

@@ -4,11 +4,13 @@ import jakarta.annotation.PreDestroy;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.concurrent.BasicThreadFactory;
 import org.dromara.common.core.config.properties.ThreadPoolProperties;
+import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.Threads;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
+import org.springframework.core.task.VirtualThreadTaskExecutor;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
 import java.util.concurrent.ScheduledExecutorService;
@@ -49,8 +51,15 @@ public class ThreadPoolConfig {
      */
     @Bean(name = "scheduledExecutorService")
     protected ScheduledExecutorService scheduledExecutorService() {
+        // daemon 必须为 true
+        BasicThreadFactory.Builder builder = new BasicThreadFactory.Builder().daemon(true);
+        if (SpringUtils.isVirtual()) {
+            builder.namingPattern("virtual-schedule-pool-%d").wrappedFactory(new VirtualThreadTaskExecutor().getVirtualThreadFactory());
+        } else {
+            builder.namingPattern("schedule-pool-%d");
+        }
         ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(core,
-            new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
+            builder.build(),
             new ThreadPoolExecutor.CallerRunsPolicy()) {
             @Override
             protected void afterExecute(Runnable r, Throwable t) {

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

@@ -60,6 +60,16 @@ public interface CacheNames {
      */
     String SYS_OSS = "sys_oss#30d";
 
+    /**
+     * 角色自定义权限
+     */
+    String SYS_ROLE_CUSTOM = "sys_role_custom#30d";
+
+    /**
+     * 部门及以下权限
+     */
+    String SYS_DEPT_AND_CHILD = "sys_dept_and_child#30d";
+
     /**
      * OSS配置
      */

+ 1 - 6
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java

@@ -68,12 +68,7 @@ public interface Constants {
     Integer CAPTCHA_EXPIRATION = 2;
 
     /**
-     * 令牌
-     */
-    String TOKEN = "token";
-
-    /**
-     * 顶级部门id
+     * 顶级父级id
      */
     Long TOP_PARENT_ID = 0L;
 

+ 75 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/SystemConstants.java

@@ -0,0 +1,75 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 系统常量信息
+ *
+ * @author Lion Li
+ */
+public interface SystemConstants {
+
+    /**
+     * 正常状态
+     */
+    String NORMAL = "0";
+
+    /**
+     * 异常状态
+     */
+    String DISABLE = "1";
+
+    /**
+     * 是否为系统默认(是)
+     */
+    String YES = "Y";
+
+    /**
+     * 是否为系统默认(否)
+     */
+    String NO = "N";
+
+    /**
+     * 是否菜单外链(是)
+     */
+    String YES_FRAME = "0";
+
+    /**
+     * 是否菜单外链(否)
+     */
+    String NO_FRAME = "1";
+
+    /**
+     * 菜单类型(目录)
+     */
+    String TYPE_DIR = "M";
+
+    /**
+     * 菜单类型(菜单)
+     */
+    String TYPE_MENU = "C";
+
+    /**
+     * 菜单类型(按钮)
+     */
+    String TYPE_BUTTON = "F";
+
+    /**
+     * Layout组件标识
+     */
+    String LAYOUT = "Layout";
+
+    /**
+     * ParentView组件标识
+     */
+    String PARENT_VIEW = "ParentView";
+
+    /**
+     * InnerLink组件标识
+     */
+    String INNER_LINK = "InnerLink";
+
+    /**
+     * 超级管理员ID
+     */
+    Long SUPER_ADMIN_ID = 1L;
+
+}

+ 0 - 10
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java

@@ -7,16 +7,6 @@ package org.dromara.common.core.constant;
  */
 public interface TenantConstants {
 
-    /**
-     * 租户正常状态
-     */
-    String NORMAL = "0";
-
-    /**
-     * 租户封禁状态
-     */
-    String DISABLE = "1";
-
     /**
      * 超级管理员ID
      */

+ 0 - 152
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java

@@ -1,152 +0,0 @@
-package org.dromara.common.core.constant;
-
-/**
- * 用户常量信息
- *
- * @author ruoyi
- */
-public interface UserConstants {
-
-    /**
-     * 平台内系统用户的唯一标志
-     */
-    String SYS_USER = "SYS_USER";
-
-    /**
-     * 正常状态
-     */
-    String NORMAL = "0";
-
-    /**
-     * 异常状态
-     */
-    String EXCEPTION = "1";
-
-    /**
-     * 用户正常状态
-     */
-    String USER_NORMAL = "0";
-
-    /**
-     * 用户封禁状态
-     */
-    String USER_DISABLE = "1";
-
-    /**
-     * 角色正常状态
-     */
-    String ROLE_NORMAL = "0";
-
-    /**
-     * 角色封禁状态
-     */
-    String ROLE_DISABLE = "1";
-
-    /**
-     * 部门正常状态
-     */
-    String DEPT_NORMAL = "0";
-
-    /**
-     * 部门停用状态
-     */
-    String DEPT_DISABLE = "1";
-
-    /**
-     * 岗位正常状态
-     */
-    String POST_NORMAL = "0";
-
-    /**
-     * 岗位停用状态
-     */
-    String POST_DISABLE = "1";
-
-    /**
-     * 字典正常状态
-     */
-    String DICT_NORMAL = "0";
-
-    /**
-     * 通用存在标志
-     */
-    String DEL_FLAG_NORMAL = "0";
-
-    /**
-     * 通用删除标志
-     */
-    String DEL_FLAG_REMOVED  = "2";
-
-    /**
-     * 是否为系统默认(是)
-     */
-    String YES = "Y";
-
-    /**
-     * 是否菜单外链(是)
-     */
-    String YES_FRAME = "0";
-
-    /**
-     * 是否菜单外链(否)
-     */
-    String NO_FRAME = "1";
-
-    /**
-     * 菜单正常状态
-     */
-    String MENU_NORMAL = "0";
-
-    /**
-     * 菜单停用状态
-     */
-    String MENU_DISABLE = "1";
-
-    /**
-     * 菜单类型(目录)
-     */
-    String TYPE_DIR = "M";
-
-    /**
-     * 菜单类型(菜单)
-     */
-    String TYPE_MENU = "C";
-
-    /**
-     * 菜单类型(按钮)
-     */
-    String TYPE_BUTTON = "F";
-
-    /**
-     * Layout组件标识
-     */
-    String LAYOUT = "Layout";
-
-    /**
-     * ParentView组件标识
-     */
-    String PARENT_VIEW = "ParentView";
-
-    /**
-     * InnerLink组件标识
-     */
-    String INNER_LINK = "InnerLink";
-
-    /**
-     * 用户名长度限制
-     */
-    int USERNAME_MIN_LENGTH = 2;
-    int USERNAME_MAX_LENGTH = 20;
-
-    /**
-     * 密码长度限制
-     */
-    int PASSWORD_MIN_LENGTH = 5;
-    int PASSWORD_MAX_LENGTH = 20;
-
-    /**
-     * 超级管理员ID
-     */
-    Long SUPER_ADMIN_ID = 1L;
-
-}

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

@@ -0,0 +1,71 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 办理任务请求对象
+ *
+ * @author may
+ */
+@Data
+public class CompleteTaskDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务id
+     */
+    private Long taskId;
+
+    /**
+     * 附件id
+     */
+    private String fileId;
+
+    /**
+     * 抄送人员
+     */
+    private List<FlowCopyDTO> flowCopyList;
+
+    /**
+     * 消息类型
+     */
+    private List<String> messageType;
+
+    /**
+     * 办理意见
+     */
+    private String message;
+
+    /**
+     * 消息通知
+     */
+    private String notice;
+
+    /**
+     * 流程变量
+     */
+    private Map<String, Object> variables;
+
+    /**
+     * 扩展变量(此处为逗号分隔的ossId)
+     */
+    private String ext;
+
+    public Map<String, Object> getVariables() {
+        if (variables == null) {
+            return new HashMap<>(16);
+        }
+        variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
+        return variables;
+    }
+
+}

+ 37 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/DeptDTO.java

@@ -0,0 +1,37 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 部门
+ *
+ * @author AprilWind
+ */
+
+@Data
+@NoArgsConstructor
+public class DeptDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 父部门ID
+     */
+    private Long parentId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+}

+ 30 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java

@@ -0,0 +1,30 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 抄送
+ *
+ * @author may
+ */
+@Data
+public class FlowCopyDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户id
+     */
+    private Long userId;
+
+    /**
+     * 用户名称
+     */
+    private String userName;
+
+}

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

@@ -0,0 +1,46 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 岗位
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class PostDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 岗位ID
+     */
+    private Long postId;
+
+    /**
+     * 部门id
+     */
+    private Long deptId;
+
+    /**
+     * 岗位编码
+     */
+    private String postCode;
+
+    /**
+     * 岗位名称
+     */
+    private String postName;
+
+    /**
+     * 岗位类别编码
+     */
+    private String postCategory;
+
+}

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

@@ -0,0 +1,45 @@
+package org.dromara.common.core.domain.dto;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 启动流程对象
+ *
+ * @author may
+ */
+@Data
+public class StartProcessDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 业务唯一值id
+     */
+    private String businessId;
+
+    /**
+     * 流程定义编码
+     */
+    private String flowCode;
+
+    /**
+     * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
+     */
+    private Map<String, Object> variables;
+
+    public Map<String, Object> getVariables() {
+        if (variables == null) {
+            return new HashMap<>(16);
+        }
+        variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
+        return variables;
+    }
+}

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

@@ -0,0 +1,101 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 任务受让人
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class TaskAssigneeDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 总大小
+     */
+    private Long total = 0L;
+
+    /**
+     *
+     */
+    private List<TaskHandler> list;
+
+    public TaskAssigneeDTO(Long total, List<TaskHandler> list) {
+        this.total = total;
+        this.list = list;
+    }
+
+    /**
+     * 将源列表转换为 TaskHandler 列表
+     *
+     * @param <T>              通用类型
+     * @param sourceList       待转换的源列表
+     * @param storageId        提取 storageId 的函数
+     * @param handlerCode      提取 handlerCode 的函数
+     * @param handlerName      提取 handlerName 的函数
+     * @param groupName        提取 groupName 的函数
+     * @param createTimeMapper 提取 createTime 的函数
+     * @return 转换后的 TaskHandler 列表
+     */
+    public static <T> List<TaskHandler> convertToHandlerList(
+        List<T> sourceList,
+        Function<T, Long> storageId,
+        Function<T, String> handlerCode,
+        Function<T, String> handlerName,
+        Function<T, Long> groupName,
+        Function<T, Date> createTimeMapper) {
+        return sourceList.stream()
+            .map(item -> new TaskHandler(
+                String.valueOf(storageId.apply(item)),
+                handlerCode.apply(item),
+                handlerName.apply(item),
+                groupName != null ? String.valueOf(groupName.apply(item)) : null,
+                createTimeMapper.apply(item)
+            )).collect(Collectors.toList());
+    }
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class TaskHandler {
+
+        /**
+         * 主键
+         */
+        private String storageId;
+
+        /**
+         * 权限编码
+         */
+        private String handlerCode;
+
+        /**
+         * 权限名称
+         */
+        private String handlerName;
+
+        /**
+         * 权限分组
+         */
+        private String groupName;
+
+        /**
+         * 创建时间
+         */
+        private Date createTime;
+    }
+
+}

+ 34 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessDeleteEvent.java

@@ -0,0 +1,34 @@
+package org.dromara.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 删除流程监听
+ *
+ * @author AprilWind
+ */
+@Data
+public class ProcessDeleteEvent implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 租户ID
+     */
+    private String tenantId;
+
+    /**
+     * 流程定义编码
+     */
+    private String flowCode;
+
+    /**
+     * 业务id
+     */
+    private String businessId;
+
+}

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

@@ -10,7 +10,6 @@ import java.io.Serializable;
  *
  * @author may
  */
-
 @Data
 public class ProcessEvent implements Serializable {
 
@@ -18,14 +17,19 @@ public class ProcessEvent implements Serializable {
     private static final long serialVersionUID = 1L;
 
     /**
-     * 流程定义key
+     * 租户ID
      */
-    private String key;
+    private String tenantId;
+
+    /**
+     * 流程定义编码
+     */
+    private String flowCode;
 
     /**
      * 业务id
      */
-    private String businessKey;
+    private String businessId;
 
     /**
      * 状态
@@ -37,5 +41,4 @@ public class ProcessEvent implements Serializable {
      */
     private boolean submit;
 
-
 }

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

@@ -10,7 +10,6 @@ import java.io.Serializable;
  *
  * @author may
  */
-
 @Data
 public class ProcessTaskEvent implements Serializable {
 
@@ -18,23 +17,28 @@ public class ProcessTaskEvent implements Serializable {
     private static final long serialVersionUID = 1L;
 
     /**
-     * 流程定义key
+     * 租户ID
+     */
+    private String tenantId;
+
+    /**
+     * 流程定义编码
      */
-    private String key;
+    private String flowCode;
 
     /**
-     * 审批节点key
+     * 审批节点编码
      */
-    private String taskDefinitionKey;
+    private String nodeCode;
 
     /**
      * 任务id
      */
-    private String taskId;
+    private Long taskId;
 
     /**
      * 业务id
      */
-    private String businessKey;
+    private String businessId;
 
 }

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

@@ -1,8 +1,9 @@
 package org.dromara.common.core.domain.model;
 
-import org.dromara.common.core.domain.dto.RoleDTO;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.dromara.common.core.domain.dto.PostDTO;
+import org.dromara.common.core.domain.dto.RoleDTO;
 
 import java.io.Serial;
 import java.io.Serializable;
@@ -111,6 +112,11 @@ public class LoginUser implements Serializable {
      */
     private List<RoleDTO> roles;
 
+    /**
+     * 岗位对象
+     */
+    private List<PostDTO> posts;
+
     /**
      * 数据权限 当前角色ID
      */

+ 2 - 4
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java

@@ -5,8 +5,6 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import org.hibernate.validator.constraints.Length;
 
-import static org.dromara.common.core.constant.UserConstants.*;
-
 /**
  * 密码登录对象
  *
@@ -20,14 +18,14 @@ public class PasswordLoginBody extends LoginBody {
      * 用户名
      */
     @NotBlank(message = "{user.username.not.blank}")
-    @Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+    @Length(min = 2, max = 20, message = "{user.username.length.valid}")
     private String username;
 
     /**
      * 用户密码
      */
     @NotBlank(message = "{user.password.not.blank}")
-    @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+    @Length(min = 5, max = 20, message = "{user.password.length.valid}")
     private String password;
 
 }

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

@@ -5,8 +5,6 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import org.hibernate.validator.constraints.Length;
 
-import static org.dromara.common.core.constant.UserConstants.*;
-
 /**
  * 用户注册对象
  *
@@ -20,14 +18,14 @@ public class RegisterBody extends LoginBody {
      * 用户名
      */
     @NotBlank(message = "{user.username.not.blank}")
-    @Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+    @Length(min = 2, max = 20, message = "{user.username.length.valid}")
     private String username;
 
     /**
      * 用户密码
      */
     @NotBlank(message = "{user.password.not.blank}")
-    @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+    @Length(min = 5, max = 20, message = "{user.password.length.valid}")
     private String password;
 
     private String userType;

+ 56 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/TaskAssigneeBody.java

@@ -0,0 +1,56 @@
+package org.dromara.common.core.domain.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 任务受让人
+ *
+ * @author AprilWind
+ */
+@Data
+@NoArgsConstructor
+public class TaskAssigneeBody implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 权限编码
+     */
+    private String handlerCode;
+
+    /**
+     * 权限名称
+     */
+    private String handlerName;
+
+    /**
+     * 权限分组
+     */
+    private String groupId;
+
+    /**
+     * 开始时间
+     */
+    private String beginTime;
+
+    /**
+     * 结束时间
+     */
+    private String endTime;
+
+    /**
+     * 当前页
+     */
+    private Integer pageNum = 1;
+
+    /**
+     * 每页显示条数
+     */
+    private Integer pageSize = 10;
+
+}

+ 71 - 8
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/BusinessStatusEnum.java

@@ -7,6 +7,10 @@ import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.StringUtils;
 
 import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * 业务状态枚举
@@ -16,30 +20,37 @@ import java.util.Arrays;
 @Getter
 @AllArgsConstructor
 public enum BusinessStatusEnum {
+
     /**
      * 已撤销
      */
     CANCEL("cancel", "已撤销"),
+
     /**
      * 草稿
      */
     DRAFT("draft", "草稿"),
+
     /**
      * 待审核
      */
     WAITING("waiting", "待审核"),
+
     /**
      * 已完成
      */
     FINISH("finish", "已完成"),
+
     /**
      * 已作废
      */
     INVALID("invalid", "已作废"),
+
     /**
      * 已退回
      */
     BACK("back", "已退回"),
+
     /**
      * 已终止
      */
@@ -55,20 +66,72 @@ public enum BusinessStatusEnum {
      */
     private final String desc;
 
+    private static final Map<String, BusinessStatusEnum> STATUS_MAP = Arrays.stream(BusinessStatusEnum.values())
+        .collect(Collectors.toConcurrentMap(BusinessStatusEnum::getStatus, Function.identity()));
+
     /**
-     * 获取业务状态
+     * 根据状态获取对应的 BusinessStatusEnum 枚举
      *
-     * @param status 状态
+     * @param status 业务状态码
+     * @return 对应的 BusinessStatusEnum 枚举,如果找不到则返回 null
+     */
+    public static BusinessStatusEnum getByStatus(String status) {
+        // 使用 STATUS_MAP 获取对应的枚举,若找不到则返回 null
+        return STATUS_MAP.get(status);
+    }
+
+    /**
+     * 根据状态获取对应的业务状态描述信息
+     *
+     * @param status 业务状态码
+     * @return 返回业务状态描述,若状态码为空或未找到对应的枚举,返回空字符串
      */
     public static String findByStatus(String status) {
         if (StringUtils.isBlank(status)) {
             return StrUtil.EMPTY;
         }
-        return Arrays.stream(BusinessStatusEnum.values())
-            .filter(statusEnum -> statusEnum.getStatus().equals(status))
-            .findFirst()
-            .map(BusinessStatusEnum::getDesc)
-            .orElse(StrUtil.EMPTY);
+        BusinessStatusEnum statusEnum = STATUS_MAP.get(status);
+        return (statusEnum != null) ? statusEnum.getDesc() : StrUtil.EMPTY;
+    }
+
+    /**
+     * 判断是否为指定的状态之一:草稿、已撤销或已退回
+     *
+     * @param status 要检查的状态
+     * @return 如果状态为草稿、已撤销或已退回之一,则返回 true;否则返回 false
+     */
+    public static boolean isDraftOrCancelOrBack(String status) {
+        return DRAFT.status.equals(status) || CANCEL.status.equals(status) || BACK.status.equals(status);
+    }
+
+    /**
+     * 判断是否为撤销,退回,作废,终止
+     *
+     * @param status status
+     * @return 结果
+     */
+    public static boolean initialState(String status) {
+        return CANCEL.status.equals(status) || BACK.status.equals(status) || INVALID.status.equals(status) || TERMINATION.status.equals(status);
+    }
+
+    /**
+     * 获取运行中的实例状态列表
+     *
+     * @return 包含运行中实例状态的不可变列表
+     * (包含 DRAFT、WAITING、BACK 和 CANCEL 状态)
+     */
+    public static List<String> runningStatus() {
+        return Arrays.asList(DRAFT.status, WAITING.status, BACK.status, CANCEL.status);
+    }
+
+    /**
+     * 获取结束实例的状态列表
+     *
+     * @return 包含结束实例状态的不可变列表
+     * (包含 FINISH、INVALID 和 TERMINATION 状态)
+     */
+    public static List<String> finishStatus() {
+        return Arrays.asList(FINISH.status, INVALID.status, TERMINATION.status);
     }
 
     /**
@@ -148,5 +211,5 @@ public enum BusinessStatusEnum {
             throw new ServiceException("流程状态为空!");
         }
     }
-}
 
+}

+ 146 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/FormatsType.java

@@ -0,0 +1,146 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+/*
+ * 日期格式
+ * "yyyy":4位数的年份,例如:2023年表示为"2023"。
+ * "yy":2位数的年份,例如:2023年表示为"23"。
+ * "MM":2位数的月份,取值范围为01到12,例如:7月表示为"07"。
+ * "M":不带前导零的月份,取值范围为1到12,例如:7月表示为"7"。
+ * "dd":2位数的日期,取值范围为01到31,例如:22日表示为"22"。
+ * "d":不带前导零的日期,取值范围为1到31,例如:22日表示为"22"。
+ * "EEEE":星期的全名,例如:星期三表示为"Wednesday"。
+ * "E":星期的缩写,例如:星期三表示为"Wed"。
+ * "DDD" 或 "D":一年中的第几天,取值范围为001到366,例如:第200天表示为"200"。
+ * 时间格式
+ * "HH":24小时制的小时数,取值范围为00到23,例如:下午5点表示为"17"。
+ * "hh":12小时制的小时数,取值范围为01到12,例如:下午5点表示为"05"。
+ * "mm":分钟数,取值范围为00到59,例如:30分钟表示为"30"。
+ * "ss":秒数,取值范围为00到59,例如:45秒表示为"45"。
+ * "SSS":毫秒数,取值范围为000到999,例如:123毫秒表示为"123"。
+ */
+
+/**
+ * 日期格式与时间格式枚举
+ */
+@Getter
+@AllArgsConstructor
+public enum FormatsType {
+
+    /**
+     * 例如:2023年表示为"23"
+     */
+    YY("yy"),
+
+    /**
+     * 例如:2023年表示为"2023"
+     */
+    YYYY("yyyy"),
+
+    /**
+     * 例例如,2023年7月可以表示为 "2023-07"
+     */
+    YYYY_MM("yyyy-MM"),
+
+    /**
+     * 例如,日期 "2023年7月22日" 可以表示为 "2023-07-22"
+     */
+    YYYY_MM_DD("yyyy-MM-dd"),
+
+    /**
+     * 例如,当前时间如果是 "2023年7月22日下午3点30分",则可以表示为 "2023-07-22 15:30"
+     */
+    YYYY_MM_DD_HH_MM("yyyy-MM-dd HH:mm"),
+
+    /**
+     * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023-07-22 15:30:45"
+     */
+    YYYY_MM_DD_HH_MM_SS("yyyy-MM-dd HH:mm:ss"),
+
+    /**
+     * 例如:下午3点30分45秒,表示为 "15:30:45"
+     */
+    HH_MM_SS("HH:mm:ss"),
+
+    /**
+     * 例例如,2023年7月可以表示为 "2023/07"
+     */
+    YYYY_MM_SLASH("yyyy/MM"),
+
+    /**
+     * 例如,日期 "2023年7月22日" 可以表示为 "2023/07/22"
+     */
+    YYYY_MM_DD_SLASH("yyyy/MM/dd"),
+
+    /**
+     * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023/07/22 15:30:45"
+     */
+    YYYY_MM_DD_HH_MM_SLASH("yyyy/MM/dd HH:mm"),
+
+    /**
+     * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023/07/22 15:30:45"
+     */
+    YYYY_MM_DD_HH_MM_SS_SLASH("yyyy/MM/dd HH:mm:ss"),
+
+    /**
+     * 例例如,2023年7月可以表示为 "2023.07"
+     */
+    YYYY_MM_DOT("yyyy.MM"),
+
+    /**
+     * 例如,日期 "2023年7月22日" 可以表示为 "2023.07.22"
+     */
+    YYYY_MM_DD_DOT("yyyy.MM.dd"),
+
+    /**
+     * 例如,当前时间如果是 "2023年7月22日下午3点30分",则可以表示为 "2023.07.22 15:30"
+     */
+    YYYY_MM_DD_HH_MM_DOT("yyyy.MM.dd HH:mm"),
+
+    /**
+     * 例如,当前时间如果是 "2023年7月22日下午3点30分45秒",则可以表示为 "2023.07.22 15:30:45"
+     */
+    YYYY_MM_DD_HH_MM_SS_DOT("yyyy.MM.dd HH:mm:ss"),
+
+    /**
+     * 例如,2023年7月可以表示为 "202307"
+     */
+    YYYYMM("yyyyMM"),
+
+    /**
+     * 例如,2023年7月22日可以表示为 "20230722"
+     */
+    YYYYMMDD("yyyyMMdd"),
+
+    /**
+     * 例如,2023年7月22日下午3点可以表示为 "2023072215"
+     */
+    YYYYMMDDHH("yyyyMMddHH"),
+
+    /**
+     * 例如,2023年7月22日下午3点30分可以表示为 "202307221530"
+     */
+    YYYYMMDDHHMM("yyyyMMddHHmm"),
+
+    /**
+     * 例如,2023年7月22日下午3点30分45秒可以表示为 "20230722153045"
+     */
+    YYYYMMDDHHMMSS("yyyyMMddHHmmss");
+
+    /**
+     * 时间格式
+     */
+    private final String timeFormat;
+
+    public static FormatsType getFormatsType(String str) {
+        for (FormatsType value : values()) {
+            if (StringUtils.contains(str, value.getTimeFormat())) {
+                return value;
+            }
+        }
+        throw new RuntimeException("'FormatsType' not found By " + str);
+    }
+}

+ 0 - 30
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java

@@ -1,30 +0,0 @@
-package org.dromara.common.core.enums;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-/**
- * 用户状态
- *
- * @author LionLi
- */
-@Getter
-@AllArgsConstructor
-public enum TenantStatus {
-    /**
-     * 正常
-     */
-    OK("0", "正常"),
-    /**
-     * 停用
-     */
-    DISABLE("1", "停用"),
-    /**
-     * 删除
-     */
-    DELETED("2", "删除");
-
-    private final String code;
-    private final String info;
-
-}

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

@@ -1,5 +1,9 @@
 package org.dromara.common.core.service;
 
+import org.dromara.common.core.domain.dto.DeptDTO;
+
+import java.util.List;
+
 /**
  * 通用 部门服务
  *
@@ -15,4 +19,19 @@ public interface DeptService {
      */
     String selectDeptNameByIds(String deptIds);
 
+    /**
+     * 根据部门ID查询部门负责人
+     *
+     * @param deptId 部门ID,用于指定需要查询的部门
+     * @return 返回该部门的负责人ID
+     */
+    Long selectDeptLeaderById(Long deptId);
+
+    /**
+     * 查询部门
+     *
+     * @return 部门列表
+     */
+    List<DeptDTO> selectDeptsByList();
+
 }

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

@@ -0,0 +1,10 @@
+package org.dromara.common.core.service;
+
+/**
+ * 通用 岗位服务
+ *
+ * @author AprilWind
+ */
+public interface PostService {
+
+}

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

@@ -0,0 +1,10 @@
+package org.dromara.common.core.service;
+
+/**
+ * 通用 角色服务
+ *
+ * @author AprilWind
+ */
+public interface RoleService {
+
+}

+ 45 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/TaskAssigneeService.java

@@ -0,0 +1,45 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.TaskAssigneeDTO;
+import org.dromara.common.core.domain.model.TaskAssigneeBody;
+
+/**
+ * 工作流设计器获取任务执行人
+ *
+ * @author Lion Li
+ */
+public interface TaskAssigneeService {
+
+    /**
+     * 查询角色并返回任务指派的列表,支持分页
+     *
+     * @param taskQuery 查询条件
+     * @return 办理人
+     */
+    TaskAssigneeDTO selectRolesByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+    /**
+     * 查询岗位并返回任务指派的列表,支持分页
+     *
+     * @param taskQuery 查询条件
+     * @return 办理人
+     */
+    TaskAssigneeDTO selectPostsByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+    /**
+     * 查询部门并返回任务指派的列表,支持分页
+     *
+     * @param taskQuery 查询条件
+     * @return 办理人
+     */
+    TaskAssigneeDTO selectDeptsByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+    /**
+     * 查询用户并返回任务指派的列表,支持分页
+     *
+     * @param taskQuery 查询条件
+     * @return 办理人
+     */
+    TaskAssigneeDTO selectUsersByTaskAssigneeList(TaskAssigneeBody taskQuery);
+
+}

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

@@ -82,4 +82,13 @@ public interface UserService {
      * @return 用户
      */
     List<UserDTO> selectUsersByDeptIds(List<Long> deptIds);
+
+    /**
+     * 通过岗位ID查询用户
+     *
+     * @param postIds 岗位ids
+     * @return 用户
+     */
+    List<UserDTO> selectUsersByPostIds(List<Long> postIds);
+
 }

+ 28 - 26
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java

@@ -1,5 +1,8 @@
 package org.dromara.common.core.service;
 
+import org.dromara.common.core.domain.dto.CompleteTaskDTO;
+import org.dromara.common.core.domain.dto.StartProcessDTO;
+
 import java.util.List;
 import java.util.Map;
 
@@ -13,64 +16,63 @@ public interface WorkflowService {
     /**
      * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
      *
-     * @param businessKeys 业务id
+     * @param businessIds 业务id
      * @return 结果
      */
-    boolean deleteRunAndHisInstance(List<String> businessKeys);
+    boolean deleteInstance(List<Long> businessIds);
 
     /**
      * 获取当前流程状态
      *
      * @param taskId 任务id
+     * @return 状态
      */
-    String getBusinessStatusByTaskId(String taskId);
+    String getBusinessStatusByTaskId(Long taskId);
 
     /**
      * 获取当前流程状态
      *
-     * @param businessKey 业务id
+     * @param businessId 业务id
+     * @return 状态
      */
-    String getBusinessStatus(String businessKey);
+    String getBusinessStatus(String businessId);
 
     /**
-     * 设置流程变量(全局变量)
+     * 设置流程变量
      *
-     * @param taskId       任务id
-     * @param variableName 变量名称
-     * @param value        变量值
+     * @param instanceId 流程实例id
+     * @param variable   流程变量
      */
-    void setVariable(String taskId, String variableName, Object value);
+    void setVariable(Long instanceId, Map<String, Object> variable);
 
     /**
-     * 设置流程变量(全局变量)
+     * 按照业务id查询流程实例id
      *
-     * @param taskId    任务id
-     * @param variables 流程变量
+     * @param businessId 业务id
+     * @return 结果
      */
-    void setVariables(String taskId, Map<String, Object> variables);
+    Long getInstanceIdByBusinessId(String businessId);
 
     /**
-     * 设置流程变量(本地变量,非全局变量)
+     * 新增租户流程定义
      *
-     * @param taskId       任务id
-     * @param variableName 变量名称
-     * @param value        变量值
+     * @param tenantId 租户id
      */
-    void setVariableLocal(String taskId, String variableName, Object value);
+    void syncDef(String tenantId);
 
     /**
-     * 设置流程变量(本地变量,非全局变量)
+     * 启动流程
      *
-     * @param taskId    任务id
-     * @param variables 流程变量
+     * @param startProcess 参数
+     * @return 结果
      */
-    void setVariablesLocal(String taskId, Map<String, Object> variables);
+    Map<String, Object> startWorkFlow(StartProcessDTO startProcess);
 
     /**
-     * 按照业务id查询流程实例id
+     * 办理任务
      *
-     * @param businessKey 业务id
+     * @param completeTask 参数
      * @return 结果
      */
-    String getInstanceIdByBusinessKey(String businessKey);
+    boolean completeTask(CompleteTaskDTO completeTask);
 }

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

@@ -1,106 +1,157 @@
 package org.dromara.common.core.utils;
 
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
 import org.apache.commons.lang3.time.DateFormatUtils;
+import org.dromara.common.core.enums.FormatsType;
+import org.dromara.common.core.exception.ServiceException;
 
 import java.lang.management.ManagementFactory;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
+import java.time.*;
 import java.util.Date;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 时间工具类
  *
  * @author ruoyi
  */
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
-
-    public static final String YYYY = "yyyy";
-
-    public static final String YYYY_MM = "yyyy-MM";
-
-    public static final String YYYY_MM_DD = "yyyy-MM-dd";
-
-    public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
-
-    public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
-
     private static final String[] PARSE_PATTERNS = {
         "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
         "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
         "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
 
+    @Deprecated
+    private DateUtils() {
+    }
+
     /**
-     * 获取当前Date型日期
+     * 获取当前日期和时间
      *
-     * @return Date() 当前日期
+     * @return 当前日期和时间的 Date 对象表示
      */
     public static Date getNowDate() {
         return new Date();
     }
 
     /**
-     * 获取当前日期, 默认格式为yyyy-MM-dd
+     * 获取当前日期的字符串表示,格式为YYYY-MM-DD
      *
-     * @return String
+     * @return 当前日期的字符串表示
      */
     public static String getDate() {
-        return dateTimeNow(YYYY_MM_DD);
+        return dateTimeNow(FormatsType.YYYY_MM_DD);
     }
 
+    /**
+     * 获取当前日期的字符串表示,格式为yyyyMMdd
+     *
+     * @return 当前日期的字符串表示
+     */
+    public static String getCurrentDate() {
+        return DateFormatUtils.format(new Date(), FormatsType.YYYYMMDD.getTimeFormat());
+    }
+
+    /**
+     * 获取当前日期的路径格式字符串,格式为"yyyy/MM/dd"
+     *
+     * @return 当前日期的路径格式字符串
+     */
+    public static String datePath() {
+        Date now = new Date();
+        return DateFormatUtils.format(now, FormatsType.YYYY_MM_DD_SLASH.getTimeFormat());
+    }
+
+    /**
+     * 获取当前时间的字符串表示,格式为YYYY-MM-DD HH:MM:SS
+     *
+     * @return 当前时间的字符串表示
+     */
     public static String getTime() {
-        return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+        return dateTimeNow(FormatsType.YYYY_MM_DD_HH_MM_SS);
     }
 
-    public static String dateTimeNow() {
-        return dateTimeNow(YYYYMMDDHHMMSS);
+    /**
+     * 获取当前时间的字符串表示,格式为 "HH:MM:SS"
+     *
+     * @return 当前时间的字符串表示,格式为 "HH:MM:SS"
+     */
+    public static String getTimeWithHourMinuteSecond() {
+        return dateTimeNow(FormatsType.HH_MM_SS);
     }
 
-    public static String dateTimeNow(final String format) {
-        return parseDateToStr(format, new Date());
+    /**
+     * 获取当前日期和时间的字符串表示,格式为YYYYMMDDHHMMSS
+     *
+     * @return 当前日期和时间的字符串表示
+     */
+    public static String dateTimeNow() {
+        return dateTimeNow(FormatsType.YYYYMMDDHHMMSS);
     }
 
-    public static String dateTime(final Date date) {
-        return parseDateToStr(YYYY_MM_DD, date);
+    /**
+     * 获取当前日期和时间的指定格式的字符串表示
+     *
+     * @param format 日期时间格式,例如"YYYY-MM-DD HH:MM:SS"
+     * @return 当前日期和时间的字符串表示
+     */
+    public static String dateTimeNow(final FormatsType format) {
+        return parseDateToStr(format, new Date());
     }
 
-    public static String parseDateToStr(final String format, final Date date) {
-        return new SimpleDateFormat(format).format(date);
+    /**
+     * 将指定日期格式化为 YYYY-MM-DD 格式的字符串
+     *
+     * @param date 要格式化的日期对象
+     * @return 格式化后的日期字符串
+     */
+    public static String formatDate(final Date date) {
+        return parseDateToStr(FormatsType.YYYY_MM_DD, date);
     }
 
-    public static Date dateTime(final String format, final String ts) {
-        try {
-            return new SimpleDateFormat(format).parse(ts);
-        } catch (ParseException e) {
-            throw new RuntimeException(e);
-        }
+    /**
+     * 将指定日期格式化为 YYYY-MM-DD HH:MM:SS 格式的字符串
+     *
+     * @param date 要格式化的日期对象
+     * @return 格式化后的日期时间字符串
+     */
+    public static String formatDateTime(final Date date) {
+        return parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, date);
     }
 
     /**
-     * 日期路径 即年/月/日 如2018/08/08
+     * 将指定日期按照指定格式进行格式化
+     *
+     * @param format 要使用的日期时间格式,例如"YYYY-MM-DD HH:MM:SS"
+     * @param date   要格式化的日期对象
+     * @return 格式化后的日期时间字符串
      */
-    public static String datePath() {
-        Date now = new Date();
-        return DateFormatUtils.format(now, "yyyy/MM/dd");
+    public static String parseDateToStr(final FormatsType format, final Date date) {
+        return new SimpleDateFormat(format.getTimeFormat()).format(date);
     }
 
     /**
-     * 日期路径 即年/月/日 如20180808
+     * 将指定格式的日期时间字符串转换为 Date 对象
+     *
+     * @param format 要解析的日期时间格式,例如"YYYY-MM-DD HH:MM:SS"
+     * @param ts     要解析的日期时间字符串
+     * @return 解析后的 Date 对象
+     * @throws RuntimeException 如果解析过程中发生异常
      */
-    public static String dateTime() {
-        Date now = new Date();
-        return DateFormatUtils.format(now, "yyyyMMdd");
+    public static Date parseDateTime(final FormatsType format, final String ts) {
+        try {
+            return new SimpleDateFormat(format.getTimeFormat()).parse(ts);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
-     * 日期型字符串转化为日期 格式
+     * 将对象转换为日期对象
+     *
+     * @param str 要转换的对象,通常是字符串
+     * @return 转换后的日期对象,如果转换失败或输入为null,则返回null
      */
     public static Date parseDate(Object str) {
         if (str == null) {
@@ -115,6 +166,8 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
 
     /**
      * 获取服务器启动时间
+     *
+     * @return 服务器启动时间的 Date 对象表示
      */
     public static Date getServerStartDate() {
         long time = ManagementFactory.getRuntimeMXBean().getStartTime();
@@ -122,35 +175,66 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
     }
 
     /**
-     * 计算相差天数
+     * 计算两个日期之间的天数差(以毫秒为单位)
+     *
+     * @param date1 第一个日期
+     * @param date2 第二个日期
+     * @return 两个日期之间的天数差的绝对值
      */
     public static int differentDaysByMillisecond(Date date1, Date date2) {
         return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
     }
 
     /**
-     * 计算两个时间差
+     * 计算两个日期之间的时间差,并以天、小时和分钟的格式返回
+     *
+     * @param endDate 结束日期
+     * @param nowDate 当前日期
+     * @return 表示时间差的字符串,格式为"天 小时 分钟"
      */
     public static String getDatePoor(Date endDate, Date nowDate) {
-        long nd = 1000 * 24 * 60 * 60;
-        long nh = 1000 * 60 * 60;
-        long nm = 1000 * 60;
-        // long ns = 1000;
-        // 获得两个时间的毫秒时间差异
-        long diff = endDate.getTime() - nowDate.getTime();
-        // 计算差多少天
-        long day = diff / nd;
-        // 计算差多少小时
-        long hour = diff % nd / nh;
-        // 计算差多少分钟
-        long min = diff % nd % nh / nm;
-        // 计算差多少秒//输出结果
-        // long sec = diff % nd % nh % nm / ns;
-        return day + "天" + hour + "小时" + min + "分钟";
-    }
-
-    /**
-     * 增加 LocalDateTime ==> Date
+        long diffInMillis = endDate.getTime() - nowDate.getTime();
+        long day = TimeUnit.MILLISECONDS.toDays(diffInMillis);
+        long hour = TimeUnit.MILLISECONDS.toHours(diffInMillis) % 24;
+        long min = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) % 60;
+        return String.format("%d天 %d小时 %d分钟", day, hour, min);
+    }
+
+    /**
+     * 计算两个时间点的差值(天、小时、分钟、秒),当值为0时不显示该单位
+     *
+     * @param endDate 结束时间
+     * @param nowDate 当前时间
+     * @return 时间差字符串,格式为 "x天 x小时 x分钟 x秒",若为 0 则不显示
+     */
+    public static String getTimeDifference(Date endDate, Date nowDate) {
+        long diffInMillis = endDate.getTime() - nowDate.getTime();
+        long day = TimeUnit.MILLISECONDS.toDays(diffInMillis);
+        long hour = TimeUnit.MILLISECONDS.toHours(diffInMillis) % 24;
+        long min = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) % 60;
+        long sec = TimeUnit.MILLISECONDS.toSeconds(diffInMillis) % 60;
+        // 构建时间差字符串,条件是值不为0才显示
+        StringBuilder result = new StringBuilder();
+        if (day > 0) {
+            result.append(String.format("%d天 ", day));
+        }
+        if (hour > 0) {
+            result.append(String.format("%d小时 ", hour));
+        }
+        if (min > 0) {
+            result.append(String.format("%d分钟 ", min));
+        }
+        if (sec > 0) {
+            result.append(String.format("%d秒", sec));
+        }
+        return result.length() > 0 ? result.toString().trim() : "0秒";
+    }
+
+    /**
+     * 将 LocalDateTime 对象转换为 Date 对象
+     *
+     * @param temporalAccessor 要转换的 LocalDateTime 对象
+     * @return 转换后的 Date 对象
      */
     public static Date toDate(LocalDateTime temporalAccessor) {
         ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
@@ -158,11 +242,46 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
     }
 
     /**
-     * 增加 LocalDate ==> Date
+     * 将 LocalDate 对象转换为 Date 对象
+     *
+     * @param temporalAccessor 要转换的 LocalDate 对象
+     * @return 转换后的 Date 对象
      */
     public static Date toDate(LocalDate temporalAccessor) {
         LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
         ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
         return Date.from(zdt.toInstant());
     }
+
+    /**
+     * 校验日期范围
+     *
+     * @param startDate 开始日期
+     * @param endDate   结束日期
+     * @param maxValue  最大时间跨度的限制值
+     * @param unit      时间跨度的单位,可选择 "DAYS"、"HOURS" 或 "MINUTES"
+     */
+    public static void validateDateRange(Date startDate, Date endDate, int maxValue, TimeUnit unit) {
+        // 校验结束日期不能早于开始日期
+        if (endDate.before(startDate)) {
+            throw new ServiceException("结束日期不能早于开始日期");
+        }
+
+        // 计算时间跨度
+        long diffInMillis = endDate.getTime() - startDate.getTime();
+
+        // 根据单位转换时间跨度
+        long diff = switch (unit) {
+            case DAYS -> TimeUnit.MILLISECONDS.toDays(diffInMillis);
+            case HOURS -> TimeUnit.MILLISECONDS.toHours(diffInMillis);
+            case MINUTES -> TimeUnit.MILLISECONDS.toMinutes(diffInMillis);
+            default -> throw new IllegalArgumentException("不支持的时间单位");
+        };
+
+        // 校验时间跨度不超过最大限制
+        if (diff > maxValue) {
+            throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase());
+        }
+    }
+
 }

+ 60 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ObjectUtils.java

@@ -0,0 +1,60 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.function.Function;
+
+/**
+ * 对象工具类
+ *
+ * @author 秋辞未寒
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ObjectUtils extends ObjectUtil {
+
+    /**
+     * 如果对象不为空,则获取对象中的某个字段 ObjectUtils.notNullGetter(user, User::getName);
+     *
+     * @param obj 对象
+     * @param func 获取方法
+     * @return 对象字段
+     */
+    public static <T, E> E notNullGetter(T obj, Function<T, E> func) {
+        if (isNotNull(obj) && isNotNull(func)) {
+            return func.apply(obj);
+        }
+        return null;
+    }
+
+    /**
+     * 如果对象不为空,则获取对象中的某个字段,否则返回默认值
+     *
+     * @param obj          对象
+     * @param func         获取方法
+     * @param defaultValue 默认值
+     * @return 对象字段
+     */
+    public static <T, E> E notNullGetter(T obj, Function<T, E> func, E defaultValue) {
+        if (isNotNull(obj) && isNotNull(func)) {
+            return func.apply(obj);
+        }
+        return defaultValue;
+    }
+
+    /**
+     * 如果值不为空,则返回值,否则返回默认值
+     *
+     * @param obj          对象
+     * @param defaultValue 默认值
+     * @return 对象字段
+     */
+    public static <T> T notNull(T obj, T defaultValue) {
+        if (isNotNull(obj)) {
+            return obj;
+        }
+        return defaultValue;
+    }
+
+}

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

@@ -25,7 +25,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * 客户端工具类
+ * 客户端工具类,提供获取请求参数、响应处理、头部信息等常用操作
  *
  * @author ruoyi
  */
@@ -33,52 +33,73 @@ import java.util.Map;
 public class ServletUtils extends JakartaServletUtil {
 
     /**
-     * 获取String参数
+     * 获取指定名称的 String 类型的请求参数
+     *
+     * @param name 参数名
+     * @return 参数值
      */
     public static String getParameter(String name) {
         return getRequest().getParameter(name);
     }
 
     /**
-     * 获取String参数
+     * 获取指定名称的 String 类型的请求参数,若参数不存在,则返回默认值
+     *
+     * @param name         参数名
+     * @param defaultValue 默认值
+     * @return 参数值或默认值
      */
     public static String getParameter(String name, String defaultValue) {
         return Convert.toStr(getRequest().getParameter(name), defaultValue);
     }
 
     /**
-     * 获取Integer参数
+     * 获取指定名称的 Integer 类型的请求参数
+     *
+     * @param name 参数名
+     * @return 参数值
      */
     public static Integer getParameterToInt(String name) {
         return Convert.toInt(getRequest().getParameter(name));
     }
 
     /**
-     * 获取Integer参数
+     * 获取指定名称的 Integer 类型的请求参数,若参数不存在,则返回默认值
+     *
+     * @param name         参数名
+     * @param defaultValue 默认值
+     * @return 参数值或默认值
      */
     public static Integer getParameterToInt(String name, Integer defaultValue) {
         return Convert.toInt(getRequest().getParameter(name), defaultValue);
     }
 
     /**
-     * 获取Boolean参数
+     * 获取指定名称的 Boolean 类型的请求参数
+     *
+     * @param name 参数名
+     * @return 参数值
      */
     public static Boolean getParameterToBool(String name) {
         return Convert.toBool(getRequest().getParameter(name));
     }
 
     /**
-     * 获取Boolean参数
+     * 获取指定名称的 Boolean 类型的请求参数,若参数不存在,则返回默认值
+     *
+     * @param name         参数名
+     * @param defaultValue 默认值
+     * @return 参数值或默认值
      */
     public static Boolean getParameterToBool(String name, Boolean defaultValue) {
         return Convert.toBool(getRequest().getParameter(name), defaultValue);
     }
 
     /**
-     * 获得所有请求参数
+     * 获取所有请求参数(以 Map 的形式返回)
      *
      * @param request 请求对象{@link ServletRequest}
-     * @return Map
+     * @return 请求参数的 Map,键为参数名,值为参数值数组
      */
     public static Map<String, String[]> getParams(ServletRequest request) {
         final Map<String, String[]> map = request.getParameterMap();
@@ -86,10 +107,10 @@ public class ServletUtils extends JakartaServletUtil {
     }
 
     /**
-     * 获得所有请求参数
+     * 获取所有请求参数(以 Map 的形式返回,值为字符串形式的拼接)
      *
      * @param request 请求对象{@link ServletRequest}
-     * @return Map
+     * @return 请求参数的 Map,键为参数名,值为拼接后的字符串
      */
     public static Map<String, String> getParamMap(ServletRequest request) {
         Map<String, String> params = new HashMap<>();
@@ -100,7 +121,9 @@ public class ServletUtils extends JakartaServletUtil {
     }
 
     /**
-     * 获取request
+     * 获取当前 HTTP 请求对象
+     *
+     * @return 当前 HTTP 请求对象
      */
     public static HttpServletRequest getRequest() {
         try {
@@ -111,7 +134,9 @@ public class ServletUtils extends JakartaServletUtil {
     }
 
     /**
-     * 获取response
+     * 获取当前 HTTP 响应对象
+     *
+     * @return 当前 HTTP 响应对象
      */
     public static HttpServletResponse getResponse() {
         try {
@@ -122,12 +147,25 @@ public class ServletUtils extends JakartaServletUtil {
     }
 
     /**
-     * 获取session
+     * 获取当前请求的 HttpSession 对象
+     * <p>
+     * 如果当前请求已经关联了一个会话(即已经存在有效的 session ID),
+     * 则返回该会话对象;如果没有关联会话,则会创建一个新的会话对象并返回。
+     * <p>
+     * HttpSession 用于存储会话级别的数据,如用户登录信息、购物车内容等,
+     * 可以在多个请求之间共享会话数据
+     *
+     * @return 当前请求的 HttpSession 对象
      */
     public static HttpSession getSession() {
         return getRequest().getSession();
     }
 
+    /**
+     * 获取当前请求的请求属性
+     *
+     * @return {@link ServletRequestAttributes} 请求属性对象
+     */
     public static ServletRequestAttributes getRequestAttributes() {
         try {
             RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
@@ -137,6 +175,13 @@ public class ServletUtils extends JakartaServletUtil {
         }
     }
 
+    /**
+     * 获取指定请求头的值,如果头部为空则返回空字符串
+     *
+     * @param request 请求对象
+     * @param name    头部名称
+     * @return 头部值
+     */
     public static String getHeader(HttpServletRequest request, String name) {
         String value = request.getHeader(name);
         if (StringUtils.isEmpty(value)) {
@@ -145,6 +190,12 @@ public class ServletUtils extends JakartaServletUtil {
         return urlDecode(value);
     }
 
+    /**
+     * 获取所有请求头的 Map,键为头部名称,值为头部值
+     *
+     * @param request 请求对象
+     * @return 请求头的 Map
+     */
     public static Map<String, String> getHeaders(HttpServletRequest request) {
         Map<String, String> map = new LinkedCaseInsensitiveMap<>();
         Enumeration<String> enumeration = request.getHeaderNames();
@@ -159,7 +210,7 @@ public class ServletUtils extends JakartaServletUtil {
     }
 
     /**
-     * 将字符串渲染到客户端
+     * 将字符串渲染到客户端(以 JSON 格式返回)
      *
      * @param response 渲染对象
      * @param string   待渲染的字符串
@@ -176,37 +227,47 @@ public class ServletUtils extends JakartaServletUtil {
     }
 
     /**
-     * 是否是Ajax异步请求
+     * 判断当前请求是否为 Ajax 异步请求
      *
-     * @param request
+     * @param request 请求对象
+     * @return 是否为 Ajax 请求
      */
     public static boolean isAjaxRequest(HttpServletRequest request) {
 
+        // 判断 Accept 头部是否包含 application/json
         String accept = request.getHeader("accept");
         if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
             return true;
         }
 
+        // 判断 X-Requested-With 头部是否包含 XMLHttpRequest
         String xRequestedWith = request.getHeader("X-Requested-With");
         if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
             return true;
         }
 
+        // 判断 URI 后缀是否为 .json 或 .xml
         String uri = request.getRequestURI();
         if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) {
             return true;
         }
 
+        // 判断请求参数 __ajax 是否为 json 或 xml
         String ajax = request.getParameter("__ajax");
         return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
     }
 
+    /**
+     * 获取客户端 IP 地址
+     *
+     * @return 客户端 IP 地址
+     */
     public static String getClientIP() {
         return getClientIP(getRequest());
     }
 
     /**
-     * 内容编码
+     * 内容进行 URL 编码
      *
      * @param str 内容
      * @return 编码后的内容
@@ -216,7 +277,7 @@ public class ServletUtils extends JakartaServletUtil {
     }
 
     /**
-     * 内容解码
+     * 内容进行 URL 解码
      *
      * @param str 内容
      * @return 解码后的内容

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

@@ -4,8 +4,6 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.lang.Validator;
 import cn.hutool.core.util.StrUtil;
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
 import org.springframework.util.AntPathMatcher;
 
 import java.util.*;
@@ -17,13 +15,16 @@ import java.util.stream.Collectors;
  *
  * @author Lion Li
  */
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class StringUtils extends org.apache.commons.lang3.StringUtils {
 
     public static final String SEPARATOR = ",";
 
     public static final String SLASH = "/";
 
+    @Deprecated
+    private StringUtils() {
+    }
+
     /**
      * 获取参数不为空值
      *
@@ -320,4 +321,21 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
             .collect(Collectors.toList());
     }
 
+    /**
+     * 不区分大小写检查 CharSequence 是否以指定的前缀开头。
+     *
+     * @param str     要检查的 CharSequence 可能为 null
+     * @param prefixs 要查找的前缀可能为 null
+     * @return 是否包含
+     */
+    public static boolean startWithAnyIgnoreCase(CharSequence str, CharSequence... prefixs) {
+        // 判断是否是以指定字符串开头
+        for (CharSequence prefix : prefixs) {
+            if (StringUtils.startsWithIgnoreCase(str, prefix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
 }

+ 0 - 12
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java

@@ -14,18 +14,6 @@ import java.util.concurrent.*;
 @Slf4j
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class Threads {
-
-    /**
-     * sleep等待,单位为毫秒
-     */
-    public static void sleep(long milliseconds) {
-        try {
-            Thread.sleep(milliseconds);
-        } catch (InterruptedException e) {
-            return;
-        }
-    }
-
     /**
      * 停止线程池
      * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.

+ 17 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java

@@ -43,6 +43,23 @@ public class TreeBuildUtils extends TreeUtil {
         return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
     }
 
+    /**
+     * 构建树形结构
+     *
+     * @param <T>        输入节点的类型
+     * @param <K>        节点ID的类型
+     * @param parentId   顶级节点
+     * @param list       节点列表,其中包含了要构建树形结构的所有节点
+     * @param nodeParser 解析器,用于将输入节点转换为树节点
+     * @return 构建好的树形结构列表
+     */
+    public static <T, K> List<Tree<K>> build(List<T> list, K parentId, NodeParser<T, K> nodeParser) {
+        if (CollUtil.isEmpty(list)) {
+            return CollUtil.newArrayList();
+        }
+        return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser);
+    }
+
     /**
      * 获取节点列表中所有节点的叶子节点
      *

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

@@ -15,7 +15,7 @@ public class SqlUtil {
     /**
      * 定义常用的 sql关键字
      */
-    public static String SQL_REGEX = "and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
+    public static String SQL_REGEX = "\u000B|and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
 
     /**
      * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)

+ 48 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPattern.java

@@ -0,0 +1,48 @@
+package org.dromara.common.core.validate.enumd;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * 自定义枚举校验
+ *
+ * @author 秋辞未寒
+ * @date 2024-12-09
+ */
+@Documented
+@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
+@Retention(RUNTIME)
+@Repeatable(EnumPattern.List.class) // 允许在同一元素上多次使用该注解
+@Constraint(validatedBy = {EnumPatternValidator.class})
+public @interface EnumPattern {
+
+    /**
+     * 需要校验的枚举类型
+     */
+    Class<? extends Enum<?>> type();
+
+    /**
+     * 枚举类型校验值字段名称
+     * 需确保该字段实现了 getter 方法
+     */
+    String fieldName();
+
+    String message() default "输入值不在枚举范围内";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+    @Documented
+    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
+    @Retention(RUNTIME)
+    @interface List {
+        EnumPattern[] value();
+    }
+
+}

+ 37 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPatternValidator.java

@@ -0,0 +1,37 @@
+package org.dromara.common.core.validate.enumd;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+
+/**
+ * 自定义枚举校验注解实现
+ *
+ * @author 秋辞未寒
+ * @date 2024-12-09
+ */
+public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
+
+    private EnumPattern annotation;;
+
+    @Override
+    public void initialize(EnumPattern annotation) {
+        ConstraintValidator.super.initialize(annotation);
+        this.annotation = annotation;
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
+        if (StringUtils.isNotBlank(value)) {
+            String fieldName = annotation.fieldName();
+            for (Object e : annotation.type().getEnumConstants()) {
+                if (value.equals(ReflectUtils.invokeGetter(e, fieldName))) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+}

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

@@ -1,11 +1,11 @@
 package org.dromara.common.encrypt.core;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ReflectUtil;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.io.Resources;
+import org.dromara.common.core.utils.ObjectUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.encrypt.annotation.EncryptField;
 import org.springframework.context.ConfigurableApplicationContext;
@@ -58,10 +58,7 @@ public class EncryptorManager {
      * 获取类加密字段缓存
      */
     public Set<Field> getFieldCache(Class<?> sourceClazz) {
-        if (ObjectUtil.isNotNull(fieldCache)) {
-            return fieldCache.get(sourceClazz);
-        }
-        return null;
+        return ObjectUtils.notNullGetter(fieldCache, f -> f.get(sourceClazz));
     }
 
     /**

+ 6 - 4
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/utils/EncryptUtils.java

@@ -19,10 +19,12 @@ import java.util.Map;
  * @author 老马
  */
 public class EncryptUtils {
+
     /**
      * 公钥
      */
     public static final String PUBLIC_KEY = "publicKey";
+
     /**
      * 私钥
      */
@@ -51,7 +53,7 @@ public class EncryptUtils {
     /**
      * AES加密
      *
-     * @param data     待密数据
+     * @param data     待密数据
      * @param password 秘钥字符串
      * @return 加密后字符串, 采用Base64编码
      */
@@ -70,7 +72,7 @@ public class EncryptUtils {
     /**
      * AES加密
      *
-     * @param data     待密数据
+     * @param data     待密数据
      * @param password 秘钥字符串
      * @return 加密后字符串, 采用Hex编码
      */
@@ -208,7 +210,7 @@ public class EncryptUtils {
     /**
      * sm2私钥解密
      *
-     * @param data       待密数据
+     * @param data       待密数据
      * @param privateKey 私钥
      * @return 解密后字符串
      */
@@ -266,7 +268,7 @@ public class EncryptUtils {
     /**
      * rsa私钥解密
      *
-     * @param data       待密数据
+     * @param data       待密数据
      * @param privateKey 私钥
      * @return 解密后字符串
      */

+ 24 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelNotation.java

@@ -0,0 +1,24 @@
+package org.dromara.common.excel.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 批注
+ * @author guzhouyanyu
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ExcelNotation {
+
+    /**
+     * col index
+     */
+    int index() default -1;
+    /**
+     * 批注内容
+     */
+    String value() default "";
+}

+ 26 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/annotation/ExcelRequired.java

@@ -0,0 +1,26 @@
+package org.dromara.common.excel.annotation;
+
+import org.apache.poi.ss.usermodel.IndexedColors;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 是否必填
+ * @author guzhouyanyu
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ExcelRequired {
+
+    /**
+     * col index
+     */
+    int index() default -1;
+    /**
+     * 字体颜色
+     */
+    IndexedColors fontColor() default IndexedColors.RED;
+}

+ 67 - 41
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java

@@ -55,6 +55,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
      * 下拉可选项
      */
     private final List<DropDownOptions> dropDownOptions;
+    private final DictService dictService;
     /**
      * 当前单选进度
      */
@@ -63,7 +64,6 @@ public class ExcelDownHandler implements SheetWriteHandler {
      * 当前联动选择进度
      */
     private int currentLinkedOptionsSheetIndex;
-    private final DictService dictService;
 
     public ExcelDownHandler(List<DropDownOptions> options) {
         this.dropDownOptions = options;
@@ -139,8 +139,8 @@ public class ExcelDownHandler implements SheetWriteHandler {
             } else if (everyOptions.getOptions().size() > 10) {
                 // 当一级选项参数个数大于10,使用额外表的形式
                 dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
-            } else if (everyOptions.getOptions().size() != 0) {
-                // 当一级选项个数不为空,使用默认形式
+            } else {
+                // 否则使用默认形式
                 dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
             }
         });
@@ -171,10 +171,24 @@ public class ExcelDownHandler implements SheetWriteHandler {
         Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
         // 将下拉表隐藏
         workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
-        // 完善横向的一级选项数据
+        // 选项数据
         List<String> firstOptions = options.getOptions();
         Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
 
+        // 采用按行填充数据的方式,避免EasyExcel出现数据无法写入的问题
+        // Attempting to write a row in the range that is already written to disk
+
+        // 使用ArrayList记载数据,防止乱序
+        List<String> columnNames = new ArrayList<>();
+        // 写入第一行,即第一级的数据
+        Row firstRow = linkedOptionsDataSheet.createRow(0);
+        for (int columnIndex = 0; columnIndex < firstOptions.size(); columnIndex++) {
+            String columnName = firstOptions.get(columnIndex);
+            firstRow.createCell(columnIndex)
+                .setCellValue(columnName);
+            columnNames.add(columnName);
+        }
+
         // 创建名称管理器
         Name name = workbook.createName();
         // 设置名称管理器的别名
@@ -190,28 +204,12 @@ public class ExcelDownHandler implements SheetWriteHandler {
         // 设置数据校验为序列模式,引用的是名称管理器中的别名
         this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
 
-        for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
-            // 先提取主表中一级下拉的列名
+        // 创建二级选项的名称管理器
+        for (int columIndex = 0; columIndex < columnNames.size(); columIndex++) {
+            // 列名
             String firstOptionsColumnName = getExcelColumnName(columIndex);
-            // 一次循环是每一个一级选项
-            int finalI = columIndex;
-            // 本次循环的一级选项值
-            String thisFirstOptionsValue = firstOptions.get(columIndex);
-            // 创建第一行的数据
-            Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
-                // 如果不存在则创建第一行
-                .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
-                // 第一行当前列
-                .createCell(columIndex)
-                // 设置值为当前一级选项值
-                .setCellValue(thisFirstOptionsValue);
-
-            // 第二行开始,设置第二级别选项参数
-            List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
-            if (CollUtil.isEmpty(secondOptions)) {
-                // 必须保证至少有一个关联选项,否则将导致Excel解析错误
-                secondOptions = Collections.singletonList("暂无_0");
-            }
+            // 对应的一级值
+            String thisFirstOptionsValue = columnNames.get(columIndex);
 
             // 以该一级选项值创建子名称管理器
             Name sonName = workbook.createName();
@@ -222,7 +220,9 @@ public class ExcelDownHandler implements SheetWriteHandler {
                 linkedOptionsSheetName,
                 firstOptionsColumnName,
                 firstOptionsColumnName,
-                secondOptions.size() + 1
+                // 二级选项存在则设置为(选项个数+1)行,否则设置为2行
+                Math.max(Optional.ofNullable(secoundOptionsMap.get(thisFirstOptionsValue))
+                    .orElseGet(ArrayList::new).size(), 1) + 1
             );
             // 设置名称管理器的引用位置
             sonName.setRefersToFormula(sonFunction);
@@ -235,25 +235,51 @@ public class ExcelDownHandler implements SheetWriteHandler {
                 // 二级只能主表每一行的每一列添加二级校验
                 markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
             }
+        }
 
-            for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
-                // 从第二行开始填充二级选项
-                int finalRowIndex = rowIndex + 1;
-                int finalColumIndex = columIndex;
-
-                Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
-                    // 没有则创建
-                    .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
-                Optional
-                    // 在本级一级选项所在的列
-                    .ofNullable(row.getCell(finalColumIndex))
-                    // 不存在则创建
-                    .orElseGet(() -> row.createCell(finalColumIndex))
-                    // 设置二级选项值
-                    .setCellValue(secondOptions.get(rowIndex));
+        // 将二级数据处理为按行区分
+        Map<Integer, List<String>> columnValueMap = new HashMap<>();
+        int currentRow = 1;
+        while (currentRow >= 0) {
+            boolean flag = false;
+            List<String> rowData = new ArrayList<>();
+            for (String columnName : columnNames) {
+                List<String> data = secoundOptionsMap.get(columnName);
+                if (CollUtil.isEmpty(data)) {
+                    // 添加空字符串填充位置
+                    rowData.add(" ");
+                    continue;
+                }
+                // 取第一个
+                String str = data.get(0);
+                rowData.add(str);
+                // 通过移除的方式避免重复
+                data.remove(0);
+                // 设置可以继续
+                flag = true;
+            }
+            columnValueMap.put(currentRow, rowData);
+            // 可以继续,则增加行数,否则置为负数跳出循环
+            if (flag) {
+                currentRow++;
+            } else {
+                currentRow = -1;
             }
         }
 
+        // 填充第二级选项数据
+        columnValueMap.forEach((rowIndex, rowValues) -> {
+            Row row = linkedOptionsDataSheet.createRow(rowIndex);
+            for (int columnIndex = 0; columnIndex < rowValues.size(); columnIndex++) {
+                String rowValue = rowValues.get(columnIndex);
+                // 填充位置的部分不渲染
+                if (StrUtil.isNotBlank(rowValue)) {
+                    row.createCell(columnIndex)
+                        .setCellValue(rowValue);
+                }
+            }
+        });
+
         currentLinkedOptionsSheetIndex++;
     }
 

+ 135 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/DataWriteHandler.java

@@ -0,0 +1,135 @@
+package org.dromara.common.excel.handler;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.excel.metadata.data.DataFormatData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.util.StyleUtil;
+import com.alibaba.excel.write.handler.CellWriteHandler;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.style.WriteCellStyle;
+import com.alibaba.excel.write.metadata.style.WriteFont;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
+import org.apache.poi.xssf.usermodel.XSSFRichTextString;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.excel.annotation.ExcelNotation;
+import org.dromara.common.excel.annotation.ExcelRequired;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 批注、必填
+ *
+ * @author guzhouyanyu
+ */
+public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
+
+    /**
+     * 批注
+     */
+    private final Map<Integer, String> notationMap;
+
+    /**
+     * 头列字体颜色
+     */
+    private final Map<Integer, Short> headColumnMap;
+
+
+    public DataWriteHandler(Class<?> clazz) {
+        notationMap = getNotationMap(clazz);
+        headColumnMap = getRequiredMap(clazz);
+    }
+
+    @Override
+    public void afterCellDispose(CellWriteHandlerContext context) {
+        if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) {
+            return;
+        }
+        WriteCellData<?> cellData = context.getFirstCellData();
+        WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
+
+        DataFormatData dataFormatData = new DataFormatData();
+        // 单元格设置为文本格式
+        dataFormatData.setIndex((short) 49);
+        writeCellStyle.setDataFormatData(dataFormatData);
+
+        if (context.getHead()) {
+            Cell cell = context.getCell();
+            WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder();
+            Sheet sheet = writeSheetHolder.getSheet();
+            Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
+            Drawing<?> drawing = sheet.createDrawingPatriarch();
+            // 设置标题字体样式
+            WriteFont headWriteFont = new WriteFont();
+            // 加粗
+            headWriteFont.setBold(true);
+            if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getColumnIndex())) {
+                // 设置字体颜色
+                headWriteFont.setColor(headColumnMap.get(cell.getColumnIndex()));
+            }
+            writeCellStyle.setWriteFont(headWriteFont);
+            CellStyle cellStyle = StyleUtil.buildCellStyle(workbook, null, writeCellStyle);
+            cell.setCellStyle(cellStyle);
+
+            if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getColumnIndex())) {
+                // 批注内容
+                String notationContext = notationMap.get(cell.getColumnIndex());
+                // 创建绘图对象
+                Comment comment = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), 0, (short) 5, 5));
+                comment.setString(new XSSFRichTextString(notationContext));
+                cell.setCellComment(comment);
+            }
+        }
+    }
+
+    /**
+     * 获取必填列
+     */
+    private static Map<Integer, Short> getRequiredMap(Class<?> clazz) {
+        Map<Integer, Short> requiredMap = new HashMap<>();
+        Field[] fields = clazz.getDeclaredFields();
+        // 检查 fields 数组是否为空
+        if (fields.length == 0) {
+            return requiredMap;
+        }
+        Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
+
+        for (int i = 0; i < filteredFields.length; i++) {
+            Field field = filteredFields[i];
+            if (!field.isAnnotationPresent(ExcelRequired.class)) {
+                continue;
+            }
+            ExcelRequired excelRequired = field.getAnnotation(ExcelRequired.class);
+            int columnIndex =  excelRequired.index() == -1 ? i : excelRequired.index();
+            requiredMap.put(columnIndex, excelRequired.fontColor().getIndex());
+        }
+        return requiredMap;
+    }
+
+    /**
+     * 获取批注
+     */
+    private static Map<Integer, String> getNotationMap(Class<?> clazz) {
+        Map<Integer, String> notationMap = new HashMap<>();
+        Field[] fields = clazz.getDeclaredFields();
+        // 检查 fields 数组是否为空
+        if (fields.length == 0) {
+            return notationMap;
+        }
+        Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
+        for (int i = 0; i < filteredFields.length; i++) {
+            Field field = filteredFields[i];
+            if (!field.isAnnotationPresent(ExcelNotation.class)) {
+                continue;
+            }
+            ExcelNotation excelNotation = field.getAnnotation(ExcelNotation.class);
+            int columnIndex =  excelNotation.index() == -1 ? i : excelNotation.index();
+            notationMap.put(columnIndex, excelNotation.value());
+        }
+        return notationMap;
+    }
+}

+ 15 - 12
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java

@@ -18,6 +18,7 @@ import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.core.utils.file.FileUtils;
 import org.dromara.common.excel.convert.ExcelBigNumberConvert;
 import org.dromara.common.excel.core.*;
+import org.dromara.common.excel.handler.DataWriteHandler;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -191,6 +192,7 @@ public class ExcelUtil {
             .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
             // 大数值自动转换 防止失真
             .registerConverter(new ExcelBigNumberConvert())
+            .registerWriteHandler(new DataWriteHandler(clazz))
             .sheet(sheetName);
         if (merge) {
             // 合并处理器
@@ -211,7 +213,7 @@ public class ExcelUtil {
      * @param data         模板需要的数据
      * @param response     响应体
      */
-    public static void exportTemplate(List<Object> data, String filename, String templatePath, HttpServletResponse response) {
+    public static <T> void exportTemplate(List<T> data, String filename, String templatePath, HttpServletResponse response) {
         try {
             resetResponse(filename, response);
             ServletOutputStream os = response.getOutputStream();
@@ -230,20 +232,21 @@ public class ExcelUtil {
      * @param data         模板需要的数据
      * @param os           输出流
      */
-    public static void exportTemplate(List<Object> data, String templatePath, OutputStream os) {
+    public static <T> void exportTemplate(List<T> data, String templatePath, OutputStream os) {
+        if (CollUtil.isEmpty(data)) {
+            throw new IllegalArgumentException("数据为空");
+        }
         ClassPathResource templateResource = new ClassPathResource(templatePath);
         ExcelWriter excelWriter = EasyExcel.write(os)
             .withTemplate(templateResource.getStream())
             .autoCloseStream(false)
             // 大数值自动转换 防止失真
             .registerConverter(new ExcelBigNumberConvert())
+            .registerWriteHandler(new DataWriteHandler(data.get(0).getClass()))
             .build();
         WriteSheet writeSheet = EasyExcel.writerSheet().build();
-        if (CollUtil.isEmpty(data)) {
-            throw new IllegalArgumentException("数据为空");
-        }
         // 单表多数据导出 模板格式为 {.属性}
-        for (Object d : data) {
+        for (T d : data) {
             excelWriter.fill(d, writeSheet);
         }
         excelWriter.finish();
@@ -299,6 +302,9 @@ public class ExcelUtil {
      * @param os           输出流
      */
     public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) {
+        if (CollUtil.isEmpty(data)) {
+            throw new IllegalArgumentException("数据为空");
+        }
         ClassPathResource templateResource = new ClassPathResource(templatePath);
         ExcelWriter excelWriter = EasyExcel.write(os)
             .withTemplate(templateResource.getStream())
@@ -307,9 +313,6 @@ public class ExcelUtil {
             .registerConverter(new ExcelBigNumberConvert())
             .build();
         WriteSheet writeSheet = EasyExcel.writerSheet().build();
-        if (CollUtil.isEmpty(data)) {
-            throw new IllegalArgumentException("数据为空");
-        }
         for (Map.Entry<String, Object> map : data.entrySet()) {
             // 设置列表后续还有数据
             FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
@@ -333,6 +336,9 @@ public class ExcelUtil {
      * @param os           输出流
      */
     public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) {
+        if (CollUtil.isEmpty(data)) {
+            throw new IllegalArgumentException("数据为空");
+        }
         ClassPathResource templateResource = new ClassPathResource(templatePath);
         ExcelWriter excelWriter = EasyExcel.write(os)
             .withTemplate(templateResource.getStream())
@@ -340,9 +346,6 @@ public class ExcelUtil {
             // 大数值自动转换 防止失真
             .registerConverter(new ExcelBigNumberConvert())
             .build();
-        if (CollUtil.isEmpty(data)) {
-            throw new IllegalArgumentException("数据为空");
-        }
         for (int i = 0; i < data.size(); i++) {
             WriteSheet writeSheet = EasyExcel.writerSheet(i).build();
             for (Map.Entry<String, Object> map : data.get(i).entrySet()) {

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

@@ -32,7 +32,7 @@ public class BigNumberSerializer extends NumberSerializer {
 
     @Override
     public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException {
-        // 超出范围 序列化字符串
+        // 超出范围 序列化字符串
         if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
             super.serialize(value, gen, provider);
         } else {

+ 5 - 6
ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java

@@ -100,7 +100,7 @@ public class LogAspect {
 
             if (e != null) {
                 operLog.setStatus(BusinessStatus.FAIL.ordinal());
-                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 3800));
             }
             // 设置方法名称
             String className = joinPoint.getTarget().getClass().getName();
@@ -113,13 +113,12 @@ public class LogAspect {
             // 设置消耗时间
             StopWatch stopWatch = KEY_CACHE.get();
             stopWatch.stop();
-            operLog.setCostTime(stopWatch.getTime());
+            operLog.setCostTime(stopWatch.getDuration().toMillis());
             // 发布事件保存数据库
             SpringUtils.context().publishEvent(operLog);
         } catch (Exception exp) {
             // 记录本地异常日志
             log.error("异常信息:{}", exp.getMessage());
-            exp.printStackTrace();
         } finally {
             KEY_CACHE.remove();
         }
@@ -146,7 +145,7 @@ public class LogAspect {
         }
         // 是否需要保存response,参数和值
         if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
-            operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
+            operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 3800));
         }
     }
 
@@ -161,11 +160,11 @@ public class LogAspect {
         String requestMethod = operLog.getRequestMethod();
         if (MapUtil.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) {
             String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
-            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+            operLog.setOperParam(StringUtils.substring(params, 0, 3800));
         } else {
             MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
             MapUtil.removeAny(paramsMap, excludeParamNames);
-            operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
+            operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 3800));
         }
     }
 

+ 7 - 1
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/MailProperties.java

@@ -43,7 +43,13 @@ public class MailProperties {
     private String pass;
 
     /**
-     * 发送方,遵循RFC-822标准
+     * 发送方,遵循RFC-822标准<br>
+     * 发件人可以是以下形式:
+     *
+     * <pre>
+     * 1. user@xxx.xx
+     * 2.  name &lt;user@xxx.xx&gt;
+     * </pre>
      */
     private String from;
 

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

@@ -37,6 +37,11 @@
             <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-jsqlparser</artifactId>
+        </dependency>
+
         <!-- sql性能分析插件 -->
         <dependency>
             <groupId>p6spy</groupId>

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

@@ -0,0 +1,50 @@
+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();
+    }
+
+}

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

@@ -2,6 +2,7 @@ package org.dromara.common.mybatis.config;
 
 import cn.hutool.core.net.NetUtil;
 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
 import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
 import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
@@ -10,8 +11,10 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
 import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
 import org.dromara.common.core.factory.YmlPropertySourceFactory;
 import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.mybatis.aspect.DataPermissionAspect;
 import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
 import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
+import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
 import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.beans.BeansException;
@@ -54,6 +57,14 @@ public class MybatisPlusConfig {
         return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
     }
 
+    /**
+     * 数据权限切面处理器
+     */
+    @Bean
+    public DataPermissionAspect dataPermissionAspect() {
+        return new DataPermissionAspect();
+    }
+
     /**
      * 分页插件,自动识别数据库类型
      */
@@ -96,6 +107,14 @@ public class MybatisPlusConfig {
         return new MybatisExceptionHandler();
     }
 
+    /**
+     * 初始化表对象处理器
+     */
+    @Bean
+    public PostInitTableInfoHandler postInitTableInfoHandler() {
+        return new PlusPostInitTableInfoHandler();
+    }
+
     /**
      * PaginationInnerInterceptor 分页插件,自动识别数据库类型
      * https://baomidou.com/pages/97710a/

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

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.metadata.OrderItem;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Data;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.StringUtils;
@@ -113,8 +114,14 @@ public class PageQuery implements Serializable {
         return list;
     }
 
+    @JsonIgnore
     public Integer getFirstNum() {
         return (pageNum - 1) * pageSize;
     }
 
+    public PageQuery(Integer pageSize, Integer pageNum) {
+        this.pageSize = pageSize;
+        this.pageNum = pageNum;
+    }
+
 }

+ 2 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/TableDataInfo.java

@@ -50,6 +50,8 @@ public class TableDataInfo<T> implements Serializable {
     public TableDataInfo(List<T> list, long total) {
         this.rows = list;
         this.total = total;
+        this.code = HttpStatus.HTTP_OK;
+        this.msg = "查询成功";
     }
 
     /**

+ 6 - 1
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java

@@ -48,7 +48,12 @@ public enum DataScopeType {
     /**
      * 仅本人数据权限
      */
-    SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
+    SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "),
+
+    /**
+     * 部门及以下或本人数据权限
+     */
+    DEPT_AND_CHILD_OR_SELF("6", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) OR #{#userName} = #{#user.userId} ", " 1 = 0 ");
 
     private final String code;
 

+ 3 - 4
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.reflection.MetaObject;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.ObjectUtils;
 import org.dromara.common.mybatis.core.domain.BaseEntity;
 import org.dromara.common.satoken.utils.LoginHelper;
 
@@ -31,8 +32,7 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
         try {
             if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
                 // 获取当前时间作为创建时间和更新时间,如果创建时间不为空,则使用创建时间,否则使用当前时间
-                Date current = ObjectUtil.isNotNull(baseEntity.getCreateTime())
-                    ? baseEntity.getCreateTime() : new Date();
+                Date current = ObjectUtils.notNull(baseEntity.getCreateTime(), new Date());
                 baseEntity.setCreateTime(current);
                 baseEntity.setUpdateTime(current);
 
@@ -44,8 +44,7 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
                         // 填充创建人、更新人和创建部门信息
                         baseEntity.setCreateBy(userId);
                         baseEntity.setUpdateBy(userId);
-                        baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept())
-                            ? baseEntity.getCreateDept() : loginUser.getDeptId());
+                        baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId()));
                     }
                 }
             } else {

+ 130 - 72
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java

@@ -3,6 +3,7 @@ package org.dromara.common.mybatis.handler;
 import cn.hutool.core.annotation.AnnotationUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
+import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import net.sf.jsqlparser.JSQLParserException;
 import net.sf.jsqlparser.expression.Expression;
@@ -28,19 +29,13 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
 import org.springframework.core.io.support.ResourcePatternResolver;
 import org.springframework.core.type.ClassMetadata;
 import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
-import org.springframework.expression.BeanResolver;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.ParserContext;
+import org.springframework.expression.*;
 import org.springframework.expression.common.TemplateParserContext;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.expression.spel.support.StandardEvaluationContext;
 import org.springframework.util.ClassUtils;
 
-import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 
@@ -54,7 +49,7 @@ import java.util.function.Function;
 public class PlusDataPermissionHandler {
 
     /**
-     * 方法或(名称)  注解的映射关系缓存
+     * 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行)
      */
     private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
 
@@ -86,24 +81,24 @@ public class PlusDataPermissionHandler {
      * @return 数据过滤条件的 SQL 片段
      */
     public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
-        // 获取数据权限配置
-        DataPermission dataPermission = getDataPermission(mappedStatementId);
-        // 获取当前登录用户信息
-        LoginUser currentUser = DataPermissionHelper.getVariable("user");
-        if (ObjectUtil.isNull(currentUser)) {
-            currentUser = LoginHelper.getLoginUser();
-            DataPermissionHelper.setVariable("user", currentUser);
-        }
-        // 如果是超级管理员或租户管理员,则不过滤数据
-        if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
-            return where;
-        }
-        // 构造数据过滤条件的 SQL 片段
-        String dataFilterSql = buildDataFilter(dataPermission, isSelect);
-        if (StringUtils.isBlank(dataFilterSql)) {
-            return where;
-        }
         try {
+            // 获取数据权限配置
+            DataPermission dataPermission = getDataPermission(mappedStatementId);
+            // 获取当前登录用户信息
+            LoginUser currentUser = DataPermissionHelper.getVariable("user");
+            if (ObjectUtil.isNull(currentUser)) {
+                currentUser = LoginHelper.getLoginUser();
+                DataPermissionHelper.setVariable("user", currentUser);
+            }
+            // 如果是超级管理员或租户管理员,则不过滤数据
+            if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
+                return where;
+            }
+            // 构造数据过滤条件的 SQL 片段
+            String dataFilterSql = buildDataFilter(dataPermission, isSelect);
+            if (StringUtils.isBlank(dataFilterSql)) {
+                return where;
+            }
             Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
             // 数据权限使用单独的括号 防止与其他条件冲突
             ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(expression);
@@ -114,6 +109,8 @@ public class PlusDataPermissionHandler {
             }
         } catch (JSQLParserException e) {
             throw new ServiceException("数据权限解析异常 => " + e.getMessage());
+        } finally {
+            DataPermissionHelper.removePermission();
         }
     }
 
@@ -132,10 +129,33 @@ public class PlusDataPermissionHandler {
             joinStr = " " + dataPermission.joinStr() + " ";
         }
         LoginUser user = DataPermissionHelper.getVariable("user");
-        StandardEvaluationContext context = new StandardEvaluationContext();
+        Object defaultValue = "-1";
+        NullSafeStandardEvaluationContext context = new NullSafeStandardEvaluationContext(defaultValue);
+        context.addPropertyAccessor(new NullSafePropertyAccessor(context.getPropertyAccessors().get(0), defaultValue));
         context.setBeanResolver(beanResolver);
         DataPermissionHelper.getContext().forEach(context::setVariable);
         Set<String> conditions = new HashSet<>();
+        // 优先设置变量
+        List<String> keys = new ArrayList<>();
+        Map<DataColumn, Boolean> ignoreMap = new HashMap<>();
+        for (DataColumn dataColumn : dataPermission.value()) {
+            if (dataColumn.key().length != dataColumn.value().length) {
+                throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
+            }
+            // 包含权限标识符 这直接跳过
+            if (StringUtils.isNotBlank(dataColumn.permission()) &&
+                CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
+            ) {
+                ignoreMap.put(dataColumn, Boolean.TRUE);
+                continue;
+            }
+            // 设置注解变量 key 为表达式变量 value 为变量值
+            for (int i = 0; i < dataColumn.key().length; i++) {
+                context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
+            }
+            keys.addAll(Arrays.stream(dataColumn.key()).map(key -> "#" + key).toList());
+        }
+
         for (RoleDTO role : user.getRoles()) {
             user.setRoleId(role.getRoleId());
             // 获取角色权限泛型
@@ -145,33 +165,30 @@ public class PlusDataPermissionHandler {
             }
             // 全部数据权限直接返回
             if (type == DataScopeType.ALL) {
-                return "";
+                return StringUtils.EMPTY;
             }
             boolean isSuccess = false;
             for (DataColumn dataColumn : dataPermission.value()) {
-                if (dataColumn.key().length != dataColumn.value().length) {
-                    throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
+                // 包含权限标识符 这直接跳过
+                if (ignoreMap.containsKey(dataColumn)) {
+                    // 修复多角色与权限标识符共用问题 https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IB4CS4
+                    conditions.add(joinStr + " 1 = 1 ");
+                    isSuccess = true;
+                    continue;
                 }
                 // 不包含 key 变量 则不处理
-                if (!StringUtils.containsAny(type.getSqlTemplate(),
-                    Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
-                )) {
+                if (!StringUtils.containsAny(type.getSqlTemplate(), keys.toArray(String[]::new))) {
                     continue;
                 }
-                // 包含权限标识符 这直接跳过
-                if (StringUtils.isNotBlank(dataColumn.permission()) &&
-                    CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
-                ) {
-                    isSuccess = true;
+                // 当前注解不满足模板 不处理
+                if (!StringUtils.containsAny(type.getSqlTemplate(), dataColumn.key())) {
                     continue;
                 }
-                // 设置注解变量 key 为表达式变量 value 为变量值
-                for (int i = 0; i < dataColumn.key().length; i++) {
-                    context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
-                }
-
+                // 忽略数据权限 防止spel表达式内有其他sql查询导致死循环调用
+                String sql = DataPermissionHelper.ignore(() ->
+                    parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class)
+                );
                 // 解析sql模板并填充
-                String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
                 conditions.add(joinStr + sql);
                 isSuccess = true;
             }
@@ -185,7 +202,7 @@ public class PlusDataPermissionHandler {
             String sql = StreamUtils.join(conditions, Function.identity(), "");
             return sql.substring(joinStr.length());
         }
-        return "";
+        return StringUtils.EMPTY;
     }
 
     /**
@@ -212,7 +229,10 @@ public class PlusDataPermissionHandler {
                     // 获取资源对应的类对象
                     Class<?> clazz = Resources.classForName(classMetadata.getClassName());
                     // 查找类中的特定注解
-                    findAnnotation(clazz);
+                    if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
+                        DataPermission dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
+                        dataPermissionCacheMap.put(clazz.getName(), dataPermission);
+                    }
                 }
             }
         } catch (Exception e) {
@@ -220,29 +240,6 @@ public class PlusDataPermissionHandler {
         }
     }
 
-    /**
-     * 在指定的类中查找特定的注解 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 注解对象
      *
@@ -250,9 +247,9 @@ public class PlusDataPermissionHandler {
      * @return DataPermission 注解对象,如果不存在则返回 null
      */
     public DataPermission getDataPermission(String mapperId) {
-        // 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象
-        if (dataPermissionCacheMap.containsKey(mapperId)) {
-            return dataPermissionCacheMap.get(mapperId);
+        // 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象
+        if (DataPermissionHelper.getPermission() != null) {
+            return DataPermissionHelper.getPermission();
         }
         // 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找
         String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
@@ -271,4 +268,65 @@ public class PlusDataPermissionHandler {
     public boolean invalid(String mapperId) {
         return getDataPermission(mapperId) == null;
     }
+
+    /**
+     * 对所有null变量找不到的变量返回默认值
+     */
+    @AllArgsConstructor
+    private static class NullSafeStandardEvaluationContext extends StandardEvaluationContext {
+
+        private final Object defaultValue;
+
+        @Override
+        public Object lookupVariable(String name) {
+            Object obj = super.lookupVariable(name);
+            // 如果读取到的值是 null,则返回默认值
+            if (obj == null) {
+                return defaultValue;
+            }
+            return obj;
+        }
+
+    }
+
+    /**
+     * 对所有null变量找不到的变量返回默认值 委托模式 将不需要处理的方法委托给原处理器
+     */
+    @AllArgsConstructor
+    private static class NullSafePropertyAccessor implements PropertyAccessor {
+
+        private final PropertyAccessor delegate;
+        private final Object defaultValue;
+
+        @Override
+        public Class<?>[] getSpecificTargetClasses() {
+            return delegate.getSpecificTargetClasses();
+        }
+
+        @Override
+        public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
+            return delegate.canRead(context, target, name);
+        }
+
+        @Override
+        public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
+            TypedValue value = delegate.read(context, target, name);
+            // 如果读取到的值是 null,则返回默认值
+            if (value.getValue() == null) {
+                return new TypedValue(defaultValue);
+            }
+            return value;
+        }
+
+        @Override
+        public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
+            return delegate.canWrite(context, target, name);
+        }
+
+        @Override
+        public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
+            delegate.write(context, target, name, newValue);
+        }
+    }
+
 }

+ 27 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusPostInitTableInfoHandler.java

@@ -0,0 +1,27 @@
+package org.dromara.common.mybatis.handler;
+
+import cn.hutool.core.convert.Convert;
+import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import org.apache.ibatis.session.Configuration;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+
+/**
+ * 修改表信息初始化方式
+ * 目前用于全局修改是否使用逻辑删除
+ *
+ * @author Lion Li
+ */
+public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
+
+    @Override
+    public void postTableInfo(TableInfo tableInfo, Configuration configuration) {
+        String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
+        // 只有关闭时 统一设置false 为true时mp自动判断不处理
+        if (!Convert.toBool(flag)) {
+            ReflectUtils.setFieldValue(tableInfo, "withLogicDelete", false);
+        }
+    }
+
+}

+ 28 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.mybatis.annotation.DataPermission;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -29,6 +30,33 @@ public class DataPermissionHelper {
 
     private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
 
+    private static final ThreadLocal<DataPermission> PERMISSION_CACHE = new ThreadLocal<>();
+
+    /**
+     * 获取当前执行mapper权限注解
+     *
+     * @return 返回当前执行mapper权限注解
+     */
+    public static DataPermission getPermission() {
+        return PERMISSION_CACHE.get();
+    }
+
+    /**
+     * 设置当前执行mapper权限注解
+     *
+     * @param dataPermission   数据权限注解
+     */
+    public static void setPermission(DataPermission dataPermission) {
+        PERMISSION_CACHE.set(dataPermission);
+    }
+
+    /**
+     * 删除当前执行mapper权限注解
+     */
+    public static void removePermission() {
+        PERMISSION_CACHE.remove();
+    }
+
     /**
      * 从上下文中获取指定键的变量值,并将其转换为指定的类型
      *

+ 2 - 2
ruoyi-common/ruoyi-common-mybatis/src/main/resources/common-mybatis.yml

@@ -24,8 +24,8 @@ mybatis-plus:
       # 主键类型
       # AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
       idType: ASSIGN_ID
-      # 逻辑已删除值(框架表均使用此值 禁止随意修改)
-      logicDeleteValue: 2
+      # 逻辑已删除值(可按需求随意修改)
+      logicDeleteValue: 1
       # 逻辑未删除值
       logicNotDeleteValue: 0
       insertStrategy: NOT_NULL

+ 11 - 121
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java

@@ -9,7 +9,6 @@ import org.dromara.common.core.utils.file.FileUtils;
 import org.dromara.common.oss.constant.OssConstant;
 import org.dromara.common.oss.entity.UploadResult;
 import org.dromara.common.oss.enumd.AccessPolicyType;
-import org.dromara.common.oss.enumd.PolicyType;
 import org.dromara.common.oss.exception.OssException;
 import org.dromara.common.oss.properties.OssProperties;
 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
@@ -22,8 +21,6 @@ import software.amazon.awssdk.services.s3.S3AsyncClient;
 import software.amazon.awssdk.services.s3.S3Configuration;
 import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration;
 import software.amazon.awssdk.services.s3.model.GetObjectResponse;
-import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
-import software.amazon.awssdk.services.s3.model.S3Exception;
 import software.amazon.awssdk.services.s3.presigner.S3Presigner;
 import software.amazon.awssdk.transfer.s3.S3TransferManager;
 import software.amazon.awssdk.transfer.s3.model.*;
@@ -35,6 +32,7 @@ import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.Duration;
+import java.util.function.Consumer;
 
 /**
  * S3 存储协议 所有兼容S3协议的云厂商均支持
@@ -115,8 +113,6 @@ public class OssClient {
                 .serviceConfiguration(config)
                 .build();
 
-            // 创建存储桶
-            createBucket();
         } catch (Exception e) {
             if (e instanceof OssException) {
                 throw e;
@@ -125,43 +121,6 @@ public class OssClient {
         }
     }
 
-    /**
-     * 同步创建存储桶
-     * 如果存储桶不存在,会进行创建;如果存储桶存在,不执行任何操作
-     *
-     * @throws OssException 当创建存储桶时发生异常时抛出
-     */
-    public void createBucket() {
-        String bucketName = properties.getBucketName();
-        try {
-            // 尝试获取存储桶的信息
-            client.headBucket(
-                    x -> x.bucket(bucketName)
-                        .build())
-                .join();
-        } catch (Exception ex) {
-            if (ex.getCause() instanceof NoSuchBucketException) {
-                try {
-                    // 存储桶不存在,尝试创建存储桶
-                    client.createBucket(
-                            x -> x.bucket(bucketName))
-                        .join();
-
-                    // 设置存储桶的访问策略(Bucket Policy)
-                    client.putBucketPolicy(
-                            x -> x.bucket(bucketName)
-                                .policy(getPolicy(bucketName, getAccessPolicy().getPolicyType())))
-                        .join();
-                } catch (S3Exception e) {
-                    // 存储桶创建或策略设置失败
-                    throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
-                }
-            } else {
-                throw new OssException("判断Bucket是否存在失败,请核对配置信息:[" + ex.getMessage() + "]");
-            }
-        }
-    }
-
     /**
      * 上传文件到 Amazon S3,并返回上传结果
      *
@@ -281,10 +240,11 @@ public class OssClient {
      *
      * @param key 文件在 Amazon S3 中的对象键
      * @param out 输出流
+     * @param consumer 自定义处理逻辑
      * @return 输出流中写入的字节数(长度)
      * @throws OssException 如果下载失败,抛出自定义异常
      */
-    public long download(String key, OutputStream out) {
+    public void download(String key, OutputStream out, Consumer<Long> consumer) {
         try {
             // 构建下载请求
             DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
@@ -300,7 +260,10 @@ public class OssClient {
             Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest);
             // 输出到流中
             try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream
-                return responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread
+                if (consumer != null) {
+                    consumer.accept(responseStream.response().contentLength());
+                }
+                responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread
             }
         } catch (Exception e) {
             throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
@@ -326,13 +289,13 @@ public class OssClient {
     /**
      * 获取私有URL链接
      *
-     * @param objectKey 对象KEY
-     * @param second    授权时间
+     * @param objectKey   对象KEY
+     * @param expiredTime 链接授权到期时间
      */
-    public String getPrivateUrl(String objectKey, Integer second) {
+    public String getPrivateUrl(String objectKey, Duration expiredTime) {
         // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
         URL url = presigner.presignGetObject(
-                x -> x.signatureDuration(Duration.ofSeconds(second))
+                x -> x.signatureDuration(expiredTime)
                     .getObjectRequest(
                         y -> y.bucket(properties.getBucketName())
                             .key(objectKey)
@@ -529,77 +492,4 @@ public class OssClient {
         return AccessPolicyType.getByType(properties.getAccessPolicy());
     }
 
-    /**
-     * 生成 AWS S3 存储桶访问策略
-     *
-     * @param bucketName 存储桶
-     * @param policyType 桶策略类型
-     * @return 符合 AWS S3 存储桶访问策略格式的字符串
-     */
-    private static String getPolicy(String bucketName, PolicyType policyType) {
-        String policy = switch (policyType) {
-            case WRITE -> """
-                {
-                  "Version": "2012-10-17",
-                  "Statement": []
-                }
-                """;
-            case READ_WRITE -> """
-                {
-                  "Version": "2012-10-17",
-                  "Statement": [
-                    {
-                      "Effect": "Allow",
-                      "Principal": "*",
-                      "Action": [
-                        "s3:GetBucketLocation",
-                        "s3:ListBucket",
-                        "s3:ListBucketMultipartUploads"
-                      ],
-                      "Resource": "arn:aws:s3:::bucketName"
-                    },
-                    {
-                      "Effect": "Allow",
-                      "Principal": "*",
-                      "Action": [
-                        "s3:AbortMultipartUpload",
-                        "s3:DeleteObject",
-                        "s3:GetObject",
-                        "s3:ListMultipartUploadParts",
-                        "s3:PutObject"
-                      ],
-                      "Resource": "arn:aws:s3:::bucketName/*"
-                    }
-                  ]
-                }
-                """;
-            case READ -> """
-                {
-                  "Version": "2012-10-17",
-                  "Statement": [
-                    {
-                      "Effect": "Allow",
-                      "Principal": "*",
-                      "Action": ["s3:GetBucketLocation"],
-                      "Resource": "arn:aws:s3:::bucketName"
-                    },
-                    {
-                      "Effect": "Deny",
-                      "Principal": "*",
-                      "Action": ["s3:ListBucket"],
-                      "Resource": "arn:aws:s3:::bucketName"
-                    },
-                    {
-                      "Effect": "Allow",
-                      "Principal": "*",
-                      "Action": "s3:GetObject",
-                      "Resource": "arn:aws:s3:::bucketName/*"
-                    }
-                  ]
-                }
-                """;
-        };
-        return policy.replaceAll("bucketName", bucketName);
-    }
-
 }

+ 3 - 8
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java

@@ -17,17 +17,17 @@ public enum AccessPolicyType {
     /**
      * private
      */
-    PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE),
+    PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE),
 
     /**
      * public
      */
-    PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE),
+    PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE),
 
     /**
      * custom
      */
-    CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ);
+    CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ);
 
     /**
      * 桶 权限类型(数据库值)
@@ -44,11 +44,6 @@ public enum AccessPolicyType {
      */
     private final ObjectCannedACL objectCannedACL;
 
-    /**
-     * 桶策略类型
-     */
-    private final PolicyType policyType;
-
     public static AccessPolicyType getByType(String type) {
         for (AccessPolicyType value : values()) {
             if (value.getType().equals(type)) {

+ 0 - 35
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/PolicyType.java

@@ -1,35 +0,0 @@
-package org.dromara.common.oss.enumd;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-/**
- * minio策略配置
- *
- * @author Lion Li
- */
-@Getter
-@AllArgsConstructor
-public enum PolicyType {
-
-    /**
-     * 只读
-     */
-    READ("read-only"),
-
-    /**
-     * 只写
-     */
-    WRITE("write-only"),
-
-    /**
-     * 读写
-     */
-    READ_WRITE("read-write");
-
-    /**
-     * 类型
-     */
-    private final String type;
-
-}

+ 6 - 0
ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/annotation/RateLimiter.java

@@ -38,4 +38,10 @@ public @interface RateLimiter {
      * 提示消息 支持国际化 格式为 {code}
      */
     String message() default "{rate.limiter.message}";
+
+    /**
+     * 限流策略超时时间 默认一天(策略存活时间 会清除已存在的策略数据)
+     */
+    int timeout() default 86400;
+
 }

+ 2 - 1
ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java

@@ -54,13 +54,14 @@ public class RateLimiterAspect {
     public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
         int time = rateLimiter.time();
         int count = rateLimiter.count();
+        int timeout = rateLimiter.timeout();
         try {
             String combineKey = getCombineKey(rateLimiter, point);
             RateType rateType = RateType.OVERALL;
             if (rateLimiter.limitType() == LimitType.CLUSTER) {
                 rateType = RateType.PER_CLIENT;
             }
-            long number = RedisUtils.rateLimiter(combineKey, rateType, count, time);
+            long number = RedisUtils.rateLimiter(combineKey, rateType, count, time, timeout);
             if (number == -1) {
                 String message = rateLimiter.message();
                 if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {

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

@@ -42,6 +42,18 @@
             <groupId>com.fasterxml.jackson.datatype</groupId>
             <artifactId>jackson-datatype-jsr310</artifactId>
         </dependency>
+
+<!--        &lt;!&ndash; redis序列化替代方案 比json快无数的跨语言二进制序列化 &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>org.apache.fury</groupId>-->
+<!--            <artifactId>fury-core</artifactId>-->
+<!--            <version>0.9.0</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>org.slf4j</groupId>-->
+<!--            <artifactId>slf4j-api</artifactId>-->
+<!--        </dependency>-->
+
     </dependencies>
 
 </project>

+ 3 - 0
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/config/RedisConfig.java

@@ -53,6 +53,9 @@ public class RedisConfig {
             om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
             // 指定序列化输入的类型,类必须是非final修饰的。序列化时将对象全类名一起保存下来
             om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
+//            LoggerFactory.useSlf4jLogging(true);
+//            FuryCodec furyCodec = new FuryCodec();
+//            CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, furyCodec, furyCodec);
             TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
             // 组合序列化 key 使用 String 内容使用通用 json 格式
             CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);

+ 3 - 3
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/QueueUtils.java

@@ -174,12 +174,12 @@ public class QueueUtils {
      *
      * @param queueName 队列名
      * @param capacity  容量
-     * @param destroy   已存在是否销毁
+     * @param destroy   是否销毁
      */
     public static <T> boolean trySetBoundedQueueCapacity(String queueName, int capacity, boolean destroy) {
         RBoundedBlockingQueue<T> boundedBlockingQueue = CLIENT.getBoundedBlockingQueue(queueName);
-        if (boundedBlockingQueue.isExists() && destroy) {
-            destroyQueue(queueName);
+        if (destroy) {
+            boundedBlockingQueue.delete();
         }
         return boundedBlockingQueue.trySetCapacity(capacity);
     }

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

@@ -4,6 +4,7 @@ import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import org.dromara.common.core.utils.SpringUtils;
 import org.redisson.api.*;
+import org.redisson.api.options.KeysScanOptions;
 
 import java.time.Duration;
 import java.util.Collection;
@@ -36,8 +37,22 @@ public class RedisUtils {
      * @return -1 表示失败
      */
     public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) {
+        return rateLimiter(key, rateType, rate, rateInterval, 0);
+    }
+
+    /**
+     * 限流
+     *
+     * @param key          限流key
+     * @param rateType     限流类型
+     * @param rate         速率
+     * @param rateInterval 速率间隔
+     * @param timeout      超时时间
+     * @return -1 表示失败
+     */
+    public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval, int timeout) {
         RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
-        rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS);
+        rateLimiter.trySetRate(rateType, rate, Duration.ofSeconds(rateInterval), Duration.ofSeconds(timeout));
         if (rateLimiter.tryAcquire()) {
             return rateLimiter.availablePermits();
         } else {
@@ -518,13 +533,34 @@ public class RedisUtils {
 
     /**
      * 获得缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id)
-     *
+     * <P>
+     * limit-设置扫描的限制数量(默认为0,查询全部)
+     * pattern-设置键的匹配模式(默认为null)
+     * chunkSize-设置每次扫描的块大小(默认为0,本方法设置为1000)
+     * type-设置键的类型(默认为null,查询全部类型)
+     * </P>
+     * @see KeysScanOptions
      * @param pattern 字符串前缀
      * @return 对象列表
      */
     public static Collection<String> keys(final String pattern) {
-        Stream<String> stream = CLIENT.getKeys().getKeysStreamByPattern(pattern);
-        return stream.collect(Collectors.toList());
+        return  keys(KeysScanOptions.defaults().pattern(pattern).chunkSize(1000));
+    }
+
+    /**
+     * 通过扫描参数获取缓存的基本对象列表
+     * @param keysScanOptions 扫描参数
+     * <P>
+     * limit-设置扫描的限制数量(默认为0,查询全部)
+     * pattern-设置键的匹配模式(默认为null)
+     * chunkSize-设置每次扫描的块大小(默认为0)
+     * type-设置键的类型(默认为null,查询全部类型)
+     * </P>
+     * @see KeysScanOptions
+     */
+    public static Collection<String> keys(final KeysScanOptions keysScanOptions) {
+        Stream<String> keysStream = CLIENT.getKeys().getKeysStream(keysScanOptions);
+        return keysStream.collect(Collectors.toList());
     }
 
     /**

+ 165 - 0
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/SequenceUtils.java

@@ -0,0 +1,165 @@
+package org.dromara.common.redis.utils;
+
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.DateUtil;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.redisson.api.RIdGenerator;
+import org.redisson.api.RedissonClient;
+
+import java.time.Duration;
+
+/**
+ * 发号器工具类
+ *
+ * @author 秋辞未寒
+ * @date 2024-12-10
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class SequenceUtils {
+
+    /**
+     * 默认初始值
+     */
+    public static final Long DEFAULT_INIT_VALUE = 1L;
+    /**
+     * 默认步长
+     */
+    public static final Long DEFAULT_STEP_VALUE = 1L;
+    /**
+     * 默认过期时间-天
+     */
+    public static final Duration DEFAULT_EXPIRE_TIME_DAY = Duration.ofDays(1);
+    /**
+     * 默认过期时间-分钟
+     */
+    public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1);
+
+    /**
+     * 获取Redisson客户端实例
+     */
+    private static final RedissonClient REDISSON_CLIENT = SpringUtils.getBean(RedissonClient.class);
+
+    /**
+     * 获取ID生成器
+     *
+     * @param key        业务key
+     * @param expireTime 过期时间
+     * @param initValue  ID初始值
+     * @param stepValue  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;
+        }
+        RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key);
+        // 设置初始值和步长
+        idGenerator.tryInit(initValue, stepValue);
+        // 设置过期时间
+        idGenerator.expire(expireTime);
+        return idGenerator;
+    }
+
+    /**
+     * 获取指定业务key的唯一id
+     *
+     * @param key        业务key
+     * @param expireTime 过期时间
+     * @param initValue  ID初始值
+     * @param stepValue  ID步长
+     * @return 唯一id
+     */
+    public static long nextId(String key, Duration expireTime, Long initValue, Long stepValue) {
+        return getIdGenerator(key, expireTime, initValue, stepValue).nextId();
+    }
+
+    /**
+     * 获取指定业务key的唯一id字符串
+     *
+     * @param key        业务key
+     * @param expireTime 过期时间
+     * @param initValue  ID初始值
+     * @param stepValue  ID步长
+     * @return 唯一id
+     */
+    public static String nextIdStr(String key, Duration expireTime, Long initValue, Long stepValue) {
+        return String.valueOf(nextId(key, expireTime, initValue, stepValue));
+    }
+
+    /**
+     * 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
+     *
+     * @param key        业务key
+     * @param expireTime 过期时间
+     * @return 唯一id
+     */
+    public static long nextId(String key, Duration expireTime) {
+        return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
+    }
+
+    /**
+     * 获取指定业务key的唯一id字符串 (ID初始值=1,ID步长=1)
+     *
+     * @param key        业务key
+     * @param expireTime 过期时间
+     * @return 唯一id
+     */
+    public static String nextIdStr(String key, Duration expireTime) {
+        return String.valueOf(nextId(key, expireTime));
+    }
+
+    /**
+     * 获取 yyyyMMdd 开头的唯一id
+     *
+     * @return 唯一id
+     */
+    public static String nextIdDate() {
+        return nextIdDate("");
+    }
+
+    /**
+     * 获取 prefix + yyyyMMdd 开头的唯一id
+     *
+     * @param prefix 业务前缀
+     * @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);
+    }
+
+    /**
+     * 获取 yyyyMMddHHmmss 开头的唯一id
+     *
+     * @return 唯一id
+     */
+    public static String nextIdDateTime() {
+        return nextIdDateTime("");
+    }
+
+    /**
+     * 获取 prefix + yyyyMMddHHmmss 开头的唯一id
+     *
+     * @param prefix 业务前缀
+     * @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);
+    }
+
+}

+ 10 - 2
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/core/dao/PlusSaTokenDao.java

@@ -51,7 +51,11 @@ public class PlusSaTokenDao implements SaTokenDao {
         if (timeout == NEVER_EXPIRE) {
             RedisUtils.setCacheObject(key, value);
         } else {
-            RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
+            if (RedisUtils.hasKey(key)) {
+                RedisUtils.setCacheObject(key, value, true);
+            } else {
+                RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
+            }
         }
         CAFFEINE.invalidate(key);
     }
@@ -114,7 +118,11 @@ public class PlusSaTokenDao implements SaTokenDao {
         if (timeout == NEVER_EXPIRE) {
             RedisUtils.setCacheObject(key, object);
         } else {
-            RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
+            if (RedisUtils.hasKey(key)) {
+                RedisUtils.setCacheObject(key, object, true);
+            } else {
+                RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
+            }
         }
         CAFFEINE.invalidate(key);
     }

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

@@ -8,13 +8,14 @@ import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
+import org.dromara.common.core.constant.SystemConstants;
 import org.dromara.common.core.constant.TenantConstants;
-import org.dromara.common.core.constant.UserConstants;
 import org.dromara.common.core.domain.model.LoginUser;
 import org.dromara.common.core.enums.UserType;
 
 import java.util.Set;
 
+
 /**
  * 登录鉴权助手
  * <p>
@@ -88,6 +89,13 @@ public class LoginHelper {
         return Convert.toLong(getExtra(USER_KEY));
     }
 
+    /**
+     * 获取用户id
+     */
+    public static String getUserIdStr() {
+        return Convert.toStr(getExtra(USER_KEY));
+    }
+
     /**
      * 获取用户账户
      */
@@ -152,7 +160,7 @@ public class LoginHelper {
      * @return 结果
      */
     public static boolean isSuperAdmin(Long userId) {
-        return UserConstants.SUPER_ADMIN_ID.equals(userId);
+        return SystemConstants.SUPER_ADMIN_ID.equals(userId);
     }
 
     /**

+ 8 - 2
ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/annotation/Sensitive.java

@@ -22,7 +22,13 @@ import java.lang.annotation.Target;
 public @interface Sensitive {
     SensitiveStrategy strategy();
 
-    String roleKey() default "";
+    /**
+     * 角色标识符 多个角色满足一个即可
+     */
+    String[] roleKey() default {};
 
-    String perms() default "";
+    /**
+     * 权限标识符 多个权限满足一个即可
+     */
+    String[] perms() default {};
 }

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

@@ -13,6 +13,6 @@ public interface SensitiveService {
     /**
      * 是否脱敏
      */
-    boolean isSensitive(String roleKey, String perms);
+    boolean isSensitive(String[] roleKey, String[] perms);
 
 }

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

@@ -80,12 +80,12 @@ public enum SensitiveStrategy {
     FIRST_MASK(DesensitizedUtil::firstMask),
 
     /**
-     * 清空为null
+     * 清空为""
      */
     CLEAR(s -> DesensitizedUtil.clear()),
 
     /**
-     * 清空为""
+     * 清空为null
      */
     CLEAR_TO_NULL(s -> DesensitizedUtil.clearToNull());
 

+ 2 - 2
ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java

@@ -26,8 +26,8 @@ import java.util.Objects;
 public class SensitiveHandler extends JsonSerializer<String> implements ContextualSerializer {
 
     private SensitiveStrategy strategy;
-    private String roleKey;
-    private String perms;
+    private String[] roleKey;
+    private String[] perms;
 
     @Override
     public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {

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

@@ -66,7 +66,7 @@ public class PlusSmsDao implements SmsDao {
      */
     @Override
     public void clean() {
-        RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + "sms:");
+        RedisUtils.deleteKeys(GlobalConstants.GLOBAL_REDIS_KEY + "sms:*");
     }
 
 }

+ 2 - 2
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/maxkey/AuthMaxKeyRequest.java

@@ -30,7 +30,7 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
     }
 
     @Override
-    protected AuthToken getAccessToken(AuthCallback authCallback) {
+    public AuthToken getAccessToken(AuthCallback authCallback) {
         String body = doPostAuthorizationCode(authCallback.getCode());
         Dict object = JsonUtils.parseMap(body);
         // oauth/token 验证异常
@@ -51,7 +51,7 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
     }
 
     @Override
-    protected AuthUser getUserInfo(AuthToken authToken) {
+    public AuthUser getUserInfo(AuthToken authToken) {
         String body = doGetUserInfo(authToken);
         Dict object = JsonUtils.parseMap(body);
         // oauth/token 验证异常

+ 17 - 4
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/topiam/AuthTopIamRequest.java

@@ -1,7 +1,10 @@
 package org.dromara.common.social.topiam;
 
+import cn.hutool.core.codec.Base64;
 import cn.hutool.core.lang.Dict;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
 import com.xkcoding.http.support.HttpHeader;
 import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.cache.AuthStateCache;
@@ -16,7 +19,7 @@ import me.zhyd.oauth.utils.UrlBuilder;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.json.utils.JsonUtils;
 
-import static org.dromara.common.social.topiam.AuthTopiamSource.TOPIAM;
+import static org.dromara.common.social.topiam.AuthTopIamSource.TOPIAM;
 
 /**
  * TopIAM 认证请求
@@ -41,7 +44,7 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
     }
 
     @Override
-    protected AuthToken getAccessToken(AuthCallback authCallback) {
+    public AuthToken getAccessToken(AuthCallback authCallback) {
         String body = doPostAuthorizationCode(authCallback.getCode());
         Dict object = JsonUtils.parseMap(body);
         checkResponse(object);
@@ -55,7 +58,7 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
     }
 
     @Override
-    protected AuthUser getUserInfo(AuthToken authToken) {
+    public AuthUser getUserInfo(AuthToken authToken) {
         String body = doGetUserInfo(authToken);
         Dict object = JsonUtils.parseMap(body);
         checkResponse(object);
@@ -70,6 +73,16 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
             .build();
     }
 
+    @Override
+    protected String doPostAuthorizationCode(String code) {
+        HttpRequest request = HttpRequest.post(source.accessToken())
+            .header("Authorization", "Basic " + Base64.encode("%s:%s".formatted(config.getClientId(), config.getClientSecret())))
+            .form("grant_type", "authorization_code")
+            .form("code", code)
+            .form("redirect_uri", config.getRedirectUri());
+        HttpResponse response = request.execute();
+        return response.body();
+    }
 
     @Override
     protected String doGetUserInfo(AuthToken authToken) {
@@ -86,7 +99,7 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
             .build();
     }
 
-    public static void checkResponse(Dict object) {
+    private static void checkResponse(Dict object) {
         // oauth/token 验证异常
         if (object.containsKey("error")) {
             throw new AuthException(object.getStr("error_description"));

Некоторые файлы не были показаны из-за большого количества измененных файлов