Forráskód Böngészése

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/iot

# Conflicts:
#	yudao-server/src/main/resources/application-local.yaml
YunaiV 5 hónapja
szülő
commit
f124584d06
100 módosított fájl, 1672 hozzáadás és 492 törlés
  1. BIN
      .image/common/ai-feature.png
  2. 35 12
      README.md
  3. 5 5
      pom.xml
  4. 398 153
      sql/mysql/ruoyi-vue-pro.sql
  5. 38 25
      yudao-dependencies/pom.xml
  6. 3 4
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java
  7. 3 3
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java
  8. 2 2
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java
  9. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java
  10. 4 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/package-info.java
  11. 4 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/package-info.java
  12. 22 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/DictDataCommonApi.java
  13. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/dto/DictDataRespDTO.java
  14. 31 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/OperateLogCommonApi.java
  15. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java
  16. 5 5
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
  17. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java
  18. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java
  19. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java
  20. 4 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/package-info.java
  21. 3 14
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/PermissionCommonApi.java
  22. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/dto/DeptDataPermissionRespDTO.java
  23. 2 2
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/TenantCommonApi.java
  24. 14 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  25. 6 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java
  26. 0 23
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
  27. 27 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java
  28. 0 7
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml
  29. 3 3
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java
  30. 7 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java
  31. 7 4
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
  32. 16 6
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java
  33. 3 3
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java
  34. 9 1
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java
  35. 68 4
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java
  36. 15 1
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java
  37. 7 1
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java
  38. 48 9
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java
  39. 2 2
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkServiceImpl.java
  40. 65 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java
  41. 0 7
      yudao-framework/yudao-spring-boot-starter-excel/pom.xml
  42. 2 2
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java
  43. 31 48
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java
  44. 1 1
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java
  45. 20 11
      yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtilsTest.java
  46. 12 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
  47. 3 3
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
  48. 72 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java
  49. 10 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java
  50. 35 2
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  51. 56 20
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java
  52. 1 1
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java
  53. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java
  54. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java
  55. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java
  56. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java
  57. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java
  58. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java
  59. 2 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java
  60. 10 4
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java
  61. 4 4
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java
  62. 1 2
      yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java
  63. 0 7
      yudao-framework/yudao-spring-boot-starter-security/pom.xml
  64. 4 4
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java
  65. 4 4
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java
  66. 4 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  67. 4 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
  68. 7 7
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
  69. 21 2
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java
  70. 18 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
  71. 0 12
      yudao-framework/yudao-spring-boot-starter-web/pom.xml
  72. 2 2
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java
  73. 4 4
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java
  74. 2 2
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
  75. 22 5
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
  76. 15 5
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
  77. 192 6
      yudao-module-ai/pom.xml
  78. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java
  79. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
  80. 42 5
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
  81. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java
  82. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java
  83. 2 2
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java
  84. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java
  85. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java
  86. 25 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
  87. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
  88. 7 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
  89. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http
  90. 1 1
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
  91. 3 6
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java
  92. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
  93. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java
  94. 1 1
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java
  95. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java
  96. 0 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java
  97. 3 3
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java
  98. 35 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http
  99. 84 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java
  100. 35 0
      yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http

BIN
.image/common/ai-feature.png


+ 35 - 12
README.md

@@ -1,5 +1,5 @@
 <p align="center">
- <img src="https://img.shields.io/badge/Spring%20Boot-3.4.1-blue.svg" alt="Downloads">
+ <img src="https://img.shields.io/badge/Spring%20Boot-3.4.5-blue.svg" alt="Downloads">
  <img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
  <img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro" alt="Downloads" />
 </p>
@@ -149,22 +149,45 @@
 
 ### 工作流程
 
-|    | 功能    | 描述                                      |
-|----|-------|-----------------------------------------|
-| 🚀 | 流程模型  | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器           |
-| 🚀 | 流程表单  | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件  |
-| 🚀 | 用户分组  | 自定义用户分组,可用于工作流的审批分组                     |
-| 🚀 | 我的流程  | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线     |
-| 🚀 | 待办任务  | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
-| 🚀 | 已办任务  | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息         |
-| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批  |
-
 ![功能图](/.image/common/bpm-feature.png)
 
+基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作:
+
 | BPMN 设计器                     | 钉钉/飞书设计器                       |
 |------------------------------|--------------------------------|
 | ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
 
+> 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!!
+>
+> 前者支持轻量配置简单流程,后者实现复杂场景深度编排
+
+| 功能列表       | 功能描述                                                                                | 是否完成 |
+|------------|-------------------------------------------------------------------------------------|------|
+| SIMPLE 设计器 | 仿钉钉/飞书设计器,支持拖拽搭建表单流程,10 分钟快速完成审批流程配置                                                | ✅    |
+| BPMN 设计器   | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求                                               | ✅    |
+| 会签         | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点                            | ✅    |
+| 或签         | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点                                                     | ✅    |
+| 依次审批       | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批,A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅    |
+| 抄送         | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人                                                     | ✅    |
+| 驳回         | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点                                              | ✅    |
+| 转办         | A 转给其 B 审批,B 审批后,进入下一节点                                                             | ✅    |
+| 委派         | A 转给其 B 审批,B 审批后,转给 A,A 继续审批后进入下一节点                                                 | ✅    |
+| 加签         | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签                                                  | ✅    |
+| 减签         | (取消加签)在当前审批人操作之前,减少审批人                                                              | ✅    |
+| 撤销         | (取消流程)流程发起人,可以对流程进行撤销处理                                                             | ✅    |
+| 终止         | 系统管理员,在任意节点终止流程实例                                                                   | ✅    |
+| 表单权限       | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限                                                       | ✅    |
+| 超时审批       | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作                                                      | ✅    |
+| 自动提醒       | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次                                          | ✅    |
+| 父子流程       | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程                      | ✅    |
+| 条件分支       | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行                                                      | ✅    |
+| 并行分支       | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行                                                        | ✅    |
+| 包容分支       | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支                           | ✅    |
+| 路由分支       | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行)                                        | ✅    |
+| 触发节点       | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等                                                | ✅    |
+| 延迟节点       | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等                                                     | ✅    |
+| 拓展设置       | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等                              | ✅    |
+
 ### 支付系统
 
 |     | 功能   | 描述                        |
@@ -285,7 +308,7 @@
 
 | 框架                                                                                          | 说明               | 版本             | 学习指南                                                           |
 |---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
-| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 3.4.1          | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
+| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 3.4.5          | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
 | [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器           | 5.7 / 8.0+     |                                                                |
 | [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件    | 1.2.23         | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
 | [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.7          | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |

+ 5 - 5
pom.xml

@@ -16,7 +16,7 @@
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
 <!--        <module>yudao-module-member</module>-->
-        <module>yudao-module-bpm</module>
+<!--        <module>yudao-module-bpm</module>-->
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-pay</module>-->
@@ -24,7 +24,7 @@
 <!--        <module>yudao-module-crm</module>-->
 <!--        <module>yudao-module-erp</module>-->
 <!--        <module>yudao-module-ai</module>-->
-        <module>yudao-module-iot</module>
+<!--        <module>yudao-module-iot</module>-->
     </modules>
 
     <name>${project.artifactId}</name>
@@ -32,17 +32,17 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>2.4.0-SNAPSHOT</revision>
+        <revision>2.5.0-SNAPSHOT</revision>
         <!-- Maven 相关 -->
         <java.version>17</java.version>
         <maven.compiler.source>${java.version}</maven.compiler.source>
         <maven.compiler.target>${java.version}</maven.compiler.target>
         <maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
-        <maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
+        <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
         <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
         <!-- 看看咋放到 bom 里 -->
         <lombok.version>1.18.36</lombok.version>
-        <spring.boot.version>3.4.1</spring.boot.version>
+        <spring.boot.version>3.4.5</spring.boot.version>
         <mapstruct.version>1.6.3</mapstruct.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 398 - 153
sql/mysql/ruoyi-vue-pro.sql


+ 38 - 25
yudao-dependencies/pom.xml

@@ -14,17 +14,17 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>2.4.0-SNAPSHOT</revision>
+        <revision>2.5.0-SNAPSHOT</revision>
         <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
         <!-- 统一依赖管理 -->
-        <spring.boot.version>3.4.1</spring.boot.version>
+        <spring.boot.version>3.4.5</spring.boot.version>
         <!-- Web 相关 -->
-        <springdoc.version>2.7.0</springdoc.version>
+        <springdoc.version>2.8.3</springdoc.version>
         <knife4j.version>4.6.0</knife4j.version>
         <!-- DB 相关 -->
         <druid.version>1.2.24</druid.version>
-        <mybatis.version>3.5.17</mybatis.version>
-        <mybatis-plus.version>3.5.9</mybatis-plus.version>
+        <mybatis.version>3.5.19</mybatis.version>
+        <mybatis-plus.version>3.5.10.1</mybatis-plus.version>
         <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
         <mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
         <easy-trans.version>3.0.6</easy-trans.version>
@@ -39,7 +39,7 @@
         <lock4j.version>2.2.7</lock4j.version>
         <!-- 监控相关 -->
         <skywalking.version>9.0.0</skywalking.version>
-        <spring-boot-admin.version>3.4.1</spring-boot-admin.version>
+        <spring-boot-admin.version>3.4.5</spring-boot-admin.version>
         <opentracing.version>0.33.0</opentracing.version>
         <!-- Test 测试相关 -->
         <podam.version>8.0.2.RELEASE</podam.version>
@@ -48,20 +48,20 @@
         <!-- Bpm 工作流相关 -->
         <flowable.version>7.0.1</flowable.version>
         <!-- 工具类相关 -->
-        <captcha-plus.version>2.0.3</captcha-plus.version>
+        <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
         <jsoup.version>1.18.3</jsoup.version>
         <lombok.version>1.18.36</lombok.version>
         <mapstruct.version>1.6.3</mapstruct.version>
         <hutool-5.version>5.8.35</hutool-5.version>
         <hutool-6.version>6.0.0-M19</hutool-6.version>
-        <easyexcel.verion>4.0.3</easyexcel.verion>
+        <easyexcel.version>4.0.3</easyexcel.version>
         <velocity.version>2.4.1</velocity.version>
         <fastjson.version>1.2.83</fastjson.version>
-        <guava.version>33.4.0-jre</guava.version>
+        <guava.version>33.4.8-jre</guava.version>
         <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
         <commons-net.version>3.11.1</commons-net.version>
         <jsch.version>0.1.55</jsch.version>
-        <tika-core.version>2.9.2</tika-core.version>
+        <tika-core.version>3.1.0</tika-core.version>
         <ip2region.version>2.7.0</ip2region.version>
         <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
         <netty.version>4.1.116.Final</netty.version>
@@ -71,10 +71,11 @@
         <!-- 三方云服务相关 -->
         <commons-io.version>2.17.0</commons-io.version>
         <commons-compress.version>1.27.1</commons-compress.version>
-        <aws-java-sdk-s3.version>1.12.777</aws-java-sdk-s3.version>
-        <justauth.version>2.0.5</justauth.version>
-        <jimureport.version>1.8.1</jimureport.version>
-        <weixin-java.version>4.6.0</weixin-java.version>
+        <awssdk.version>2.30.14</awssdk.version>
+        <justauth.version>1.16.7</justauth.version>
+        <justauth-starter.version>1.4.0</justauth-starter.version>
+        <jimureport.version>1.9.4</jimureport.version>
+        <weixin-java.version>4.7.5.B</weixin-java.version>
     </properties>
 
     <dependencyManagement>
@@ -174,6 +175,7 @@
                 <version>${druid.version}</version>
             </dependency>
             <dependency>
+                <!-- 注意:必须声明,避免 flowable 和 mybatis-plus 引入的 mybatis 版本不一致!!! -->
                 <groupId>org.mybatis</groupId>
                 <artifactId>mybatis</artifactId>
                 <version>${mybatis.version}</version>
@@ -479,7 +481,7 @@
             <dependency>
                 <groupId>com.alibaba</groupId>
                 <artifactId>easyexcel</artifactId>
-                <version>${easyexcel.verion}</version>
+                <version>${easyexcel.version}</version>
             </dependency>
             <dependency>
                 <groupId>commons-io</groupId>
@@ -534,9 +536,9 @@
             </dependency>
 
             <dependency>
-                <groupId>com.xingyuv</groupId>
-                <artifactId>spring-boot-starter-captcha-plus</artifactId>
-                <version>${captcha-plus.version}</version>
+                <groupId>com.anji-plus</groupId>
+                <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
+                <version>${anji-plus-captcha.version}</version>
             </dependency>
 
             <dependency>
@@ -553,16 +555,22 @@
 
             <!-- 三方云服务相关 -->
             <dependency>
-                <groupId>com.amazonaws</groupId>
-                <artifactId>aws-java-sdk-s3</artifactId>
-                <version>${aws-java-sdk-s3.version}</version>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3</artifactId>
+                <version>${awssdk.version}</version>
             </dependency>
 
             <dependency>
-                <groupId>com.xingyuv</groupId>
-                <artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
+                <groupId>me.zhyd.oauth</groupId>
+                <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
                 <version>${justauth.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.xkcoding.justauth</groupId>
+                <artifactId>justauth-spring-boot-starter</artifactId>
+                <version>${justauth-starter.version}</version>
                 <exclusions>
+                    <!-- 移除,避免和项目里的 hutool-all 冲突 -->
                     <exclusion>
                         <groupId>cn.hutool</groupId>
                         <artifactId>hutool-core</artifactId>
@@ -591,10 +599,15 @@
                 <groupId>org.jeecgframework.jimureport</groupId>
                 <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
                 <version>${jimureport.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.jeecgframework.jimureport</groupId>
+                <artifactId>jimubi-spring-boot3-starter</artifactId>
+                <version>${jimureport.version}</version>
                 <exclusions>
                     <exclusion>
-                        <groupId>com.alibaba</groupId>
-                        <artifactId>druid</artifactId>
+                        <groupId>com.github.jsqlparser</groupId>
+                        <artifactId>jsqlparser</artifactId>
                     </exclusion>
                 </exclusions>
             </dependency>

+ 3 - 4
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java

@@ -1,7 +1,6 @@
-package cn.iocoder.yudao.module.infra.api.logger;
-
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
+package cn.iocoder.yudao.framework.common.biz.infra.logger;
 
+import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO;
 import jakarta.validation.Valid;
 import org.springframework.scheduling.annotation.Async;
 
@@ -10,7 +9,7 @@ import org.springframework.scheduling.annotation.Async;
  *
  * @author 芋道源码
  */
-public interface ApiAccessLogApi {
+public interface ApiAccessLogCommonApi {
 
     /**
      * 创建 API 访问日志

+ 3 - 3
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java

@@ -1,6 +1,6 @@
-package cn.iocoder.yudao.module.infra.api.logger;
+package cn.iocoder.yudao.framework.common.biz.infra.logger;
 
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
+import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
 
 import jakarta.validation.Valid;
 import org.springframework.scheduling.annotation.Async;
@@ -10,7 +10,7 @@ import org.springframework.scheduling.annotation.Async;
  *
  * @author 芋道源码
  */
-public interface ApiErrorLogApi {
+public interface ApiErrorLogCommonApi {
 
     /**
      * 创建 API 错误日志

+ 2 - 2
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java

@@ -1,8 +1,8 @@
-package cn.iocoder.yudao.module.infra.api.logger.dto;
+package cn.iocoder.yudao.framework.common.biz.infra.logger.dto;
 
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
-import jakarta.validation.constraints.NotNull;
 import java.time.LocalDateTime;
 
 /**

+ 1 - 1
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.infra.api.logger.dto;
+package cn.iocoder.yudao.framework.common.biz.infra.logger.dto;
 
 import lombok.Data;
 

+ 4 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 针对 infra 模块的 api 包
+ */
+package cn.iocoder.yudao.framework.common.biz.infra;

+ 4 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 特殊:用于 framework 下,starter 需要调用 biz 业务模块的接口定义!
+ */
+package cn.iocoder.yudao.framework.common.biz;

+ 22 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/DictDataCommonApi.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.framework.common.biz.system.dict;
+
+import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
+
+import java.util.List;
+
+/**
+ * 字典数据 API 接口
+ *
+ * @author 芋道源码
+ */
+public interface DictDataCommonApi {
+
+    /**
+     * 获得指定字典类型的字典数据列表
+     *
+     * @param dictType 字典类型
+     * @return 字典数据列表
+     */
+    List<DictDataRespDTO> getDictDataList(String dictType);
+
+}

+ 1 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/dto/DictDataRespDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/dto/DictDataRespDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.api.dict.dto;
+package cn.iocoder.yudao.framework.common.biz.system.dict.dto;
 
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import lombok.Data;

+ 31 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/OperateLogCommonApi.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.common.biz.system.logger;
+
+import cn.iocoder.yudao.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO;
+import jakarta.validation.Valid;
+import org.springframework.scheduling.annotation.Async;
+
+/**
+ * 操作日志 API 接口
+ *
+ * @author 芋道源码
+ */
+public interface OperateLogCommonApi {
+
+    /**
+     * 创建操作日志
+     *
+     * @param createReqDTO 请求
+     */
+    void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO);
+
+    /**
+     * 【异步】创建操作日志
+     *
+     * @param createReqDTO 请求
+     */
+    @Async
+    default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) {
+        createOperateLog(createReqDTO);
+    }
+
+}

+ 1 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/OperateLogCreateReqDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.api.logger.dto;
+package cn.iocoder.yudao.framework.common.biz.system.logger.dto;
 
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import jakarta.validation.constraints.NotEmpty;

+ 5 - 5
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApi.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java

@@ -1,8 +1,8 @@
-package cn.iocoder.yudao.module.system.api.oauth2;
+package cn.iocoder.yudao.framework.common.biz.system.oauth2;
 
-import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
-import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
-import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO;
 
 import jakarta.validation.Valid;
 
@@ -11,7 +11,7 @@ import jakarta.validation.Valid;
  *
  * @author 芋道源码
  */
-public interface OAuth2TokenApi {
+public interface OAuth2TokenCommonApi {
 
     /**
      * 创建访问令牌

+ 1 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.api.oauth2.dto;
+package cn.iocoder.yudao.framework.common.biz.system.oauth2.dto;
 
 import lombok.Data;
 

+ 1 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.api.oauth2.dto;
+package cn.iocoder.yudao.framework.common.biz.system.oauth2.dto;
 
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.validation.InEnum;

+ 1 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.api.oauth2.dto;
+package cn.iocoder.yudao.framework.common.biz.system.oauth2.dto;
 
 import lombok.Data;
 import lombok.experimental.Accessors;

+ 4 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 针对 system 模块的 api 包
+ */
+package cn.iocoder.yudao.framework.common.biz.system;

+ 3 - 14
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/PermissionCommonApi.java

@@ -1,24 +1,13 @@
-package cn.iocoder.yudao.module.system.api.permission;
+package cn.iocoder.yudao.framework.common.biz.system.permission;
 
-import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
-
-import java.util.Collection;
-import java.util.Set;
+import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
 
 /**
  * 权限 API 接口
  *
  * @author 芋道源码
  */
-public interface PermissionApi {
-
-    /**
-     * 获得拥有多个角色的用户编号集合
-     *
-     * @param roleIds 角色编号集合
-     * @return 用户编号集合
-     */
-    Set<Long> getUserRoleIdListByRoleIds(Collection<Long> roleIds);
+public interface PermissionCommonApi {
 
     /**
      * 判断是否有权限,任一一个即可

+ 1 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/dto/DeptDataPermissionRespDTO.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/dto/DeptDataPermissionRespDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.api.permission.dto;
+package cn.iocoder.yudao.framework.common.biz.system.permission.dto;
 
 import lombok.Data;
 

+ 2 - 2
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/tenant/TenantApi.java → yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/TenantCommonApi.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.api.tenant;
+package cn.iocoder.yudao.framework.common.biz.system.tenant;
 
 import java.util.List;
 
@@ -7,7 +7,7 @@ import java.util.List;
  *
  * @author 芋道源码
  */
-public interface TenantApi {
+public interface TenantCommonApi {
 
     /**
      * 获得所有租户

+ 14 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java

@@ -11,6 +11,7 @@ import java.util.function.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import static cn.hutool.core.convert.Convert.toCollection;
 import static java.util.Arrays.asList;
 
 /**
@@ -335,4 +336,17 @@ public class CollectionUtils {
         return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
     }
 
+    /**
+     * 转换为 LinkedHashSet
+     *
+     * @param <T>         元素类型
+     * @param elementType 集合中元素类型
+     * @param value       被转换的值
+     * @return {@link LinkedHashSet}
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> LinkedHashSet<T> toLinkedHashSet(Class<T> elementType, Object value) {
+        return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
+    }
+
 }

+ 6 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java

@@ -8,12 +8,16 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
 
 import java.time.*;
+import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.TemporalAdjusters;
 import java.util.ArrayList;
 import java.util.List;
 
+import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN;
+import static cn.hutool.core.date.DatePattern.createFormatter;
+
 /**
  * 时间工具类,用于 {@link java.time.LocalDateTime}
  *
@@ -26,6 +30,8 @@ public class LocalDateTimeUtils {
      */
     public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
 
+    public static DateTimeFormatter UTC_MS_WITH_XXX_OFFSET_FORMATTER = createFormatter(UTC_MS_WITH_XXX_OFFSET_PATTERN);
+
     /**
      * 解析时间
      *

+ 0 - 23
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java

@@ -1,14 +1,9 @@
 package cn.iocoder.yudao.framework.common.util.io;
 
-import cn.hutool.core.io.FileTypeUtil;
 import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.file.FileNameUtil;
 import cn.hutool.core.util.IdUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.crypto.digest.DigestUtil;
 import lombok.SneakyThrows;
 
-import java.io.ByteArrayInputStream;
 import java.io.File;
 
 /**
@@ -63,22 +58,4 @@ public class FileUtils {
         return file;
     }
 
-    /**
-     * 生成文件路径
-     *
-     * @param content      文件内容
-     * @param originalName 原始文件名
-     * @return path,唯一不可重复
-     */
-    public static String generatePath(byte[] content, String originalName) {
-        String sha256Hex = DigestUtil.sha256Hex(content);
-        // 情况一:如果存在 name,则优先使用 name 的后缀
-        if (StrUtil.isNotBlank(originalName)) {
-            String extName = FileNameUtil.extName(originalName);
-            return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
-        }
-        // 情况二:基于 content 计算
-        return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
-    }
-
 }

+ 27 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.string;
 import cn.hutool.core.text.StrPool;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
+import org.aspectj.lang.JoinPoint;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -77,4 +78,30 @@ public class StrUtils {
                 .collect(Collectors.joining("\n"));
     }
 
+    /**
+     * 拼接方法的参数
+     *
+     * 特殊:排除一些无法序列化的参数,如 ServletRequest、ServletResponse、MultipartFile
+     *
+     * @param joinPoint 连接点
+     * @return 拼接后的参数
+     */
+    public static String joinMethodArgs(JoinPoint joinPoint) {
+        Object[] args = joinPoint.getArgs();
+        if (ArrayUtil.isEmpty(args)) {
+            return "";
+        }
+        return ArrayUtil.join(args, ",", item -> {
+            if (item == null) {
+                return "";
+            }
+            // 讨论可见:https://t.zsxq.com/XUJVk、https://t.zsxq.com/MnKcL
+            String clazzName = item.getClass().getName();
+            if (StrUtil.startWithAny(clazzName, "javax.servlet", "jakarta.servlet", "org.springframework.web")) {
+                return "";
+            }
+            return item;
+        });
+    }
+
 }

+ 0 - 7
yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml

@@ -34,13 +34,6 @@
             <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
         </dependency>
 
-        <!-- 业务组件 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 -->
-            <version>${revision}</version>
-        </dependency>
-
         <!-- Test 测试相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java

@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.framework.datapermission.config;
 
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
 import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
 import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
-import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -18,11 +18,11 @@ import java.util.List;
  */
 @AutoConfiguration
 @ConditionalOnClass(LoginUser.class)
-@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
+@ConditionalOnBean(value = {PermissionCommonApi.class, DeptDataPermissionRuleCustomizer.class})
 public class YudaoDeptDataPermissionAutoConfiguration {
 
     @Bean
-    public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
+    public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi,
                                                          List<DeptDataPermissionRuleCustomizer> customizers) {
         // 创建 DeptDataPermissionRule 对象
         DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java

@@ -12,6 +12,8 @@ import net.sf.jsqlparser.schema.Table;
 
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
+
 /**
  * 基于 {@link DataPermissionRule} 的数据权限处理器
  *
@@ -27,6 +29,11 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
 
     @Override
     public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
+        // 特殊:跨租户访问
+        if (skipPermissionCheck()) {
+            return null;
+        }
+
         // 获得 Mapper 对应的数据权限的规则
         List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
         if (CollUtil.isEmpty(rules)) {

+ 7 - 4
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@@ -11,12 +13,13 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
-import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
-import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import net.sf.jsqlparser.expression.*;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.NullValue;
 import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
 import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
 import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
@@ -59,7 +62,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
 
     static final Expression EXPRESSION_NULL = new NullValue();
 
-    private final PermissionApi permissionApi;
+    private final PermissionCommonApi permissionApi;
 
     /**
      * 基于部门的表字段配置

+ 16 - 6
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java

@@ -32,13 +32,12 @@ public class DataPermissionUtils {
      * @param runnable 逻辑
      */
     public static void executeIgnore(Runnable runnable) {
-        DataPermission dataPermission = getDisableDataPermissionDisable();
-        DataPermissionContextHolder.add(dataPermission);
+        addDisableDataPermission();
         try {
             // 执行 runnable
             runnable.run();
         } finally {
-            DataPermissionContextHolder.remove();
+            removeDataPermission();
         }
     }
 
@@ -50,14 +49,25 @@ public class DataPermissionUtils {
      */
     @SneakyThrows
     public static <T> T executeIgnore(Callable<T> callable) {
-        DataPermission dataPermission = getDisableDataPermissionDisable();
-        DataPermissionContextHolder.add(dataPermission);
+        addDisableDataPermission();
         try {
             // 执行 callable
             return callable.call();
         } finally {
-            DataPermissionContextHolder.remove();
+            removeDataPermission();
         }
     }
 
+    /**
+     * 添加忽略数据权限
+     */
+    public static void addDisableDataPermission(){
+        DataPermission dataPermission = getDisableDataPermissionDisable();
+        DataPermissionContextHolder.add(dataPermission);
+    }
+
+    public static void removeDataPermission(){
+        DataPermissionContextHolder.remove();
+    }
+
 }

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java

@@ -2,13 +2,13 @@ package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ReflectUtil;
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
-import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
 import net.sf.jsqlparser.expression.Alias;
 import net.sf.jsqlparser.expression.Expression;
 import org.junit.jupiter.api.BeforeEach;
@@ -39,7 +39,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
     private DeptDataPermissionRule rule;
 
     @Mock
-    private PermissionApi permissionApi;
+    private PermissionCommonApi permissionApi;
 
     @BeforeEach
     @SuppressWarnings("unchecked")

+ 9 - 1
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java

@@ -4,6 +4,7 @@ import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -30,7 +31,14 @@ public class TenantProperties {
      *
      * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
      */
-    private Set<String> ignoreUrls = Collections.emptySet();
+    private Set<String> ignoreUrls = new HashSet<>();
+
+    /**
+     * 需要忽略跨(切换)租户访问的请求
+     *
+     * 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的!
+     */
+    private Set<String> ignoreVisitUrls = Collections.emptySet();
 
     /**
      * 需要忽略多租户的表

+ 68 - 4
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java

@@ -1,8 +1,11 @@
 package cn.iocoder.yudao.framework.tenant.config;
 
+import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
 import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
+import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
 import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
 import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect;
@@ -14,16 +17,18 @@ import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
 import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
+import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
-import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import jakarta.annotation.Resource;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Primary;
 import org.springframework.data.redis.cache.BatchStrategies;
@@ -32,16 +37,28 @@ import org.springframework.data.redis.cache.RedisCacheManager;
 import org.springframework.data.redis.cache.RedisCacheWriter;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
-
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import org.springframework.web.util.pattern.PathPattern;
+
+import java.util.Map;
 import java.util.Objects;
 
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
 @AutoConfiguration
 @ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
 @EnableConfigurationProperties(TenantProperties.class)
 public class YudaoTenantAutoConfiguration {
 
+    @Resource
+    private ApplicationContext applicationContext;
+
     @Bean
-    public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) {
+    public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) {
         return new TenantFrameworkServiceImpl(tenantApi);
     }
 
@@ -67,13 +84,60 @@ public class YudaoTenantAutoConfiguration {
     // ========== WEB ==========
 
     @Bean
-    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
+    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter(TenantProperties tenantProperties) {
         FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
         registrationBean.setFilter(new TenantContextWebFilter());
         registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
+        addIgnoreUrls(tenantProperties);
         return registrationBean;
     }
 
+    /**
+     * 如果 Controller 接口上,有 {@link TenantIgnore} 注解,那么添加到忽略的 URL 中
+     *
+     * @param tenantProperties 租户配置
+     */
+    private void addIgnoreUrls(TenantProperties tenantProperties) {
+        // 获得接口对应的 HandlerMethod 集合
+        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
+                applicationContext.getBean("requestMappingHandlerMapping");
+        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
+        // 获得有 @TenantIgnore 注解的接口
+        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
+            HandlerMethod handlerMethod = entry.getValue();
+            if (!handlerMethod.hasMethodAnnotation(TenantIgnore.class)) {
+                continue;
+            }
+            // 添加到忽略的 URL 中
+            if (entry.getKey().getPatternsCondition() != null) {
+                tenantProperties.getIgnoreUrls().addAll(entry.getKey().getPatternsCondition().getPatterns());
+            }
+            if (entry.getKey().getPathPatternsCondition() != null) {
+                tenantProperties.getIgnoreUrls().addAll(
+                        convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
+            }
+        }
+    }
+
+    @Bean
+    public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties,
+                                                                       SecurityFrameworkService securityFrameworkService) {
+        return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService);
+    }
+
+    @Bean
+    public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties,
+                                                   TenantVisitContextInterceptor tenantVisitContextInterceptor) {
+        return new WebMvcConfigurer() {
+
+            @Override
+            public void addInterceptors(InterceptorRegistry registry) {
+                registry.addInterceptor(tenantVisitContextInterceptor)
+                        .excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0]));
+            }
+        };
+    }
+
     // ========== Security ==========
 
     @Bean

+ 15 - 1
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.framework.tenant.core.aop;
 
+import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
+
 import java.lang.annotation.*;
 
 /**
@@ -9,10 +11,22 @@ import java.lang.annotation.*;
  * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
  * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
  *
+ * 特殊:
+ * 1、如果添加到 Controller 类上,则该 URL 自动添加到 {@link TenantProperties#getIgnoreUrls()} 中
+ * 2、如果添加到 DO 实体类上,则它对应的表名“相当于”自动添加到 {@link TenantProperties#getIgnoreTables()} 中
+ *
  * @author 芋道源码
  */
-@Target({ElementType.METHOD})
+@Target({ElementType.METHOD, ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 @Inherited
 public @interface TenantIgnore {
+
+    /**
+     * 是否开启忽略租户,默认为 true 开启
+     *
+     * 支持 Spring EL 表达式,如果返回 true 则满足条件,进行租户的忽略
+     */
+    String enable() default "true";
+
 }

+ 7 - 1
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.tenant.core.aop;
 
+import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import lombok.extern.slf4j.Slf4j;
@@ -24,7 +25,12 @@ public class TenantIgnoreAspect {
     public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
         Boolean oldIgnore = TenantContextHolder.isIgnore();
         try {
-            TenantContextHolder.setIgnore(true);
+            // 计算条件,满足的情况下,才进行忽略
+            Object enable = SpringExpressionUtils.parseExpression(tenantIgnore.enable());
+            if (Boolean.TRUE.equals(enable)) {
+                TenantContextHolder.setIgnore(true);
+            }
+
             // 执行逻辑
             return joinPoint.proceed();
         } finally {

+ 48 - 9
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java

@@ -1,15 +1,17 @@
 package cn.iocoder.yudao.framework.tenant.core.db;
 
-import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
 import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils;
 import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.LongValue;
 
-import java.util.HashSet;
-import java.util.Set;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
@@ -18,16 +20,21 @@ import java.util.Set;
  */
 public class TenantDatabaseInterceptor implements TenantLineHandler {
 
-    private final Set<String> ignoreTables = new HashSet<>();
+    /**
+     * 忽略的表
+     *
+     * KEY:表名
+     * VALUE:是否忽略
+     */
+    private final Map<String, Boolean> ignoreTables = new HashMap<>();
 
     public TenantDatabaseInterceptor(TenantProperties properties) {
         // 不同 DB 下,大小写的习惯不同,所以需要都添加进去
         properties.getIgnoreTables().forEach(table -> {
-            ignoreTables.add(table.toLowerCase());
-            ignoreTables.add(table.toUpperCase());
+            addIgnoreTable(table, true);
         });
         // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
-        ignoreTables.add("DUAL");
+        addIgnoreTable("DUAL", true);
     }
 
     @Override
@@ -37,8 +44,40 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
 
     @Override
     public boolean ignoreTable(String tableName) {
-        return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
-                || CollUtil.contains(ignoreTables, SqlParserUtils.removeWrapperSymbol(tableName)); // 情况二,忽略多租户的表
+        // 情况一,全局忽略多租户
+        if (TenantContextHolder.isIgnore()) {
+            return true;
+        }
+        // 情况二,忽略多租户的表
+        tableName = SqlParserUtils.removeWrapperSymbol(tableName);
+        Boolean ignore = ignoreTables.get(tableName.toLowerCase());
+        if (ignore == null) {
+            ignore = computeIgnoreTable(tableName);
+            synchronized (ignoreTables) {
+                addIgnoreTable(tableName, ignore);
+            }
+        }
+        return ignore;
+    }
+
+    private void addIgnoreTable(String tableName, boolean ignore) {
+        ignoreTables.put(tableName.toLowerCase(), ignore);
+        ignoreTables.put(tableName.toUpperCase(), ignore);
+    }
+
+    private boolean computeIgnoreTable(String tableName) {
+        // 找不到的表,说明不是 yudao 项目里的,不进行拦截(忽略租户)
+        TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
+        if (tableInfo == null) {
+            return true;
+        }
+        // 如果继承了 TenantBaseDO 基类,显然不忽略租户
+        if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
+            return false;
+        }
+        // 如果添加了 @TenantIgnore 注解,显然也不忽略租户
+        TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);
+        return tenantIgnore != null;
     }
 
 }

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkServiceImpl.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.framework.tenant.core.service;
 
+import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
-import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import lombok.RequiredArgsConstructor;
@@ -21,7 +21,7 @@ public class TenantFrameworkServiceImpl implements TenantFrameworkService {
 
     private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException();
 
-    private final TenantApi tenantApi;
+    private final TenantCommonApi tenantApi;
 
     /**
      * 针对 {@link #getTenantIds()} 的缓存

+ 65 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java

@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.framework.tenant.core.web;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
+
+@RequiredArgsConstructor
+@Slf4j
+public class TenantVisitContextInterceptor implements HandlerInterceptor {
+
+    private static final String PERMISSION = "system:tenant:visit";
+
+    private final TenantProperties tenantProperties;
+
+    private final SecurityFrameworkService securityFrameworkService;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+        // 如果和当前租户编号一致,则直接跳过
+        Long visitTenantId = WebFrameworkUtils.getVisitTenantId(request);
+        if (visitTenantId == null) {
+            return true;
+        }
+        if (ObjUtil.equal(visitTenantId, TenantContextHolder.getTenantId())) {
+            return true;
+        }
+        // 必须是登录用户
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        if (loginUser == null) {
+            return true;
+        }
+
+        // 校验用户是否可切换租户
+        if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) {
+            throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户");
+        }
+
+        // 【重点】切换租户编号
+        loginUser.setVisitTenantId(visitTenantId);
+        TenantContextHolder.setTenantId(visitTenantId);
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+        // 【重点】清理切换,换回原租户编号
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        if (loginUser != null && loginUser.getTenantId() != null) {
+            TenantContextHolder.setTenantId(loginUser.getTenantId());
+        }
+    }
+
+}

+ 0 - 7
yudao-framework/yudao-spring-boot-starter-excel/pom.xml

@@ -27,13 +27,6 @@
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
 
-        <!-- 业务组件 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行 Dict 的查询 -->
-            <version>${revision}</version>
-        </dependency>
-
         <!-- Web 相关 -->
         <dependency>
             <groupId>org.springframework</groupId>

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.dict.config;
 
+import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
 import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
-import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.context.annotation.Bean;
 
@@ -10,7 +10,7 @@ public class YudaoDictAutoConfiguration {
 
     @Bean
     @SuppressWarnings("InstantiationOfUtilityClass")
-    public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) {
+    public DictFrameworkUtils dictUtils(DictDataCommonApi dictDataApi) {
         DictFrameworkUtils.init(dictDataApi);
         return new DictFrameworkUtils();
     }

+ 31 - 48
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java

@@ -1,10 +1,9 @@
 package cn.iocoder.yudao.framework.dict.core;
 
-import cn.hutool.core.util.ObjectUtil;
-import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
 import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
-import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
-import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
+import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import lombok.SneakyThrows;
@@ -12,6 +11,9 @@ import lombok.extern.slf4j.Slf4j;
 
 import java.time.Duration;
 import java.util.List;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
 /**
  * 字典工具类
@@ -21,76 +23,57 @@ import java.util.List;
 @Slf4j
 public class DictFrameworkUtils {
 
-    private static DictDataApi dictDataApi;
-
-    private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();
-
-    // TODO @puhui999:GET_DICT_DATA_CACHE、GET_DICT_DATA_LIST_CACHE、PARSE_DICT_DATA_CACHE 这 3 个缓存是有点重叠,可以思考下,有没可能减少 1 个。微信讨论好私聊,再具体改哈
-    /**
-     * 针对 {@link #getDictDataLabel(String, String)} 的缓存
-     */
-    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
-            Duration.ofMinutes(1L), // 过期时间 1 分钟
-            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
-
-                @Override
-                public DictDataRespDTO load(KeyValue<String, String> key) {
-                    return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
-                }
-
-            });
-
-    /**
-     * 针对 {@link #getDictDataLabelList(String)} 的缓存
-     */
-    private static final LoadingCache<String, List<String>> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache(
-            Duration.ofMinutes(1L), // 过期时间 1 分钟
-            new CacheLoader<String, List<String>>() {
-
-                @Override
-                public List<String> load(String dictType) {
-                    return dictDataApi.getDictDataLabelList(dictType);
-                }
-
-            });
+    private static DictDataCommonApi dictDataApi;
 
     /**
-     * 针对 {@link #parseDictDataValue(String, String)} 的缓存
+     * 针对 dictType 的字段数据缓存
      */
-    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
+    private static final LoadingCache<String, List<DictDataRespDTO>> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
             Duration.ofMinutes(1L), // 过期时间 1 分钟
-            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
+            new CacheLoader<String, List<DictDataRespDTO>>() {
 
                 @Override
-                public DictDataRespDTO load(KeyValue<String, String> key) {
-                    return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
+                public List<DictDataRespDTO> load(String dictType) {
+                    return dictDataApi.getDictDataList(dictType);
                 }
 
             });
 
-    public static void init(DictDataApi dictDataApi) {
+    public static void init(DictDataCommonApi dictDataApi) {
         DictFrameworkUtils.dictDataApi = dictDataApi;
         log.info("[init][初始化 DictFrameworkUtils 成功]");
     }
 
+    public static void clearCache() {
+        GET_DICT_DATA_CACHE.invalidateAll();
+    }
+
     @SneakyThrows
-    public static String getDictDataLabel(String dictType, Integer value) {
-        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel();
+    public static String parseDictDataLabel(String dictType, Integer value) {
+        if (value == null) {
+            return null;
+        }
+        return parseDictDataLabel(dictType, String.valueOf(value));
     }
 
     @SneakyThrows
-    public static String getDictDataLabel(String dictType, String value) {
-        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
+    public static String parseDictDataLabel(String dictType, String value) {
+        List<DictDataRespDTO> dictDatas = GET_DICT_DATA_CACHE.get(dictType);
+        DictDataRespDTO dictData = CollUtil.findOne(dictDatas, data -> Objects.equals(data.getValue(), value));
+        return dictData != null ? dictData.getLabel(): null;
     }
 
     @SneakyThrows
     public static List<String> getDictDataLabelList(String dictType) {
-        return GET_DICT_DATA_LIST_CACHE.get(dictType);
+        List<DictDataRespDTO> dictDatas = GET_DICT_DATA_CACHE.get(dictType);
+        return convertList(dictDatas, DictDataRespDTO::getLabel);
     }
 
     @SneakyThrows
     public static String parseDictDataValue(String dictType, String label) {
-        return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
+        List<DictDataRespDTO> dictDatas = GET_DICT_DATA_CACHE.get(dictType);
+        DictDataRespDTO dictData = CollUtil.findOne(dictDatas, data -> Objects.equals(data.getLabel(), label));
+        return dictData!= null ? dictData.getValue(): null;
     }
 
 }

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java

@@ -56,7 +56,7 @@ public class DictConvert implements Converter<Object> {
         // 使用字典格式化
         String type = getType(contentProperty);
         String value = String.valueOf(object);
-        String label = DictFrameworkUtils.getDictDataLabel(type, value);
+        String label = DictFrameworkUtils.parseDictDataLabel(type, value);
         if (label == null) {
             log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
             return new WriteCellData<>("");

+ 20 - 11
yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtilsTest.java

@@ -1,16 +1,18 @@
 package cn.iocoder.yudao.framework.dict.core.util;
 
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
 import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
-import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
 
+import java.util.List;
+
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
 /**
@@ -19,33 +21,40 @@ import static org.mockito.Mockito.when;
 public class DictFrameworkUtilsTest extends BaseMockitoUnitTest {
 
     @Mock
-    private DictDataApi dictDataApi;
+    private DictDataCommonApi dictDataApi;
 
     @BeforeEach
     public void setUp() {
         DictFrameworkUtils.init(dictDataApi);
+        DictFrameworkUtils.clearCache();
     }
 
     @Test
-    public void testGetDictDataLabel() {
+    public void testParseDictDataLabel() {
         // mock 数据
-        DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        List<DictDataRespDTO> dictDatas = List.of(
+                randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("cat").setLabel("猫")),
+                randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("dog").setLabel("狗"))
+        );
         // mock 方法
-        when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(dataRespDTO);
+        when(dictDataApi.getDictDataList(eq("animal"))).thenReturn(dictDatas);
 
         // 断言返回值
-        assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue()));
+        assertEquals("狗", DictFrameworkUtils.parseDictDataLabel("animal", "dog"));
     }
 
     @Test
     public void testParseDictDataValue() {
         // mock 数据
-        DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        List<DictDataRespDTO> dictDatas = List.of(
+                randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("cat").setLabel("猫")),
+                randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("dog").setLabel("狗"))
+        );
         // mock 方法
-        when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(resp);
+        when(dictDataApi.getDictDataList(eq("animal"))).thenReturn(dictDatas);
 
         // 断言返回值
-        assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel()));
+        assertEquals("dog", DictFrameworkUtils.parseDictDataValue("animal", "狗"));
     }
 
 }

+ 12 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java

@@ -6,6 +6,7 @@ import cn.hutool.system.SystemUtil;
 import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
 import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
 import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
+import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob;
 import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
 import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
@@ -73,6 +74,17 @@ public class YudaoRedisMQConsumerAutoConfiguration {
         return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
     }
 
+    /**
+     * 创建 Redis Stream 消息清理任务
+     */
+    @Bean
+    @ConditionalOnBean(AbstractRedisStreamMessageListener.class)
+    public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
+                                                                     RedisMQTemplate redisTemplate,
+                                                                     RedissonClient redissonClient) {
+        return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient);
+    }
+
     /**
      * 创建 Redis Stream 集群消费的容器
      *

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java

@@ -23,13 +23,13 @@ import java.util.Objects;
 @AllArgsConstructor
 public class RedisPendingMessageResendJob {
 
-    private static final String LOCK_KEY = "redis:pending:msg:lock";
+    private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
 
     /**
      * 消息超时时间,默认 5 分钟
      *
      * 1. 超时的消息才会被重新投递
-     * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到
+     * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息 5 分钟过期后,再等 1 分钟才会被扫瞄到
      */
     private static final int EXPIRE_TIME = 5 * 60;
 
@@ -39,7 +39,7 @@ public class RedisPendingMessageResendJob {
     private final RedissonClient redissonClient;
 
     /**
-     * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
+     * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
      */
     @Scheduled(cron = "35 * * * * ?")
     public void messageResend() {

+ 72 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.framework.mq.redis.core.job;
+
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.data.redis.core.StreamOperations;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.List;
+
+/**
+ * Redis Stream 消息清理任务
+ * 用于定期清理已消费的消息,防止内存占用过大
+ *
+ * @see <a href="https://www.cnblogs.com/nanxiang/p/16179519.html">记一次 redis stream 数据类型内存不释放问题</a>
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@AllArgsConstructor
+public class RedisStreamMessageCleanupJob {
+
+    private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
+
+    /**
+     * 保留的消息数量,默认保留最近 10000 条消息
+     */
+    private static final long MAX_COUNT = 10000;
+
+    private final List<AbstractRedisStreamMessageListener<?>> listeners;
+    private final RedisMQTemplate redisTemplate;
+    private final RedissonClient redissonClient;
+
+    /**
+     * 每小时执行一次清理任务
+     */
+    @Scheduled(cron = "0 0 * * * ?")
+    public void cleanup() {
+        RLock lock = redissonClient.getLock(LOCK_KEY);
+        // 尝试加锁
+        if (lock.tryLock()) {
+            try {
+                execute();
+            } catch (Exception ex) {
+                log.error("[cleanup][执行异常]", ex);
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 执行清理逻辑
+     */
+    private void execute() {
+        StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
+        listeners.forEach(listener -> {
+            try {
+                // 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息
+                Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
+                if (trimCount != null && trimCount > 0) {
+                    log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
+                }
+            } catch (Exception ex) {
+                log.error("[execute][Stream({}) 清理异常]", listener.getStreamKey(), ex);
+            }
+        });
+    }
+}

+ 10 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java

@@ -53,4 +53,14 @@ public abstract class BaseDO implements Serializable, TransPojo {
     @TableLogic
     private Boolean deleted;
 
+    /**
+     * 把 creator、createTime、updateTime、updater 都清空,避免前端直接传递 creator 之类的字段,直接就被更新了
+     */
+    public void clean(){
+        this.creator = null;
+        this.createTime = null;
+        this.updater = null;
+        this.updateTime = null;
+    }
+
 }

+ 35 - 2
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -92,10 +92,36 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
 
     default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
                         SFunction<T, ?> field3, Object value3) {
-        return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)
-                .eq(field3, value3));
+        return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
     }
 
+    /**
+     * 获取满足条件的第 1 条记录
+     *
+     * 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题
+     *
+     * @param field 字段名
+     * @param value 字段值
+     * @return 实体
+     */
+    default T selectFirstOne(SFunction<T, ?> field, Object value) {
+        // 如果明确使用 MySQL 等场景,可以考虑使用 LIMIT 1 进行优化
+        List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field, value));
+        return CollUtil.getFirst(list);
+    }
+
+    default T selectFirstOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
+        List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
+        return CollUtil.getFirst(list);
+    }
+
+    default T selectFirstOne(SFunction<T,?> field1, Object value1, SFunction<T,?> field2, Object value2,
+                             SFunction<T,?> field3, Object value3) {
+        List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
+        return CollUtil.getFirst(list);
+    }
+
+
     default Long selectCount() {
         return selectCount(new QueryWrapper<>());
     }
@@ -189,4 +215,11 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         return delete(new LambdaQueryWrapper<T>().eq(field, value));
     }
 
+    default int deleteBatch(SFunction<T, ?> field, Collection<?> values) {
+        if (CollUtil.isEmpty(values)) {
+            return 0;
+        }
+        return delete(new LambdaQueryWrapper<T>().in(field, values));
+    }
+
 }

+ 56 - 20
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java

@@ -4,7 +4,6 @@ import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
-import com.github.yulichang.toolkit.MPJWrappers;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.springframework.util.StringUtils;
 
@@ -15,93 +14,93 @@ import java.util.function.Consumer;
  * 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能:
  * <p>
  * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
- *
+ * 2. SFunction<S, ?> column + <S> 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
  * @param <T> 数据类型
  */
 public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
 
-    public MPJLambdaWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
-        MPJWrappers.lambdaJoin().like(column, val);
+    public <S> MPJLambdaWrapperX<T> likeIfPresent(SFunction<S, ?> column, String val) {
         if (StringUtils.hasText(val)) {
             return (MPJLambdaWrapperX<T>) super.like(column, val);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
+    public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) {
         if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
             return (MPJLambdaWrapperX<T>) super.in(column, values);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
+    public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Object... values) {
         if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
             return (MPJLambdaWrapperX<T>) super.in(column, values);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
+    public <S> MPJLambdaWrapperX<T> eqIfPresent(SFunction<S, ?> column, Object val) {
         if (ObjectUtil.isNotEmpty(val)) {
             return (MPJLambdaWrapperX<T>) super.eq(column, val);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
+    public <S> MPJLambdaWrapperX<T> neIfPresent(SFunction<S, ?> column, Object val) {
         if (ObjectUtil.isNotEmpty(val)) {
             return (MPJLambdaWrapperX<T>) super.ne(column, val);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
+    public <S> MPJLambdaWrapperX<T> gtIfPresent(SFunction<S, ?> column, Object val) {
         if (val != null) {
             return (MPJLambdaWrapperX<T>) super.gt(column, val);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
+    public <S> MPJLambdaWrapperX<T> geIfPresent(SFunction<S, ?> column, Object val) {
         if (val != null) {
             return (MPJLambdaWrapperX<T>) super.ge(column, val);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
+    public <S> MPJLambdaWrapperX<T> ltIfPresent(SFunction<S, ?> column, Object val) {
         if (val != null) {
             return (MPJLambdaWrapperX<T>) super.lt(column, val);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
+    public <S> MPJLambdaWrapperX<T> leIfPresent(SFunction<S, ?> column, Object val) {
         if (val != null) {
             return (MPJLambdaWrapperX<T>) super.le(column, val);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
+    public <S> MPJLambdaWrapperX<T> betweenIfPresent(SFunction<S, ?> column, Object[] values) {
+        Object val1 = ArrayUtils.get(values, 0);
+        Object val2 = ArrayUtils.get(values, 1);
+        return betweenIfPresent(column, val1, val2);
+    }
+
+    public <S> MPJLambdaWrapperX<T> betweenIfPresent(SFunction<S, ?> column, Object val1, Object val2) {
         if (val1 != null && val2 != null) {
             return (MPJLambdaWrapperX<T>) super.between(column, val1, val2);
         }
         if (val1 != null) {
-            return (MPJLambdaWrapperX<T>) ge(column, val1);
+            return (MPJLambdaWrapperX<T>) super.ge(column, val1);
         }
         if (val2 != null) {
-            return (MPJLambdaWrapperX<T>) le(column, val2);
+            return (MPJLambdaWrapperX<T>) super.le(column, val2);
         }
         return this;
     }
 
-    public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
-        Object val1 = ArrayUtils.get(values, 0);
-        Object val2 = ArrayUtils.get(values, 1);
-        return betweenIfPresent(column, val1, val2);
-    }
 
     // ========== 重写父类方法,方便链式调用 ==========
 
@@ -310,4 +309,41 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
         return this;
     }
 
+    // ========== 关键重写:使 leftJoin 返回当前类型 this ==========
+    @Override
+    public <A, B> MPJLambdaWrapperX<T> leftJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right) {
+        super.leftJoin(clazz, left, right);
+        return this;
+    }
+
+    @Override
+    public <A, B> MPJLambdaWrapperX<T> rightJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right) {
+        super.rightJoin(clazz, left, right);
+        return this;
+    }
+
+    @Override
+    public <A, B> MPJLambdaWrapperX<T> innerJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right) {
+        super.innerJoin(clazz, left, right);
+        return this;
+    }
+
+    // ========== 添加扩展 Join 支持 ext 函数式参数 ==========
+    public <A, B> MPJLambdaWrapperX<T> leftJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right, Consumer<MPJLambdaWrapperX<T>> ext) {
+        super.leftJoin(clazz, left, right);
+        if (ext != null) ext.accept(this);
+        return this;
+    }
+
+    public <A, B> MPJLambdaWrapperX<T> rightJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right, Consumer<MPJLambdaWrapperX<T>> ext) {
+        super.rightJoin(clazz, left, right);
+        if (ext != null) ext.accept(this);
+        return this;
+    }
+
+    public <A, B> MPJLambdaWrapperX<T> innerJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right, Consumer<MPJLambdaWrapperX<T>> ext) {
+        super.innerJoin(clazz, left, right);
+        if (ext != null) ext.accept(this);
+        return this;
+    }
 }

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java

@@ -13,7 +13,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 
 /**
- * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现
+ * 字段字段的 TypeHandler 实现类,基于 {@link AES} 实现
  * 可通过 jasypt.encryptor.password 配置项,设置密钥
  *
  * @author 芋道源码

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl;
 
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent;
 import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 import org.aspectj.lang.JoinPoint;
@@ -18,7 +18,7 @@ public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
     @Override
     public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
         String methodName = joinPoint.getSignature().toString();
-        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String argsStr = StrUtils.joinMethodArgs(joinPoint);
         return SecureUtil.md5(methodName + argsStr);
     }
 

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl;
 
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent;
 import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@@ -19,7 +19,7 @@ public class UserIdempotentKeyResolver implements IdempotentKeyResolver {
     @Override
     public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
         String methodName = joinPoint.getSignature().toString();
-        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String argsStr = StrUtils.joinMethodArgs(joinPoint);
         Long userId = WebFrameworkUtils.getLoginUserId();
         Integer userType = WebFrameworkUtils.getLoginUserType();
         return SecureUtil.md5(methodName + argsStr + userId + userType);

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
 
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
 import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
 import org.aspectj.lang.JoinPoint;
@@ -19,7 +19,7 @@ public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver {
     @Override
     public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
         String methodName = joinPoint.getSignature().toString();
-        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String argsStr = StrUtils.joinMethodArgs(joinPoint);
         String clientIp = ServletUtils.getClientIP();
         return SecureUtil.md5(methodName + argsStr + clientIp);
     }

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
 
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
 import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
 import org.aspectj.lang.JoinPoint;
@@ -18,7 +18,7 @@ public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {
     @Override
     public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
         String methodName = joinPoint.getSignature().toString();
-        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String argsStr = StrUtils.joinMethodArgs(joinPoint);
         return SecureUtil.md5(methodName + argsStr);
     }
 

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
 
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
 import cn.hutool.system.SystemUtil;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
 import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
 import org.aspectj.lang.JoinPoint;
@@ -19,7 +19,7 @@ public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver
     @Override
     public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
         String methodName = joinPoint.getSignature().toString();
-        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String argsStr = StrUtils.joinMethodArgs(joinPoint);
         String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
         return SecureUtil.md5(methodName + argsStr + serverNode);
     }

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
 
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
 import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@@ -19,7 +19,7 @@ public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {
     @Override
     public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
         String methodName = joinPoint.getSignature().toString();
-        String argsStr = StrUtil.join(",", joinPoint.getArgs());
+        String argsStr = StrUtils.joinMethodArgs(joinPoint);
         Long userId = WebFrameworkUtils.getLoginUserId();
         Integer userType = WebFrameworkUtils.getLoginUserType();
         return SecureUtil.md5(methodName + argsStr + userId + userType);

+ 2 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java

@@ -44,6 +44,7 @@ public class RateLimiterRedisDAO {
         RateLimiterConfig config = rateLimiter.getConfig();
         if (config == null) {
             rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
+            rateLimiter.expire(rateInterval, TimeUnit.SECONDS); // 原因参见 https://t.zsxq.com/lcR0W
             return rateLimiter;
         }
         // 2. 如果存在,并且配置相同,则直接返回
@@ -54,6 +55,7 @@ public class RateLimiterRedisDAO {
         }
         // 3. 如果存在,并且配置不同,则进行新建
         rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
+        rateLimiter.expire(rateInterval, TimeUnit.SECONDS); // 原因参见 https://t.zsxq.com/lcR0W
         return rateLimiter;
     }
 

+ 10 - 4
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.framework.signature.core.aop;
 
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.BooleanUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.digest.DigestUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
 import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
@@ -69,13 +71,17 @@ public class ApiSignatureAspect {
 
         // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
         String nonce = request.getHeader(signature.nonce());
-        signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit());
+        if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()))) {
+            String timestamp = request.getHeader(signature.timestamp());
+            log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature);
+            throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求");
+        }
         return true;
     }
 
     /**
      * 校验请求头加签参数
-     *
+     * <p>
      * 1. appId 是否为空
      * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
      * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
@@ -118,7 +124,7 @@ public class ApiSignatureAspect {
 
     /**
      * 构建签名字符串
-     *
+     * <p>
      * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
      *
      * @param signature signature
@@ -139,7 +145,7 @@ public class ApiSignatureAspect {
     /**
      * 获取请求头加签参数 Map
      *
-     * @param request 请求
+     * @param request   请求
      * @param signature 签名注解
      * @return signature params
      */

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java

@@ -17,7 +17,7 @@ public class ApiSignatureRedisDAO {
 
     /**
      * 验签随机数
-     *
+     * <p>
      * KEY 格式:signature_nonce:%s // 参数为 随机数
      * VALUE 格式:String
      * 过期时间:不固定
@@ -26,7 +26,7 @@ public class ApiSignatureRedisDAO {
 
     /**
      * 签名密钥
-     *
+     * <p>
      * HASH 结构
      * KEY 格式:%s // 参数为 appid
      * VALUE 格式:String
@@ -40,8 +40,8 @@ public class ApiSignatureRedisDAO {
         return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce));
     }
 
-    public void setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
-        stringRedisTemplate.opsForValue().set(formatNonceKey(appId, nonce), "", time, timeUnit);
+    public Boolean setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
+        return stringRedisTemplate.opsForValue().setIfAbsent(formatNonceKey(appId, nonce), "", time, timeUnit);
     }
 
     private static String formatNonceKey(String appId, String nonce) {

+ 1 - 2
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java

@@ -63,13 +63,12 @@ public class ApiSignatureTest {
         when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
         // mock 方法
         when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
+        when(signatureRedisDAO.setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS))).thenReturn(true);
 
         // 调用
         boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
         // 断言结果
         assertTrue(result);
-        // 断言调用
-        verify(signatureRedisDAO).setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS));
     }
 
 }

+ 0 - 7
yudao-framework/yudao-spring-boot-starter-security/pom.xml

@@ -59,13 +59,6 @@
             <groupId>io.github.mouzt</groupId>
             <artifactId>bizlog-sdk</artifactId>
         </dependency>
-
-        <!-- 业务组件 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
-            <version>${revision}</version>
-        </dependency>
     </dependencies>
 
 </project>

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java

@@ -1,11 +1,11 @@
 package cn.iocoder.yudao.framework.operatelog.core.service;
 
+import cn.iocoder.yudao.framework.common.biz.system.logger.OperateLogCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO;
 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
-import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
-import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogCreateReqDTO;
 import com.mzt.logapi.beans.LogRecord;
 import com.mzt.logapi.service.ILogRecordService;
 import jakarta.annotation.Resource;
@@ -17,7 +17,7 @@ import java.util.List;
 /**
  * 操作日志 ILogRecordService 实现类
  *
- * 基于 {@link OperateLogApi} 实现,记录操作日志
+ * 基于 {@link OperateLogCommonApi} 实现,记录操作日志
  *
  * @author HUIHUI
  */
@@ -25,7 +25,7 @@ import java.util.List;
 public class LogRecordServiceImpl implements ILogRecordService {
 
     @Resource
-    private OperateLogApi operateLogApi;
+    private OperateLogCommonApi operateLogApi;
 
     @Override
     public void record(LogRecord logRecord) {

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.framework.security.config;
 
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
 import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
 import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
 import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
@@ -7,8 +9,6 @@ import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPoint
 import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
 import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkServiceImpl;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
-import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
-import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
 import jakarta.annotation.Resource;
 import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -69,12 +69,12 @@ public class YudaoSecurityAutoConfiguration {
      */
     @Bean
     public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler,
-                                                               OAuth2TokenApi oauth2TokenApi) {
+                                                               OAuth2TokenCommonApi oauth2TokenApi) {
         return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
     }
 
     @Bean("ss") // 使用 Spring Security 的缩写,方便使用
-    public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) {
+    public SecurityFrameworkService securityFrameworkService(PermissionCommonApi permissionApi) {
         return new SecurityFrameworkServiceImpl(permissionApi);
     }
 

+ 4 - 1
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java

@@ -7,6 +7,7 @@ import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
 import jakarta.annotation.Resource;
 import jakarta.annotation.security.PermitAll;
+import jakarta.servlet.DispatcherType;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.AutoConfigureOrder;
 import org.springframework.context.ApplicationContext;
@@ -142,7 +143,9 @@ public class YudaoWebSecurityConfigurerAdapter {
                 // ②:每个项目的自定义规则
                 .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c)))
                 // ③:兜底规则,必须认证
-                .authorizeHttpRequests(c -> c.anyRequest().authenticated());
+                .authorizeHttpRequests(c -> c
+                        .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() // WebFlux 异步请求,无需认证,目的:SSE 场景
+                        .anyRequest().authenticated());
 
         // 添加 Token Filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java

@@ -56,6 +56,10 @@ public class LoginUser {
      */
     @JsonIgnore
     private Map<String, Object> context;
+    /**
+     * 访问的租户编号
+     */
+    private Long visitTenantId;
 
     public void setContext(String key, Object value) {
         if (context == null) {

+ 7 - 7
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.security.core.filter;
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
@@ -10,16 +12,14 @@ import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
-import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
-import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.security.access.AccessDeniedException;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
 
 /**
@@ -35,7 +35,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
 
     private final GlobalExceptionHandler globalExceptionHandler;
 
-    private final OAuth2TokenApi oauth2TokenApi;
+    private final OAuth2TokenCommonApi oauth2TokenApi;
 
     @Override
     @SuppressWarnings("NullableProblems")

+ 21 - 2
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java

@@ -1,14 +1,15 @@
 package cn.iocoder.yudao.framework.security.core.service;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
-import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
 import lombok.AllArgsConstructor;
 
 import java.util.Arrays;
 
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
 
 /**
  * 默认的 {@link SecurityFrameworkService} 实现类
@@ -18,7 +19,7 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
 @AllArgsConstructor
 public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
 
-    private final PermissionApi permissionApi;
+    private final PermissionCommonApi permissionApi;
 
     @Override
     public boolean hasPermission(String permission) {
@@ -27,6 +28,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
 
     @Override
     public boolean hasAnyPermissions(String... permissions) {
+        // 特殊:跨租户访问
+        if (skipPermissionCheck()) {
+            return true;
+        }
+
+        // 权限校验
         Long userId = getLoginUserId();
         if (userId == null) {
             return false;
@@ -41,6 +48,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
 
     @Override
     public boolean hasAnyRoles(String... roles) {
+        // 特殊:跨租户访问
+        if (skipPermissionCheck()) {
+            return true;
+        }
+
+        // 权限校验
         Long userId = getLoginUserId();
         if (userId == null) {
             return false;
@@ -55,6 +68,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
 
     @Override
     public boolean hasAnyScopes(String... scope) {
+        // 特殊:跨租户访问
+        if (skipPermissionCheck()) {
+            return true;
+        }
+
+        // 权限校验
         LoginUser user = SecurityFrameworkUtils.getLoginUser();
         if (user == null) {
             return false;

+ 18 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.security.core.util;
 
 import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@@ -137,4 +138,21 @@ public class SecurityFrameworkUtils {
         return authenticationToken;
     }
 
+    /**
+     * 是否条件跳过权限校验,包括数据权限、功能权限
+     *
+     * @return 是否跳过
+     */
+    public static boolean skipPermissionCheck() {
+        LoginUser loginUser = getLoginUser();
+        if (loginUser == null) {
+            return false;
+        }
+        if (loginUser.getVisitTenantId() == null) {
+            return false;
+        }
+        // 重点:跨租户访问时,无法进行权限校验
+        return ObjUtil.notEqual(loginUser.getVisitTenantId(), loginUser.getTenantId());
+    }
+
 }

+ 0 - 12
yudao-framework/yudao-spring-boot-starter-web/pom.xml

@@ -53,18 +53,6 @@
             <scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
         </dependency>
 
-        <!-- 业务组件 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
-            <version>${revision}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行错误码的记录 -->
-            <version>${revision}</version>
-        </dependency>
-
         <!-- xss -->
         <dependency>
             <groupId>org.jsoup</groupId>

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java

@@ -2,10 +2,10 @@ package cn.iocoder.yudao.framework.apilog.config;
 
 import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter;
 import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
+import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiAccessLogCommonApi;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
-import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
 import jakarta.servlet.Filter;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -25,7 +25,7 @@ public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
     @ConditionalOnProperty(prefix = "yudao.access-log", value = "enable", matchIfMissing = true) // 允许使用 yudao.access-log.enable=false 禁用访问日志
     public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
                                                                          @Value("${spring.application.name}") String applicationName,
-                                                                         ApiAccessLogApi apiAccessLogApi) {
+                                                                         ApiAccessLogCommonApi apiAccessLogApi) {
         ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi);
         return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
     }

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java

@@ -9,6 +9,8 @@ import cn.hutool.core.util.BooleanUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
 import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
+import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiAccessLogCommonApi;
+import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO;
 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@@ -17,8 +19,6 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
-import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
 import com.fasterxml.jackson.databind.JsonNode;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -53,9 +53,9 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
 
     private final String applicationName;
 
-    private final ApiAccessLogApi apiAccessLogApi;
+    private final ApiAccessLogCommonApi apiAccessLogApi;
 
-    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogApi apiAccessLogApi) {
+    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogCommonApi apiAccessLogApi) {
         super(webProperties);
         this.applicationName = applicationName;
         this.apiAccessLogApi = apiAccessLogApi;

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java

@@ -1,12 +1,12 @@
 package cn.iocoder.yudao.framework.web.config;
 
+import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
 import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
-import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
 import jakarta.annotation.Resource;
 import jakarta.servlet.Filter;
 import org.springframework.beans.factory.annotation.Value;
@@ -59,7 +59,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
 
     @Bean
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogApi apiErrorLogApi) {
+    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogCommonApi apiErrorLogApi) {
         return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
     }
 

+ 22 - 5
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.web.core.handler;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
@@ -13,8 +14,8 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
-import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
+import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
+import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
 import com.fasterxml.jackson.databind.exc.InvalidFormatException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.ConstraintViolation;
@@ -27,6 +28,7 @@ import org.springframework.security.access.AccessDeniedException;
 import org.springframework.util.Assert;
 import org.springframework.validation.BindException;
 import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
 import org.springframework.web.HttpRequestMethodNotSupportedException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
@@ -37,6 +39,7 @@ import org.springframework.web.servlet.NoHandlerFoundException;
 import org.springframework.web.servlet.resource.NoResourceFoundException;
 
 import java.time.LocalDateTime;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -60,7 +63,7 @@ public class GlobalExceptionHandler {
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     private final String applicationName;
 
-    private final ApiErrorLogApi apiErrorLogApi;
+    private final ApiErrorLogCommonApi apiErrorLogApi;
 
     /**
      * 处理所有异常,主要是提供给 Filter 使用
@@ -135,9 +138,23 @@ public class GlobalExceptionHandler {
     @ExceptionHandler(MethodArgumentNotValidException.class)
     public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
         log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
+        // 获取 errorMessage
+        String errorMessage = null;
         FieldError fieldError = ex.getBindingResult().getFieldError();
-        assert fieldError != null; // 断言,避免告警
-        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
+        if (fieldError == null) {
+            // 组合校验,参考自 https://t.zsxq.com/3HVTx
+            List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
+            if (CollUtil.isNotEmpty(allErrors)) {
+                errorMessage = allErrors.get(0).getDefaultMessage();
+            }
+        } else {
+            errorMessage = fieldError.getDefaultMessage();
+        }
+        // 转换 CommonResult
+        if (StrUtil.isEmpty(errorMessage)) {
+            return CommonResult.error(BAD_REQUEST);
+        }
+        return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage));
     }
 
     /**

+ 15 - 5
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java

@@ -1,19 +1,16 @@
 package cn.iocoder.yudao.framework.web.core.util;
 
 import cn.hutool.core.util.NumberUtil;
-import cn.hutool.extra.servlet.ServletUtil;
 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
 import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.http.HttpServletRequest;
-
 /**
  * 专属于 web 包的工具类
  *
@@ -27,6 +24,7 @@ public class WebFrameworkUtils {
     private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
 
     public static final String HEADER_TENANT_ID = "tenant-id";
+    public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id";
 
     /**
      * 终端的 Header
@@ -53,6 +51,18 @@ public class WebFrameworkUtils {
         return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
     }
 
+    /**
+     * 获得访问的租户编号,从 header 中
+     * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
+     *
+     * @param request 请求
+     * @return 租户编号
+     */
+    public static Long getVisitTenantId(HttpServletRequest request) {
+        String tenantId = request.getHeader(HEADER_VISIT_TENANT_ID);
+        return NumberUtil.isNumber(tenantId)? Long.valueOf(tenantId) : null;
+    }
+
     public static void setLoginUserId(ServletRequest request, Long userId) {
         request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
     }

+ 192 - 6
yudao-module-ai/pom.xml

@@ -8,12 +8,7 @@
         <version>${revision}</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
-    <modules>
-        <module>yudao-module-ai-api</module>
-        <module>yudao-module-ai-biz</module>
-        <module>yudao-spring-boot-starter-ai</module>
-    </modules>
-    <packaging>pom</packaging>
+    <packaging>jar</packaging>
     <artifactId>yudao-module-ai</artifactId>
 
     <name>${project.artifactId}</name>
@@ -23,5 +18,196 @@
           国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
           国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
     </description>
+    <properties>
+        <spring-ai.version>1.0.0-M6</spring-ai.version>
+        <tinyflow.version>1.0.2</tinyflow.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-infra</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- Job 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-job</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
+
+        <!-- Spring AI Model 模型接入 -->
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <!-- 通义千问 -->
+            <groupId>com.alibaba.cloud.ai</groupId>
+            <artifactId>spring-ai-alibaba-starter</artifactId>
+            <version>${spring-ai.version}.1</version>
+        </dependency>
+        <dependency>
+            <!-- 文心一言 -->
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <!-- 智谱 GLM -->
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-minimax-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-moonshot-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+
+        <!-- 向量存储:https://db-engines.com/en/ranking/vector+dbms -->
+        <dependency>
+            <!-- Qdrant:https://qdrant.tech/ -->
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-qdrant-store</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+
+        <dependency>
+            <!-- Redis:https://redis.io/docs/latest/develop/get-started/vector-database/ -->
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-redis-store</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <!-- Milvus:https://milvus.io/ -->
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-milvus-store</artifactId>
+            <version>${spring-ai.version}</version>
+            <exclusions>
+                <!-- 解决和 logback 的日志冲突 -->
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-reload4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <!-- Tika:负责内容的解析 -->
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-tika-document-reader</artifactId>
+            <version>${spring-ai.version}</version>
+            <!-- TODO 芋艿:boot 项目里,不引入 cloud 依赖!!!另外,这样也是为了解决启动报错的问题! -->
+            <exclusions>
+                <exclusion>
+                    <artifactId>spring-cloud-function-context</artifactId>
+                    <groupId>org.springframework.cloud</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>spring-cloud-function-core</artifactId>
+                    <groupId>org.springframework.cloud</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- TinyFlow:AI 工作流 -->
+        <dependency>
+            <groupId>dev.tinyflow</groupId>
+            <artifactId>tinyflow-java-core</artifactId>
+            <version>${tinyflow.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.jfinal</groupId>
+                    <artifactId>enjoy</artifactId>
+                </exclusion>
+                <exclusion>
+                    <!-- 解决 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1318/ 问题 -->
+                    <groupId>com.agentsflex</groupId>
+                    <artifactId>agents-flex-store-elasticsearch</artifactId>
+                </exclusion>
+                <exclusion>
+                    <!-- TODO @芋艿:暂时移除 groovy,和 iot 冲突 -->
+                    <groupId>org.codehaus.groovy</groupId>
+                    <artifactId>groovy-all</artifactId>
+                </exclusion>
+                <!-- 解决和 logback 的日志冲突 -->
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-simple</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-slf4j-impl</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-reload4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
 
 </project>

+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java


+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http


+ 42 - 5
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java

@@ -12,15 +12,18 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessage
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
 import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
 import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
-import jakarta.annotation.security.PermitAll;
 import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.MediaType;
@@ -33,7 +36,7 @@ import java.util.List;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 聊天消息")
@@ -48,6 +51,10 @@ public class AiChatMessageController {
     private AiChatConversationService chatConversationService;
     @Resource
     private AiChatRoleService chatRoleService;
+    @Resource
+    private AiKnowledgeSegmentService knowledgeSegmentService;
+    @Resource
+    private AiKnowledgeDocumentService knowledgeDocumentService;
 
     @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢")
     @PostMapping("/send")
@@ -57,7 +64,6 @@ public class AiChatMessageController {
 
     @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
     @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
-    @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
     public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
         return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId());
     }
@@ -71,8 +77,38 @@ public class AiChatMessageController {
         if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
             return success(Collections.emptyList());
         }
+        // 1. 获取消息列表
         List<AiChatMessageDO> messageList = chatMessageService.getChatMessageListByConversationId(conversationId);
-        return success(BeanUtils.toBean(messageList, AiChatMessageRespVO.class));
+        if (CollUtil.isEmpty(messageList)) {
+            return success(Collections.emptyList());
+        }
+
+        // 2. 拼接数据,主要是知识库段落信息
+        Map<Long, AiKnowledgeSegmentDO> segmentMap = knowledgeSegmentService.getKnowledgeSegmentMap(convertListByFlatMap(messageList,
+                message -> CollUtil.isEmpty(message.getSegmentIds()) ? null : message.getSegmentIds().stream()));
+        Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
+                convertList(segmentMap.values(), AiKnowledgeSegmentDO::getDocumentId));
+        List<AiChatMessageRespVO> messageVOList = BeanUtils.toBean(messageList, AiChatMessageRespVO.class);
+        for (int i = 0; i < messageList.size(); i++) {
+            AiChatMessageDO message = messageList.get(i);
+            if (CollUtil.isEmpty(message.getSegmentIds())) {
+                continue;
+            }
+            // 设置知识库段落信息
+            messageVOList.get(i).setSegments(convertList(message.getSegmentIds(), segmentId -> {
+                AiKnowledgeSegmentDO segment = segmentMap.get(segmentId);
+                if (segment == null) {
+                    return null;
+                }
+                AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
+                if (document == null) {
+                    return null;
+                }
+                return new AiChatMessageRespVO.KnowledgeSegment().setId(segment.getId()).setContent(segment.getContent())
+                        .setDocumentId(segment.getDocumentId()).setDocumentName(document.getName());
+            }));
+        }
+        return success(messageVOList);
     }
 
     @Operation(summary = "删除消息")
@@ -105,7 +141,8 @@ public class AiChatMessageController {
         Map<Long, AiChatRoleDO> roleMap = chatRoleService.getChatRoleMap(
                 convertSet(pageResult.getList(), AiChatMessageDO::getRoleId));
         return success(BeanUtils.toBean(pageResult, AiChatMessageRespVO.class,
-                respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(), role -> respVO.setRoleName(role.getName()))));
+                respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(),
+                        role -> respVO.setRoleName(role.getName()))));
     }
 
     @Operation(summary = "管理员删除消息")

+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java


+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java


+ 2 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation;
 
-import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
 import com.fhs.core.trans.anno.Trans;
 import com.fhs.core.trans.constant.TransType;
@@ -31,7 +31,7 @@ public class AiChatConversationRespVO implements VO {
     private Long roleId;
 
     @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = "name", ref = "modelName")
+    @Trans(type = TransType.SIMPLE, target = AiModelDO.class, fields = "name", ref = "modelName")
     private Long modelId;
 
     @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "ERNIE-Bot-turbo-0922")

+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java


+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java


+ 25 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 @Schema(description = "管理后台 - AI 聊天消息 Response VO")
 @Data
@@ -39,6 +40,12 @@ public class AiChatMessageRespVO {
     @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     private Boolean useContext;
 
+    @Schema(description = "知识库段落编号数组", example = "[1,2,3]")
+    private List<Long> segmentIds;
+
+    @Schema(description = "知识库段落数组")
+    private List<KnowledgeSegment> segments;
+
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51")
     private LocalDateTime createTime;
 
@@ -47,4 +54,22 @@ public class AiChatMessageRespVO {
     @Schema(description = "角色名字", example = "小黄")
     private String roleName;
 
+    @Schema(description = "知识库段落", example = "Java 开发手册")
+    @Data
+    public static class KnowledgeSegment {
+
+        @Schema(description = "段落编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+        private Long id;
+
+        @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
+        private String content;
+
+        @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+        private Long documentId;
+
+        @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品使用手册")
+        private String documentName;
+
+    }
+
 }

+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java


+ 7 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 @Schema(description = "管理后台 - AI 聊天消息发送 Response VO")
 @Data
@@ -28,6 +29,12 @@ public class AiChatMessageSendRespVO {
         @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
         private String content;
 
+        @Schema(description = "知识库段落编号数组", example = "[1,2,3]")
+        private List<Long> segmentIds;
+
+        @Schema(description = "知识库段落数组")
+        private List<AiChatMessageRespVO.KnowledgeSegment> segments;
+
         @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
         private LocalDateTime createTime;
 

+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http


+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.ai.controller.admin.image;
 
 import cn.hutool.core.util.ObjUtil;
-import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
+import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;

+ 3 - 6
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java

@@ -14,18 +14,15 @@ import java.util.Map;
 @Data
 public class AiImageDrawReqVO {
 
-    @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
-    private String platform; // 参见 AiPlatformEnum 枚举
+    @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "模型编号不能为空")
+    private Long modelId;
 
     @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "画一个长城")
     @NotEmpty(message = "提示词不能为空")
     @Size(max = 1200, message = "提示词最大 1200")
     private String prompt;
 
-    @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6")
-    @NotEmpty(message = "模型不能为空")
-    private String model;
-
     /**
      * 1. dall-e-2 模型:256x256、512x512、1024x1024
      * 2. dall-e-3 模型:1024x1024, 1792x1024, 或 1024x1792

+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java


+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java


+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
 
-import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
+import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 

+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java


+ 0 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java


+ 3 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java → yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java

@@ -13,9 +13,9 @@ public class AiMidjourneyImagineReqVO {
     @NotEmpty(message = "提示词不能为空!")
     private String prompt;
 
-    @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "midjourney")
-    @NotEmpty(message = "模型不能为空")
-    private String model; // 参考 MidjourneyApi.ModelEnum
+    @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "模型编号不能为空")
+    private Long modelId;
 
     @Schema(description = "图片宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     @NotNull(message = "图片宽度不能为空")

+ 35 - 0
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http

@@ -0,0 +1,35 @@
+### 创建知识库
+POST {{baseUrl}}/ai/knowledge/create
+Content-Type: application/json
+Authorization: {{token}}
+tenant-id: {{adminTenantId}}
+
+{
+  "name": "测试标题",
+  "description": "测试描述",
+  "embeddingModelId": 30,
+  "topK": 3,
+  "similarityThreshold": 0.5,
+  "status": 0
+}
+
+### 更新知识库
+PUT {{baseUrl}}/ai/knowledge/update
+Content-Type: application/json
+Authorization: {{token}}
+tenant-id: {{adminTenantId}}
+
+{
+  "id": 1,
+  "name": "测试标题(更新)",
+  "description": "测试描述",
+  "embeddingModelId": 30,
+  "topK": 5,
+  "similarityThreshold": 0.6,
+  "status": 0
+}
+
+### 获取知识库分页
+GET {{baseUrl}}/ai/knowledge/page?pageNo=1&pageSize=10
+Authorization: {{token}}
+tenant-id: {{adminTenantId}}

+ 84 - 0
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java

@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+@Tag(name = "管理后台 - AI 知识库")
+@RestController
+@RequestMapping("/ai/knowledge")
+@Validated
+public class AiKnowledgeController {
+
+    @Resource
+    private AiKnowledgeService knowledgeService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获取知识库分页")
+    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
+    public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) {
+        PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class));
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得知识库")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
+    public CommonResult<AiKnowledgeRespVO> getKnowledge(@RequestParam("id") Long id) {
+        AiKnowledgeDO knowledge = knowledgeService.getKnowledge(id);
+        return success(BeanUtils.toBean(knowledge, AiKnowledgeRespVO.class));
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建知识库")
+    @PreAuthorize("@ss.hasPermission('ai:knowledge:create')")
+    public CommonResult<Long> createKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO createReqVO) {
+        return success(knowledgeService.createKnowledge(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新知识库")
+    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
+    public CommonResult<Boolean> updateKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO updateReqVO) {
+        knowledgeService.updateKnowledge(updateReqVO);
+        return success(true);
+    }
+    
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除知识库")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
+    public CommonResult<Boolean> deleteKnowledge(@RequestParam("id") Long id) {
+        knowledgeService.deleteKnowledge(id);
+        return success(true);
+    }
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得知识库的精简列表")
+    public CommonResult<List<AiKnowledgeRespVO>> getKnowledgeSimpleList() {
+        List<AiKnowledgeDO> list = knowledgeService.getKnowledgeSimpleListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(convertList(list, knowledge -> new AiKnowledgeRespVO()
+                .setId(knowledge.getId()).setName(knowledge.getName())));
+    }
+
+}

+ 35 - 0
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http

@@ -0,0 +1,35 @@
+### 创建知识文档
+POST {{baseUrl}}/ai/knowledge/document/create
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenantId}}
+
+{
+  "knowledgeId": 2,
+  "name": "测试文档",
+  "url": "https://static.iocoder.cn/README.md",
+  "segmentMaxTokens": 800
+}
+
+### 批量创建知识文档
+POST {{baseUrl}}/ai/knowledge/document/create-list
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenantId}}
+
+{
+  "knowledgeId": 1,
+  "list": [
+    {
+      "name": "测试文档1",
+      "url": "https://static.iocoder.cn/README.md",
+      "segmentMaxTokens": 800
+    },
+    {
+      "name": "测试文档2",
+      "url": "https://static.iocoder.cn/README_yudao.md",
+      "segmentMaxTokens": 400
+    }
+  ]
+}
+

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott