浏览代码

!577 发布 5.2.2 正式版 安全性提升
Merge pull request !577 from 疯狂的狮子Li/dev

疯狂的狮子Li 1 年之前
父节点
当前提交
098d3347a0
共有 100 个文件被更改,包括 1499 次插入1989 次删除
  1. 1 1
      .run/ruoyi-monitor-admin.run.xml
  2. 3 3
      .run/ruoyi-server.run.xml
  3. 1 1
      .run/ruoyi-snailjob-server.run.xml
  4. 3 1
      README.md
  5. 36 35
      pom.xml
  6. 22 15
      ruoyi-admin/pom.xml
  7. 6 6
      ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java
  8. 14 3
      ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java
  9. 10 12
      ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java
  10. 16 20
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java
  11. 16 17
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
  12. 16 20
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java
  13. 15 17
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
  14. 3 0
      ruoyi-admin/src/main/resources/application-dev.yml
  15. 3 0
      ruoyi-admin/src/main/resources/application-prod.yml
  16. 10 4
      ruoyi-admin/src/main/resources/application.yml
  17. 1 0
      ruoyi-common/pom.xml
  18. 8 1
      ruoyi-common/ruoyi-common-bom/pom.xml
  19. 0 5
      ruoyi-common/ruoyi-common-core/pom.xml
  20. 5 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
  21. 0 5
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
  22. 10 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
  23. 16 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java
  24. 5 0
      ruoyi-common/ruoyi-common-excel/pom.xml
  25. 6 1
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
  26. 1 1
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java
  27. 0 46
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java
  28. 0 108
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java
  29. 0 483
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java
  30. 0 659
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java
  31. 0 40
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java
  32. 7 5
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java
  33. 0 33
      ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java
  34. 7 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java
  35. 7 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java
  36. 2 10
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java
  37. 6 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java
  38. 15 5
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java
  39. 39 4
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java
  40. 2 2
      ruoyi-common/ruoyi-common-ratelimiter/src/main/java/org/dromara/common/ratelimiter/aspectj/RateLimiterAspect.java
  41. 5 3
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java
  42. 2 2
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/PlusSpringCacheManager.java
  43. 20 0
      ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java
  44. 51 1
      ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/core/SensitiveStrategy.java
  45. 2 2
      ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java
  46. 36 0
      ruoyi-common/ruoyi-common-sse/pom.xml
  47. 36 0
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/config/SseAutoConfiguration.java
  48. 21 0
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/config/SseProperties.java
  49. 87 0
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java
  50. 160 0
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/core/SseEmitterManager.java
  51. 29 0
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/dto/SseMessageDto.java
  52. 48 0
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/listener/SseTopicListener.java
  53. 58 0
      ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/utils/SseMessageUtils.java
  54. 1 0
      ruoyi-common/ruoyi-common-sse/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  55. 40 4
      ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/helper/TenantHelper.java
  56. 9 0
      ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/manager/TenantSpringCacheManager.java
  57. 13 0
      ruoyi-common/ruoyi-common-web/pom.xml
  58. 17 0
      ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java
  59. 24 31
      ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java
  60. 1 1
      ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/utils/WebSocketUtils.java
  61. 12 1
      ruoyi-extend/ruoyi-monitor-admin/pom.xml
  62. 1 3
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/SecurityConfig.java
  63. 18 3
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/CustomNotifier.java
  64. 3 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml
  65. 64 0
      ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/starter/filter/ActuatorAuthFilter.java
  66. 29 0
      ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/starter/filter/SecurityConfig.java
  67. 3 0
      ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml
  68. 3 0
      ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml
  69. 7 6
      ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mapper/TestDemoMapper.java
  70. 7 2
      ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/TestDemoServiceImpl.java
  71. 32 0
      ruoyi-modules/ruoyi-generator/pom.xml
  72. 105 0
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/config/MyBatisDataSourceMonitor.java
  73. 0 1
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/domain/GenTableColumn.java
  74. 0 13
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/mapper/GenTableColumnMapper.java
  75. 0 16
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/mapper/GenTableMapper.java
  76. 118 11
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java
  77. 9 0
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/IGenTableService.java
  78. 3 0
      ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java
  79. 0 83
      ruoyi-modules/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml
  80. 8 222
      ruoyi-modules/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml
  81. 7 0
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/vo.java.vm
  82. 6 0
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm
  83. 2 2
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm
  84. 2 2
      ruoyi-modules/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm
  85. 5 0
      ruoyi-modules/ruoyi-system/pom.xml
  86. 4 4
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/monitor/SysLogininforController.java
  87. 5 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysDeptController.java
  88. 2 2
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysNoticeController.java
  89. 3 1
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysProfileController.java
  90. 2 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysTenantController.java
  91. 8 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysTenantPackageController.java
  92. 2 2
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java
  93. 8 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysPostService.java
  94. 8 3
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysTenantPackageService.java
  95. 9 2
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDataScopeServiceImpl.java
  96. 1 1
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java
  97. 4 2
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java
  98. 11 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysPostServiceImpl.java
  99. 4 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysRoleServiceImpl.java
  100. 12 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTenantPackageServiceImpl.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.1" />
+        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.2.2" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
       </settings>

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

@@ -1,12 +1,12 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
+  <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="演示机">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-server:5.2.1" />
+        <option name="imageTag" value="ruoyi/ruoyi-server:5.2.2" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
       </settings>
     </deployment>
     <method v="2" />
   </configuration>
-</component>
+</component>

+ 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.1" />
+        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.2.2" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
       </settings>

+ 3 - 1
README.md

@@ -9,7 +9,7 @@
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
 [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 <br>
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.2-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
 [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2-blue.svg)]()
 [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
 [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@@ -61,6 +61,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 | 数据库连接池      | 采用 HikariCP Spring官方内置连接池 配置简单 以性能与稳定性闻名天下                                                                        | 采用 druid bug众多 社区维护差 活跃度低 配置众多繁琐性能一般                                               |
 | 数据库主键       | 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁                                                                  | 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一                                                     |
 | WebSocket协议 | 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物                                                         | 无                                                                                  |
+| SSE推送       | 采用 Spring SSE 实现 扩展了Token鉴权与分布式会话同步                                                                               | 无                                                                                  |
 | 序列化         | 采用 Jackson Spring官方内置序列化 靠谱!!!                                                                                    | 采用 fastjson bugjson 远近闻名                                                           | 
 | 分布式幂等       | 参考美团GTIS防重系统简化实现(细节可看文档)                                                                                          | 手动编写注解基于aop实现                                                                      |
 | 分布式锁        | 采用 Lock4j 底层基于 Redisson                                                                                           | 无                                                                                  |
@@ -72,6 +73,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 | 接口文档        | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了                                                     | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成                                                | 
 | 校验框架        | 采用 Validation 支持注解与工具类校验 注解支持国际化                                                                                  | 仅支持注解 且注解不支持国际化                                                                    |
 | Excel框架     | 采用 Alibaba EasyExcel 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等                                               | 基于 POI 手写实现 功能有限 复杂 扩展性差                                                           |
+| 工作流支持       | 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能                                                                                   | 无                                                                                  |
 | 工具类框架       | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码                                                       | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等                                            | 
 | 监控框架        | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控                                    | 无                                                                                  | 
 | 链路追踪        | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗<br/>用了它即可实时查看请求经过的每一处每一个节点                                            | 无                                                                                  |

+ 36 - 35
pom.xml

@@ -13,45 +13,46 @@
     <description>RuoYi-Vue-Plus多租户管理系统</description>
 
     <properties>
-        <revision>5.2.1</revision>
-        <spring-boot.version>3.2.6</spring-boot.version>
+        <revision>5.2.2</revision>
+        <spring-boot.version>3.2.9</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.5.0</springdoc.version>
+        <springdoc.version>2.6.0</springdoc.version>
         <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
-        <poi.version>5.2.3</poi.version>
-        <easyexcel.version>3.3.4</easyexcel.version>
+        <easyexcel.version>4.0.2</easyexcel.version>
         <velocity.version>2.3</velocity.version>
         <satoken.version>1.38.0</satoken.version>
         <mybatis-plus.version>3.5.7</mybatis-plus.version>
         <p6spy.version>3.9.1</p6spy.version>
-        <hutool.version>5.8.27</hutool.version>
+        <hutool.version>5.8.31</hutool.version>
         <okhttp.version>4.10.0</okhttp.version>
         <spring-boot-admin.version>3.2.3</spring-boot-admin.version>
-        <redisson.version>3.31.0</redisson.version>
+        <redisson.version>3.34.1</redisson.version>
         <lock4j.version>2.2.7</lock4j.version>
         <dynamic-ds.version>4.3.1</dynamic-ds.version>
-        <alibaba-ttl.version>2.14.4</alibaba-ttl.version>
-        <snailjob.version>1.0.1</snailjob.version>
-        <mapstruct-plus.version>1.3.6</mapstruct-plus.version>
+        <snailjob.version>1.1.2</snailjob.version>
+        <mapstruct-plus.version>1.4.4</mapstruct-plus.version>
         <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
-        <lombok.version>1.18.32</lombok.version>
+        <lombok.version>1.18.34</lombok.version>
         <bouncycastle.version>1.76</bouncycastle.version>
         <justauth.version>1.16.6</justauth.version>
         <!-- 离线IP地址定位库 -->
         <ip2region.version>2.7.0</ip2region.version>
+        <undertow.version>2.3.15.Final</undertow.version>
 
         <!-- OSS 配置 -->
         <aws.sdk.version>2.25.15</aws.sdk.version>
         <aws.crt.version>0.29.13</aws.crt.version>
         <!-- SMS 配置 -->
-        <sms4j.version>3.2.1</sms4j.version>
+        <sms4j.version>3.3.2</sms4j.version>
         <!-- 限制框架中的fastjson版本 -->
         <fastjson.version>1.2.83</fastjson.version>
+        <!-- 面向运行时的D-ORM依赖 -->
+        <anyline.version>8.7.2-20240808</anyline.version>
         <!--工作流配置-->
-        <flowable.version>7.0.0</flowable.version>
+        <flowable.version>7.0.1</flowable.version>
 
         <!-- 插件版本 -->
         <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
@@ -155,26 +156,10 @@
                 <version>${lombok.version}</version>
             </dependency>
 
-            <dependency>
-                <groupId>org.apache.poi</groupId>
-                <artifactId>poi</artifactId>
-                <version>${poi.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.apache.poi</groupId>
-                <artifactId>poi-ooxml</artifactId>
-                <version>${poi.version}</version>
-            </dependency>
             <dependency>
                 <groupId>com.alibaba</groupId>
                 <artifactId>easyexcel</artifactId>
                 <version>${easyexcel.version}</version>
-                <exclusions>
-                    <exclusion>
-                        <groupId>org.apache.poi</groupId>
-                        <artifactId>poi-ooxml-schemas</artifactId>
-                    </exclusion>
-                </exclusions>
             </dependency>
 
             <!-- velocity代码生成使用模板 -->
@@ -307,12 +292,6 @@
                 <version>${snailjob.version}</version>
             </dependency>
 
-            <dependency>
-                <groupId>com.alibaba</groupId>
-                <artifactId>transmittable-thread-local</artifactId>
-                <version>${alibaba-ttl.version}</version>
-            </dependency>
-
             <!-- 加密包引入 -->
             <dependency>
                 <groupId>org.bouncycastle</groupId>
@@ -333,6 +312,28 @@
                 <version>${ip2region.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>io.undertow</groupId>
+                <artifactId>undertow-core</artifactId>
+                <version>${undertow.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.undertow</groupId>
+                <artifactId>undertow-servlet</artifactId>
+                <version>${undertow.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.undertow</groupId>
+                <artifactId>undertow-websockets-jsr</artifactId>
+                <version>${undertow.version}</version>
+            </dependency>
+
+            <dependency>
+                <artifactId>commons-compress</artifactId>
+                <groupId>org.apache.commons</groupId>
+                <version>1.26.2</version>
+            </dependency>
+
             <dependency>
                 <groupId>com.alibaba</groupId>
                 <artifactId>fastjson</artifactId>

+ 22 - 15
ruoyi-admin/pom.xml

@@ -22,21 +22,28 @@
             <groupId>com.mysql</groupId>
             <artifactId>mysql-connector-j</artifactId>
         </dependency>
-        <!-- Oracle -->
-        <dependency>
-            <groupId>com.oracle.database.jdbc</groupId>
-            <artifactId>ojdbc8</artifactId>
-        </dependency>
-        <!-- PostgreSql -->
-        <dependency>
-            <groupId>org.postgresql</groupId>
-            <artifactId>postgresql</artifactId>
-        </dependency>
-        <!-- SqlServer -->
-        <dependency>
-            <groupId>com.microsoft.sqlserver</groupId>
-            <artifactId>mssql-jdbc</artifactId>
-        </dependency>
+
+<!--        &lt;!&ndash; mp支持的数据库均支持 只需要增加对应的jdbc依赖即可 &ndash;&gt;-->
+<!--        &lt;!&ndash; Oracle &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>com.oracle.database.jdbc</groupId>-->
+<!--            <artifactId>ojdbc8</artifactId>-->
+<!--        </dependency>-->
+<!--        &lt;!&ndash; 兼容oracle低版本 &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>com.oracle.database.nls</groupId>-->
+<!--            <artifactId>orai18n</artifactId>-->
+<!--        </dependency>-->
+<!--        &lt;!&ndash; PostgreSql &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>org.postgresql</groupId>-->
+<!--            <artifactId>postgresql</artifactId>-->
+<!--        </dependency>-->
+<!--        &lt;!&ndash; SqlServer &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>com.microsoft.sqlserver</groupId>-->
+<!--            <artifactId>mssql-jdbc</artifactId>-->
+<!--        </dependency>-->
 
         <dependency>
             <groupId>org.dromara</groupId>

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

@@ -24,9 +24,9 @@ import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
 import org.dromara.common.social.config.properties.SocialProperties;
 import org.dromara.common.social.utils.SocialUtils;
+import org.dromara.common.sse.dto.SseMessageDto;
+import org.dromara.common.sse.utils.SseMessageUtils;
 import org.dromara.common.tenant.helper.TenantHelper;
-import org.dromara.common.websocket.dto.WebSocketMessageDto;
-import org.dromara.common.websocket.utils.WebSocketUtils;
 import org.dromara.system.domain.bo.SysTenantBo;
 import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysTenantVo;
@@ -102,11 +102,11 @@ public class AuthController {
 
         Long userId = LoginHelper.getUserId();
         scheduledExecutorService.schedule(() -> {
-            WebSocketMessageDto dto = new WebSocketMessageDto();
+            SseMessageDto dto = new SseMessageDto();
             dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
-            dto.setSessionKeys(List.of(userId));
-            WebSocketUtils.publishMessage(dto);
-        }, 3, TimeUnit.SECONDS);
+            dto.setUserIds(List.of(userId));
+            SseMessageUtils.publishMessage(dto);
+        }, 5, TimeUnit.SECONDS);
         return R.ok(loginVo);
     }
 

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

@@ -3,6 +3,8 @@ package org.dromara.web.listener;
 import cn.dev33.satoken.config.SaTokenConfig;
 import cn.dev33.satoken.listener.SaTokenListener;
 import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.convert.Convert;
 import cn.hutool.http.useragent.UserAgent;
 import cn.hutool.http.useragent.UserAgentUtil;
 import lombok.RequiredArgsConstructor;
@@ -81,7 +83,10 @@ public class UserActionListener implements SaTokenListener {
      */
     @Override
     public void doLogout(String loginType, Object loginId, String tokenValue) {
-        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+        TenantHelper.dynamic(tenantId, () -> {
+            RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        });
         log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
     }
 
@@ -90,7 +95,10 @@ public class UserActionListener implements SaTokenListener {
      */
     @Override
     public void doKickout(String loginType, Object loginId, String tokenValue) {
-        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+        TenantHelper.dynamic(tenantId, () -> {
+            RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        });
         log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
     }
 
@@ -99,7 +107,10 @@ public class UserActionListener implements SaTokenListener {
      */
     @Override
     public void doReplaced(String loginType, Object loginId, String tokenValue) {
-        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
+        TenantHelper.dynamic(tenantId, () -> {
+            RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        });
         log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
     }
 

+ 10 - 12
ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java

@@ -4,13 +4,14 @@ import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Opt;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.lock.annotation.Lock4j;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.model.AuthUser;
+import org.dromara.common.core.constant.CacheConstants;
 import org.dromara.common.core.constant.Constants;
-import org.dromara.common.core.constant.GlobalConstants;
 import org.dromara.common.core.constant.TenantConstants;
 import org.dromara.common.core.domain.dto.RoleDTO;
 import org.dromara.common.core.domain.model.LoginUser;
@@ -155,16 +156,13 @@ public class SysLoginService {
         loginUser.setUserType(user.getUserType());
         loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
         loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
-        TenantHelper.dynamic(user.getTenantId(), () -> {
-            SysDeptVo dept = null;
-            if (ObjectUtil.isNotNull(user.getDeptId())) {
-                dept = deptService.selectDeptById(user.getDeptId());
-            }
-            loginUser.setDeptName(ObjectUtil.isNull(dept) ? "" : dept.getDeptName());
-            loginUser.setDeptCategory(ObjectUtil.isNull(dept) ? "" : dept.getDeptCategory());
-            List<SysRoleVo> roles = roleService.selectRolesByUserId(user.getUserId());
-            loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
-        });
+        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());
+        loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
         return loginUser;
     }
 
@@ -186,7 +184,7 @@ public class SysLoginService {
      * 登录校验
      */
     public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
-        String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
+        String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
         String loginFail = Constants.LOGIN_FAIL;
 
         // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)

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

@@ -21,7 +21,6 @@ import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.tenant.helper.TenantHelper;
-import org.dromara.system.domain.SysClient;
 import org.dromara.system.domain.SysUser;
 import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysUserVo;
@@ -51,13 +50,12 @@ public class EmailAuthStrategy implements IAuthStrategy {
         String tenantId = loginBody.getTenantId();
         String email = loginBody.getEmail();
         String emailCode = loginBody.getEmailCode();
-
-        // 通过邮箱查找用户
-        SysUserVo user = loadUserByEmail(tenantId, email);
-
-        loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
-        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
-        LoginUser loginUser = loginService.buildLoginUser(user);
+        LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+            SysUserVo user = loadUserByEmail(email);
+            loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
+            // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+            return loginService.buildLoginUser(user);
+        });
         loginUser.setClientKey(client.getClientKey());
         loginUser.setDeviceType(client.getDeviceType());
         SaLoginModel model = new SaLoginModel();
@@ -89,18 +87,16 @@ public class EmailAuthStrategy implements IAuthStrategy {
         return code.equals(emailCode);
     }
 
-    private SysUserVo loadUserByEmail(String tenantId, String email) {
-        return TenantHelper.dynamic(tenantId, () -> {
-            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
-            if (ObjectUtil.isNull(user)) {
-                log.info("登录用户:{} 不存在.", email);
-                throw new UserException("user.not.exists", email);
-            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
-                log.info("登录用户:{} 已被停用.", email);
-                throw new UserException("user.blocked", email);
-            }
-            return user;
-        });
+    private SysUserVo loadUserByEmail(String email) {
+        SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", email);
+            throw new UserException("user.not.exists", email);
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", email);
+            throw new UserException("user.blocked", email);
+        }
+        return user;
     }
 
 }

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

@@ -62,11 +62,12 @@ public class PasswordAuthStrategy implements IAuthStrategy {
         if (captchaEnabled) {
             validateCaptcha(tenantId, username, code, uuid);
         }
-
-        SysUserVo user = loadUserByUsername(tenantId, username);
-        loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
-        // 此处可根据登录用户的数据不同 自行创建 loginUser
-        LoginUser loginUser = loginService.buildLoginUser(user);
+        LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+            SysUserVo user = loadUserByUsername(username);
+            loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
+            // 此处可根据登录用户的数据不同 自行创建 loginUser
+            return loginService.buildLoginUser(user);
+        });
         loginUser.setClientKey(client.getClientKey());
         loginUser.setDeviceType(client.getDeviceType());
         SaLoginModel model = new SaLoginModel();
@@ -107,18 +108,16 @@ public class PasswordAuthStrategy implements IAuthStrategy {
         }
     }
 
-    private SysUserVo loadUserByUsername(String tenantId, String username) {
-        return TenantHelper.dynamic(tenantId, () -> {
-            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
-            if (ObjectUtil.isNull(user)) {
-                log.info("登录用户:{} 不存在.", username);
-                throw new UserException("user.not.exists", username);
-            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
-                log.info("登录用户:{} 已被停用.", username);
-                throw new UserException("user.blocked", username);
-            }
-            return user;
-        });
+    private SysUserVo loadUserByUsername(String username) {
+        SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", username);
+            throw new UserException("user.not.exists", username);
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", username);
+            throw new UserException("user.blocked", username);
+        }
+        return user;
     }
 
 }

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

@@ -21,7 +21,6 @@ import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.tenant.helper.TenantHelper;
-import org.dromara.system.domain.SysClient;
 import org.dromara.system.domain.SysUser;
 import org.dromara.system.domain.vo.SysClientVo;
 import org.dromara.system.domain.vo.SysUserVo;
@@ -51,13 +50,12 @@ public class SmsAuthStrategy implements IAuthStrategy {
         String tenantId = loginBody.getTenantId();
         String phonenumber = loginBody.getPhonenumber();
         String smsCode = loginBody.getSmsCode();
-
-        // 通过手机号查找用户
-        SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
-
-        loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
-        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
-        LoginUser loginUser = loginService.buildLoginUser(user);
+        LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+            SysUserVo user = loadUserByPhonenumber(phonenumber);
+            loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
+            // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+            return loginService.buildLoginUser(user);
+        });
         loginUser.setClientKey(client.getClientKey());
         loginUser.setDeviceType(client.getDeviceType());
         SaLoginModel model = new SaLoginModel();
@@ -89,18 +87,16 @@ public class SmsAuthStrategy implements IAuthStrategy {
         return code.equals(smsCode);
     }
 
-    private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
-        return TenantHelper.dynamic(tenantId, () -> {
-            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
-            if (ObjectUtil.isNull(user)) {
-                log.info("登录用户:{} 不存在.", phonenumber);
-                throw new UserException("user.not.exists", phonenumber);
-            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
-                log.info("登录用户:{} 已被停用.", phonenumber);
-                throw new UserException("user.blocked", phonenumber);
-            }
-            return user;
-        });
+    private SysUserVo loadUserByPhonenumber(String phonenumber) {
+        SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", phonenumber);
+            throw new UserException("user.not.exists", phonenumber);
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", phonenumber);
+            throw new UserException("user.blocked", phonenumber);
+        }
+        return user;
     }
 
 }

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

@@ -92,11 +92,11 @@ public class SocialAuthStrategy implements IAuthStrategy {
         } else {
             social = list.get(0);
         }
-        // 查找用户
-        SysUserVo user = loadUser(social.getTenantId(), social.getUserId());
-
-        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
-        LoginUser loginUser = loginService.buildLoginUser(user);
+        LoginUser loginUser = TenantHelper.dynamic(social.getTenantId(), () -> {
+            SysUserVo user = loadUser(social.getUserId());
+            // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+            return loginService.buildLoginUser(user);
+        });
         loginUser.setClientKey(client.getClientKey());
         loginUser.setDeviceType(client.getDeviceType());
         SaLoginModel model = new SaLoginModel();
@@ -116,18 +116,16 @@ public class SocialAuthStrategy implements IAuthStrategy {
         return loginVo;
     }
 
-    private SysUserVo loadUser(String tenantId, Long userId) {
-        return TenantHelper.dynamic(tenantId, () -> {
-            SysUserVo user = userMapper.selectVoById(userId);
-            if (ObjectUtil.isNull(user)) {
-                log.info("登录用户:{} 不存在.", "");
-                throw new UserException("user.not.exists", "");
-            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
-                log.info("登录用户:{} 已被停用.", "");
-                throw new UserException("user.blocked", "");
-            }
-            return user;
-        });
+    private SysUserVo loadUser(Long userId) {
+        SysUserVo user = userMapper.selectVoById(userId);
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", "");
+            throw new UserException("user.not.exists", "");
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", "");
+            throw new UserException("user.blocked", "");
+        }
+        return user;
     }
 
 }

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

@@ -5,6 +5,9 @@ spring.boot.admin.client:
   url: http://localhost:9090/admin
   instance:
     service-host-type: IP
+    metadata:
+      username: ${spring.boot.admin.client.username}
+      userpassword: ${spring.boot.admin.client.password}
   username: ruoyi
   password: 123456
 

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

@@ -8,6 +8,9 @@ spring.boot.admin.client:
   url: http://localhost:9090/admin
   instance:
     service-host-type: IP
+    metadata:
+      username: ${spring.boot.admin.client.username}
+      userpassword: ${spring.boot.admin.client.password}
   username: ruoyi
   password: 123456
 

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

@@ -121,9 +121,6 @@ security:
     # swagger 文档配置
     - /*/api-docs
     - /*/api-docs/**
-    # actuator 监控配置
-    - /actuator
-    - /actuator/**
 
 # 多租户配置
 tenant:
@@ -259,10 +256,15 @@ management:
     logfile:
       external-file: ./logs/sys-console.log
 
+--- # 默认/推荐使用sse推送
+sse:
+  enabled: true
+  path: /resource/sse
+
 --- # websocket
 websocket:
   # 如果关闭 需要和前端开关一起关闭
-  enabled: true
+  enabled: false
   # 路径
   path: /resource/websocket
   # 设置访问源地址
@@ -270,6 +272,10 @@ websocket:
 
 --- #flowable配置
 flowable:
+  # 开关 用于启动/停用工作流
+  enabled: true
+  process.enabled: ${flowable.enabled}
+  eventregistry.enabled: ${flowable.enabled}
   async-executor-activate: false #关闭定时任务JOB
   #  将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
   database-schema-update: true

+ 1 - 0
ruoyi-common/pom.xml

@@ -33,6 +33,7 @@
         <module>ruoyi-common-encrypt</module>
         <module>ruoyi-common-tenant</module>
         <module>ruoyi-common-websocket</module>
+        <module>ruoyi-common-sse</module>
     </modules>
 
     <artifactId>ruoyi-common</artifactId>

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

@@ -14,7 +14,7 @@
     </description>
 
     <properties>
-        <revision>5.2.1</revision>
+        <revision>5.2.2</revision>
     </properties>
 
     <dependencyManagement>
@@ -172,6 +172,13 @@
                 <version>${revision}</version>
             </dependency>
 
+            <!-- SSE模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-sse</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>
 

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

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

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

@@ -22,4 +22,9 @@ public interface CacheConstants {
      */
     String SYS_DICT_KEY = "sys_dict:";
 
+    /**
+     * 登录账户密码错误次数 redis key
+     */
+    String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+
 }

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

@@ -27,11 +27,6 @@ public interface GlobalConstants {
      */
     String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
 
-    /**
-     * 登录账户密码错误次数 redis key
-     */
-    String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
-
     /**
      * 三方认证 redis key
      */

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

@@ -67,6 +67,16 @@ public interface UserConstants {
      */
     String DICT_NORMAL = "0";
 
+    /**
+     * 通用存在标志
+     */
+    String DEL_FLAG_NORMAL = "0";
+
+    /**
+     * 通用删除标志
+     */
+    String DEL_FLAG_REMOVED  = "2";
+
     /**
      * 是否为系统默认(是)
      */

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

@@ -66,4 +66,20 @@ public interface UserService {
      * @return 用户ids
      */
     List<Long> selectUserIdsByRoleIds(List<Long> roleIds);
+
+    /**
+     * 通过角色ID查询用户
+     *
+     * @param roleIds 角色ids
+     * @return 用户
+     */
+    List<UserDTO> selectUsersByRoleIds(List<Long> roleIds);
+
+    /**
+     * 通过部门ID查询用户
+     *
+     * @param deptIds 部门ids
+     * @return 用户
+     */
+    List<UserDTO> selectUsersByDeptIds(List<Long> deptIds);
 }

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

@@ -25,6 +25,11 @@
             <groupId>com.alibaba</groupId>
             <artifactId>easyexcel</artifactId>
         </dependency>
+        <dependency>
+            <artifactId>commons-compress</artifactId>
+            <groupId>org.apache.commons</groupId>
+            <version>1.26.2</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 6 - 1
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java

@@ -107,7 +107,7 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
                     }
 
                     if (!cellValue.equals(val)) {
-                        if ((i - repeatCell.getCurrent() > 1) && isMerge(list, i, field)) {
+                        if ((i - repeatCell.getCurrent() > 1)) {
                             cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
                         }
                         map.put(field, new RepeatCell(val, i));
@@ -115,6 +115,11 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
                         if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
                             cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
                         }
+                    } else if (!isMerge(list, i, field)) {
+                        if ((i - repeatCell.getCurrent() > 1)) {
+                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
+                        }
+                        map.put(field, new RepeatCell(val, i));
                     }
                 }
             }

+ 1 - 1
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java

@@ -1,7 +1,7 @@
 package org.dromara.common.mail.config;
 
+import cn.hutool.extra.mail.MailAccount;
 import org.dromara.common.mail.config.properties.MailProperties;
-import org.dromara.common.mail.utils.MailAccount;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;

+ 0 - 46
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/GlobalMailAccount.java

@@ -1,46 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.io.IORuntimeException;
-
-/**
- * 全局邮件帐户,依赖于邮件配置文件{@link MailAccount#MAIL_SETTING_PATHS}
- *
- * @author looly
- */
-public enum GlobalMailAccount {
-    INSTANCE;
-
-    private final MailAccount mailAccount;
-
-    /**
-     * 构造
-     */
-    GlobalMailAccount() {
-        mailAccount = createDefaultAccount();
-    }
-
-    /**
-     * 获得邮件帐户
-     *
-     * @return 邮件帐户
-     */
-    public MailAccount getAccount() {
-        return this.mailAccount;
-    }
-
-    /**
-     * 创建默认帐户
-     *
-     * @return MailAccount
-     */
-    private MailAccount createDefaultAccount() {
-        for (String mailSettingPath : MailAccount.MAIL_SETTING_PATHS) {
-            try {
-                return new MailAccount(mailSettingPath);
-            } catch (IORuntimeException ignore) {
-                //ignore
-            }
-        }
-        return null;
-    }
-}

+ 0 - 108
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/InternalMailUtil.java

@@ -1,108 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.util.ArrayUtil;
-import jakarta.mail.internet.AddressException;
-import jakarta.mail.internet.InternetAddress;
-import jakarta.mail.internet.MimeUtility;
-
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * 邮件内部工具类
- *
- * @author looly
- * @since 3.2.3
- */
-public class InternalMailUtil {
-
-    /**
-     * 将多个字符串邮件地址转为{@link InternetAddress}列表<br>
-     * 单个字符串地址可以是多个地址合并的字符串
-     *
-     * @param addrStrs 地址数组
-     * @param charset  编码(主要用于中文用户名的编码)
-     * @return 地址数组
-     * @since 4.0.3
-     */
-    public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {
-        final List<InternetAddress> resultList = new ArrayList<>(addrStrs.length);
-        InternetAddress[] addrs;
-        for (String addrStr : addrStrs) {
-            addrs = parseAddress(addrStr, charset);
-            if (ArrayUtil.isNotEmpty(addrs)) {
-                Collections.addAll(resultList, addrs);
-            }
-        }
-        return resultList.toArray(new InternetAddress[0]);
-    }
-
-    /**
-     * 解析第一个地址
-     *
-     * @param address 地址字符串
-     * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
-     * @return 地址列表
-     */
-    public static InternetAddress parseFirstAddress(String address, Charset charset) {
-        final InternetAddress[] internetAddresses = parseAddress(address, charset);
-        if (ArrayUtil.isEmpty(internetAddresses)) {
-            try {
-                return new InternetAddress(address);
-            } catch (AddressException e) {
-                throw new MailException(e);
-            }
-        }
-        return internetAddresses[0];
-    }
-
-    /**
-     * 将一个地址字符串解析为多个地址<br>
-     * 地址间使用" "、","、";"分隔
-     *
-     * @param address 地址字符串
-     * @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
-     * @return 地址列表
-     */
-    public static InternetAddress[] parseAddress(String address, Charset charset) {
-        InternetAddress[] addresses;
-        try {
-            addresses = InternetAddress.parse(address);
-        } catch (AddressException e) {
-            throw new MailException(e);
-        }
-        //编码用户名
-        if (ArrayUtil.isNotEmpty(addresses)) {
-            final String charsetStr = null == charset ? null : charset.name();
-            for (InternetAddress internetAddress : addresses) {
-                try {
-                    internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);
-                } catch (UnsupportedEncodingException e) {
-                    throw new MailException(e);
-                }
-            }
-        }
-
-        return addresses;
-    }
-
-    /**
-     * 编码中文字符<br>
-     * 编码失败返回原字符串
-     *
-     * @param text    被编码的文本
-     * @param charset 编码
-     * @return 编码后的结果
-     */
-    public static String encodeText(String text, Charset charset) {
-        try {
-            return MimeUtility.encodeText(text, charset.name(), null);
-        } catch (UnsupportedEncodingException e) {
-            // ignore
-        }
-        return text;
-    }
-}

+ 0 - 483
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/Mail.java

@@ -1,483 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.builder.Builder;
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.IORuntimeException;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
-import jakarta.activation.DataHandler;
-import jakarta.activation.DataSource;
-import jakarta.activation.FileDataSource;
-import jakarta.activation.FileTypeMap;
-import jakarta.mail.*;
-import jakarta.mail.internet.MimeBodyPart;
-import jakarta.mail.internet.MimeMessage;
-import jakarta.mail.internet.MimeMultipart;
-import jakarta.mail.internet.MimeUtility;
-import jakarta.mail.util.ByteArrayDataSource;
-
-import java.io.*;
-import java.nio.charset.Charset;
-import java.util.Date;
-
-/**
- * 邮件发送客户端
- *
- * @author looly
- * @since 3.2.0
- */
-public class Mail implements Builder<MimeMessage> {
-    @Serial
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 邮箱帐户信息以及一些客户端配置信息
-     */
-    private final MailAccount mailAccount;
-    /**
-     * 收件人列表
-     */
-    private String[] tos;
-    /**
-     * 抄送人列表(carbon copy)
-     */
-    private String[] ccs;
-    /**
-     * 密送人列表(blind carbon copy)
-     */
-    private String[] bccs;
-    /**
-     * 回复地址(reply-to)
-     */
-    private String[] reply;
-    /**
-     * 标题
-     */
-    private String title;
-    /**
-     * 内容
-     */
-    private String content;
-    /**
-     * 是否为HTML
-     */
-    private boolean isHtml;
-    /**
-     * 正文、附件和图片的混合部分
-     */
-    private final Multipart multipart = new MimeMultipart();
-    /**
-     * 是否使用全局会话,默认为false
-     */
-    private boolean useGlobalSession = false;
-
-    /**
-     * debug输出位置,可以自定义debug日志
-     */
-    private PrintStream debugOutput;
-
-    /**
-     * 创建邮件客户端
-     *
-     * @param mailAccount 邮件帐号
-     * @return Mail
-     */
-    public static Mail create(MailAccount mailAccount) {
-        return new Mail(mailAccount);
-    }
-
-    /**
-     * 创建邮件客户端,使用全局邮件帐户
-     *
-     * @return Mail
-     */
-    public static Mail create() {
-        return new Mail();
-    }
-
-    // --------------------------------------------------------------- Constructor start
-
-    /**
-     * 构造,使用全局邮件帐户
-     */
-    public Mail() {
-        this(GlobalMailAccount.INSTANCE.getAccount());
-    }
-
-    /**
-     * 构造
-     *
-     * @param mailAccount 邮件帐户,如果为null使用默认配置文件的全局邮件配置
-     */
-    public Mail(MailAccount mailAccount) {
-        mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
-        this.mailAccount = mailAccount.defaultIfEmpty();
-    }
-    // --------------------------------------------------------------- Constructor end
-
-    // --------------------------------------------------------------- Getters and Setters start
-
-    /**
-     * 设置收件人
-     *
-     * @param tos 收件人列表
-     * @return this
-     * @see #setTos(String...)
-     */
-    public Mail to(String... tos) {
-        return setTos(tos);
-    }
-
-    /**
-     * 设置多个收件人
-     *
-     * @param tos 收件人列表
-     * @return this
-     */
-    public Mail setTos(String... tos) {
-        this.tos = tos;
-        return this;
-    }
-
-    /**
-     * 设置多个抄送人(carbon copy)
-     *
-     * @param ccs 抄送人列表
-     * @return this
-     * @since 4.0.3
-     */
-    public Mail setCcs(String... ccs) {
-        this.ccs = ccs;
-        return this;
-    }
-
-    /**
-     * 设置多个密送人(blind carbon copy)
-     *
-     * @param bccs 密送人列表
-     * @return this
-     * @since 4.0.3
-     */
-    public Mail setBccs(String... bccs) {
-        this.bccs = bccs;
-        return this;
-    }
-
-    /**
-     * 设置多个回复地址(reply-to)
-     *
-     * @param reply 回复地址(reply-to)列表
-     * @return this
-     * @since 4.6.0
-     */
-    public Mail setReply(String... reply) {
-        this.reply = reply;
-        return this;
-    }
-
-    /**
-     * 设置标题
-     *
-     * @param title 标题
-     * @return this
-     */
-    public Mail setTitle(String title) {
-        this.title = title;
-        return this;
-    }
-
-    /**
-     * 设置正文<br>
-     * 正文可以是普通文本也可以是HTML(默认普通文本),可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
-     *
-     * @param content 正文
-     * @return this
-     */
-    public Mail setContent(String content) {
-        this.content = content;
-        return this;
-    }
-
-    /**
-     * 设置是否是HTML
-     *
-     * @param isHtml 是否为HTML
-     * @return this
-     */
-    public Mail setHtml(boolean isHtml) {
-        this.isHtml = isHtml;
-        return this;
-    }
-
-    /**
-     * 设置正文
-     *
-     * @param content 正文内容
-     * @param isHtml  是否为HTML
-     * @return this
-     */
-    public Mail setContent(String content, boolean isHtml) {
-        setContent(content);
-        return setHtml(isHtml);
-    }
-
-    /**
-     * 设置文件类型附件,文件可以是图片文件,此时自动设置cid(正文中引用图片),默认cid为文件名
-     *
-     * @param files 附件文件列表
-     * @return this
-     */
-    public Mail setFiles(File... files) {
-        if (ArrayUtil.isEmpty(files)) {
-            return this;
-        }
-
-        final DataSource[] attachments = new DataSource[files.length];
-        for (int i = 0; i < files.length; i++) {
-            attachments[i] = new FileDataSource(files[i]);
-        }
-        return setAttachments(attachments);
-    }
-
-    /**
-     * 增加附件或图片,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件
-     *
-     * @param attachments 附件列表
-     * @return this
-     * @since 4.0.9
-     */
-    public Mail setAttachments(DataSource... attachments) {
-        if (ArrayUtil.isNotEmpty(attachments)) {
-            final Charset charset = this.mailAccount.getCharset();
-            MimeBodyPart bodyPart;
-            String nameEncoded;
-            try {
-                for (DataSource attachment : attachments) {
-                    bodyPart = new MimeBodyPart();
-                    bodyPart.setDataHandler(new DataHandler(attachment));
-                    nameEncoded = attachment.getName();
-                    if (this.mailAccount.isEncodefilename()) {
-                        nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
-                    }
-                    // 普通附件文件名
-                    bodyPart.setFileName(nameEncoded);
-                    if (StrUtil.startWith(attachment.getContentType(), "image/")) {
-                        // 图片附件,用于正文中引用图片
-                        bodyPart.setContentID(nameEncoded);
-                    }
-                    this.multipart.addBodyPart(bodyPart);
-                }
-            } catch (MessagingException e) {
-                throw new MailException(e);
-            }
-        }
-        return this;
-    }
-
-    /**
-     * 增加图片,图片的键对应到邮件模板中的占位字符串,图片类型默认为"image/jpeg"
-     *
-     * @param cid         图片与占位符,占位符格式为cid:${cid}
-     * @param imageStream 图片文件
-     * @return this
-     * @since 4.6.3
-     */
-    public Mail addImage(String cid, InputStream imageStream) {
-        return addImage(cid, imageStream, null);
-    }
-
-    /**
-     * 增加图片,图片的键对应到邮件模板中的占位字符串
-     *
-     * @param cid         图片与占位符,占位符格式为cid:${cid}
-     * @param imageStream 图片流,不关闭
-     * @param contentType 图片类型,null赋值默认的"image/jpeg"
-     * @return this
-     * @since 4.6.3
-     */
-    public Mail addImage(String cid, InputStream imageStream, String contentType) {
-        ByteArrayDataSource imgSource;
-        try {
-            imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
-        } catch (IOException e) {
-            throw new IORuntimeException(e);
-        }
-        imgSource.setName(cid);
-        return setAttachments(imgSource);
-    }
-
-    /**
-     * 增加图片,图片的键对应到邮件模板中的占位字符串
-     *
-     * @param cid       图片与占位符,占位符格式为cid:${cid}
-     * @param imageFile 图片文件
-     * @return this
-     * @since 4.6.3
-     */
-    public Mail addImage(String cid, File imageFile) {
-        InputStream in = null;
-        try {
-            in = FileUtil.getInputStream(imageFile);
-            return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
-        } finally {
-            IoUtil.close(in);
-        }
-    }
-
-    /**
-     * 设置字符集编码
-     *
-     * @param charset 字符集编码
-     * @return this
-     * @see MailAccount#setCharset(Charset)
-     */
-    public Mail setCharset(Charset charset) {
-        this.mailAccount.setCharset(charset);
-        return this;
-    }
-
-    /**
-     * 设置是否使用全局会话,默认为true
-     *
-     * @param isUseGlobalSession 是否使用全局会话,默认为true
-     * @return this
-     * @since 4.0.2
-     */
-    public Mail setUseGlobalSession(boolean isUseGlobalSession) {
-        this.useGlobalSession = isUseGlobalSession;
-        return this;
-    }
-
-    /**
-     * 设置debug输出位置,可以自定义debug日志
-     *
-     * @param debugOutput debug输出位置
-     * @return this
-     * @since 5.5.6
-     */
-    public Mail setDebugOutput(PrintStream debugOutput) {
-        this.debugOutput = debugOutput;
-        return this;
-    }
-    // --------------------------------------------------------------- Getters and Setters end
-
-    @Override
-    public MimeMessage build() {
-        try {
-            return buildMsg();
-        } catch (MessagingException e) {
-            throw new MailException(e);
-        }
-    }
-
-    /**
-     * 发送
-     *
-     * @return message-id
-     * @throws MailException 邮件发送异常
-     */
-    public String send() throws MailException {
-        try {
-            return doSend();
-        } catch (MessagingException e) {
-            if (e instanceof SendFailedException) {
-                // 当地址无效时,显示更加详细的无效地址信息
-                final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
-                final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
-                throw new MailException(msg, e);
-            }
-            throw new MailException(e);
-        }
-    }
-
-    // --------------------------------------------------------------- Private method start
-
-    /**
-     * 执行发送
-     *
-     * @return message-id
-     * @throws MessagingException 发送异常
-     */
-    private String doSend() throws MessagingException {
-        final MimeMessage mimeMessage = buildMsg();
-        Transport.send(mimeMessage);
-        return mimeMessage.getMessageID();
-    }
-
-    /**
-     * 构建消息
-     *
-     * @return {@link MimeMessage}消息
-     * @throws MessagingException 消息异常
-     */
-    private MimeMessage buildMsg() throws MessagingException {
-        final Charset charset = this.mailAccount.getCharset();
-        final MimeMessage msg = new MimeMessage(getSession());
-        // 发件人
-        final String from = this.mailAccount.getFrom();
-        if (StrUtil.isEmpty(from)) {
-            // 用户未提供发送方,则从Session中自动获取
-            msg.setFrom();
-        } else {
-            msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
-        }
-        // 标题
-        msg.setSubject(this.title, (null == charset) ? null : charset.name());
-        // 发送时间
-        msg.setSentDate(new Date());
-        // 内容和附件
-        msg.setContent(buildContent(charset));
-        // 收件人
-        msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
-        // 抄送人
-        if (ArrayUtil.isNotEmpty(this.ccs)) {
-            msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
-        }
-        // 密送人
-        if (ArrayUtil.isNotEmpty(this.bccs)) {
-            msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
-        }
-        // 回复地址(reply-to)
-        if (ArrayUtil.isNotEmpty(this.reply)) {
-            msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
-        }
-
-        return msg;
-    }
-
-    /**
-     * 构建邮件信息主体
-     *
-     * @param charset 编码,{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}
-     * @return 邮件信息主体
-     * @throws MessagingException 消息异常
-     */
-    private Multipart buildContent(Charset charset) throws MessagingException {
-        final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
-        // 正文
-        final MimeBodyPart body = new MimeBodyPart();
-        body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
-        this.multipart.addBodyPart(body);
-
-        return this.multipart;
-    }
-
-    /**
-     * 获取默认邮件会话<br>
-     * 如果为全局单例的会话,则全局只允许一个邮件帐号,否则每次发送邮件会新建一个新的会话
-     *
-     * @return 邮件会话 {@link Session}
-     */
-    private Session getSession() {
-        final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);
-
-        if (null != this.debugOutput) {
-            session.setDebugOut(debugOutput);
-        }
-
-        return session;
-    }
-    // --------------------------------------------------------------- Private method end
-}

+ 0 - 659
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailAccount.java

@@ -1,659 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.util.CharsetUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.setting.Setting;
-
-import java.io.Serial;
-import java.io.Serializable;
-import java.nio.charset.Charset;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-
-/**
- * 邮件账户对象
- *
- * @author Luxiaolei
- */
-public class MailAccount implements Serializable {
-    @Serial
-    private static final long serialVersionUID = -6937313421815719204L;
-
-    private static final String MAIL_PROTOCOL = "mail.transport.protocol";
-    private static final String SMTP_HOST = "mail.smtp.host";
-    private static final String SMTP_PORT = "mail.smtp.port";
-    private static final String SMTP_AUTH = "mail.smtp.auth";
-    private static final String SMTP_TIMEOUT = "mail.smtp.timeout";
-    private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
-    private static final String SMTP_WRITE_TIMEOUT = "mail.smtp.writetimeout";
-
-    // SSL
-    private static final String STARTTLS_ENABLE = "mail.smtp.starttls.enable";
-    private static final String SSL_ENABLE = "mail.smtp.ssl.enable";
-    private static final String SSL_PROTOCOLS = "mail.smtp.ssl.protocols";
-    private static final String SOCKET_FACTORY = "mail.smtp.socketFactory.class";
-    private static final String SOCKET_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback";
-    private static final String SOCKET_FACTORY_PORT = "smtp.socketFactory.port";
-
-    // System Properties
-    private static final String SPLIT_LONG_PARAMS = "mail.mime.splitlongparameters";
-    //private static final String ENCODE_FILE_NAME = "mail.mime.encodefilename";
-    //private static final String CHARSET = "mail.mime.charset";
-
-    // 其他
-    private static final String MAIL_DEBUG = "mail.debug";
-
-    public static final String[] MAIL_SETTING_PATHS = new String[]{"config/mail.setting", "config/mailAccount.setting", "mail.setting"};
-
-    /**
-     * SMTP服务器域名
-     */
-    private String host;
-    /**
-     * SMTP服务端口
-     */
-    private Integer port;
-    /**
-     * 是否需要用户名密码验证
-     */
-    private Boolean auth;
-    /**
-     * 用户名
-     */
-    private String user;
-    /**
-     * 密码
-     */
-    private String pass;
-    /**
-     * 发送方,遵循RFC-822标准
-     */
-    private String from;
-
-    /**
-     * 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
-     */
-    private boolean debug;
-    /**
-     * 编码用于编码邮件正文和发送人、收件人等中文
-     */
-    private Charset charset = CharsetUtil.CHARSET_UTF_8;
-    /**
-     * 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
-     */
-    private boolean splitlongparameters = false;
-    /**
-     * 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
-     */
-    private boolean encodefilename = true;
-
-    /**
-     * 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
-     */
-    private boolean starttlsEnable = false;
-    /**
-     * 使用 SSL安全连接
-     */
-    private Boolean sslEnable;
-
-    /**
-     * SSL协议,多个协议用空格分隔
-     */
-    private String sslProtocols;
-
-    /**
-     * 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
-     */
-    private String socketFactoryClass = "javax.net.ssl.SSLSocketFactory";
-    /**
-     * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
-     */
-    private boolean socketFactoryFallback;
-    /**
-     * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
-     */
-    private int socketFactoryPort = 465;
-
-    /**
-     * SMTP超时时长,单位毫秒,缺省值不超时
-     */
-    private long timeout;
-    /**
-     * Socket连接超时值,单位毫秒,缺省值不超时
-     */
-    private long connectionTimeout;
-    /**
-     * Socket写出超时值,单位毫秒,缺省值不超时
-     */
-    private long writeTimeout;
-
-    /**
-     * 自定义的其他属性,此自定义属性会覆盖默认属性
-     */
-    private final Map<String, Object> customProperty = new HashMap<>();
-
-    // -------------------------------------------------------------- Constructor start
-
-    /**
-     * 构造,所有参数需自行定义或保持默认值
-     */
-    public MailAccount() {
-    }
-
-    /**
-     * 构造
-     *
-     * @param settingPath 配置文件路径
-     */
-    public MailAccount(String settingPath) {
-        this(new Setting(settingPath));
-    }
-
-    /**
-     * 构造
-     *
-     * @param setting 配置文件
-     */
-    public MailAccount(Setting setting) {
-        setting.toBean(this);
-    }
-
-    // -------------------------------------------------------------- Constructor end
-
-    /**
-     * 获得SMTP服务器域名
-     *
-     * @return SMTP服务器域名
-     */
-    public String getHost() {
-        return host;
-    }
-
-    /**
-     * 设置SMTP服务器域名
-     *
-     * @param host SMTP服务器域名
-     * @return this
-     */
-    public MailAccount setHost(String host) {
-        this.host = host;
-        return this;
-    }
-
-    /**
-     * 获得SMTP服务端口
-     *
-     * @return SMTP服务端口
-     */
-    public Integer getPort() {
-        return port;
-    }
-
-    /**
-     * 设置SMTP服务端口
-     *
-     * @param port SMTP服务端口
-     * @return this
-     */
-    public MailAccount setPort(Integer port) {
-        this.port = port;
-        return this;
-    }
-
-    /**
-     * 是否需要用户名密码验证
-     *
-     * @return 是否需要用户名密码验证
-     */
-    public Boolean isAuth() {
-        return auth;
-    }
-
-    /**
-     * 设置是否需要用户名密码验证
-     *
-     * @param isAuth 是否需要用户名密码验证
-     * @return this
-     */
-    public MailAccount setAuth(boolean isAuth) {
-        this.auth = isAuth;
-        return this;
-    }
-
-    /**
-     * 获取用户名
-     *
-     * @return 用户名
-     */
-    public String getUser() {
-        return user;
-    }
-
-    /**
-     * 设置用户名
-     *
-     * @param user 用户名
-     * @return this
-     */
-    public MailAccount setUser(String user) {
-        this.user = user;
-        return this;
-    }
-
-    /**
-     * 获取密码
-     *
-     * @return 密码
-     */
-    public String getPass() {
-        return pass;
-    }
-
-    /**
-     * 设置密码
-     *
-     * @param pass 密码
-     * @return this
-     */
-    public MailAccount setPass(String pass) {
-        this.pass = pass;
-        return this;
-    }
-
-    /**
-     * 获取发送方,遵循RFC-822标准
-     *
-     * @return 发送方,遵循RFC-822标准
-     */
-    public String getFrom() {
-        return from;
-    }
-
-    /**
-     * 设置发送方,遵循RFC-822标准<br>
-     * 发件人可以是以下形式:
-     *
-     * <pre>
-     * 1. user@xxx.xx
-     * 2.  name &lt;user@xxx.xx&gt;
-     * </pre>
-     *
-     * @param from 发送方,遵循RFC-822标准
-     * @return this
-     */
-    public MailAccount setFrom(String from) {
-        this.from = from;
-        return this;
-    }
-
-    /**
-     * 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
-     *
-     * @return 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
-     * @since 4.0.2
-     */
-    public boolean isDebug() {
-        return debug;
-    }
-
-    /**
-     * 设置是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
-     *
-     * @param debug 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
-     * @return this
-     * @since 4.0.2
-     */
-    public MailAccount setDebug(boolean debug) {
-        this.debug = debug;
-        return this;
-    }
-
-    /**
-     * 获取字符集编码
-     *
-     * @return 编码,可能为{@code null}
-     */
-    public Charset getCharset() {
-        return charset;
-    }
-
-    /**
-     * 设置字符集编码,此选项不会修改全局配置,若修改全局配置,请设置此项为{@code null}并设置:
-     * <pre>
-     * 	System.setProperty("mail.mime.charset", charset);
-     * </pre>
-     *
-     * @param charset 字符集编码,{@code null} 则表示使用全局设置的默认编码,全局编码为mail.mime.charset系统属性
-     * @return this
-     */
-    public MailAccount setCharset(Charset charset) {
-        this.charset = charset;
-        return this;
-    }
-
-    /**
-     * 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)
-     *
-     * @return 对于超长参数是否切分为多份
-     */
-    public boolean isSplitlongparameters() {
-        return splitlongparameters;
-    }
-
-    /**
-     * 设置对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名)<br>
-     * 注意此项为全局设置,此项会调用
-     * <pre>
-     * System.setProperty("mail.mime.splitlongparameters", true)
-     * </pre>
-     *
-     * @param splitlongparameters 对于超长参数是否切分为多份
-     */
-    public void setSplitlongparameters(boolean splitlongparameters) {
-        this.splitlongparameters = splitlongparameters;
-    }
-
-    /**
-     * 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
-     *
-     * @return 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
-     * @since 5.7.16
-     */
-    public boolean isEncodefilename() {
-
-        return encodefilename;
-    }
-
-    /**
-     * 设置对于文件名是否使用{@link #charset}编码,此选项不会修改全局配置<br>
-     * 如果此选项设置为{@code false},则是否编码取决于两个系统属性:
-     * <ul>
-     *     <li>mail.mime.encodefilename  是否编码附件文件名</li>
-     *     <li>mail.mime.charset         编码文件名的编码</li>
-     * </ul>
-     *
-     * @param encodefilename 对于文件名是否使用{@link #charset}编码
-     * @since 5.7.16
-     */
-    public void setEncodefilename(boolean encodefilename) {
-        this.encodefilename = encodefilename;
-    }
-
-    /**
-     * 是否使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
-     *
-     * @return 是否使用 STARTTLS安全连接
-     */
-    public boolean isStarttlsEnable() {
-        return this.starttlsEnable;
-    }
-
-    /**
-     * 设置是否使用STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
-     *
-     * @param startttlsEnable 是否使用STARTTLS安全连接
-     * @return this
-     */
-    public MailAccount setStarttlsEnable(boolean startttlsEnable) {
-        this.starttlsEnable = startttlsEnable;
-        return this;
-    }
-
-    /**
-     * 是否使用 SSL安全连接
-     *
-     * @return 是否使用 SSL安全连接
-     */
-    public Boolean isSslEnable() {
-        return this.sslEnable;
-    }
-
-    /**
-     * 设置是否使用SSL安全连接
-     *
-     * @param sslEnable 是否使用SSL安全连接
-     * @return this
-     */
-    public MailAccount setSslEnable(Boolean sslEnable) {
-        this.sslEnable = sslEnable;
-        return this;
-    }
-
-    /**
-     * 获取SSL协议,多个协议用空格分隔
-     *
-     * @return SSL协议,多个协议用空格分隔
-     * @since 5.5.7
-     */
-    public String getSslProtocols() {
-        return sslProtocols;
-    }
-
-    /**
-     * 设置SSL协议,多个协议用空格分隔
-     *
-     * @param sslProtocols SSL协议,多个协议用空格分隔
-     * @since 5.5.7
-     */
-    public void setSslProtocols(String sslProtocols) {
-        this.sslProtocols = sslProtocols;
-    }
-
-    /**
-     * 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
-     *
-     * @return 指定实现javax.net.SocketFactory接口的类的名称, 这个类将被用于创建SMTP的套接字
-     */
-    public String getSocketFactoryClass() {
-        return socketFactoryClass;
-    }
-
-    /**
-     * 设置指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
-     *
-     * @param socketFactoryClass 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
-     * @return this
-     */
-    public MailAccount setSocketFactoryClass(String socketFactoryClass) {
-        this.socketFactoryClass = socketFactoryClass;
-        return this;
-    }
-
-    /**
-     * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
-     *
-     * @return 如果设置为true, 未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
-     */
-    public boolean isSocketFactoryFallback() {
-        return socketFactoryFallback;
-    }
-
-    /**
-     * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
-     *
-     * @param socketFactoryFallback 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
-     * @return this
-     */
-    public MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) {
-        this.socketFactoryFallback = socketFactoryFallback;
-        return this;
-    }
-
-    /**
-     * 获取指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
-     *
-     * @return 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
-     */
-    public int getSocketFactoryPort() {
-        return socketFactoryPort;
-    }
-
-    /**
-     * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
-     *
-     * @param socketFactoryPort 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
-     * @return this
-     */
-    public MailAccount setSocketFactoryPort(int socketFactoryPort) {
-        this.socketFactoryPort = socketFactoryPort;
-        return this;
-    }
-
-    /**
-     * 设置SMTP超时时长,单位毫秒,缺省值不超时
-     *
-     * @param timeout SMTP超时时长,单位毫秒,缺省值不超时
-     * @return this
-     * @since 4.1.17
-     */
-    public MailAccount setTimeout(long timeout) {
-        this.timeout = timeout;
-        return this;
-    }
-
-    /**
-     * 设置Socket连接超时值,单位毫秒,缺省值不超时
-     *
-     * @param connectionTimeout Socket连接超时值,单位毫秒,缺省值不超时
-     * @return this
-     * @since 4.1.17
-     */
-    public MailAccount setConnectionTimeout(long connectionTimeout) {
-        this.connectionTimeout = connectionTimeout;
-        return this;
-    }
-
-    /**
-     * 设置Socket写出超时值,单位毫秒,缺省值不超时
-     *
-     * @param writeTimeout Socket写出超时值,单位毫秒,缺省值不超时
-     * @return this
-     * @since 5.8.3
-     */
-    public MailAccount setWriteTimeout(long writeTimeout) {
-        this.writeTimeout = writeTimeout;
-        return this;
-    }
-
-    /**
-     * 获取自定义属性列表
-     *
-     * @return 自定义参数列表
-     * @since 5.6.4
-     */
-    public Map<String, Object> getCustomProperty() {
-        return customProperty;
-    }
-
-    /**
-     * 设置自定义属性,如mail.smtp.ssl.socketFactory
-     *
-     * @param key   属性名,空白被忽略
-     * @param value 属性值, null被忽略
-     * @return this
-     * @since 5.6.4
-     */
-    public MailAccount setCustomProperty(String key, Object value) {
-        if (StrUtil.isNotBlank(key) && ObjectUtil.isNotNull(value)) {
-            this.customProperty.put(key, value);
-        }
-        return this;
-    }
-
-    /**
-     * 获得SMTP相关信息
-     *
-     * @return {@link Properties}
-     */
-    public Properties getSmtpProps() {
-        //全局系统参数
-        System.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters));
-
-        final Properties p = new Properties();
-        p.put(MAIL_PROTOCOL, "smtp");
-        p.put(SMTP_HOST, this.host);
-        p.put(SMTP_PORT, String.valueOf(this.port));
-        p.put(SMTP_AUTH, String.valueOf(this.auth));
-        if (this.timeout > 0) {
-            p.put(SMTP_TIMEOUT, String.valueOf(this.timeout));
-        }
-        if (this.connectionTimeout > 0) {
-            p.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout));
-        }
-        // issue#2355
-        if (this.writeTimeout > 0) {
-            p.put(SMTP_WRITE_TIMEOUT, String.valueOf(this.writeTimeout));
-        }
-
-        p.put(MAIL_DEBUG, String.valueOf(this.debug));
-
-        if (this.starttlsEnable) {
-            //STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
-            p.put(STARTTLS_ENABLE, "true");
-
-            if (null == this.sslEnable) {
-                //为了兼容旧版本,当用户没有此项配置时,按照starttlsEnable开启状态时对待
-                this.sslEnable = true;
-            }
-        }
-
-        // SSL
-        if (null != this.sslEnable && this.sslEnable) {
-            p.put(SSL_ENABLE, "true");
-            p.put(SOCKET_FACTORY, socketFactoryClass);
-            p.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback));
-            p.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort));
-            // issue#IZN95@Gitee,在Linux下需自定义SSL协议版本
-            if (StrUtil.isNotBlank(this.sslProtocols)) {
-                p.put(SSL_PROTOCOLS, this.sslProtocols);
-            }
-        }
-
-        // 补充自定义属性,允许自定属性覆盖已经设置的值
-        p.putAll(this.customProperty);
-
-        return p;
-    }
-
-    /**
-     * 如果某些值为null,使用默认值
-     *
-     * @return this
-     */
-    public MailAccount defaultIfEmpty() {
-        // 去掉发件人的姓名部分
-        final String fromAddress = InternalMailUtil.parseFirstAddress(this.from, this.charset).getAddress();
-
-        if (StrUtil.isBlank(this.host)) {
-            // 如果SMTP地址为空,默认使用smtp.<发件人邮箱后缀>
-            this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));
-        }
-        if (StrUtil.isBlank(user)) {
-            // 如果用户名为空,默认为发件人(issue#I4FYVY@Gitee)
-            //this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
-            this.user = fromAddress;
-        }
-        if (null == this.auth) {
-            // 如果密码非空白,则使用认证模式
-            this.auth = (false == StrUtil.isBlank(this.pass));
-        }
-        if (null == this.port) {
-            // 端口在SSL状态下默认与socketFactoryPort一致,非SSL状态下默认为25
-            this.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25;
-        }
-        if (null == this.charset) {
-            // 默认UTF-8编码
-            this.charset = CharsetUtil.CHARSET_UTF_8;
-        }
-
-        return this;
-    }
-
-    @Override
-    public String toString() {
-        return "MailAccount [host=" + host + ", port=" + port + ", auth=" + auth + ", user=" + user + ", pass=" + (StrUtil.isEmpty(this.pass) ? "" : "******") + ", from=" + from + ", startttlsEnable="
-            + starttlsEnable + ", socketFactoryClass=" + socketFactoryClass + ", socketFactoryFallback=" + socketFactoryFallback + ", socketFactoryPort=" + socketFactoryPort + "]";
-    }
-}

+ 0 - 40
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailException.java

@@ -1,40 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import cn.hutool.core.exceptions.ExceptionUtil;
-import cn.hutool.core.util.StrUtil;
-
-import java.io.Serial;
-
-/**
- * 邮件异常
- *
- * @author xiaoleilu
- */
-public class MailException extends RuntimeException {
-    @Serial
-    private static final long serialVersionUID = 8247610319171014183L;
-
-    public MailException(Throwable e) {
-        super(ExceptionUtil.getMessage(e), e);
-    }
-
-    public MailException(String message) {
-        super(message);
-    }
-
-    public MailException(String messageTemplate, Object... params) {
-        super(StrUtil.format(messageTemplate, params));
-    }
-
-    public MailException(String message, Throwable throwable) {
-        super(message, throwable);
-    }
-
-    public MailException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
-        super(message, throwable, enableSuppression, writableStackTrace);
-    }
-
-    public MailException(Throwable throwable, String messageTemplate, Object... params) {
-        super(StrUtil.format(messageTemplate, params), throwable);
-    }
-}

+ 7 - 5
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/MailUtils.java

@@ -5,6 +5,9 @@ import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.CharUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.mail.JakartaMail;
+import cn.hutool.extra.mail.JakartaUserPassAuthenticator;
+import cn.hutool.extra.mail.MailAccount;
 import jakarta.mail.Authenticator;
 import jakarta.mail.Session;
 import lombok.AccessLevel;
@@ -17,7 +20,7 @@ import java.io.InputStream;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-
+import java.util.Map.Entry;
 
 /**
  * 邮件工具类
@@ -385,7 +388,7 @@ public class MailUtils {
     public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
         Authenticator authenticator = null;
         if (mailAccount.isAuth()) {
-            authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
+            authenticator = new JakartaUserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
         }
 
         return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
@@ -412,7 +415,7 @@ public class MailUtils {
      */
     private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content,
                                Map<String, InputStream> imageMap, boolean isHtml, File... files) {
-        final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
+        final JakartaMail mail = JakartaMail.create(mailAccount).setUseGlobalSession(useGlobalSession);
 
         // 可选抄送人
         if (CollUtil.isNotEmpty(ccs)) {
@@ -431,7 +434,7 @@ public class MailUtils {
 
         // 图片
         if (MapUtil.isNotEmpty(imageMap)) {
-            for (Map.Entry<String, InputStream> entry : imageMap.entrySet()) {
+            for (Entry<String, InputStream> entry : imageMap.entrySet()) {
                 mail.addImage(entry.getKey(), entry.getValue());
                 // 关闭流
                 IoUtil.close(entry.getValue());
@@ -463,5 +466,4 @@ public class MailUtils {
         return result;
     }
     // ------------------------------------------------------------------------------------------------------------------------ Private method end
-
 }

+ 0 - 33
ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/utils/UserPassAuthenticator.java

@@ -1,33 +0,0 @@
-package org.dromara.common.mail.utils;
-
-import jakarta.mail.Authenticator;
-import jakarta.mail.PasswordAuthentication;
-
-/**
- * 用户名密码验证器
- *
- * @author looly
- * @since 3.1.2
- */
-public class UserPassAuthenticator extends Authenticator {
-
-    private final String user;
-    private final String pass;
-
-    /**
-     * 构造
-     *
-     * @param user 用户名
-     * @param pass 密码
-     */
-    public UserPassAuthenticator(String user, String pass) {
-        this.user = user;
-        this.pass = pass;
-    }
-
-    @Override
-    protected PasswordAuthentication getPasswordAuthentication() {
-        return new PasswordAuthentication(this.user, this.pass);
-    }
-
-}

+ 7 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataColumn.java

@@ -30,4 +30,11 @@ public @interface DataColumn {
      */
     String[] value() default "dept_id";
 
+    /**
+     * 权限标识符 用于通过菜单权限标识符来获取数据权限
+     * 拥有此标识符的角色 将不会拼接此角色的数据过滤sql
+     *
+     * @return 权限标识符
+     */
+    String permission() default "";
 }

+ 7 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/annotation/DataPermission.java

@@ -20,4 +20,11 @@ public @interface DataPermission {
      */
     DataColumn[] value();
 
+    /**
+     * 权限拼接标识符(用于指定连接语句的sql符号)
+     * 如不填 默认 select 用 OR 其他语句用 AND
+     * 内容 OR 或者 AND
+     */
+    String joinStr() default "";
+
 }

+ 2 - 10
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/enums/DataScopeType.java

@@ -13,9 +13,9 @@ import org.dromara.common.mybatis.helper.DataPermissionHelper;
  * 内置数据:
  * - {@code user}: 当前登录用户信息,参考 {@link LoginUser}
  * 内置服务:
- * - {@code sdss}: 系统数据权限服务,参考 {@link ISysDataScopeService}
+ * - {@code sdss}: 系统数据权限服务,参考 ISysDataScopeService
  * 如需扩展数据,可以通过 {@link DataPermissionHelper} 进行操作
- * 如需扩展服务,可以通过 {@link ISysDataScopeService} 自行编写
+ * 如需扩展服务,可以通过 ISysDataScopeService 自行编写
  * </p>
  *
  * @author Lion Li
@@ -32,29 +32,21 @@ public enum DataScopeType {
 
     /**
      * 自定数据权限
-     * 使用 SpEL 表达式:`#{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} )`
-     * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
      */
     CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", " 1 = 0 "),
 
     /**
      * 部门数据权限
-     * 使用 SpEL 表达式:`#{#deptName} = #{#user.deptId}`
-     * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
      */
     DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "),
 
     /**
      * 部门及以下数据权限
-     * 使用 SpEL 表达式:`#{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )}`
-     * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
      */
     DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "),
 
     /**
      * 仅本人数据权限
-     * 使用 SpEL 表达式:`#{#userName} = #{#user.userId}`
-     * 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
      */
     SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
 

+ 6 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/InjectionMetaObjectHandler.java

@@ -48,6 +48,10 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
                             ? baseEntity.getCreateDept() : loginUser.getDeptId());
                     }
                 }
+            } else {
+                Date date = new Date();
+                this.strictInsertFill(metaObject, "createTime", Date.class, date);
+                this.strictInsertFill(metaObject, "updateTime", Date.class, date);
             }
         } catch (Exception e) {
             throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
@@ -72,6 +76,8 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
                 if (ObjectUtil.isNotNull(userId)) {
                     baseEntity.setUpdateBy(userId);
                 }
+            } else {
+                this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
             }
         } catch (Exception e) {
             throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);

+ 15 - 5
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/PlusDataPermissionHandler.java

@@ -99,7 +99,7 @@ public class PlusDataPermissionHandler {
             return where;
         }
         // 构造数据过滤条件的 SQL 片段
-        String dataFilterSql = buildDataFilter(dataPermission.value(), isSelect);
+        String dataFilterSql = buildDataFilter(dataPermission, isSelect);
         if (StringUtils.isBlank(dataFilterSql)) {
             return where;
         }
@@ -120,14 +120,17 @@ public class PlusDataPermissionHandler {
     /**
      * 构建数据过滤条件的 SQL 语句
      *
-     * @param dataColumns 数据权限注解中的列信息
-     * @param isSelect    标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
+     * @param dataPermission 数据权限注解
+     * @param isSelect       标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
      * @return 构建的数据过滤条件的 SQL 语句
      * @throws ServiceException 如果角色的数据范围异常或者 key 与 value 的长度不匹配,则抛出 ServiceException 异常
      */
-    private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
+    private String buildDataFilter(DataPermission dataPermission, boolean isSelect) {
         // 更新或删除需满足所有条件
         String joinStr = isSelect ? " OR " : " AND ";
+        if (StringUtils.isNotBlank(dataPermission.joinStr())) {
+            joinStr = " " + dataPermission.joinStr() + " ";
+        }
         LoginUser user = DataPermissionHelper.getVariable("user");
         StandardEvaluationContext context = new StandardEvaluationContext();
         context.setBeanResolver(beanResolver);
@@ -145,7 +148,7 @@ public class PlusDataPermissionHandler {
                 return "";
             }
             boolean isSuccess = false;
-            for (DataColumn dataColumn : dataColumns) {
+            for (DataColumn dataColumn : dataPermission.value()) {
                 if (dataColumn.key().length != dataColumn.value().length) {
                     throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
                 }
@@ -155,6 +158,13 @@ public class PlusDataPermissionHandler {
                 )) {
                     continue;
                 }
+                // 包含权限标识符 这直接跳过
+                if (StringUtils.isNotBlank(dataColumn.permission()) &&
+                    CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
+                ) {
+                    isSuccess = true;
+                    continue;
+                }
                 // 设置注解变量 key 为表达式变量 value 为变量值
                 for (int i = 0; i < dataColumn.key().length; i++) {
                     context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);

+ 39 - 4
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/helper/DataPermissionHelper.java

@@ -2,14 +2,17 @@ package org.dromara.common.mybatis.helper;
 
 import cn.dev33.satoken.context.SaHolder;
 import cn.dev33.satoken.context.model.SaStorage;
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
 import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Stack;
 import java.util.function.Supplier;
 
 /**
@@ -24,6 +27,8 @@ public class DataPermissionHelper {
 
     private static final String DATA_PERMISSION_KEY = "data:permission";
 
+    private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
+
     /**
      * 从上下文中获取指定键的变量值,并将其转换为指定的类型
      *
@@ -66,23 +71,54 @@ public class DataPermissionHelper {
         throw new NullPointerException("data permission context type exception");
     }
 
+    private static IgnoreStrategy getIgnoreStrategy() {
+        Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
+        if (ignoreStrategyLocal instanceof ThreadLocal<?> IGNORE_STRATEGY_LOCAL) {
+            if (IGNORE_STRATEGY_LOCAL.get() instanceof IgnoreStrategy ignoreStrategy) {
+                return ignoreStrategy;
+            }
+        }
+        return null;
+    }
+
     /**
      * 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
      */
     public static void enableIgnore() {
-        InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
+        IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
+        if (ObjectUtil.isNull(ignoreStrategy)) {
+            InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
+        } else {
+            ignoreStrategy.setDataPermission(true);
+        }
+        Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
+        reentrantStack.push(reentrantStack.size() + 1);
     }
 
     /**
      * 关闭忽略数据权限
      */
     public static void disableIgnore() {
-        InterceptorIgnoreHelper.clearIgnoreStrategy();
+        IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
+        if (ObjectUtil.isNotNull(ignoreStrategy)) {
+            boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
+                && !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
+                && !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
+                && !Boolean.TRUE.equals(ignoreStrategy.getTenantLine())
+                && CollectionUtil.isEmpty(ignoreStrategy.getOthers());
+            Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
+            boolean empty = reentrantStack.isEmpty() || reentrantStack.pop() == 1;
+            if (noOtherIgnoreStrategy && empty) {
+                InterceptorIgnoreHelper.clearIgnoreStrategy();
+            } else if (empty) {
+                ignoreStrategy.setDataPermission(false);
+            }
+
+        }
     }
 
     /**
      * 在忽略数据权限中执行
-     * <p>禁止在忽略数据权限中执行忽略数据权限</p>
      *
      * @param handle 处理执行方法
      */
@@ -97,7 +133,6 @@ public class DataPermissionHelper {
 
     /**
      * 在忽略数据权限中执行
-     * <p>禁止在忽略数据权限中执行忽略数据权限</p>
      *
      * @param handle 处理执行方法
      */

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

@@ -80,11 +80,11 @@ public class RateLimiterAspect {
 
     private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
         String key = rateLimiter.key();
-        if (StringUtils.isNotBlank(key)) {
+        // 判断 key 不为空 和 不是表达式
+        if (StringUtils.isNotBlank(key) && StringUtils.containsAny(key, "#")) {
             MethodSignature signature = (MethodSignature) point.getSignature();
             Method targetMethod = signature.getMethod();
             Object[] args = point.getArgs();
-            //noinspection DataFlowIssue
             MethodBasedEvaluationContext context =
                 new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
             context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));

+ 5 - 3
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/manager/CaffeineCacheDecorator.java

@@ -15,15 +15,17 @@ public class CaffeineCacheDecorator implements Cache {
     private static final com.github.benmanes.caffeine.cache.Cache<Object, Object>
         CAFFEINE = SpringUtils.getBean("caffeine");
 
+    private final String name;
     private final Cache cache;
 
-    public CaffeineCacheDecorator(Cache cache) {
+    public CaffeineCacheDecorator(String name, Cache cache) {
+        this.name = name;
         this.cache = cache;
     }
 
     @Override
     public String getName() {
-        return cache.getName();
+        return name;
     }
 
     @Override
@@ -32,7 +34,7 @@ public class CaffeineCacheDecorator implements Cache {
     }
 
     public String getUniqueKey(Object key) {
-        return cache.getName() + ":" + key;
+        return name + ":" + key;
     }
 
     @Override

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

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

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

@@ -1,11 +1,15 @@
 package org.dromara.common.security.config;
 
 import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.filter.SaServletFilter;
+import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
 import cn.dev33.satoken.interceptor.SaInterceptor;
 import cn.dev33.satoken.router.SaRouter;
 import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.HttpStatus;
 import org.dromara.common.core.utils.ServletUtils;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StringUtils;
@@ -14,6 +18,7 @@ import org.dromara.common.security.config.properties.SecurityProperties;
 import org.dromara.common.security.handler.AllUrlHandler;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@@ -71,4 +76,19 @@ public class SecurityConfig implements WebMvcConfigurer {
             .excludePathPatterns(securityProperties.getExcludes());
     }
 
+    /**
+     * 对 actuator 健康检查接口 做账号密码鉴权
+     */
+    @Bean
+    public SaServletFilter getSaServletFilter() {
+        String username = SpringUtils.getProperty("spring.boot.admin.client.username");
+        String password = SpringUtils.getProperty("spring.boot.admin.client.password");
+        return new SaServletFilter()
+            .addInclude("/actuator", "/actuator/**")
+            .setAuth(obj -> {
+                SaHttpBasicUtil.check(username + ":" + password);
+            })
+            .setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
+    }
+
 }

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

@@ -37,7 +37,57 @@ public enum SensitiveStrategy {
     /**
      * 银行卡
      */
-    BANK_CARD(DesensitizedUtil::bankCard);
+    BANK_CARD(DesensitizedUtil::bankCard),
+
+    /**
+     * 中文名
+     */
+    CHINESE_NAME(DesensitizedUtil::chineseName),
+
+    /**
+     * 固定电话
+     */
+    FIXED_PHONE(DesensitizedUtil::fixedPhone),
+
+    /**
+     * 用户ID
+     */
+    USER_ID(s -> String.valueOf(DesensitizedUtil.userId())),
+
+    /**
+     * 密码
+     */
+    PASSWORD(DesensitizedUtil::password),
+
+    /**
+     * ipv4
+     */
+    IPV4(DesensitizedUtil::ipv4),
+
+    /**
+     * ipv6
+     */
+    IPV6(DesensitizedUtil::ipv6),
+
+    /**
+     * 中国大陆车牌,包含普通车辆、新能源车辆
+     */
+    CAR_LICENSE(DesensitizedUtil::carLicense),
+
+    /**
+     * 只显示第一个字符
+     */
+    FIRST_MASK(DesensitizedUtil::firstMask),
+
+    /**
+     * 清空为null
+     */
+    CLEAR(s -> DesensitizedUtil.clear()),
+
+    /**
+     * 清空为""
+     */
+    CLEAR_TO_NULL(s -> DesensitizedUtil.clearToNull());
 
     //可自行添加其他脱敏策略
 

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

@@ -58,9 +58,9 @@ public class SocialUtils  {
             case "linkedin" -> new AuthLinkedinRequest(builder.build(), STATE_CACHE);
             case "microsoft" -> new AuthMicrosoftRequest(builder.build(), STATE_CACHE);
             case "renren" -> new AuthRenrenRequest(builder.build(), STATE_CACHE);
-            case "stack_overflow" -> new AuthStackOverflowRequest(builder.stackOverflowKey("").build(), STATE_CACHE);
+            case "stack_overflow" -> new AuthStackOverflowRequest(builder.build(), STATE_CACHE);
             case "huawei" -> new AuthHuaweiRequest(builder.build(), STATE_CACHE);
-            case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(builder.agentId("").build(), STATE_CACHE);
+            case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(builder.build(), STATE_CACHE);
             case "gitlab" -> new AuthGitlabRequest(builder.build(), STATE_CACHE);
             case "wechat_mp" -> new AuthWeChatMpRequest(builder.build(), STATE_CACHE);
             case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE);

+ 36 - 0
ruoyi-common/ruoyi-common-sse/pom.xml

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.dromara</groupId>
+        <artifactId>ruoyi-common</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-common-sse</artifactId>
+
+    <description>
+        ruoyi-common-sse 模块
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-satoken</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-json</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 36 - 0
ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/config/SseAutoConfiguration.java

@@ -0,0 +1,36 @@
+package org.dromara.common.sse.config;
+
+import org.dromara.common.sse.controller.SseController;
+import org.dromara.common.sse.core.SseEmitterManager;
+import org.dromara.common.sse.listener.SseTopicListener;
+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;
+
+/**
+ * SSE 自动装配
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@ConditionalOnProperty(value = "sse.enabled", havingValue = "true")
+@EnableConfigurationProperties(SseProperties.class)
+public class SseAutoConfiguration {
+
+    @Bean
+    public SseEmitterManager sseEmitterManager() {
+        return new SseEmitterManager();
+    }
+
+    @Bean
+    public SseTopicListener sseTopicListener() {
+        return new SseTopicListener();
+    }
+
+    @Bean
+    public SseController sseController(SseEmitterManager sseEmitterManager) {
+        return new SseController(sseEmitterManager);
+    }
+
+}

+ 21 - 0
ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/config/SseProperties.java

@@ -0,0 +1,21 @@
+package org.dromara.common.sse.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * SSE 配置项
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties("sse")
+public class SseProperties {
+
+    private Boolean enabled;
+
+    /**
+     * 路径
+     */
+    private String path;
+}

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

@@ -0,0 +1,87 @@
+package org.dromara.common.sse.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.sse.core.SseEmitterManager;
+import org.dromara.common.sse.dto.SseMessageDto;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.List;
+
+/**
+ * SSE 控制器
+ *
+ * @author Lion Li
+ */
+@RestController
+@ConditionalOnProperty(value = "sse.enabled", havingValue = "true")
+@RequiredArgsConstructor
+public class SseController implements DisposableBean {
+
+    private final SseEmitterManager sseEmitterManager;
+
+    /**
+     * 建立 SSE 连接
+     */
+    @GetMapping(value = "${sse.path}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter connect() {
+        String tokenValue = StpUtil.getTokenValue();
+        Long userId = LoginHelper.getUserId();
+        return sseEmitterManager.connect(userId, tokenValue);
+    }
+
+    /**
+     * 关闭 SSE 连接
+     */
+    @SaIgnore
+    @GetMapping(value = "${sse.path}/close")
+    public R<Void> close() {
+        String tokenValue = StpUtil.getTokenValue();
+        Long userId = LoginHelper.getUserId();
+        sseEmitterManager.disconnect(userId, tokenValue);
+        return R.ok();
+    }
+
+    /**
+     * 向特定用户发送消息
+     *
+     * @param userId 目标用户的 ID
+     * @param msg    要发送的消息内容
+     */
+    @GetMapping(value = "${sse.path}/send")
+    public R<Void> send(Long userId, String msg) {
+        SseMessageDto dto = new SseMessageDto();
+        dto.setUserIds(List.of(userId));
+        dto.setMessage(msg);
+        sseEmitterManager.publishMessage(dto);
+        return R.ok();
+    }
+
+    /**
+     * 向所有用户发送消息
+     *
+     * @param msg 要发送的消息内容
+     */
+    @GetMapping(value = "${sse.path}/sendAll")
+    public R<Void> send(String msg) {
+        sseEmitterManager.publishAll(msg);
+        return R.ok();
+    }
+
+    /**
+     * 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误
+     */
+    @Override
+    public void destroy() throws Exception {
+        // 销毁时不需要做什么 此方法避免无用操作报错
+    }
+
+}

+ 160 - 0
ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/core/SseEmitterManager.java

@@ -0,0 +1,160 @@
+package org.dromara.common.sse.core;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.sse.dto.SseMessageDto;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+/**
+ * 管理 Server-Sent Events (SSE) 连接
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class SseEmitterManager {
+
+    /**
+     * 订阅的频道
+     */
+    private final static String SSE_TOPIC = "global:sse";
+
+    private final static Map<Long, Map<String, SseEmitter>> USER_TOKEN_EMITTERS = new ConcurrentHashMap<>();
+
+    /**
+     * 建立与指定用户的 SSE 连接
+     *
+     * @param userId 用户的唯一标识符,用于区分不同用户的连接
+     * @param token  用户的唯一令牌,用于识别具体的连接
+     * @return 返回一个 SseEmitter 实例,客户端可以通过该实例接收 SSE 事件
+     */
+    public SseEmitter connect(Long userId, String token) {
+        // 从 USER_TOKEN_EMITTERS 中获取或创建当前用户的 SseEmitter 映射表(ConcurrentHashMap)
+        // 每个用户可以有多个 SSE 连接,通过 token 进行区分
+        Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.computeIfAbsent(userId, k -> new ConcurrentHashMap<>());
+
+        // 创建一个新的 SseEmitter 实例,超时时间设置为 0 表示无限制
+        SseEmitter emitter = new SseEmitter(0L);
+
+        emitters.put(token, emitter);
+
+        // 当 emitter 完成、超时或发生错误时,从映射表中移除对应的 token
+        emitter.onCompletion(() -> emitters.remove(token));
+        emitter.onTimeout(() -> emitters.remove(token));
+        emitter.onError((e) -> emitters.remove(token));
+
+        try {
+            // 向客户端发送一条连接成功的事件
+            emitter.send(SseEmitter.event().comment("connected"));
+        } catch (IOException e) {
+            // 如果发送消息失败,则从映射表中移除 emitter
+            emitters.remove(token);
+        }
+        return emitter;
+    }
+
+    /**
+     * 断开指定用户的 SSE 连接
+     *
+     * @param userId 用户的唯一标识符,用于区分不同用户的连接
+     * @param token  用户的唯一令牌,用于识别具体的连接
+     */
+    public void disconnect(Long userId, String token) {
+        Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
+        if (emitters != null) {
+            try {
+                emitters.get(token).send(SseEmitter.event().comment("disconnected"));
+            } catch (Exception ignore) {
+            }
+            emitters.remove(token);
+        }
+    }
+
+    /**
+     * 订阅SSE消息主题,并提供一个消费者函数来处理接收到的消息
+     *
+     * @param consumer 处理SSE消息的消费者函数
+     */
+    public void subscribeMessage(Consumer<SseMessageDto> consumer) {
+        RedisUtils.subscribe(SSE_TOPIC, SseMessageDto.class, consumer);
+    }
+
+    /**
+     * 向指定的用户会话发送消息
+     *
+     * @param userId  要发送消息的用户id
+     * @param message 要发送的消息内容
+     */
+    public void sendMessage(Long userId, String message) {
+        Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
+        if (emitters != null) {
+            for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
+                try {
+                    entry.getValue().send(SseEmitter.event()
+                        .name("message")
+                        .data(message));
+                } catch (Exception e) {
+                    emitters.remove(entry.getKey());
+                }
+            }
+        }
+    }
+
+    /**
+     * 本机全用户会话发送消息
+     *
+     * @param message 要发送的消息内容
+     */
+    public void sendMessage(String message) {
+        for (Long userId : USER_TOKEN_EMITTERS.keySet()) {
+            sendMessage(userId, message);
+        }
+    }
+
+    /**
+     * 发布SSE订阅消息
+     *
+     * @param sseMessageDto 要发布的SSE消息对象
+     */
+    public void publishMessage(SseMessageDto sseMessageDto) {
+        List<Long> unsentUserIds = new ArrayList<>();
+        // 当前服务内用户,直接发送消息
+        for (Long userId : sseMessageDto.getUserIds()) {
+            if (USER_TOKEN_EMITTERS.containsKey(userId)) {
+                sendMessage(userId, sseMessageDto.getMessage());
+                continue;
+            }
+            unsentUserIds.add(userId);
+        }
+        // 不在当前服务内用户,发布订阅消息
+        if (CollUtil.isNotEmpty(unsentUserIds)) {
+            SseMessageDto broadcastMessage = new SseMessageDto();
+            broadcastMessage.setMessage(sseMessageDto.getMessage());
+            broadcastMessage.setUserIds(unsentUserIds);
+            RedisUtils.publish(SSE_TOPIC, broadcastMessage, consumer -> {
+                log.info("SSE发送主题订阅消息topic:{} session keys:{} message:{}",
+                    SSE_TOPIC, unsentUserIds, sseMessageDto.getMessage());
+            });
+        }
+    }
+
+    /**
+     * 向所有的用户发布订阅的消息(群发)
+     *
+     * @param message 要发布的消息内容
+     */
+    public void publishAll(String message) {
+        SseMessageDto broadcastMessage = new SseMessageDto();
+        broadcastMessage.setMessage(message);
+        RedisUtils.publish(SSE_TOPIC, broadcastMessage, consumer -> {
+            log.info("SSE发送主题订阅消息topic:{} message:{}", SSE_TOPIC, message);
+        });
+    }
+}

+ 29 - 0
ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/dto/SseMessageDto.java

@@ -0,0 +1,29 @@
+package org.dromara.common.sse.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 消息的dto
+ *
+ * @author zendwang
+ */
+@Data
+public class SseMessageDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 需要推送到的session key 列表
+     */
+    private List<Long> userIds;
+
+    /**
+     * 需要发送的消息
+     */
+    private String message;
+}

+ 48 - 0
ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/listener/SseTopicListener.java

@@ -0,0 +1,48 @@
+package org.dromara.common.sse.listener;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.sse.core.SseEmitterManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.Ordered;
+
+/**
+ * SSE 主题订阅监听器
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class SseTopicListener implements ApplicationRunner, Ordered {
+
+    @Autowired
+    private SseEmitterManager sseEmitterManager;
+
+    /**
+     * 在Spring Boot应用程序启动时初始化SSE主题订阅监听器
+     *
+     * @param args 应用程序参数
+     * @throws Exception 初始化过程中可能抛出的异常
+     */
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        sseEmitterManager.subscribeMessage((message) -> {
+            log.info("SSE主题订阅收到消息session keys={} message={}", message.getUserIds(), message.getMessage());
+            // 如果key不为空就按照key发消息 如果为空就群发
+            if (CollUtil.isNotEmpty(message.getUserIds())) {
+                message.getUserIds().forEach(key -> {
+                    sseEmitterManager.sendMessage(key, message.getMessage());
+                });
+            } else {
+                sseEmitterManager.sendMessage(message.getMessage());
+            }
+        });
+        log.info("初始化SSE主题订阅监听器成功");
+    }
+
+    @Override
+    public int getOrder() {
+        return -1;
+    }
+}

+ 58 - 0
ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/utils/SseMessageUtils.java

@@ -0,0 +1,58 @@
+package org.dromara.common.sse.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.sse.core.SseEmitterManager;
+import org.dromara.common.sse.dto.SseMessageDto;
+
+/**
+ * SSE工具类
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class SseMessageUtils {
+
+    private final static SseEmitterManager MANAGER = SpringUtils.getBean(SseEmitterManager.class);
+
+    /**
+     * 向指定的WebSocket会话发送消息
+     *
+     * @param userId  要发送消息的用户id
+     * @param message 要发送的消息内容
+     */
+    public static void sendMessage(Long userId, String message) {
+        MANAGER.sendMessage(userId, message);
+    }
+
+    /**
+     * 本机全用户会话发送消息
+     *
+     * @param message 要发送的消息内容
+     */
+    public static void sendMessage(String message) {
+        MANAGER.sendMessage(message);
+    }
+
+    /**
+     * 发布SSE订阅消息
+     *
+     * @param sseMessageDto 要发布的SSE消息对象
+     */
+    public static void publishMessage(SseMessageDto sseMessageDto) {
+        MANAGER.publishMessage(sseMessageDto);
+    }
+
+    /**
+     * 向所有的用户发布订阅的消息(群发)
+     *
+     * @param message 要发布的消息内容
+     */
+    public static void publishAll(String message) {
+        MANAGER.publishAll(message);
+    }
+
+}

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

@@ -0,0 +1 @@
+org.dromara.common.sse.config.SseAutoConfiguration

+ 40 - 4
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/helper/TenantHelper.java

@@ -1,8 +1,9 @@
 package org.dromara.common.tenant.helper;
 
 import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.convert.Convert;
-import com.alibaba.ttl.TransmittableThreadLocal;
+import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
 import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
 import lombok.AccessLevel;
@@ -11,9 +12,11 @@ import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.GlobalConstants;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
 
+import java.util.Stack;
 import java.util.function.Supplier;
 
 /**
@@ -27,7 +30,9 @@ public class TenantHelper {
 
     private static final String DYNAMIC_TENANT_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "dynamicTenant";
 
-    private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new TransmittableThreadLocal<>();
+    private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new ThreadLocal<>();
+
+    private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
 
     /**
      * 租户功能是否启用
@@ -36,18 +41,49 @@ public class TenantHelper {
         return Convert.toBool(SpringUtils.getProperty("tenant.enable"), false);
     }
 
+    private static IgnoreStrategy getIgnoreStrategy() {
+        Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
+        if (ignoreStrategyLocal instanceof ThreadLocal<?> IGNORE_STRATEGY_LOCAL) {
+            if (IGNORE_STRATEGY_LOCAL.get() instanceof IgnoreStrategy ignoreStrategy) {
+                return ignoreStrategy;
+            }
+        }
+        return null;
+    }
+
     /**
      * 开启忽略租户(开启后需手动调用 {@link #disableIgnore()} 关闭)
      */
     public static void enableIgnore() {
-        InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
+        IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
+        if (ObjectUtil.isNull(ignoreStrategy)) {
+            InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
+        } else {
+            ignoreStrategy.setTenantLine(true);
+        }
+        Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
+        reentrantStack.push(reentrantStack.size() + 1);
     }
 
     /**
      * 关闭忽略租户
      */
     public static void disableIgnore() {
-        InterceptorIgnoreHelper.clearIgnoreStrategy();
+        IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
+        if (ObjectUtil.isNotNull(ignoreStrategy)) {
+            boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
+                && !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
+                && !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
+                && !Boolean.TRUE.equals(ignoreStrategy.getDataPermission())
+                && CollectionUtil.isEmpty(ignoreStrategy.getOthers());
+            Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
+            boolean empty = reentrantStack.isEmpty() || reentrantStack.pop() == 1;
+            if (noOtherIgnoreStrategy && empty) {
+                InterceptorIgnoreHelper.clearIgnoreStrategy();
+            } else if (empty) {
+                ignoreStrategy.setTenantLine(false);
+            }
+        }
     }
 
     /**

+ 9 - 0
ruoyi-common/ruoyi-common-tenant/src/main/java/org/dromara/common/tenant/manager/TenantSpringCacheManager.java

@@ -1,5 +1,7 @@
 package org.dromara.common.tenant.manager;
 
+import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.constant.GlobalConstants;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.redis.manager.PlusSpringCacheManager;
@@ -11,6 +13,7 @@ import org.springframework.cache.Cache;
  *
  * @author Lion Li
  */
+@Slf4j
 public class TenantSpringCacheManager extends PlusSpringCacheManager {
 
     public TenantSpringCacheManager() {
@@ -18,10 +21,16 @@ public class TenantSpringCacheManager extends PlusSpringCacheManager {
 
     @Override
     public Cache getCache(String name) {
+        if (InterceptorIgnoreHelper.willIgnoreTenantLine("")) {
+            return super.getCache(name);
+        }
         if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
             return super.getCache(name);
         }
         String tenantId = TenantHelper.getTenantId();
+        if (StringUtils.isBlank(tenantId)) {
+            log.error("无法获取有效的租户id -> Null");
+        }
         if (StringUtils.startsWith(name, tenantId)) {
             // 如果存在则直接返回
             return super.getCache(name);

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

@@ -43,6 +43,19 @@
             <artifactId>spring-boot-starter-undertow</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>io.undertow</groupId>
+            <artifactId>undertow-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.undertow</groupId>
+            <artifactId>undertow-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.undertow</groupId>
+            <artifactId>undertow-websockets-jsr</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>

+ 17 - 0
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java

@@ -16,10 +16,13 @@ import org.springframework.web.HttpRequestMethodNotSupportedException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.MissingPathVariableException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 import org.springframework.web.servlet.NoHandlerFoundException;
 
+import java.io.IOException;
+
 /**
  * 全局异常处理器
  *
@@ -89,6 +92,20 @@ public class GlobalExceptionHandler {
         return R.fail(HttpStatus.HTTP_NOT_FOUND, e.getMessage());
     }
 
+    /**
+     * 拦截未知的运行时异常
+     */
+    @ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR)
+    @ExceptionHandler(IOException.class)
+    public void handleRuntimeException(IOException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        if (requestURI.contains("sse")) {
+            // sse 经常性连接中断 例如关闭浏览器 直接屏蔽
+            return;
+        }
+        log.error("请求地址'{}',连接中断", requestURI, e);
+    }
+
     /**
      * 拦截未知的运行时异常
      */

+ 24 - 31
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java

@@ -6,7 +6,6 @@ import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.StopWatch;
-import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.web.filter.RepeatedlyRequestWrapper;
@@ -19,7 +18,6 @@ import java.util.Map;
 
 /**
  * web的调用时间统计拦截器
- * dev环境有效
  *
  * @author Lion Li
  * @since 3.3.0
@@ -27,37 +25,34 @@ import java.util.Map;
 @Slf4j
 public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
 
-    private final String prodProfile = "prod";
-
     private final static ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-        if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
-            String url = request.getMethod() + " " + request.getRequestURI();
+        String url = request.getMethod() + " " + request.getRequestURI();
 
-            // 打印请求参数
-            if (isJsonRequest(request)) {
-                String jsonParam = "";
-                if (request instanceof RepeatedlyRequestWrapper) {
-                    BufferedReader reader = request.getReader();
-                    jsonParam = IoUtil.read(reader);
-                }
-                log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
+        // 打印请求参数
+        if (isJsonRequest(request)) {
+            String jsonParam = "";
+            if (request instanceof RepeatedlyRequestWrapper) {
+                BufferedReader reader = request.getReader();
+                jsonParam = IoUtil.read(reader);
+            }
+            log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
+        } else {
+            Map<String, String[]> parameterMap = request.getParameterMap();
+            if (MapUtil.isNotEmpty(parameterMap)) {
+                String parameters = JsonUtils.toJsonString(parameterMap);
+                log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
             } else {
-                Map<String, String[]> parameterMap = request.getParameterMap();
-                if (MapUtil.isNotEmpty(parameterMap)) {
-                    String parameters = JsonUtils.toJsonString(parameterMap);
-                    log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
-                } else {
-                    log.info("[PLUS]开始请求 => URL[{}],无参数", url);
-                }
+                log.info("[PLUS]开始请求 => URL[{}],无参数", url);
             }
-
-            StopWatch stopWatch = new StopWatch();
-            KEY_CACHE.set(stopWatch);
-            stopWatch.start();
         }
+
+        StopWatch stopWatch = new StopWatch();
+        KEY_CACHE.set(stopWatch);
+        stopWatch.start();
+
         return true;
     }
 
@@ -68,12 +63,10 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
 
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
-        if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
-            StopWatch stopWatch = KEY_CACHE.get();
-            stopWatch.stop();
-            log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getTime());
-            KEY_CACHE.remove();
-        }
+        StopWatch stopWatch = KEY_CACHE.get();
+        stopWatch.stop();
+        log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getTime());
+        KEY_CACHE.remove();
     }
 
     /**

+ 1 - 1
ruoyi-common/ruoyi-common-websocket/src/main/java/org/dromara/common/websocket/utils/WebSocketUtils.java

@@ -113,7 +113,7 @@ public class WebSocketUtils {
      * @param session WebSocket会话
      * @param message 要发送的WebSocket消息对象
      */
-    private static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
+    private synchronized static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
         if (session == null || !session.isOpen()) {
             log.warn("[send] session会话已经关闭");
         } else {

+ 12 - 1
ruoyi-extend/ruoyi-monitor-admin/pom.xml

@@ -12,10 +12,21 @@
     <artifactId>ruoyi-monitor-admin</artifactId>
 
     <dependencies>
-        <!-- SpringWeb模块 -->
+        <!-- SpringBoot Web容器 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>spring-boot-starter-tomcat</artifactId>
+                    <groupId>org.springframework.boot</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- web 容器使用 undertow 性能更强 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-undertow</artifactId>
         </dependency>
 
         <!-- spring security 安全认证 -->

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

@@ -39,9 +39,7 @@ public class SecurityConfig {
             .authorizeHttpRequests((authorize) ->
                 authorize.requestMatchers(
                         new AntPathRequestMatcher(adminContextPath + "/assets/**"),
-                        new AntPathRequestMatcher(adminContextPath + "/login"),
-                        new AntPathRequestMatcher("/actuator"),
-                        new AntPathRequestMatcher("/actuator/**")
+                        new AntPathRequestMatcher(adminContextPath + "/login")
                     ).permitAll()
                     .anyRequest().authenticated())
             .formLogin((formLogin) ->

+ 18 - 3
ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/CustomNotifier.java

@@ -9,6 +9,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 import reactor.core.publisher.Mono;
 
+import static de.codecentric.boot.admin.server.domain.values.StatusInfo.*;
+
 /**
  * 自定义事件通知处理
  *
@@ -28,13 +30,26 @@ public class CustomNotifier extends AbstractEventNotifier {
         return Mono.fromRunnable(() -> {
             // 实例状态改变事件
             if (event instanceof InstanceStatusChangedEvent) {
+                // 获取实例注册名称
                 String registName = instance.getRegistration().getName();
+                // 获取实例ID
                 String instanceId = event.getInstance().getValue();
+                // 获取实例状态
                 String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus();
-                log.info("Instance Status Change: [{}],[{}],[{}]", registName, instanceId, status);
+                // 获取服务URL
+                String serviceUrl = instance.getRegistration().getServiceUrl();
+                String statusName = switch (status) {
+                    case STATUS_UP -> "服务上线"; // 实例成功启动并可以正常处理请求
+                    case STATUS_OFFLINE -> "服务离线"; //实例被手动或自动地从服务中移除
+                    case STATUS_RESTRICTED -> "服务受限"; //表示实例在某些方面受限,可能无法完全提供所有服务
+                    case STATUS_OUT_OF_SERVICE -> "停止服务状态"; //表示实例已被标记为停止提供服务,可能是计划内维护或测试
+                    case STATUS_DOWN -> "服务下线"; //实例因崩溃、错误或其他原因停止运行
+                    case STATUS_UNKNOWN -> "服务未知异常"; //监控系统无法确定实例的当前状态
+                    default -> "未知状态"; //没有匹配的状态
+                };
+                log.info("Instance Status Change: 状态名称【{}】, 注册名称【{}】, 实例ID【{}】, 状态【{}】, 服务URL【{}】",
+                    statusName, registName, instanceId, status, serviceUrl);
             }
-
         });
     }
-
 }

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

@@ -41,5 +41,8 @@ spring.boot.admin.client:
   url: http://localhost:9090/admin
   instance:
     service-host-type: IP
+    metadata:
+      username: ${spring.boot.admin.client.username}
+      userpassword: ${spring.boot.admin.client.password}
   username: ruoyi
   password: 123456

+ 64 - 0
ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/starter/filter/ActuatorAuthFilter.java

@@ -0,0 +1,64 @@
+package com.aizuda.snailjob.server.starter.filter;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public class ActuatorAuthFilter implements Filter {
+
+    private final String username;
+    private final String password;
+
+    public ActuatorAuthFilter(String username, String password) {
+        this.username = username;
+        this.password = password;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+        // 获取 Authorization 头
+        String authHeader = request.getHeader("Authorization");
+
+        if (authHeader == null || !authHeader.startsWith("Basic ")) {
+            // 如果没有提供 Authorization 或者格式不对,则返回 401
+            response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+            return;
+        }
+
+        // 解码 Base64 编码的用户名和密码
+        String base64Credentials = authHeader.substring("Basic ".length());
+        byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
+        String credentials = new String(credDecoded, StandardCharsets.UTF_8);
+        String[] split = credentials.split(":");
+        if (split.length != 2) {
+            response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+            return;
+        }
+        // 验证用户名和密码
+        if (!username.equals(split[0]) && password.equals(split[1])) {
+            response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+            return;
+        }
+        // 如果认证成功,继续处理请求
+        filterChain.doFilter(request, response);
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}

+ 29 - 0
ruoyi-extend/ruoyi-snailjob-server/src/main/java/com/aizuda/snailjob/server/starter/filter/SecurityConfig.java

@@ -0,0 +1,29 @@
+package com.aizuda.snailjob.server.starter.filter;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 权限安全配置
+ *
+ * @author Lion Li
+ */
+@Configuration
+public class SecurityConfig {
+
+    @Value("${spring.boot.admin.client.username}")
+    private String username;
+    @Value("${spring.boot.admin.client.password}")
+    private String password;
+
+    @Bean
+    public FilterRegistrationBean<ActuatorAuthFilter> actuatorFilterRegistrationBean() {
+        FilterRegistrationBean<ActuatorAuthFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new ActuatorAuthFilter(username, password));
+        registrationBean.addUrlPatterns("/actuator", "/actuator/**");
+        return registrationBean;
+    }
+
+}

+ 3 - 0
ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-dev.yml

@@ -43,5 +43,8 @@ spring.boot.admin.client:
   url: http://localhost:9090/admin
   instance:
     service-host-type: IP
+    metadata:
+      username: ${spring.boot.admin.client.username}
+      userpassword: ${spring.boot.admin.client.password}
   username: ruoyi
   password: 123456

+ 3 - 0
ruoyi-extend/ruoyi-snailjob-server/src/main/resources/application-prod.yml

@@ -43,5 +43,8 @@ spring.boot.admin.client:
   url: http://localhost:9090/admin
   instance:
     service-host-type: IP
+    metadata:
+      username: ${spring.boot.admin.client.username}
+      userpassword: ${spring.boot.admin.client.password}
   username: ruoyi
   password: 123456

+ 7 - 6
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mapper/TestDemoMapper.java

@@ -4,14 +4,14 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Constants;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import org.apache.poi.ss.formula.functions.T;
+import org.apache.ibatis.annotations.Param;
 import org.dromara.common.mybatis.annotation.DataColumn;
 import org.dromara.common.mybatis.annotation.DataPermission;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 import org.dromara.demo.domain.TestDemo;
 import org.dromara.demo.domain.vo.TestDemoVo;
-import org.apache.ibatis.annotations.Param;
 
+import java.io.Serializable;
 import java.util.Collection;
 import java.util.List;
 
@@ -44,16 +44,17 @@ public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
     List<TestDemo> selectList(@Param(Constants.WRAPPER) Wrapper<TestDemo> queryWrapper);
 
     @Override
-    @DataPermission({
+    @DataPermission(value = {
         @DataColumn(key = "deptName", value = "dept_id"),
         @DataColumn(key = "userName", value = "user_id")
-    })
-    int updateById(@Param(Constants.ENTITY) TestDemo entity);
+    }, joinStr = "AND")
+    List<TestDemo> selectBatchIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);
 
     @Override
     @DataPermission({
         @DataColumn(key = "deptName", value = "dept_id"),
         @DataColumn(key = "userName", value = "user_id")
     })
-    int deleteByIds(@Param(Constants.COLL) Collection<?> idList);
+    int updateById(@Param(Constants.ENTITY) TestDemo entity);
+
 }

+ 7 - 2
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/TestDemoServiceImpl.java

@@ -3,6 +3,8 @@ package org.dromara.demo.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
@@ -12,7 +14,6 @@ import org.dromara.demo.domain.bo.TestDemoBo;
 import org.dromara.demo.domain.vo.TestDemoVo;
 import org.dromara.demo.mapper.TestDemoMapper;
 import org.dromara.demo.service.ITestDemoService;
-import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -99,7 +100,11 @@ public class TestDemoServiceImpl implements ITestDemoService {
     @Override
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if (isValid) {
-            //TODO 做一些业务上的校验,判断是否需要校验
+            // 做一些业务上的校验,判断是否需要校验
+            List<TestDemo> list = baseMapper.selectBatchIds(ids);
+            if (list.size() != ids.size()) {
+                throw new ServiceException("您没有删除权限!");
+            }
         }
         return baseMapper.deleteByIds(ids) > 0;
     }

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

@@ -47,6 +47,38 @@
             <groupId>org.apache.velocity</groupId>
             <artifactId>velocity-engine-core</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.anyline</groupId>
+            <artifactId>anyline-environment-spring-data-jdbc</artifactId>
+            <version>${anyline.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.anyline</groupId>
+            <artifactId>anyline-data-jdbc-mysql</artifactId>
+            <version>${anyline.version}</version>
+        </dependency>
+
+        <!-- anyline支持100+种类型数据库 添加对应的jdbc依赖与anyline对应数据库依赖包即可 -->
+<!--        <dependency>-->
+<!--            <groupId>org.anyline</groupId>-->
+<!--            <artifactId>anyline-data-jdbc-oracle</artifactId>-->
+<!--        <version>${anyline.version}</version>-->
+<!--        </dependency>-->
+
+<!--        <dependency>-->
+<!--            <groupId>org.anyline</groupId>-->
+<!--            <artifactId>anyline-data-jdbc-postgresql</artifactId>-->
+<!--        <version>${anyline.version}</version>-->
+<!--        </dependency>-->
+
+<!--        <dependency>-->
+<!--            <groupId>org.anyline</groupId>-->
+<!--            <artifactId>anyline-data-jdbc-mssql</artifactId>-->
+<!--        <version>${anyline.version}</version>-->
+<!--        </dependency>-->
+
     </dependencies>
 
 </project>

+ 105 - 0
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/config/MyBatisDataSourceMonitor.java

@@ -0,0 +1,105 @@
+package org.dromara.generator.config;
+
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.anyline.data.datasource.DataSourceMonitor;
+import org.anyline.data.runtime.DataRuntime;
+import org.anyline.util.ConfigTable;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceUtils;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * anyline 适配 动态数据源改造
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@Component
+public class MyBatisDataSourceMonitor implements DataSourceMonitor {
+
+    public MyBatisDataSourceMonitor() {
+        // 调整执行模式为自定义
+        ConfigTable.KEEP_ADAPTER = 2;
+        // 禁用缓存
+        ConfigTable.METADATA_CACHE_SCOPE = 0;
+    }
+
+    private final Map<String, String> features = new HashMap<>();
+
+    /**
+     * 数据源特征 用来定准 adapter 包含数据库或JDBC协议关键字<br/>
+     * 一般会通过 产品名_url 合成 如果返回null 上层方法会通过driver_产品名_url合成
+     *
+     * @param datasource 数据源
+     * @return String 返回null由上层自动提取
+     */
+    @Override
+    public String feature(DataRuntime runtime, Object datasource) {
+        String feature = null;
+        if (datasource instanceof JdbcTemplate jdbc) {
+            DataSource ds = jdbc.getDataSource();
+            if (ds instanceof DynamicRoutingDataSource) {
+                String key = DynamicDataSourceContextHolder.peek();
+                feature = features.get(key);
+                if (null == feature) {
+                    Connection con = null;
+                    try {
+                        con = DataSourceUtils.getConnection(ds);
+                        DatabaseMetaData meta = con.getMetaData();
+                        String url = meta.getURL();
+                        feature = meta.getDatabaseProductName().toLowerCase().replace(" ", "") + "_" + url;
+                        features.put(key, feature);
+                    } catch (Exception e) {
+                        log.error(e.getMessage(), e);
+                    } finally {
+                        if (null != con && !DataSourceUtils.isConnectionTransactional(con, ds)) {
+                            DataSourceUtils.releaseConnection(con, ds);
+                        }
+                    }
+                }
+            }
+        }
+        return feature;
+    }
+
+    /**
+     * 数据源唯一标识 如果不实现则默认feature
+     * @param datasource 数据源
+     * @return String 返回null由上层自动提取
+     */
+    @Override
+    public String key(DataRuntime runtime, Object datasource) {
+        if(datasource instanceof JdbcTemplate jdbc){
+            DataSource ds = jdbc.getDataSource();
+            if(ds instanceof DynamicRoutingDataSource){
+                return DynamicDataSourceContextHolder.peek();
+            }
+        }
+        return runtime.getKey();
+    }
+
+    /**
+     * ConfigTable.KEEP_ADAPTER=2 : 根据当前接口判断是否保持同一个数据源绑定同一个adapter<br/>
+     * DynamicRoutingDataSource类型的返回false,因为同一个DynamicRoutingDataSource可能对应多类数据库, 如果项目中只有一种数据库 应该直接返回true
+     *
+     * @param datasource 数据源
+     * @return boolean
+     */
+    @Override
+    public boolean keepAdapter(DataRuntime runtime, Object datasource) {
+        if (datasource instanceof JdbcTemplate jdbc) {
+            DataSource ds = jdbc.getDataSource();
+            return !(ds instanceof DynamicRoutingDataSource);
+        }
+        return true;
+    }
+
+}

+ 0 - 1
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/domain/GenTableColumn.java

@@ -17,7 +17,6 @@ import jakarta.validation.constraints.NotBlank;
  *
  * @author Lion Li
  */
-
 @Data
 @EqualsAndHashCode(callSuper = true)
 @TableName("gen_table_column")

+ 0 - 13
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/mapper/GenTableColumnMapper.java

@@ -1,13 +1,9 @@
 package org.dromara.generator.mapper;
 
-import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
-import org.apache.ibatis.annotations.Param;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 import org.dromara.generator.domain.GenTableColumn;
 
-import java.util.List;
-
 /**
  * 业务字段 数据层
  *
@@ -15,14 +11,5 @@ import java.util.List;
  */
 @InterceptorIgnore(dataPermission = "true", tenantLine = "true")
 public interface GenTableColumnMapper extends BaseMapperPlus<GenTableColumn, GenTableColumn> {
-    /**
-     * 根据表名称查询列信息
-     *
-     * @param tableName 表名称
-     * @param dataName  数据源名称
-     * @return 列信息
-     */
-    @DS("#dataName")
-    List<GenTableColumn> selectDbTableColumnsByName(@Param("tableName") String tableName, String dataName);
 
 }

+ 0 - 16
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/mapper/GenTableMapper.java

@@ -17,22 +17,6 @@ import java.util.List;
 @InterceptorIgnore(dataPermission = "true", tenantLine = "true")
 public interface GenTableMapper extends BaseMapperPlus<GenTable, GenTable> {
 
-    /**
-     * 查询据库列表
-     *
-     * @param genTable 查询条件
-     * @return 数据库表集合
-     */
-    Page<GenTable> selectPageDbTableList(@Param("page") Page<GenTable> page, @Param("genTable") GenTable genTable);
-
-    /**
-     * 查询据库列表
-     *
-     * @param tableNames 表名称组
-     * @return 数据库表集合
-     */
-    List<GenTable> selectDbTableListByNames(String[] tableNames);
-
     /**
      * 查询所有表信息
      *

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

@@ -9,15 +9,20 @@ import com.baomidou.dynamic.datasource.annotation.DSTransactional;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.anyline.metadata.Column;
+import org.anyline.metadata.Table;
+import org.anyline.proxy.ServiceProxy;
 import org.apache.velocity.Template;
 import org.apache.velocity.VelocityContext;
 import org.apache.velocity.app.Velocity;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StreamUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.core.utils.file.FileUtils;
@@ -41,11 +46,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -63,6 +64,8 @@ public class GenTableServiceImpl implements IGenTableService {
     private final GenTableColumnMapper genTableColumnMapper;
     private final IdentifierGenerator identifierGenerator;
 
+    private static final String[] TABLE_IGNORE = new String[]{"sj_", "act_", "flw_", "gen_"};
+
     /**
      * 查询业务字段列表
      *
@@ -99,7 +102,7 @@ public class GenTableServiceImpl implements IGenTableService {
         Map<String, Object> params = genTable.getParams();
         QueryWrapper<GenTable> wrapper = Wrappers.query();
         wrapper
-            .eq(StringUtils.isNotEmpty(genTable.getDataName()),"data_name", genTable.getDataName())
+            .eq(StringUtils.isNotEmpty(genTable.getDataName()), "data_name", genTable.getDataName())
             .like(StringUtils.isNotBlank(genTable.getTableName()), "lower(table_name)", StringUtils.lowerCase(genTable.getTableName()))
             .like(StringUtils.isNotBlank(genTable.getTableComment()), "lower(table_comment)", StringUtils.lowerCase(genTable.getTableComment()))
             .between(params.get("beginTime") != null && params.get("endTime") != null,
@@ -107,11 +110,67 @@ public class GenTableServiceImpl implements IGenTableService {
         return wrapper;
     }
 
+    /**
+     * 查询数据库列表
+     *
+     * @param genTable  包含查询条件的GenTable对象
+     * @param pageQuery 包含分页信息的PageQuery对象
+     * @return 包含分页结果的TableDataInfo对象
+     */
     @DS("#genTable.dataName")
     @Override
     public TableDataInfo<GenTable> selectPageDbTableList(GenTable genTable, PageQuery pageQuery) {
-        genTable.getParams().put("genTableNames",baseMapper.selectTableNameList(genTable.getDataName()));
-        Page<GenTable> page = baseMapper.selectPageDbTableList(pageQuery.build(), genTable);
+        // 获取查询条件
+        String tableName = genTable.getTableName();
+        String tableComment = genTable.getTableComment();
+
+        LinkedHashMap<String, Table<?>> tablesMap = ServiceProxy.metadata().tables();
+        if (CollUtil.isEmpty(tablesMap)) {
+            return TableDataInfo.build();
+        }
+        List<String> tableNames = baseMapper.selectTableNameList(genTable.getDataName());
+        String[] tableArrays;
+        if (CollUtil.isNotEmpty(tableNames)) {
+            tableArrays = tableNames.toArray(new String[0]);
+        } else {
+            tableArrays = new String[0];
+        }
+        // 过滤并转换表格数据
+        List<GenTable> tables = tablesMap.values().stream()
+            .filter(x -> !StringUtils.containsAnyIgnoreCase(x.getName(), TABLE_IGNORE))
+            .filter(x -> {
+                if (CollUtil.isEmpty(tableNames)) {
+                    return true;
+                }
+                return !StringUtils.equalsAnyIgnoreCase(x.getName(), tableArrays);
+            })
+            .filter(x -> {
+                boolean nameMatches = true;
+                boolean commentMatches = true;
+                // 进行表名称的模糊查询
+                if (StringUtils.isNotBlank(tableName)) {
+                    nameMatches = StringUtils.containsIgnoreCase(x.getName(), tableName);
+                }
+                // 进行表描述的模糊查询
+                if (StringUtils.isNotBlank(tableComment)) {
+                    commentMatches = StringUtils.containsIgnoreCase(x.getComment(), tableComment);
+                }
+                // 同时匹配名称和描述
+                return nameMatches && commentMatches;
+            })
+            .map(x -> {
+                GenTable gen = new GenTable();
+                gen.setTableName(x.getName());
+                gen.setTableComment(x.getComment());
+                gen.setCreateTime(x.getCreateTime());
+                gen.setUpdateTime(x.getUpdateTime());
+                return gen;
+            }).toList();
+
+        IPage<GenTable> page = pageQuery.build();
+        page.setTotal(tables.size());
+        // 手动分页 set数据
+        page.setRecords(CollUtil.page((int) page.getCurrent() - 1, (int) page.getSize(), tables));
         return TableDataInfo.build(page);
     }
 
@@ -125,7 +184,29 @@ public class GenTableServiceImpl implements IGenTableService {
     @DS("#dataName")
     @Override
     public List<GenTable> selectDbTableListByNames(String[] tableNames, String dataName) {
-        return baseMapper.selectDbTableListByNames(tableNames);
+        Set<String> tableNameSet = new HashSet<>(List.of(tableNames));
+        LinkedHashMap<String, Table<?>> tablesMap = ServiceProxy.metadata().tables();
+
+        if (CollUtil.isEmpty(tablesMap)) {
+            return new ArrayList<>();
+        }
+
+        List<Table<?>> tableList = tablesMap.values().stream()
+            .filter(x -> !StringUtils.containsAnyIgnoreCase(x.getName(), TABLE_IGNORE))
+            .filter(x -> tableNameSet.contains(x.getName())).toList();
+
+        if (CollUtil.isEmpty(tableList)) {
+            return new ArrayList<>();
+        }
+        return tableList.stream().map(x -> {
+            GenTable gen = new GenTable();
+            gen.setDataName(dataName);
+            gen.setTableName(x.getName());
+            gen.setTableComment(x.getComment());
+            gen.setCreateTime(x.getCreateTime());
+            gen.setUpdateTime(x.getUpdateTime());
+            return gen;
+        }).toList();
     }
 
     /**
@@ -187,7 +268,7 @@ public class GenTableServiceImpl implements IGenTableService {
                 int row = baseMapper.insert(table);
                 if (row > 0) {
                     // 保存列信息
-                    List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName, dataName);
+                    List<GenTableColumn> genTableColumns = SpringUtils.getAopProxy(this).selectDbTableColumnsByName(tableName, dataName);
                     List<GenTableColumn> saveColumns = new ArrayList<>();
                     for (GenTableColumn column : genTableColumns) {
                         GenUtils.initColumnField(column, table);
@@ -203,6 +284,32 @@ public class GenTableServiceImpl implements IGenTableService {
         }
     }
 
+    /**
+     * 根据表名称查询列信息
+     *
+     * @param tableName 表名称
+     * @param dataName  数据源名称
+     * @return 列信息
+     */
+    @DS("#dataName")
+    @Override
+    public List<GenTableColumn> selectDbTableColumnsByName(String tableName, String dataName) {
+        LinkedHashMap<String, Column> columns = ServiceProxy.metadata().columns(tableName);
+        List<GenTableColumn> tableColumns = new ArrayList<>();
+        columns.forEach((columnName, column) -> {
+            GenTableColumn tableColumn = new GenTableColumn();
+            tableColumn.setIsPk(String.valueOf(column.isPrimaryKey()));
+            tableColumn.setColumnName(column.getName());
+            tableColumn.setColumnComment(column.getComment());
+            tableColumn.setColumnType(column.getTypeName().toLowerCase());
+            tableColumn.setSort(column.getPosition());
+            tableColumn.setIsRequired(column.isNullable() == 0 ? "1" : "0");
+            tableColumn.setIsIncrement(column.isAutoIncrement() == -1 ? "0" : "1");
+            tableColumns.add(tableColumn);
+        });
+        return tableColumns;
+    }
+
     /**
      * 预览代码
      *
@@ -298,7 +405,7 @@ public class GenTableServiceImpl implements IGenTableService {
         List<GenTableColumn> tableColumns = table.getColumns();
         Map<String, GenTableColumn> tableColumnMap = StreamUtils.toIdentityMap(tableColumns, GenTableColumn::getColumnName);
 
-        List<GenTableColumn> dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(table.getTableName(), table.getDataName());
+        List<GenTableColumn> dbTableColumns = SpringUtils.getAopProxy(this).selectDbTableColumnsByName(table.getTableName(), table.getDataName());
         if (CollUtil.isEmpty(dbTableColumns)) {
             throw new ServiceException("同步数据失败,原表结构不存在");
         }

+ 9 - 0
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/IGenTableService.java

@@ -85,6 +85,15 @@ public interface IGenTableService {
      */
     void importGenTable(List<GenTable> tableList, String dataName);
 
+    /**
+     * 根据表名称查询列信息
+     *
+     * @param tableName 表名称
+     * @param dataName  数据源名称
+     * @return 列信息
+     */
+    List<GenTableColumn> selectDbTableColumnsByName(String tableName, String dataName);
+
     /**
      * 预览代码
      *

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

@@ -215,6 +215,9 @@ public class VelocityUtils {
                 importList.add("com.fasterxml.jackson.annotation.JsonFormat");
             } else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
                 importList.add("java.math.BigDecimal");
+            } else if (!column.isSuperColumn() && "imageUpload".equals(column.getHtmlType())) {
+                importList.add("org.dromara.common.translation.annotation.Translation");
+                importList.add("org.dromara.common.translation.constant.TransConstant");
             }
         }
         return importList;

+ 0 - 83
ruoyi-modules/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml

@@ -7,87 +7,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <resultMap type="org.dromara.generator.domain.GenTableColumn" id="GenTableColumnResult">
     </resultMap>
 
-    <select id="selectDbTableColumnsByName" parameterType="String" resultMap="GenTableColumnResult">
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
-            select column_name,
-                   (case when (is_nullable = 'no' <![CDATA[ && ]]> column_key != 'PRI') then '1' else '0' end) as is_required,
-                   (case when column_key = 'PRI' then '1' else '0' end) as is_pk,
-                   ordinal_position as sort,
-                   column_comment,
-                   (case when extra = 'auto_increment' then '1' else '0' end) as is_increment,
-                   column_type
-            from information_schema.columns where table_schema = (select database()) and table_name = (#{tableName})
-            order by ordinal_position
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
-            select lower(temp.column_name) as column_name,
-                    (case when (temp.nullable = 'N'  and  temp.constraint_type != 'P') then '1' else '0' end) as is_required,
-                    (case when temp.constraint_type = 'P' then '1' else '0' end) as is_pk,
-                    temp.column_id as sort,
-                    temp.comments as column_comment,
-                    (case when temp.constraint_type = 'P' then '1' else '0' end) as is_increment,
-                    lower(temp.data_type) as column_type
-            from (
-                select col.column_id, col.column_name,col.nullable, col.data_type, colc.comments, uc.constraint_type, row_number()
-                    over (partition by col.column_name order by uc.constraint_type desc) as row_flg
-                from user_tab_columns col
-                left join user_col_comments colc on colc.table_name = col.table_name and colc.column_name = col.column_name
-                left join user_cons_columns ucc on ucc.table_name = col.table_name and ucc.column_name = col.column_name
-                left join user_constraints uc on uc.constraint_name = ucc.constraint_name
-                where col.table_name = upper(#{tableName})
-            ) temp
-            WHERE temp.row_flg = 1
-            ORDER BY temp.column_id
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
-            SELECT column_name, is_required, is_pk, sort, column_comment, is_increment, column_type
-            FROM (
-                SELECT c.relname AS table_name,
-                       a.attname AS column_name,
-                       d.description AS column_comment,
-                       CASE WHEN a.attnotnull AND con.conname IS NULL THEN 1 ELSE 0
-                       END AS is_required,
-                       CASE WHEN con.conname IS NOT NULL THEN 1 ELSE 0
-                       END AS is_pk,
-                       a.attnum AS sort,
-                       CASE WHEN "position"(pg_get_expr(ad.adbin, ad.adrelid),
-                           ((c.relname::text || '_'::text) || a.attname::text) || '_seq'::text) > 0 THEN 1 ELSE 0
-                       END AS is_increment,
-                       btrim(
-                           CASE WHEN t.typelem <![CDATA[ <> ]]> 0::oid AND t.typlen = '-1'::integer THEN 'ARRAY'::text ELSE
-                                CASE WHEN t.typtype = 'd'::"char" THEN format_type(t.typbasetype, NULL::integer)
-                                ELSE format_type(a.atttypid, NULL::integer) END
-                           END, '"'::text
-                       ) AS column_type
-                FROM pg_attribute a
-                    JOIN (pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid) ON a.attrelid = c.oid
-                    LEFT JOIN pg_description d ON d.objoid = c.oid AND a.attnum = d.objsubid
-                    LEFT JOIN pg_constraint con ON con.conrelid = c.oid AND (a.attnum = ANY (con.conkey))
-                    LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
-                    LEFT JOIN pg_type t ON a.atttypid = t.oid
-                WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
-                    AND a.attnum > 0
-                    AND n.nspname = 'public'::name
-                ORDER BY c.relname, a.attnum
-            ) temp
-            WHERE table_name = (#{tableName})
-                AND column_type <![CDATA[ <> ]]> '-'
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
-            SELECT
-                cast(A.NAME as nvarchar) as column_name,
-                cast(B.NAME as nvarchar) + (case when B.NAME = 'numeric' then '(' + cast(A.prec as nvarchar) + ',' + cast(A.scale as nvarchar) + ')' else '' end) as column_type,
-                cast(G.[VALUE] as nvarchar) as column_comment,
-                (SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE Z WHERE TABLE_NAME = D.NAME and A.NAME = Z.column_name  ) as is_pk,
-                colorder as sort
-            FROM SYSCOLUMNS A
-                LEFT JOIN SYSTYPES B ON A.XTYPE = B.XUSERTYPE
-                INNER JOIN SYSOBJECTS D ON A.ID = D.ID AND D.XTYPE='U' AND D.NAME != 'DTPROPERTIES'
-                LEFT JOIN SYS.EXTENDED_PROPERTIES G ON A.ID = G.MAJOR_ID AND A.COLID = G.MINOR_ID
-                LEFT JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID AND F.MINOR_ID = 0
-            WHERE D.NAME = #{tableName}
-            ORDER BY A.COLORDER
-        </if>
-    </select>
-
 </mapper>

+ 8 - 222
ruoyi-modules/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml

@@ -14,239 +14,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <id property="columnId" column="column_id"/>
     </resultMap>
 
-    <select id="selectPageDbTableList" resultMap="GenTableResult">
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
-            select table_name, table_comment, create_time, update_time
-            from information_schema.tables
-            where table_schema = (select database())
-            AND table_name NOT LIKE 'sj_%' AND table_name NOT LIKE 'gen_%'
-            AND table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
-            <if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
-                AND table_name NOT IN
-                <foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
-                    #{item}
-                </foreach>
-            </if>
-            <if test="genTable.tableName != null and genTable.tableName != ''">
-                AND lower(table_name) like lower(concat('%', #{genTable.tableName}, '%'))
-            </if>
-            <if test="genTable.tableComment != null and genTable.tableComment != ''">
-                AND lower(table_comment) like lower(concat('%', #{genTable.tableComment}, '%'))
-            </if>
-            order by create_time desc
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
-            select lower(dt.table_name) as table_name, dtc.comments as table_comment, uo.created as create_time, uo.last_ddl_time as update_time
-            from user_tables dt, user_tab_comments dtc, user_objects uo
-            where dt.table_name = dtc.table_name
-            and dt.table_name = uo.object_name
-            and uo.object_type = 'TABLE'
-            AND dt.table_name NOT LIKE 'SJ_%' AND dt.table_name NOT LIKE 'GEN_%'
-            AND dt.table_name NOT LIKE 'ACT_%' AND dt.table_name NOT LIKE 'FLW_%'
-            <if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
-                AND lower(dt.table_name) NOT IN
-                <foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
-                    #{item}
-                </foreach>
-            </if>
-            <if test="genTable.tableName != null and genTable.tableName != ''">
-                AND lower(dt.table_name) like lower(concat(concat('%', #{genTable.tableName}), '%'))
-            </if>
-            <if test="genTable.tableComment != null and genTable.tableComment != ''">
-                AND lower(dtc.comments) like lower(concat(concat('%', #{genTable.tableComment}), '%'))
-            </if>
-            order by create_time desc
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
-            select table_name, table_comment, create_time, update_time
-            from (
-                SELECT c.relname AS table_name,
-                        obj_description(c.oid) AS table_comment,
-                        CURRENT_TIMESTAMP AS create_time,
-                        CURRENT_TIMESTAMP AS update_time
-                FROM pg_class c
-                    LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
-                WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
-                    AND c.relname != 'spatial_%'::text
-                    AND n.nspname = 'public'::name
-                    AND n.nspname <![CDATA[ <> ]]> ''::name
-            ) list_table
-            where table_name NOT LIKE 'sj_%' AND table_name NOT LIKE 'gen_%'
-            AND table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
-            <if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
-                AND table_name NOT IN
-                <foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
-                    #{item}
-                </foreach>
-            </if>
-            <if test="genTable.tableName != null and genTable.tableName != ''">
-                AND lower(table_name) like lower(concat('%', #{genTable.tableName}, '%'))
-            </if>
-            <if test="genTable.tableComment != null and genTable.tableComment != ''">
-                AND lower(table_comment) like lower(concat('%', #{genTable.tableComment}, '%'))
-            </if>
-            order by create_time desc
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
-            SELECT cast(D.NAME as nvarchar) as table_name,
-                   cast(F.VALUE as nvarchar) as table_comment,
-                   crdate as create_time,
-                   refdate as update_time
-            FROM SYSOBJECTS D
-                INNER JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID
-                    AND F.MINOR_ID = 0 AND D.XTYPE = 'U' AND D.NAME != 'DTPROPERTIES'
-                    AND D.NAME NOT LIKE 'sj_%' AND D.NAME NOT LIKE 'gen_%'
-                    AND D.NAME NOT LIKE 'act_%' AND D.NAME NOT LIKE 'flw_%'
-            <if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
-                AND D.NAME NOT IN
-                <foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
-                    #{item}
-                </foreach>
-            </if>
-            <if test="genTable.tableName != null and genTable.tableName != ''">
-                AND lower(D.NAME) like lower(concat(N'%', N'${genTable.tableName}', N'%'))
-            </if>
-            <if test="genTable.tableComment != null and genTable.tableComment != ''">
-                AND lower(CAST(F.VALUE AS nvarchar)) like lower(concat(N'%', N'${genTable.tableComment}', N'%'))
-            </if>
-            order by crdate desc
-        </if>
-    </select>
-
-    <select id="selectDbTableListByNames" resultMap="GenTableResult">
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
-            select table_name, table_comment, create_time, update_time from information_schema.tables
-            where table_schema = (select database())
-            and table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
-            and table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
-            and table_name in
-            <foreach collection="array" item="name" open="(" separator="," close=")">
-                 #{name}
-            </foreach>
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
-            select lower(dt.table_name) as table_name, dtc.comments as table_comment, uo.created as create_time, uo.last_ddl_time as update_time
-            from user_tables dt, user_tab_comments dtc, user_objects uo
-            where dt.table_name = dtc.table_name
-            and dt.table_name = uo.object_name
-            and uo.object_type = 'TABLE'
-            and dt.table_name NOT LIKE 'SJ_%' AND dt.table_name NOT LIKE 'GEN_%'
-            and dt.table_name NOT LIKE 'ACT_%' AND dt.table_name NOT LIKE 'FLW_%'
-            and lower(dt.table_name) in
-            <foreach collection="array" item="name" open="(" separator="," close=")">
-                #{name}
-            </foreach>
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
-            select table_name, table_comment, create_time, update_time
-            from (
-                SELECT c.relname AS table_name,
-                        obj_description(c.oid) AS table_comment,
-                        CURRENT_TIMESTAMP AS create_time,
-                        CURRENT_TIMESTAMP AS update_time
-                FROM pg_class c
-                    LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
-                WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
-                    AND c.relname != 'spatial_%'::text
-                    AND n.nspname = 'public'::name
-                    AND n.nspname <![CDATA[ <> ]]> ''::name
-            ) list_table
-            where table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
-            and table_name NOT LIKE 'act_%' and table_name NOT LIKE 'flw_%'
-            and table_name in
-            <foreach collection="array" item="name" open="(" separator="," close=")">
-                #{name}
-            </foreach>
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
-            SELECT cast(D.NAME as nvarchar) as table_name,
-                   cast(F.VALUE as nvarchar) as table_comment,
-                   crdate as create_time,
-                   refdate as update_time
-            FROM SYSOBJECTS D
-                INNER JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID
-                    AND F.MINOR_ID = 0 AND D.XTYPE = 'U' AND D.NAME != 'DTPROPERTIES'
-                    AND D.NAME NOT LIKE 'sj_%' AND D.NAME NOT LIKE 'gen_%'
-                    AND D.NAME NOT LIKE 'act_%' AND D.NAME NOT LIKE 'flw_%'
-                    AND D.NAME in
-                    <foreach collection="array" item="name" open="(" separator="," close=")">
-                        #{name}
-                    </foreach>
-        </if>
-    </select>
-
-    <select id="selectTableByName" parameterType="String" resultMap="GenTableResult">
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
-            select table_name, table_comment, create_time, update_time from information_schema.tables
-            where table_schema = (select database())
-            and table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
-            and table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
-            and table_name = #{tableName}
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
-            select lower(dt.table_name) as table_name, dtc.comments as table_comment, uo.created as create_time, uo.last_ddl_time as update_time
-            from user_tables dt, user_tab_comments dtc, user_objects uo
-            where dt.table_name = dtc.table_name
-            and dt.table_name = uo.object_name
-            and uo.object_type = 'TABLE'
-            AND dt.table_name NOT LIKE 'SJ_%' AND dt.table_name NOT LIKE 'GEN_%'
-            AND dt.table_name NOT LIKE 'ACT_%' AND dt.table_name NOT LIKE 'FLW_%'
-            AND dt.table_name NOT IN (select table_name from gen_table)
-            and lower(dt.table_name) = #{tableName}
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
-            select table_name, table_comment, create_time, update_time
-            from (
-                SELECT c.relname AS table_name,
-                        obj_description(c.oid) AS table_comment,
-                        CURRENT_TIMESTAMP AS create_time,
-                        CURRENT_TIMESTAMP AS update_time
-                FROM pg_class c
-                    LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
-                WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
-                    AND c.relname != 'spatial_%'::text
-                    AND n.nspname = 'public'::name
-                    AND n.nspname <![CDATA[ <> ]]> ''::name
-            ) list_table
-            where table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
-            and table_name NOT LIKE 'act_%' and table_name NOT LIKE 'flw_%'
-            and table_name = #{tableName}
-        </if>
-        <if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
-            SELECT cast(D.NAME as nvarchar) as table_name,
-                   cast(F.VALUE as nvarchar) as table_comment,
-                   crdate as create_time,
-                   refdate as update_time
-            FROM SYSOBJECTS D
-                INNER JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID
-                    AND F.MINOR_ID = 0 AND D.XTYPE = 'U' AND D.NAME != 'DTPROPERTIES'
-                    AND D.NAME NOT LIKE 'sj_%' AND D.NAME NOT LIKE 'gen_%'
-                    AND D.NAME NOT LIKE 'act_%' AND D.NAME NOT LIKE 'flw_%'
-                    AND D.NAME = #{tableName}
-        </if>
-    </select>
-
-    <select id="selectGenTableById" parameterType="Long" resultMap="GenTableResult">
+    <sql id="genSelect">
         SELECT t.table_id, t.data_name, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.gen_type, t.gen_path, t.options, t.remark,
                c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
         FROM gen_table t
-             LEFT JOIN gen_table_column c ON t.table_id = c.table_id
+                 LEFT JOIN gen_table_column c ON t.table_id = c.table_id
+    </sql>
+
+    <select id="selectGenTableById" parameterType="Long" resultMap="GenTableResult">
+        <include refid="genSelect"/>
         where t.table_id = #{tableId} order by c.sort
     </select>
 
     <select id="selectGenTableByName" parameterType="String" resultMap="GenTableResult">
-        SELECT t.table_id, t.data_name, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.gen_type, t.gen_path, t.options, t.remark,
-               c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
-        FROM gen_table t
-             LEFT JOIN gen_table_column c ON t.table_id = c.table_id
+        <include refid="genSelect"/>
         where t.table_name = #{tableName} order by c.sort
     </select>
 
     <select id="selectGenTableAll" parameterType="String" resultMap="GenTableResult">
-        SELECT t.table_id, t.data_name, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.options, t.remark,
-               c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
-        FROM gen_table t
-             LEFT JOIN gen_table_column c ON t.table_id = c.table_id
+        <include refid="genSelect"/>
         order by c.sort
     </select>
 

+ 7 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/vo.java.vm

@@ -53,6 +53,13 @@ public class ${ClassName}Vo implements Serializable {
 #end
     private $column.javaType $column.javaField;
 
+#if($column.htmlType == "imageUpload")
+    /**
+     * ${column.columnComment}Url
+     */
+    @Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "${column.javaField}")
+    private String ${column.javaField}Url";
+#end
 #end
 #end
 

+ 6 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/ts/types.ts.vm

@@ -9,6 +9,12 @@ export interface ${BusinessName}VO {
                         #elseif($column.javaType == 'Boolean') boolean;
                         #else string;
                     #end
+#if($column.htmlType == "imageUpload")
+  /**
+   * ${column.columnComment}Url
+   */
+  ${column.javaField}Url: string;
+#end
 #end
 #end
 #if ($table.tree)

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

@@ -99,9 +99,9 @@
           </template>
         </el-table-column>
 #elseif($column.list && $column.htmlType == "imageUpload")
-        <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+        <el-table-column label="${comment}" align="center" prop="${javaField}Url" width="100">
           <template #default="scope">
-            <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+            <image-preview :src="scope.row.${javaField}Url" :width="50" :height="50"/>
           </template>
         </el-table-column>
 #elseif($column.list && $column.dictType && "" != $column.dictType)

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

@@ -101,9 +101,9 @@
           </template>
         </el-table-column>
 #elseif($column.list && $column.htmlType == "imageUpload")
-        <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+        <el-table-column label="${comment}" align="center" prop="${javaField}Url" width="100">
           <template #default="scope">
-            <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+            <image-preview :src="scope.row.${javaField}Url" :width="50" :height="50"/>
           </template>
         </el-table-column>
 #elseif($column.list && $column.dictType && "" != $column.dictType)

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

@@ -95,6 +95,11 @@
             <artifactId>ruoyi-common-websocket</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-sse</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 4 - 4
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/monitor/SysLogininforController.java

@@ -1,7 +1,9 @@
 package org.dromara.system.controller.monitor;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
-import org.dromara.common.core.constant.GlobalConstants;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.constant.CacheConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.excel.utils.ExcelUtil;
 import org.dromara.common.log.annotation.Log;
@@ -13,8 +15,6 @@ import org.dromara.common.web.core.BaseController;
 import org.dromara.system.domain.bo.SysLogininforBo;
 import org.dromara.system.domain.vo.SysLogininforVo;
 import org.dromara.system.service.ISysLogininforService;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -79,7 +79,7 @@ public class SysLogininforController extends BaseController {
     @Log(title = "账户解锁", businessType = BusinessType.OTHER)
     @GetMapping("/unlock/{userName}")
     public R<Void> unlock(@PathVariable("userName") String userName) {
-        String loginName = GlobalConstants.PWD_ERR_CNT_KEY + userName;
+        String loginName = CacheConstants.PWD_ERR_CNT_KEY + userName;
         if (RedisUtils.hasKey(loginName)) {
             RedisUtils.deleteObject(loginName);
         }

+ 5 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysDeptController.java

@@ -12,6 +12,7 @@ import org.dromara.common.web.core.BaseController;
 import org.dromara.system.domain.bo.SysDeptBo;
 import org.dromara.system.domain.vo.SysDeptVo;
 import org.dromara.system.service.ISysDeptService;
+import org.dromara.system.service.ISysPostService;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -29,6 +30,7 @@ import java.util.List;
 public class SysDeptController extends BaseController {
 
     private final ISysDeptService deptService;
+    private final ISysPostService postService;
 
     /**
      * 获取部门列表
@@ -117,6 +119,9 @@ public class SysDeptController extends BaseController {
         if (deptService.checkDeptExistUser(deptId)) {
             return R.warn("部门存在用户,不允许删除");
         }
+        if (postService.countPostByDeptId(deptId) > 0) {
+            return R.warn("部门存在岗位,不允许删除");
+        }
         deptService.checkDeptDataScope(deptId);
         return toAjax(deptService.deleteDeptById(deptId));
     }

+ 2 - 2
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysNoticeController.java

@@ -8,8 +8,8 @@ import org.dromara.common.log.annotation.Log;
 import org.dromara.common.log.enums.BusinessType;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.sse.utils.SseMessageUtils;
 import org.dromara.common.web.core.BaseController;
-import org.dromara.common.websocket.utils.WebSocketUtils;
 import org.dromara.system.domain.bo.SysNoticeBo;
 import org.dromara.system.domain.vo.SysNoticeVo;
 import org.dromara.system.service.ISysNoticeService;
@@ -62,7 +62,7 @@ public class SysNoticeController extends BaseController {
             return R.fail();
         }
         String type = dictService.getDictLabel("sys_notice_type", notice.getNoticeType());
-        WebSocketUtils.publishAll("[" + type + "] " + notice.getNoticeTitle());
+        SseMessageUtils.publishAll("[" + type + "] " + notice.getNoticeTitle());
         return R.ok();
     }
 

+ 3 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysProfileController.java

@@ -11,6 +11,7 @@ import org.dromara.common.encrypt.annotation.ApiEncrypt;
 import org.dromara.common.idempotent.annotation.RepeatSubmit;
 import org.dromara.common.log.annotation.Log;
 import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.helper.DataPermissionHelper;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.web.core.BaseController;
 import org.dromara.system.domain.bo.SysUserBo;
@@ -72,7 +73,8 @@ public class SysProfileController extends BaseController {
         if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) {
             return R.fail("修改用户'" + username + "'失败,邮箱账号已存在");
         }
-        if (userService.updateUserProfile(user) > 0) {
+        int rows = DataPermissionHelper.ignore(() -> userService.updateUserProfile(user));
+        if (rows > 0) {
             return R.ok();
         }
         return R.fail("修改个人信息异常,请联系管理员");

+ 2 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysTenantController.java

@@ -24,6 +24,7 @@ import org.dromara.common.web.core.BaseController;
 import org.dromara.system.domain.bo.SysTenantBo;
 import org.dromara.system.domain.vo.SysTenantVo;
 import org.dromara.system.service.ISysTenantService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -38,6 +39,7 @@ import java.util.List;
 @RequiredArgsConstructor
 @RestController
 @RequestMapping("/system/tenant")
+@ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
 public class SysTenantController extends BaseController {
 
     private final ISysTenantService tenantService;

+ 8 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysTenantPackageController.java

@@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -34,6 +35,7 @@ import java.util.List;
 @RequiredArgsConstructor
 @RestController
 @RequestMapping("/system/tenant/package")
+@ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
 public class SysTenantPackageController extends BaseController {
 
     private final ISysTenantPackageService tenantPackageService;
@@ -92,6 +94,9 @@ public class SysTenantPackageController extends BaseController {
     @RepeatSubmit()
     @PostMapping()
     public R<Void> add(@Validated(AddGroup.class) @RequestBody SysTenantPackageBo bo) {
+        if (!tenantPackageService.checkPackageNameUnique(bo)) {
+            return R.fail("新增套餐'" + bo.getPackageName() + "'失败,套餐名称已存在");
+        }
         return toAjax(tenantPackageService.insertByBo(bo));
     }
 
@@ -104,6 +109,9 @@ public class SysTenantPackageController extends BaseController {
     @RepeatSubmit()
     @PutMapping()
     public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysTenantPackageBo bo) {
+        if (!tenantPackageService.checkPackageNameUnique(bo)) {
+            return R.fail("修改套餐'" + bo.getPackageName() + "'失败,套餐名称已存在");
+        }
         return toAjax(tenantPackageService.updateByBo(bo));
     }
 

+ 2 - 2
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java

@@ -61,13 +61,13 @@ public class SysUserVo implements Serializable {
     /**
      * 用户邮箱
      */
-    @Sensitive(strategy = SensitiveStrategy.EMAIL)
+    @Sensitive(strategy = SensitiveStrategy.EMAIL, perms = "system:user:edit")
     private String email;
 
     /**
      * 手机号码
      */
-    @Sensitive(strategy = SensitiveStrategy.PHONE)
+    @Sensitive(strategy = SensitiveStrategy.PHONE, perms = "system:user:edit")
     private String phonenumber;
 
     /**

+ 8 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysPostService.java

@@ -80,6 +80,14 @@ public interface ISysPostService {
      */
     long countUserPostById(Long postId);
 
+    /**
+     * 通过部门ID查询岗位使用数量
+     *
+     * @param deptId 部门id
+     * @return 结果
+     */
+    long countPostByDeptId(Long deptId);
+
     /**
      * 删除岗位信息
      *

+ 8 - 3
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysTenantPackageService.java

@@ -1,9 +1,9 @@
 package org.dromara.system.service;
 
-import org.dromara.system.domain.vo.SysTenantPackageVo;
-import org.dromara.system.domain.bo.SysTenantPackageBo;
-import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.system.domain.bo.SysTenantPackageBo;
+import org.dromara.system.domain.vo.SysTenantPackageVo;
 
 import java.util.Collection;
 import java.util.List;
@@ -45,6 +45,11 @@ public interface ISysTenantPackageService {
      */
     Boolean updateByBo(SysTenantPackageBo bo);
 
+    /**
+     * 校验套餐名称是否唯一
+     */
+    boolean checkPackageNameUnique(SysTenantPackageBo bo);
+
     /**
      * 修改套餐状态
      */

+ 9 - 2
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDataScopeServiceImpl.java

@@ -2,6 +2,7 @@ package org.dromara.system.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.utils.StreamUtils;
@@ -38,6 +39,9 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
      */
     @Override
     public String getRoleCustom(Long roleId) {
+        if (ObjectUtil.isNull(roleId)) {
+            return "-1";
+        }
         List<SysRoleDept> list = roleDeptMapper.selectList(
             new LambdaQueryWrapper<SysRoleDept>()
                 .select(SysRoleDept::getDeptId)
@@ -45,7 +49,7 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
         if (CollUtil.isNotEmpty(list)) {
             return StreamUtils.join(list, rd -> Convert.toStr(rd.getDeptId()));
         }
-        return null;
+        return "-1";
     }
 
     /**
@@ -56,6 +60,9 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
      */
     @Override
     public String getDeptAndChild(Long deptId) {
+        if (ObjectUtil.isNull(deptId)) {
+            return "-1";
+        }
         List<SysDept> deptList = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
             .select(SysDept::getDeptId)
             .apply(DataBaseHelper.findInSet(deptId, "ancestors")));
@@ -64,7 +71,7 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
         if (CollUtil.isNotEmpty(ids)) {
             return StreamUtils.join(ids, Convert::toStr);
         }
-        return null;
+        return "-1";
     }
 
 }

+ 1 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java

@@ -78,7 +78,7 @@ public class SysDeptServiceImpl implements ISysDeptService, DeptService {
 
     private LambdaQueryWrapper<SysDept> buildQueryWrapper(SysDeptBo bo) {
         LambdaQueryWrapper<SysDept> lqw = Wrappers.lambdaQuery();
-        lqw.eq(SysDept::getDelFlag, "0");
+        lqw.eq(SysDept::getDelFlag, UserConstants.DEL_FLAG_NORMAL);
         lqw.eq(ObjectUtil.isNotNull(bo.getDeptId()), SysDept::getDeptId, bo.getDeptId());
         lqw.eq(ObjectUtil.isNotNull(bo.getParentId()), SysDept::getParentId, bo.getParentId());
         lqw.like(StringUtils.isNotBlank(bo.getDeptName()), SysDept::getDeptName, bo.getDeptName());

+ 4 - 2
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java

@@ -77,8 +77,9 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
     @Override
     public List<SysOssVo> listByIds(Collection<Long> ossIds) {
         List<SysOssVo> list = new ArrayList<>();
+        SysOssServiceImpl ossService = SpringUtils.getAopProxy(this);
         for (Long id : ossIds) {
-            SysOssVo vo = SpringUtils.getAopProxy(this).getById(id);
+            SysOssVo vo = ossService.getById(id);
             if (ObjectUtil.isNotNull(vo)) {
                 try {
                     list.add(this.matchingUrl(vo));
@@ -100,8 +101,9 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
     @Override
     public String selectUrlByIds(String ossIds) {
         List<String> list = new ArrayList<>();
+        SysOssServiceImpl ossService = SpringUtils.getAopProxy(this);
         for (Long id : StringUtils.splitTo(ossIds, Convert::toLong)) {
-            SysOssVo vo = SpringUtils.getAopProxy(this).getById(id);
+            SysOssVo vo = ossService.getById(id);
             if (ObjectUtil.isNotNull(vo)) {
                 try {
                     list.add(this.matchingUrl(vo).getUrl());

+ 11 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysPostServiceImpl.java

@@ -177,6 +177,17 @@ public class SysPostServiceImpl implements ISysPostService {
         return userPostMapper.selectCount(new LambdaQueryWrapper<SysUserPost>().eq(SysUserPost::getPostId, postId));
     }
 
+    /**
+     * 通过部门ID查询岗位使用数量
+     *
+     * @param deptId 部门id
+     * @return 结果
+     */
+    @Override
+    public long countPostByDeptId(Long deptId) {
+        return baseMapper.selectCount(new LambdaQueryWrapper<SysPost>().eq(SysPost::getDeptId, deptId));
+    }
+
     /**
      * 删除岗位信息
      *

+ 4 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysRoleServiceImpl.java

@@ -293,6 +293,10 @@ public class SysRoleServiceImpl implements ISysRoleService {
     @Transactional(rollbackFor = Exception.class)
     public int updateRole(SysRoleBo bo) {
         SysRole role = MapstructUtils.convert(bo, SysRole.class);
+
+        if (UserConstants.ROLE_DISABLE.equals(role.getStatus()) && this.countUserRoleByRoleId(role.getRoleId()) > 0) {
+            throw new ServiceException("角色已分配,不能禁用!");
+        }
         // 修改角色信息
         baseMapper.updateById(role);
         // 删除角色与菜单关联

+ 12 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTenantPackageServiceImpl.java

@@ -1,6 +1,7 @@
 package org.dromara.system.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -116,6 +117,17 @@ public class SysTenantPackageServiceImpl implements ISysTenantPackageService {
         return baseMapper.updateById(update) > 0;
     }
 
+    /**
+     * 校验套餐名称是否唯一
+     */
+    @Override
+    public boolean checkPackageNameUnique(SysTenantPackageBo bo) {
+        boolean exist = baseMapper.exists(new LambdaQueryWrapper<SysTenantPackage>()
+            .eq(SysTenantPackage::getPackageName, bo.getPackageName())
+            .ne(ObjectUtil.isNotNull(bo.getPackageId()), SysTenantPackage::getPackageId, bo.getPackageId()));
+        return !exist;
+    }
+
     /**
      * 修改套餐状态
      *

部分文件因为文件数量过多而无法显示