Переглянути джерело

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

# Conflicts:
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
YunaiV 8 місяців тому
батько
коміт
4e8e491035
100 змінених файлів з 2469 додано та 342 видалено
  1. 50 36
      sql/mysql/ruoyi-vue-pro.sql
  2. 18 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
  3. 4 1
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java
  4. 13 1
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
  5. 3 4
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java
  6. 7 7
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java
  7. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java
  8. 14 10
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java
  9. 2 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java
  10. 6 6
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java
  11. 0 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java
  12. 0 33
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
  13. 0 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java
  14. 0 33
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
  15. 6 6
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
  16. 46 28
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
  17. 6 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
  18. 1 1
      yudao-module-ai/pom.xml
  19. 3 18
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
  20. 9 2
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
  21. 39 0
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java
  22. 1 1
      yudao-module-ai/yudao-module-ai-biz/pom.xml
  23. 8 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
  24. 14 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java
  25. 51 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java
  26. 51 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java
  27. 51 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java
  28. 14 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java
  29. 38 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java
  30. 26 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java
  31. 28 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateMyReqVO.java
  32. 26 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java
  33. 26 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java
  34. 32 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateMyReqVO.java
  35. 20 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java
  36. 34 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java
  37. 17 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java
  38. 22 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java
  39. 29 5
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
  40. 30 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java
  41. 36 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java
  42. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java
  43. 61 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java
  44. 64 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java
  45. 60 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java
  46. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java
  47. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
  48. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java
  49. 8 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java
  50. 24 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java
  51. 25 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java
  52. 25 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java
  53. 12 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java
  54. 9 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
  55. 6 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
  56. 39 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java
  57. 141 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java
  58. 38 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java
  59. 42 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
  60. 50 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java
  61. 78 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java
  62. 18 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java
  63. 25 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
  64. 18 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
  65. 20 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
  66. 35 9
      yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
  67. 51 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
  68. 2 1
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java
  69. 13 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java
  70. 53 4
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java
  71. 28 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java
  72. 52 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java
  73. 3 2
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java
  74. 3 2
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java
  75. 4 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
  76. 2 1
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java
  77. 2 1
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java
  78. 2 1
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java
  79. 61 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
  80. 456 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
  81. 70 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java
  82. 1 2
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java
  83. 2 1
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java
  84. 3 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
  85. 12 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
  86. 11 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
  87. 8 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  88. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  89. 3 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
  90. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
  91. 2 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
  92. 11 0
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java
  93. 11 0
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java
  94. 1 1
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
  95. 0 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
  96. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
  97. 0 13
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  98. 8 5
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java
  99. 1 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm
  100. 5 6
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm

+ 50 - 36
sql/mysql/ruoyi-vue-pro.sql

@@ -11,7 +11,7 @@
  Target Server Version : 80200 (8.2.0)
  File Encoding         : 65001
 
- Date: 28/07/2024 23:20:28
+ Date: 31/08/2024 09:22:45
 */
 
 SET NAMES utf8mb4;
@@ -62,7 +62,7 @@ COMMIT;
 -- ----------------------------
 DROP TABLE IF EXISTS `infra_api_error_log`;
 CREATE TABLE `infra_api_error_log`  (
-  `id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
   `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '链路追踪编号',
   `user_id` int NOT NULL DEFAULT 0 COMMENT '用户编号',
   `user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型',
@@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 19166 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 20014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -128,7 +128,7 @@ CREATE TABLE `infra_codegen_column`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2470 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 2483 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
 
 -- ----------------------------
 -- Records of infra_codegen_column
@@ -166,7 +166,7 @@ CREATE TABLE `infra_codegen_table`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 186 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 187 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
 
 -- ----------------------------
 -- Records of infra_codegen_table
@@ -179,7 +179,7 @@ COMMIT;
 -- ----------------------------
 DROP TABLE IF EXISTS `infra_config`;
 CREATE TABLE `infra_config`  (
-  `id` int NOT NULL AUTO_INCREMENT COMMENT '参数主键',
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '参数主键',
   `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数分组',
   `type` tinyint NOT NULL COMMENT '参数类型',
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '参数名称',
@@ -250,7 +250,7 @@ CREATE TABLE `infra_file`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1447 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1472 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file
@@ -438,7 +438,7 @@ CREATE TABLE `system_dict_data`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1588 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1592 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
 
 -- ----------------------------
 -- Records of system_dict_data
@@ -858,6 +858,10 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1585, 2, '回复', '2', 'ai_write_type', 0, '', '', '', '1', '2024-07-10 21:26:06', '1', '2024-07-10 21:26:06', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1586, 2, '腾讯云', 'TENCENT', 'system_sms_channel_code', 0, '', '', '', '1', '2024-07-22 22:23:16', '1', '2024-07-22 22:23:16', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1587, 3, '华为云', 'HUAWEI', 'system_sms_channel_code', 0, '', '', '', '1', '2024-07-22 22:23:46', '1', '2024-07-22 22:23:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1588, 1, 'OpenAI 微软', 'AzureOpenAI', 'ai_platform', 0, '', '', '', '1', '2024-08-10 14:07:41', '1', '2024-08-10 14:07:41', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1589, 10, 'BPMN 设计器', '10', 'bpm_model_type', 0, 'primary', '', '', '1', '2024-08-26 15:22:17', '1', '2024-08-26 16:46:02', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1590, 20, 'SIMPLE 设计器', '20', 'bpm_model_type', 0, 'success', '', '', '1', '2024-08-26 15:22:27', '1', '2024-08-26 16:45:58', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1591, 4, '七牛云', 'QINIU', 'system_sms_channel_code', 0, '', '', '', '1', '2024-08-31 08:45:03', '1', '2024-08-31 08:45:24', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -877,7 +881,7 @@ CREATE TABLE `system_dict_type`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 629 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
+) ENGINE = InnoDB AUTO_INCREMENT = 630 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
 
 -- ----------------------------
 -- Records of system_dict_type
@@ -975,6 +979,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (626, '写作长度', 'ai_write_length', 0, '', '1', '2024-07-07 15:18:41', '1', '2024-07-07 15:18:41', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (627, '写作格式', 'ai_write_format', 0, '', '1', '2024-07-07 15:14:34', '1', '2024-07-07 15:14:34', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (628, 'AI 写作类型', 'ai_write_type', 0, '', '1', '2024-07-10 21:25:29', '1', '2024-07-10 21:25:29', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (629, 'BPM 流程模型类型', 'bpm_model_type', 0, '', '1', '2024-08-26 15:21:43', '1', '2024-08-26 15:21:43', b'0', '1970-01-01 00:00:00');
 COMMIT;
 
 -- ----------------------------
@@ -998,7 +1003,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3261 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 3289 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -1129,7 +1134,7 @@ CREATE TABLE `system_menu`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2798 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
+) ENGINE = InnoDB AUTO_INCREMENT = 2808 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
 
 -- ----------------------------
 -- Records of system_menu
@@ -1293,7 +1298,6 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1193, '流程模型', '', 2, 1, 1186, 'model', 'fa-solid:project-diagram', 'bpm/model/index', 'BpmModel', 0, b'1', b'1', b'1', '1', '2021-12-31 23:24:58', '1', '2024-03-19 12:25:19', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1194, '模型查询', 'bpm:model:query', 3, 1, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:10', '1', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1195, '模型创建', 'bpm:model:create', 3, 2, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:24', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1196, '模型导入', 'bpm:model:import', 3, 3, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:35', '1', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1197, '模型更新', 'bpm:model:update', 3, 4, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:28', '1', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1198, '模型删除', 'bpm:model:delete', 3, 5, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:43', '1', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1199, '模型发布', 'bpm:model:deploy', 3, 6, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:03:24', '1', '2022-04-20 17:03:10', b'0');
@@ -1952,7 +1956,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2784, '绘画管理', '', 2, 11, 2760, 'image', 'fa:file-image-o', 'ai/image/manager/index.vue', 'AiImageManager', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 21:37:13', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2785, '绘画查询', 'ai:image:query', 3, 1, 2784, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 22:21:57', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2786, '绘画删除', 'ai:image:delete', 3, 4, 2784, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 22:22:08', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2787, '会话更新公开状态', 'ai:image:update-public-status', 3, 2, 2784, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-06-26 22:47:56', '1', '2024-06-26 22:47:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2787, '绘图更新', 'ai:image:update', 3, 2, 2784, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-06-26 22:47:56', '1', '2024-08-31 09:21:35', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2788, '音乐管理', '', 2, 12, 2760, 'music', 'fa:music', 'ai/music/manager/index.vue', 'AiMusicManager', 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '1', '2024-06-27 23:04:19', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2789, '音乐查询', 'ai:music:query', 3, 1, 2788, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '', '2024-06-27 15:03:33', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2790, '音乐更新', 'ai:music:update', 3, 3, 2788, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '', '2024-06-27 15:03:33', b'0');
@@ -1961,8 +1965,18 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2793, '写作管理', '', 2, 13, 2760, 'write', 'fa:bookmark-o', 'ai/write/manager/index.vue', 'AiWriteManager', 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '1', '2024-07-10 21:31:59', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2794, 'AI 写作查询', 'ai:write:query', 3, 1, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiWrite', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-29 21:11:52', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2798, 'AI 思维导图', '', 2, 5, 2758, 'mind-map', 'fa:sitemap', 'ai/mindmap/index/index.vue', 'AiMindMap', 0, b'1', b'1', b'1', '1', '2024-07-29 21:31:59', '1', '2024-07-29 21:33:20', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2799, '导图管理', '', 2, 14, 2760, 'mind-map', 'fa:map', 'ai/mindmap/manager/index', 'AiMindMapManager', 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '1', '2024-08-10 17:24:28', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2800, '思维导图查询', 'ai:mind-map:query', 3, 1, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2801, '思维导图删除', 'ai:mind-map:delete', 3, 4, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2802, '会话查询', 'promotion:kefu-conversation:query', 3, 1, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:17:52', '1', '2024-08-31 09:18:52', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2803, '会话更新', 'promotion:kefu-conversation:update', 3, 2, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:18:15', '1', '2024-08-31 09:19:29', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2804, '消息查询', 'promotion:kefu-message:query', 3, 10, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:18:42', '1', '2024-08-31 09:18:42', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2805, '会话删除', 'promotion:kefu-conversation:delete', 3, 3, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:19:51', '1', '2024-08-31 09:20:32', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2806, '消息发送', 'promotion:kefu-message:send', 3, 12, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:20:06', '1', '2024-08-31 09:20:06', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2807, '消息更新', 'promotion:kefu-message:update', 3, 11, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:20:22', '1', '2024-08-31 09:20:22', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -2084,7 +2098,7 @@ CREATE TABLE `system_oauth2_access_token`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
   INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 8784 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 9563 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -2206,7 +2220,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1598 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 1620 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -2239,7 +2253,7 @@ CREATE TABLE `system_operate_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 9053 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
+) ENGINE = InnoDB AUTO_INCREMENT = 9056 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
 
 -- ----------------------------
 -- Records of system_operate_log
@@ -2298,7 +2312,7 @@ CREATE TABLE `system_role`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 154 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
 
 -- ----------------------------
 -- Records of system_role
@@ -2307,9 +2321,10 @@ BEGIN;
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '超级管理员', 'super_admin', 1, 1, '', 0, 1, '超级管理员', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:21', b'0', 1);
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '普通角色', 'common', 2, 2, '', 0, 1, '普通角色', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:20', b'0', 1);
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 'CRM 管理员', 'crm_admin', 2, 1, '', 0, 1, 'CRM 专属角色', '1', '2024-02-24 10:51:13', '1', '2024-02-24 02:51:32', b'0', 1);
-INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '测试账号', 'test', 0, 2, '[105,106,107]', 0, 2, '', '', '2021-01-06 13:49:35', '1', '2024-07-27 23:30:47', b'0', 1);
+INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '测试账号', 'test', 0, 1, '[]', 0, 2, '', '', '2021-01-06 13:49:35', '1', '2024-08-11 10:41:10', b'0', 1);
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121);
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122);
+INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (153, '某角色', 'tt', 4, 1, '', 0, 2, '', '1', '2024-08-17 14:09:35', '1', '2024-08-17 14:09:35', b'0', 1);
 COMMIT;
 
 -- ----------------------------
@@ -2390,7 +2405,6 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1623, 101, 1193, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1624, 101, 1194, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1625, 101, 1195, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1626, 101, 1196, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1627, 101, 1197, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1628, 101, 1198, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1629, 101, 1199, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
@@ -3195,9 +3209,8 @@ CREATE TABLE `system_sms_channel`  (
 -- Records of system_sms_channel
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2023-12-02 22:10:17', b'0');
+INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', b'0');
 INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0');
-INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, '测试演示', 'DEBUG_DING_TALK', 0, '仅测试', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2022-04-10 23:07:59', '1', '2023-12-02 22:10:08', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -3222,7 +3235,7 @@ CREATE TABLE `system_sms_code`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号'
-) ENGINE = InnoDB AUTO_INCREMENT = 628 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
+) ENGINE = InnoDB AUTO_INCREMENT = 632 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
 
 -- ----------------------------
 -- Records of system_sms_code
@@ -3263,7 +3276,7 @@ CREATE TABLE `system_sms_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 987 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 1088 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
 
 -- ----------------------------
 -- Records of system_sms_log
@@ -3293,24 +3306,25 @@ CREATE TABLE `system_sms_template`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
+) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
 
 -- ----------------------------
 -- Records of system_sms_template
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', '测试备注', '4383920', 6, 'DEBUG_DING_TALK', '', '2021-03-31 10:49:38', '1', '2023-12-02 22:32:47', b'0');
+INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', '测试备注', '4383920', 4, 'DEBUG_DING_TALK', '', '2021-03-31 10:49:38', '1', '2024-08-18 11:57:18', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, 1, 0, 'test_02', '公告通知', '您的验证码{code},该验证码5分钟内有效,请勿泄漏于他人!', '[\"code\"]', NULL, 'SMS_207945135', 2, 'ALIYUN', '', '2021-03-31 11:56:30', '1', '2021-04-10 01:22:02', b'0');
-INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, 3, 0, 'test-01', '测试模板', '哈哈哈 {name}', '[\"name\"]', 'f哈哈哈', '4383920', 6, 'DEBUG_DING_TALK', '1', '2021-04-10 01:07:21', '1', '2022-12-10 21:26:09', b'0');
+INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, 3, 0, 'test-01', '测试模板', '哈哈哈 {name}', '[\"name\"]', 'f哈哈哈', '4383920', 4, 'DEBUG_DING_TALK', '1', '2021-04-10 01:07:21', '1', '2024-08-18 11:57:07', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 3, 0, 'test-04', '测试下', '老鸡{name},牛逼{code}', '[\"name\",\"code\"]', '哈哈哈哈', 'suibian', 4, 'DEBUG_DING_TALK', '1', '2021-04-13 00:29:53', '1', '2023-12-02 22:35:34', b'0');
-INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, 0, 'user-sms-login', '前台用户短信登录', '您的验证码是{code}', '[\"code\"]', NULL, '4372216', 6, 'DEBUG_DING_TALK', '1', '2021-10-11 08:10:00', '1', '2022-12-10 21:25:59', b'0');
+INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, 0, 'user-sms-login', '前台用户短信登录', '您的验证码是{code}', '[\"code\"]', NULL, '4372216', 4, 'DEBUG_DING_TALK', '1', '2021-10-11 08:10:00', '1', '2024-08-18 11:57:06', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 2, 0, 'bpm_task_assigned', '【工作流】任务被分配', '您收到了一条新的待办任务:{processInstanceName}-{taskName},申请人:{startUserNickname},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"startUserNickname\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-21 22:31:19', '1', '2022-01-22 00:03:36', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (10, 2, 0, 'bpm_process_instance_reject', '【工作流】流程被不通过', '您的流程被审批不通过:{processInstanceName},原因:{reason},查看链接:{detailUrl}', '[\"processInstanceName\",\"reason\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:03:31', '1', '2022-05-01 12:33:14', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 2, 0, 'bpm_process_instance_approve', '【工作流】流程被通过', '您的流程被审批通过:{processInstanceName},查看链接:{detailUrl}', '[\"processInstanceName\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:04:31', '1', '2022-03-27 20:32:21', b'0');
-INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 2, 0, 'demo', '演示模板', '我就是测试一下下', '[]', NULL, 'biubiubiu', 6, 'DEBUG_DING_TALK', '1', '2022-04-10 23:22:49', '1', '2023-03-24 23:45:07', b'0');
+INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 2, 0, 'demo', '演示模板', '我就是测试一下下', '[]', NULL, 'biubiubiu', 4, 'DEBUG_DING_TALK', '1', '2022-04-10 23:22:49', '1', '2024-08-18 11:57:04', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (14, 1, 0, 'user-update-mobile', '会员用户 - 修改手机', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:04', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, 1, 0, 'user-update-password', '会员用户 - 修改密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:18', b'0');
 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 1, 0, 'user-reset-password', '会员用户 - 重置密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-12-02 22:35:27', b'0');
+INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, 2, 0, 'bpm_task_timeout', '【工作流】任务审批超时', '您收到了一条超时的待办任务:{processInstanceName}-{taskName},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"detailUrl\"]', '', 'X', 4, 'DEBUG_DING_TALK', '1', '2024-08-16 21:59:15', '1', '2024-08-16 21:59:34', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -3367,7 +3381,7 @@ CREATE TABLE `system_social_user`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
+) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
 
 -- ----------------------------
 -- Records of system_social_user
@@ -3392,7 +3406,7 @@ CREATE TABLE `system_social_user_bind`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 119 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
+) ENGINE = InnoDB AUTO_INCREMENT = 120 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
 
 -- ----------------------------
 -- Records of system_social_user_bind
@@ -3559,10 +3573,10 @@ CREATE TABLE `system_users`  (
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-07-28 11:35:00', 'admin', '2021-01-05 17:03:47', NULL, '2024-07-28 11:35:00', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-03-18 21:09:04', '', '2021-01-13 23:50:35', NULL, '2024-03-18 21:09:04', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-07-13 23:13:16', '', '2021-01-21 02:13:53', NULL, '2024-07-13 23:13:16', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-08-26 16:54:00', 'admin', '2021-01-05 17:03:47', NULL, '2024-08-26 16:54:00', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 0, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', '1', '2024-08-17 11:06:13', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', NULL, '2024-08-11 17:48:12', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 09:38:08', '', '2021-01-21 02:13:53', NULL, '2024-08-11 09:38:08', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2022-02-27 08:26:51', b'0', 118);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2022-02-27 08:26:53', b'0', 119);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2022-02-27 08:26:56', b'0', 120);
@@ -3572,7 +3586,7 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', NULL, '2022-03-19 18:38:51', b'0', 122);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-03-24 22:21:05', '1', '2022-03-19 21:50:58', NULL, '2024-03-24 22:21:05', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$04$GcyP0Vyzb2F2Yni5PuIK9ueGxM0tkZGMtDwVRwrNbtMvorzbpNsV2', '阿呆', '11222', 102, '[1,2]', '7648@qq.com', '15601691229', 2, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2024-04-04 09:37:14', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2022-07-09 17:40:26', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号02', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2024-08-11 10:12:03', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$04$OB1SuphCdiLVRpiYRKeqH.8NYS7UIp5vmIv1W7U4w6toiFeOAATVK', '狗蛋', NULL, 103, '[1]', '', '15601691239', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-03-17 09:10:27', '1', '2022-07-09 17:44:43', '1', '2024-04-04 09:48:05', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (131, 'hh', '$2a$04$jyH9h6.gaw8mpOjPfHIpx.8as2Rzfcmdlj5rlJFwgCw4rsv/MTb2K', '呵呵', NULL, 100, '[]', '777@qq.com', '15601882312', 1, '', 0, '', NULL, '1', '2024-04-27 08:45:56', '1', '2024-04-27 08:45:56', b'0', 1);
 COMMIT;

+ 18 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java

@@ -111,7 +111,7 @@ public class HttpUtils {
             authorization = Base64.decodeStr(authorization);
             clientId = StrUtil.subBefore(authorization, ":", false);
             clientSecret = StrUtil.subAfter(authorization, ":", false);
-        // 再从 Param 中获取
+            // 再从 Param 中获取
         } else {
             clientId = request.getParameter("client_id");
             clientSecret = request.getParameter("client_secret");
@@ -143,4 +143,21 @@ public class HttpUtils {
         }
     }
 
+    /**
+     * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
+     *
+     * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
+     *
+     * @param url URL
+     * @param headers 请求头
+     * @return 请求结果
+     */
+    public static String get(String url, Map<String, String> headers) {
+        try (HttpResponse response = HttpRequest.get(url)
+                .addHeaders(headers)
+                .execute()) {
+            return response.body();
+        }
+    }
+
 }

+ 4 - 1
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.tenant.core.job;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
@@ -44,8 +45,10 @@ public class TenantJobAspect {
             // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
             TenantUtils.execute(tenantId, () -> {
                 try {
-                    joinPoint.proceed();
+                    Object result = joinPoint.proceed();
+                    results.put(tenantId, StrUtil.toStringOrNull(result));
                 } catch (Throwable e) {
+                    log.error("[execute][租户({}) 执行 Job 发生异常", tenantId, e);
                     results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
                 }
             });

+ 13 - 1
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java

@@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
 import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
 import com.baomidou.mybatisplus.extension.incrementer.*;
+import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
+import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
 import org.apache.ibatis.annotations.Mapper;
@@ -16,6 +18,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.core.env.ConfigurableEnvironment;
 
+import java.util.concurrent.TimeUnit;
+
 /**
  * MyBaits 配置类
  *
@@ -26,6 +30,14 @@ import org.springframework.core.env.ConfigurableEnvironment;
         lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
 public class YudaoMybatisAutoConfiguration {
 
+    static {
+        // 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存
+        JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(
+                (cache) -> cache.maximumSize(1024)
+                        .expireAfterWrite(5, TimeUnit.SECONDS))
+        );
+    }
+
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
@@ -34,7 +46,7 @@ public class YudaoMybatisAutoConfiguration {
     }
 
     @Bean
-    public MetaObjectHandler defaultMetaObjectHandler(){
+    public MetaObjectHandler defaultMetaObjectHandler() {
         return new DefaultDBFieldHandler(); // 自动填充参数类
     }
 

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

@@ -69,7 +69,7 @@ public class ApiSignatureAspect {
 
         // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
         String nonce = request.getHeader(signature.nonce());
-        signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
+        signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit());
         return true;
     }
 
@@ -113,7 +113,7 @@ public class ApiSignatureAspect {
         }
 
         // 3. 检查 nonce 是否存在,有且仅能使用一次
-        return signatureRedisDAO.getNonce(nonce) == null;
+        return signatureRedisDAO.getNonce(appId, nonce) == null;
     }
 
     /**
@@ -165,5 +165,4 @@ public class ApiSignatureAspect {
         return sortedMap;
     }
 
-}
-
+}

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

@@ -22,7 +22,7 @@ public class ApiSignatureRedisDAO {
      * VALUE 格式:String
      * 过期时间:不固定
      */
-    private static final String SIGNATURE_NONCE = "api_signature_nonce:%s";
+    private static final String SIGNATURE_NONCE = "api_signature_nonce:%s:%s";
 
     /**
      * 签名密钥
@@ -36,16 +36,16 @@ public class ApiSignatureRedisDAO {
 
     // ========== 验签随机数 ==========
 
-    public String getNonce(String nonce) {
-        return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
+    public String getNonce(String appId, String nonce) {
+        return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce));
     }
 
-    public void setNonce(String nonce, int time, TimeUnit timeUnit) {
-        stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
+    public void setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
+        stringRedisTemplate.opsForValue().set(formatNonceKey(appId, nonce), "", time, timeUnit);
     }
 
-    private static String formatNonceKey(String key) {
-        return String.format(SIGNATURE_NONCE, key);
+    private static String formatNonceKey(String appId, String nonce) {
+        return String.format(SIGNATURE_NONCE, appId, nonce);
     }
 
     // ========== 签名密钥 ==========

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

@@ -69,7 +69,7 @@ public class ApiSignatureTest {
         // 断言结果
         assertTrue(result);
         // 断言调用
-        verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS));
+        verify(signatureRedisDAO).setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS));
     }
 
-}
+}

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

@@ -29,18 +29,22 @@ public class LogRecordServiceImpl implements ILogRecordService {
 
     @Override
     public void record(LogRecord logRecord) {
-        // 1. 补全通用字段
         OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO();
-        reqDTO.setTraceId(TracerUtils.getTraceId());
-        // 补充用户信息
-        fillUserFields(reqDTO);
-        // 补全模块信息
-        fillModuleFields(reqDTO, logRecord);
-        // 补全请求信息
-        fillRequestFields(reqDTO);
+        try {
+            reqDTO.setTraceId(TracerUtils.getTraceId());
+            // 补充用户信息
+            fillUserFields(reqDTO);
+            // 补全模块信息
+            fillModuleFields(reqDTO, logRecord);
+            // 补全请求信息
+            fillRequestFields(reqDTO);
 
-        // 2. 异步记录日志
-        operateLogApi.createOperateLog(reqDTO);
+            // 2. 异步记录日志
+            operateLogApi.createOperateLogAsync(reqDTO);
+        } catch (Throwable ex) {
+            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
+            log.error("[record][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
+        }
     }
 
     private static void fillUserFields(OperateLogCreateReqDTO reqDTO) {

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

@@ -2,15 +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.apilog.core.service.ApiAccessLogFrameworkService;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
 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 cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
 import jakarta.servlet.Filter;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -23,18 +18,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 @AutoConfiguration(after = YudaoWebAutoConfiguration.class)
 public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
 
-    @Bean
-    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
-        return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
-    }
-
-    @Bean
-    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
-        return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
-    }
-
     /**
      * 创建 ApiAccessLogFilter Bean,记录 API 请求日志
      */
@@ -42,8 +25,8 @@ 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,
-                                                                         ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
-        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
+                                                                         ApiAccessLogApi apiAccessLogApi) {
+        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi);
         return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
     }
 

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

@@ -9,7 +9,6 @@ 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.apilog.core.service.ApiAccessLogFrameworkService;
 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;
@@ -18,6 +17,7 @@ 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;
@@ -36,7 +36,7 @@ import java.time.temporal.ChronoUnit;
 import java.util.Iterator;
 import java.util.Map;
 
-import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*;
+import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
@@ -53,12 +53,12 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
 
     private final String applicationName;
 
-    private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
+    private final ApiAccessLogApi apiAccessLogApi;
 
-    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
+    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogApi apiAccessLogApi) {
         super(webProperties);
         this.applicationName = applicationName;
-        this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
+        this.apiAccessLogApi = apiAccessLogApi;
     }
 
     @Override
@@ -91,7 +91,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
             if (!enable) {
                 return;
             }
-            apiAccessLogFrameworkService.createApiAccessLog(accessLog);
+            apiAccessLogApi.createApiAccessLogAsync(accessLog);
         } catch (Throwable th) {
             log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
         }

+ 0 - 19
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java

@@ -1,19 +0,0 @@
-package cn.iocoder.yudao.framework.apilog.core.service;
-
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
-
-/**
- * API 访问日志 Framework Service 接口
- *
- * @author 芋道源码
- */
-public interface ApiAccessLogFrameworkService {
-
-    /**
-     * 创建 API 访问日志
-     *
-     * @param reqDTO API 访问日志
-     */
-    void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO);
-
-}

+ 0 - 33
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java

@@ -1,33 +0,0 @@
-package cn.iocoder.yudao.framework.apilog.core.service;
-
-import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
-
-/**
- * API 访问日志 Framework Service 实现类
- *
- * 基于 {@link ApiAccessLogApi} 服务,记录访问日志
- *
- * @author 芋道源码
- */
-@RequiredArgsConstructor
-@Slf4j
-public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
-
-    private final ApiAccessLogApi apiAccessLogApi;
-
-    @Override
-    @Async
-    public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) {
-        try {
-            apiAccessLogApi.createApiAccessLog(reqDTO);
-        } catch (Throwable ex) {
-            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
-            log.error("[createApiAccessLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
-        }
-    }
-
-}

+ 0 - 19
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java

@@ -1,19 +0,0 @@
-package cn.iocoder.yudao.framework.apilog.core.service;
-
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
-
-/**
- * API 错误日志 Framework Service 接口
- *
- * @author 芋道源码
- */
-public interface ApiErrorLogFrameworkService {
-
-    /**
-     * 创建 API 错误日志
-     *
-     * @param reqDTO API 错误日志
-     */
-    void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO);
-
-}

+ 0 - 33
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java

@@ -1,33 +0,0 @@
-package cn.iocoder.yudao.framework.apilog.core.service;
-
-import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
-
-/**
- * API 错误日志 Framework Service 实现类
- *
- * 基于 {@link ApiErrorLogApi} 服务,记录错误日志
- *
- * @author 芋道源码
- */
-@RequiredArgsConstructor
-@Slf4j
-public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
-
-    private final ApiErrorLogApi apiErrorLogApi;
-
-    @Override
-    @Async
-    public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) {
-        try {
-            apiErrorLogApi.createApiErrorLog(reqDTO);
-        } catch (Throwable ex) {
-            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
-            log.error("[createApiErrorLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
-        }
-    }
-
-}

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

@@ -1,12 +1,14 @@
 package cn.iocoder.yudao.framework.web.config;
 
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
 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;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -25,9 +27,6 @@ import org.springframework.web.filter.CorsFilter;
 import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
-import jakarta.annotation.Resource;
-import jakarta.servlet.Filter;
-
 @AutoConfiguration
 @EnableConfigurationProperties(WebProperties.class)
 public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
@@ -59,8 +58,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
     }
 
     @Bean
-    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) {
-        return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService);
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogApi apiErrorLogApi) {
+        return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
     }
 
     @Bean

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

@@ -4,7 +4,7 @@ import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
+import cn.hutool.extra.servlet.JakartaServletUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
@@ -13,14 +13,16 @@ 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 com.fasterxml.jackson.databind.exc.InvalidFormatException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.ConstraintViolationException;
 import jakarta.validation.ValidationException;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.util.Assert;
 import org.springframework.validation.BindException;
@@ -58,7 +60,7 @@ public class GlobalExceptionHandler {
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     private final String applicationName;
 
-    private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
+    private final ApiErrorLogApi apiErrorLogApi;
 
     /**
      * 处理所有异常,主要是提供给 Filter 使用
@@ -88,7 +90,7 @@ public class GlobalExceptionHandler {
             return validationException((ValidationException) ex);
         }
         if (ex instanceof NoHandlerFoundException) {
-            return noHandlerFoundExceptionHandler(request, (NoHandlerFoundException) ex);
+            return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
         }
         if (ex instanceof NoResourceFoundException) {
             return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex);
@@ -123,7 +125,7 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(MethodArgumentTypeMismatchException.class)
     public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
-        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
+        log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex);
         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
     }
 
@@ -149,6 +151,22 @@ public class GlobalExceptionHandler {
         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
     }
 
+    /**
+     * 处理 SpringMVC 请求参数类型错误
+     *
+     * 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String
+     */
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
+        log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
+        if(ex.getCause() instanceof InvalidFormatException) {
+            InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause();
+            return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
+        }else {
+            return defaultExceptionHandler(ServletUtils.getRequest(), ex);
+        }
+    }
+
     /**
      * 处理 Validator 校验不通过产生的异常
      */
@@ -177,7 +195,7 @@ public class GlobalExceptionHandler {
      * 2. spring.mvc.static-path-pattern 为 /statics/**
      */
     @ExceptionHandler(NoHandlerFoundException.class)
-    public CommonResult<?> noHandlerFoundExceptionHandler(HttpServletRequest req, NoHandlerFoundException ex) {
+    public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
         log.warn("[noHandlerFoundExceptionHandler]", ex);
         return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
     }
@@ -253,7 +271,7 @@ public class GlobalExceptionHandler {
         // 情况二:处理异常
         log.error("[defaultExceptionHandler]", ex);
         // 插入异常日志
-        this.createExceptionLog(req, ex);
+        createExceptionLog(req, ex);
         // 返回 ERROR CommonResult
         return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
     }
@@ -265,7 +283,7 @@ public class GlobalExceptionHandler {
             // 初始化 errorLog
             buildExceptionLog(errorLog, req, e);
             // 执行插入 errorLog
-            apiErrorLogFrameworkService.createApiErrorLog(errorLog);
+            apiErrorLogApi.createApiErrorLogAsync(errorLog);
         } catch (Throwable th) {
             log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(),  JsonUtils.toJsonString(errorLog), th);
         }
@@ -279,7 +297,7 @@ public class GlobalExceptionHandler {
         errorLog.setExceptionName(e.getClass().getName());
         errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
         errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
-        errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e));
+        errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
         StackTraceElement[] stackTraceElements = e.getStackTrace();
         Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
         StackTraceElement stackTraceElement = stackTraceElements[0];
@@ -292,12 +310,12 @@ public class GlobalExceptionHandler {
         errorLog.setApplicationName(applicationName);
         errorLog.setRequestUrl(request.getRequestURI());
         Map<String, Object> requestParams = MapUtil.<String, Object>builder()
-                .put("query", ServletUtils.getParamMap(request))
-                .put("body", ServletUtils.getBody(request)).build();
+                .put("query", JakartaServletUtil.getParamMap(request))
+                .put("body", JakartaServletUtil.getBody(request)).build();
         errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
         errorLog.setRequestMethod(request.getMethod());
         errorLog.setUserAgent(ServletUtils.getUserAgent(request));
-        errorLog.setUserIp(ServletUtils.getClientIP(request));
+        errorLog.setUserIp(JakartaServletUtil.getClientIP(request));
         errorLog.setExceptionTime(LocalDateTime.now());
     }
 
@@ -314,51 +332,51 @@ public class GlobalExceptionHandler {
         }
         // 1. 数据报表
         if (message.contains("report_")) {
-            log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
+            log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
+                    "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
         }
         // 2. 工作流
         if (message.contains("bpm_")) {
-            log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
+            log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
+                    "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
         }
         // 3. 微信公众号
         if (message.contains("mp_")) {
-            log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
+            log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
+                    "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
         }
         // 4. 商城系统
         if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
-            log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
+            log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
+                    "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
         }
         // 5. ERP 系统
         if (message.contains("erp_")) {
-            log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
+            log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
+                    "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
         }
         // 6. CRM 系统
         if (message.contains("crm_")) {
-            log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
+            log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
+                    "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
         }
         // 7. 支付平台
         if (message.contains("pay_")) {
-            log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
+            log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
+                    "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
         }
         // 8. AI 大模型
         if (message.contains("ai_")) {
-            log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://doc.iocoder.cn/ai/build/ 开启]");
+            log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://doc.iocoder.cn/ai/build/ 开启]");
+                    "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
         }
         return null;
     }

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
 import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
 import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
 import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
+import cn.iocoder.yudao.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer;
 import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
 import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
 import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
@@ -76,6 +77,11 @@ public class YudaoWebSocketAutoConfiguration {
         return new WebSocketSessionManagerImpl();
     }
 
+    @Bean
+    public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
+        return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
+    }
+
     // ==================== Sender 相关 ====================
 
     @Configuration

+ 1 - 1
yudao-module-ai/pom.xml

@@ -18,7 +18,7 @@
 
     <name>${project.artifactId}</name>
     <description>
-        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
+        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
         目前已接入各种模型,不限于:
           国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
           国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno

+ 3 - 18
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java

@@ -1,11 +1,8 @@
 package cn.iocoder.yudao.module.ai.enums;
 
-import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
-import java.util.Arrays;
-
 /**
  * AI 内置聊天角色的枚举
  *
@@ -13,16 +10,16 @@ import java.util.Arrays;
  */
 @AllArgsConstructor
 @Getter
-public enum AiChatRoleEnum implements IntArrayValuable {
+public enum AiChatRoleEnum {
 
-    AI_WRITE_ROLE(1, "写作助手", """
+    AI_WRITE_ROLE("写作助手", """
             你是一位出色的写作助手,能够帮助用户生成创意和灵感,并在用户提供场景和提示词时生成对应的回复。你的任务包括:
             1.	撰写建议:根据用户提供的主题或问题,提供详细的写作建议、情节发展方向、角色设定以及背景描写,确保内容结构清晰、有逻辑。
             2.	回复生成:根据用户提供的场景和提示词,生成合适的对话或文字回复,确保语气和风格符合场景需求。
             除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。
             """),
 
-    AI_MIND_MAP_ROLE(2, "脑图助手", """
+    AI_MIND_MAP_ROLE("导图助手", """
              你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
              # Geek-AI 助手
              ## 完整的开源系统
@@ -39,11 +36,6 @@ public enum AiChatRoleEnum implements IntArrayValuable {
             除此之外不要任何解释性语句。
             """);
 
-    // TODO @xin:这个 role 是不是删除掉好点哈。= = 目前主要是没做角色枚举。这里多了 role 反倒容易误解哈
-    /**
-     * 角色
-     */
-    private final Integer role;
     /**
      * 角色名
      */
@@ -54,11 +46,4 @@ public enum AiChatRoleEnum implements IntArrayValuable {
      */
     private final String systemMessage;
 
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiChatRoleEnum::getRole).toArray();
-
-    @Override
-    public int[] array() {
-        return ARRAYS;
-    }
-
 }

+ 9 - 2
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java

@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
 
 /**
  * AI 错误码枚举类
- *
+ * <p>
  * ai 系统,使用 1-040-000-000 段
  */
 public interface ErrorCodeConstants {
@@ -45,9 +45,16 @@ public interface ErrorCodeConstants {
     // ========== API 音乐 1-040-006-000 ==========
     ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
 
-
     // ========== API 写作 1-022-007-000 ==========
     ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
     ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!");
 
+    // ========== API 思维导图 1-040-008-000 ==========
+    ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!");
+
+    // ========== API 知识库 1-022-008-000 ==========
+    ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!");
+    ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_001, "文档不存在!");
+    ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_002, "段落不存在!");
+
 }

+ 39 - 0
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.ai.enums.knowledge;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * AI 知识库-文档状态的枚举
+ *
+ * @author xiaoxin
+ */
+@AllArgsConstructor
+@Getter
+public enum AiKnowledgeDocumentStatusEnum implements IntArrayValuable {
+
+    IN_PROGRESS(10, "索引中"),
+    SUCCESS(20, "可用"),
+    FAIL(30, "失败");
+
+    /**
+     * 状态
+     */
+    private final Integer status;
+
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiKnowledgeDocumentStatusEnum::getStatus).toArray();
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/pom.xml

@@ -12,7 +12,7 @@
 
     <name>${project.artifactId}</name>
     <description>
-        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
+        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
         目前已接入各种模型,不限于:
         国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
         国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno

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

@@ -5,10 +5,7 @@ import cn.iocoder.yudao.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;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -45,6 +42,13 @@ public class AiImageController {
         return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
     }
 
+    @GetMapping("/public-page")
+    @Operation(summary = "获取公开的绘图分页")
+    public CommonResult<PageResult<AiImageRespVO>> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
+        PageResult<AiImageDO> pageResult = imageService.getImagePagePublic(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
+    }
+
     @GetMapping("/get-my")
     @Operation(summary = "获取【我的】绘图记录")
     @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 绘画公开的分页 Request VO")
+@Data
+public class AiImagePublicPageReqVO extends PageParam {
+
+    @Schema(description = "提示词")
+    private String prompt;
+
+}

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

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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.AiKnowledgeCreateMyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
+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.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - AI 知识库")
+@RestController
+@RequestMapping("/ai/knowledge")
+@Validated
+public class AiKnowledgeController {
+
+    @Resource
+    private AiKnowledgeService knowledgeService;
+
+    @GetMapping("/my-page")
+    @Operation(summary = "获取【我的】知识库分页")
+    public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePageMy(@Validated PageParam pageReqVO) {
+        PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePageMy(getLoginUserId(), pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class));
+    }
+
+    @PostMapping("/create-my")
+    @Operation(summary = "创建【我的】知识库")
+    public CommonResult<Long> createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) {
+        return success(knowledgeService.createKnowledgeMy(createReqVO, getLoginUserId()));
+    }
+
+    @PutMapping("/update-my")
+    @Operation(summary = "更新【我的】知识库")
+    public CommonResult<Boolean> updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) {
+        knowledgeService.updateKnowledgeMy(updateReqVO, getLoginUserId());
+        return success(true);
+    }
+
+}

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

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
+
+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.document.AiKnowledgeDocumentPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - AI 知识库文档")
+@RestController
+@RequestMapping("/ai/knowledge/document")
+@Validated
+public class AiKnowledgeDocumentController {
+
+    @Resource
+    private AiKnowledgeDocumentService documentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "新建文档")
+    public CommonResult<Long> createKnowledgeDocument(@Valid AiKnowledgeDocumentCreateReqVO reqVO) {
+        Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO);
+        return success(knowledgeDocumentId);
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获取文档分页")
+    public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPageMy(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
+        PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新文档")
+    public CommonResult<Boolean> updateKnowledgeDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) {
+        documentService.updateKnowledgeDocument(reqVO);
+        return success(true);
+    }
+
+}

+ 51 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
+
+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.segment.AiKnowledgeSegmentPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - AI 知识库段落")
+@RestController
+@RequestMapping("/ai/knowledge/segment")
+@Validated
+public class AiKnowledgeSegmentController {
+
+    @Resource
+    private AiKnowledgeSegmentService segmentService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获取段落分页")
+    public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPageMy(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
+        PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新段落内容")
+    public CommonResult<Boolean> updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentUpdateReqVO reqVO) {
+        segmentService.updateKnowledgeSegment(reqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "启禁用段落内容")
+    public CommonResult<Boolean> updateKnowledgeSegmentStatus(@Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
+        segmentService.updateKnowledgeSegmentStatus(reqVO);
+        return success(true);
+    }
+
+}

+ 14 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 知识库文档的分页 Request VO")
+@Data
+public class AiKnowledgeDocumentPageReqVO extends PageParam {
+
+    @Schema(description = "文档名称", example = "Java 开发手册")
+    private String name;
+
+}

+ 38 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 知识库-文档 Response VO")
+@Data
+public class AiKnowledgeDocumentRespVO extends PageParam {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long id;
+
+    @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long knowledgeId;
+
+    @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
+    private String name;
+
+    @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 是一门面向对象的语言.....")
+    private String content;
+
+    @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn")
+    private String url;
+
+    @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Integer tokens;
+
+    @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008")
+    private Integer wordCount;
+
+    @Schema(description = "切片状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer sliceStatus;
+
+    @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status;
+
+}

+ 26 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+
+@Schema(description = "管理后台 - AI 更新 知识库-文档 Request VO")
+@Data
+public class AiKnowledgeDocumentUpdateReqVO {
+
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+    @Schema(description = "是否启用", example = "1")
+    @InEnum(CommonStatusEnum.class)
+    private Integer status;
+
+    @Schema(description = "名称", example = "Java 开发手册")
+    private String name;
+
+}

+ 28 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateMyReqVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO")
+@Data
+public class AiKnowledgeCreateMyReqVO {
+
+    @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
+    @NotBlank(message = "知识库名称不能为空")
+    private String name;
+
+    @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档")
+    private String description;
+
+    @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
+    private List<Long> visibilityPermissions;
+
+    @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "嵌入模型不能为空")
+    private Long modelId;
+
+}

+ 26 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.hibernate.validator.constraints.URL;
+
+
+@Schema(description = "管理后台 - AI 知识库文档的创建 Request VO")
+@Data
+public class AiKnowledgeDocumentCreateReqVO {
+
+    @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
+    @NotNull(message = "知识库编号不能为空")
+    private Long knowledgeId;
+
+    @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "三方登陆")
+    @NotBlank(message = "文档名称不能为空")
+    private String name;
+
+    @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn")
+    @URL(message = "文档 URL 格式不正确")
+    private String url;
+
+}

+ 26 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+
+@Schema(description = "管理后台 - AI 知识库 Response VO")
+@Data
+public class AiKnowledgeRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long id;
+
+    @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
+    private String name;
+
+    @Schema(description = "知识库描述", example = "帮助你快速构建系统")
+    private String description;
+
+    @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14")
+    private Long modelId;
+
+    @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat")
+    private String model;
+
+}

+ 32 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateMyReqVO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO")
+@Data
+public class AiKnowledgeUpdateMyReqVO {
+
+    @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
+    @NotNull(message = "知识库编号不能为空")
+    private Long id;
+
+    @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
+    @NotBlank(message = "知识库名称不能为空")
+    private String name;
+
+    @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
+    private String description;
+
+    @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
+    private List<Long> visibilityPermissions;
+
+    @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "嵌入模型不能为空")
+    private Long modelId;
+
+}

+ 20 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 知识库分段的分页 Request VO")
+@Data
+public class AiKnowledgeSegmentPageReqVO extends PageParam {
+
+    @Schema(description = "分段状态", example = "1")
+    private Integer status;
+
+    @Schema(description = "文档编号", example = "1")
+    private Integer documentId;
+
+    @Schema(description = "分段内容关键字", example = "Java 开发")
+    private String keyword;
+
+}

+ 34 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 知识库-文档 Response VO")
+@Data
+public class AiKnowledgeSegmentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long id;
+
+    @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long documentId;
+
+    @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long knowledgeId;
+
+    @Schema(description = "向量库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1858496a-1dde-4edf-a43e-0aed08f37f8c")
+    private String vectorId;
+
+    @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
+    private String content;
+
+    @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Integer tokens;
+
+    @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008")
+    private Integer wordCount;
+
+    @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status;
+
+}

+ 17 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+
+@Schema(description = "管理后台 - AI 更新 知识库-段落 request VO")
+@Data
+public class AiKnowledgeSegmentUpdateReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long id;
+
+    @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
+    private String content;
+
+}

+ 22 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+
+@Schema(description = "管理后台 - AI 知识库段落的更新状态 Request VO")
+@Data
+public class AiKnowledgeSegmentUpdateStatusReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long id;
+
+    @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "是否启用不能为空")
+    @InEnum(CommonStatusEnum.class)
+    private Integer status;
+
+}

+ 29 - 5
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java

@@ -1,20 +1,25 @@
 package cn.iocoder.yudao.module.ai.controller.admin.mindmap;
 
 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.mindmap.vo.AiMindMapGenerateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapRespVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import cn.iocoder.yudao.module.ai.service.mindmap.AiMindMapService;
 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 org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
 import reactor.core.publisher.Flux;
 
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - AI 思维导图")
@@ -26,10 +31,29 @@ public class AiMindMapController {
     private AiMindMapService mindMapService;
 
     @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
-    @Operation(summary = "图生成(流式)", description = "流式返回,响应较快")
+    @Operation(summary = "图生成(流式)", description = "流式返回,响应较快")
     @PermitAll  // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
     public Flux<CommonResult<String>> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) {
         return mindMapService.generateMindMap(generateReqVO, getLoginUserId());
     }
 
+    // ================ 导图管理 ================
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除思维导图")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('ai:mind-map:delete')")
+    public CommonResult<Boolean> deleteMindMap(@RequestParam("id") Long id) {
+        mindMapService.deleteMindMap(id);
+        return success(true);
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得思维导图分页")
+    @PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
+    public CommonResult<PageResult<AiMindMapRespVO>> getMindMapPage(@Valid AiMindMapPageReqVO pageReqVO) {
+        PageResult<AiMindMapDO> pageResult = mindMapService.getMindMapPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiMindMapRespVO.class));
+    }
+
 }

+ 30 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 思维导图分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AiMindMapPageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "4325")
+    private Long userId;
+
+    @Schema(description = "生成内容提示", example = "Java 学习路线")
+    private String prompt;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 36 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 思维导图 Response VO")
+@Data
+public class AiMindMapRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3373")
+    private Long id;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4325")
+    private Long userId;
+
+    @Schema(description = "生成内容提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
+    private String prompt;
+
+    @Schema(description = "生成的思维导图内容")
+    private String generatedContent;
+
+    @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
+    private String platform;
+
+    @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125")
+    private String model;
+
+    @Schema(description = "错误信息")
+    private String errorMessage;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java

@@ -30,7 +30,7 @@ public class AiImageDO extends BaseDO {
     /**
      * 编号
      */
-    @TableId(type = IdType.AUTO)
+    @TableId
     private Long id;
 
     /**

+ 61 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * AI 知识库 DO
+ *
+ * @author xiaoxin
+ */
+@TableName(value = "ai_knowledge", autoResultMap = true)
+@Data
+public class AiKnowledgeDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 用户编号
+     * <p>
+     * 关联 AdminUserDO 的 userId 字段
+     */
+    private Long userId;
+    /**
+     * 知识库名称
+     */
+    private String name;
+    /**
+     * 知识库描述
+     */
+    private String description;
+    // TODO @新:如果全部可见,需要怎么设置?
+    /**
+     * 可见权限,只能选择哪些人可见
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<Long> visibilityPermissions;
+    /**
+     * 嵌入模型编号
+     */
+    private Long modelId;
+    /**
+     * 模型标识
+     */
+    private String model;
+    /**
+     * 状态
+     * <p>
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+}

+ 64 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+/**
+ * AI 知识库-文档 DO
+ *
+ * @author xiaoxin
+ */
+@TableName(value = "ai_knowledge_document")
+@Data
+public class AiKnowledgeDocumentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 知识库编号
+     *
+     * 关联 {@link AiKnowledgeDO#getId()}
+     */
+    private Long knowledgeId;
+    /**
+     * 文件名称
+     */
+    private String name;
+    /**
+     * 内容
+     */
+    private String content;
+    /**
+     * 文件 URL
+     */
+    private String url;
+    /**
+     * token 数量
+     */
+    private Integer tokens;
+    /**
+     * 字符数
+     */
+    private Integer wordCount;
+    /**
+     * 切片状态
+     * <p>
+     * 枚举 {@link AiKnowledgeDocumentStatusEnum}
+     */
+    private Integer sliceStatus;
+
+    /**
+     * 状态
+     * <p>
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+
+}

+ 60 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+/**
+ * AI 知识库-文档分段 DO
+ *
+ * @author xiaoxin
+ */
+@TableName(value = "ai_knowledge_segment")
+@Data
+public class AiKnowledgeSegmentDO extends BaseDO {
+
+    public static final String FIELD_KNOWLEDGE_ID = "knowledgeId";
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 向量库的编号
+     */
+    private String vectorId;
+    /**
+     * 知识库编号
+     *
+     * 关联 {@link AiKnowledgeDO#getId()}
+     */
+    private Long knowledgeId;
+    /**
+     * 文档编号
+     *
+     * 关联 {@link AiKnowledgeDocumentDO#getId()}
+     */
+    private Long documentId;
+    /**
+     * 切片内容
+     */
+    private String content;
+    /**
+     * 字符数
+     */
+    private Integer wordCount;
+    /**
+     * token 数量
+     */
+    private Integer tokens;
+    /**
+     * 状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+
+}

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java

@@ -19,7 +19,7 @@ public class AiMindMapDO extends BaseDO {
     /**
      * 编号
      */
-    @TableId(type = IdType.AUTO)
+    @TableId
     private Long id;
 
     /**

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java

@@ -25,7 +25,7 @@ public class AiMusicDO extends BaseDO {
     /**
      * 编号
      */
-    @TableId(type = IdType.AUTO)
+    @TableId
     private Long id;
 
     /**

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java

@@ -20,7 +20,7 @@ public class AiWriteDO extends BaseDO {
     /**
      * 编号
      */
-    @TableId(type = IdType.AUTO)
+    @TableId
     private Long id;
 
     /**

+ 8 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicPageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -41,6 +42,13 @@ public interface AiImageMapper extends BaseMapperX<AiImageDO> {
                 .orderByDesc(AiImageDO::getId));
     }
 
+    default PageResult<AiImageDO> selectPage(AiImagePublicPageReqVO pageReqVO) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
+                .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE)
+                .likeIfPresent(AiImageDO::getPrompt, pageReqVO.getPrompt())
+                .orderByDesc(AiImageDO::getId));
+    }
+
     default List<AiImageDO> selectListByStatusAndPlatform(Integer status, String platform) {
         return selectList(AiImageDO::getStatus, status,
                 AiImageDO::getPlatform, platform);

+ 24 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * AI 知识库-文档 Mapper
+ *
+ * @author xiaoxin
+ */
+@Mapper
+public interface AiKnowledgeDocumentMapper extends BaseMapperX<AiKnowledgeDocumentDO> {
+
+    default PageResult<AiKnowledgeDocumentDO> selectPage(AiKnowledgeDocumentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<AiKnowledgeDocumentDO>()
+                .likeIfPresent(AiKnowledgeDocumentDO::getName, reqVO.getName())
+                .orderByDesc(AiKnowledgeDocumentDO::getId));
+    }
+
+}

+ 25 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * AI 知识库基础信息 Mapper
+ *
+ * @author xiaoxin
+ */
+@Mapper
+public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> {
+
+    default PageResult<AiKnowledgeDO> selectPageByMy(Long userId, PageParam pageReqVO) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<AiKnowledgeDO>()
+                .eq(AiKnowledgeDO::getUserId, userId)
+                .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
+                .orderByDesc(AiKnowledgeDO::getId));
+    }
+}

+ 25 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * AI 知识库-分片 Mapper
+ *
+ * @author xiaoxin
+ */
+@Mapper
+public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegmentDO> {
+
+    default PageResult<AiKnowledgeSegmentDO> selectPage(AiKnowledgeSegmentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
+                .eq(AiKnowledgeSegmentDO::getDocumentId, reqVO.getDocumentId())
+                .eqIfPresent(AiKnowledgeSegmentDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword())
+                .orderByDesc(AiKnowledgeSegmentDO::getId));
+    }
+}

+ 12 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.module.ai.dal.mysql.mindmap;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -11,4 +14,13 @@ import org.apache.ibatis.annotations.Mapper;
  */
 @Mapper
 public interface AiMindMapMapper extends BaseMapperX<AiMindMapDO> {
+
+    default PageResult<AiMindMapDO> selectPage(AiMindMapPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<AiMindMapDO>()
+                .eqIfPresent(AiMindMapDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(AiMindMapDO::getPrompt, reqVO.getPrompt())
+                .betweenIfPresent(AiMindMapDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(AiMindMapDO::getId));
+    }
+
 }

+ 9 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java

@@ -2,9 +2,7 @@ package cn.iocoder.yudao.module.ai.service.image;
 
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -28,6 +26,14 @@ public interface AiImageService {
      */
     PageResult<AiImageDO> getImagePageMy(Long userId, AiImagePageReqVO pageReqVO);
 
+    /**
+     * 获取公开的绘图分页
+     *
+     * @param pageReqVO 分页条件
+     * @return 绘图分页
+     */
+    PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO);
+
     /**
      * 获得绘图记录
      *

+ 6 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java

@@ -12,9 +12,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 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.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -70,6 +68,11 @@ public class AiImageServiceImpl implements AiImageService {
         return imageMapper.selectPageMy(userId, pageReqVO);
     }
 
+    @Override
+    public PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
+        return imageMapper.selectPage(pageReqVO);
+    }
+
     @Override
     public AiImageDO getImage(Long id) {
         return imageMapper.selectById(id);

+ 39 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
+
+/**
+ * AI 知识库-文档 Service 接口
+ *
+ * @author xiaoxin
+ */
+public interface AiKnowledgeDocumentService {
+
+    /**
+     * 创建文档
+     *
+     * @param createReqVO 文档创建 Request VO
+     * @return 文档编号
+     */
+    Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO);
+
+
+    /**
+     * 获取文档分页
+     *
+     * @param pageReqVO 分页参数
+     * @return 文档分页
+     */
+    PageResult<AiKnowledgeDocumentDO> getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO);
+
+    /**
+     * 更新文档
+     *
+     * @param reqVO 更新信息
+     */
+    void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO);
+}

+ 141 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java

@@ -0,0 +1,141 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.http.HttpUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
+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.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper;
+import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
+import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum;
+import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
+import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.reader.tika.TikaDocumentReader;
+import org.springframework.ai.tokenizer.TokenCountEstimator;
+import org.springframework.ai.transformer.splitter.TokenTextSplitter;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS;
+
+/**
+ * AI 知识库文档 Service 实现类
+ *
+ * @author xiaoxin
+ */
+@Service
+@Slf4j
+public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentService {
+
+    @Resource
+    private AiKnowledgeDocumentMapper documentMapper;
+    @Resource
+    private AiKnowledgeSegmentMapper segmentMapper;
+
+    @Resource
+    private TokenTextSplitter tokenTextSplitter;
+    @Resource
+    private TokenCountEstimator tokenCountEstimator;
+
+    @Resource
+    private AiApiKeyService apiKeyService;
+    @Resource
+    private AiKnowledgeService knowledgeService;
+    @Resource
+    private AiChatModelService chatModelService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) {
+        // 0. 校验
+        AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
+        AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
+
+        // 1.1 下载文档
+        TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl()));
+        List<Document> documents = loader.get();
+        Document document = CollUtil.getFirst(documents);
+        // 1.2 文档记录入库
+        String content = document.getContent();
+        AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class)
+                .setTokens(tokenCountEstimator.estimate(content)).setWordCount(content.length())
+                .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus());
+        documentMapper.insert(documentDO);
+        Long documentId = documentDO.getId();
+        if (CollUtil.isEmpty(documents)) {
+            return documentId;
+        }
+
+        // 2.1 文档分段
+        List<Document> segments = tokenTextSplitter.apply(documents);
+        // 2.2 分段内容入库
+        List<AiKnowledgeSegmentDO> segmentDOList = CollectionUtils.convertList(segments,
+                segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId)
+                        .setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId())
+                        .setTokens(tokenCountEstimator.estimate(segment.getContent())).setWordCount(segment.getContent().length())
+                        .setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        segmentMapper.insertBatch(segmentDOList);
+
+        // 3.1 获取向量存储实例
+        VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
+        // 3.2 向量化并存储
+        segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId()));
+        vectorStore.add(segments);
+        return documentId;
+    }
+
+    @Override
+    public PageResult<AiKnowledgeDocumentDO> getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) {
+        return documentMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) {
+        // 1. 校验文档是否存在
+        validateKnowledgeDocumentExists(reqVO.getId());
+        // 2. 更新文档
+        AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class);
+        documentMapper.updateById(document);
+    }
+
+    /**
+     * 校验文档是否存在
+     *
+     * @param id 文档编号
+     * @return 文档信息
+     */
+    private AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) {
+        AiKnowledgeDocumentDO knowledgeDocument = documentMapper.selectById(id);
+        if (knowledgeDocument == null) {
+            throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS);
+        }
+        return knowledgeDocument;
+    }
+
+    private org.springframework.core.io.Resource downloadFile(String url) {
+        try {
+            byte[] bytes = HttpUtil.downloadBytes(url);
+            return new ByteArrayResource(bytes);
+        } catch (Exception e) {
+            log.error("[downloadFile][url({}) 下载失败]", url, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 38 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
+
+/**
+ * AI 知识库段落 Service 接口
+ *
+ * @author xiaoxin
+ */
+public interface AiKnowledgeSegmentService {
+
+    /**
+     * 获取段落分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 文档分页
+     */
+    PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO);
+
+    /**
+     * 更新段落的内容
+     *
+     * @param reqVO 更新内容
+     */
+    void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO);
+
+    /**
+     * 更新段落的状态
+     *
+     * @param reqVO 更新内容
+     */
+    void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
+
+}

+ 42 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+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.segment.AiKnowledgeSegmentPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
+import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * AI 知识库分片 Service 实现类
+ *
+ * @author xiaoxin
+ */
+@Service
+@Slf4j
+public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService {
+
+    @Resource
+    private AiKnowledgeSegmentMapper segmentMapper;
+
+    @Override
+    public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
+        return segmentMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) {
+        segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
+        // TODO @xin 重新向量化
+    }
+
+    @Override
+    public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
+        segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
+        // TODO @xin 1.禁用删除向量 2.启用重新向量化
+    }
+}

+ 50 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
+
+/**
+ * AI 知识库-基础信息 Service 接口
+ *
+ * @author xiaoxin
+ */
+public interface AiKnowledgeService {
+
+    /**
+     * 创建【我的】知识库
+     *
+     * @param createReqVO 创建信息
+     * @param userId      用户编号
+     * @return 编号
+     */
+    Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId);
+
+
+    /**
+     * 创建【我的】知识库
+     *
+     * @param updateReqVO 更新信息
+     * @param userId      用户编号
+     */
+    void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId);
+
+
+    /**
+     * 校验知识库是否存在
+     *
+     * @param id 记录编号
+     */
+    AiKnowledgeDO validateKnowledgeExists(Long id);
+
+    /**
+     * 获得【我的】知识库分页
+     *
+     * @param userId 用户编号
+     * @param pageReqVO   分页查询
+     * @return 知识库分页
+     */
+    PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO);
+}

+ 78 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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.AiKnowledgeCreateMyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper;
+import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_EXISTS;
+
+/**
+ * AI 知识库-基础信息 Service 实现类
+ *
+ * @author xiaoxin
+ */
+@Service
+@Slf4j
+public class AiKnowledgeServiceImpl implements AiKnowledgeService {
+
+    @Resource
+    private AiChatModelService chatModalService;
+
+    @Resource
+    private AiKnowledgeMapper knowledgeMapper;
+
+    @Override
+    public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) {
+        // 1. 校验模型配置
+        AiChatModelDO model = chatModalService.validateChatModel(createReqVO.getModelId());
+
+        // 2. 插入知识库
+        AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class)
+                .setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus());
+        knowledgeMapper.insert(knowledgeBase);
+        return knowledgeBase.getId();
+    }
+
+    @Override
+    public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) {
+        // 1.1 校验知识库存在
+        AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId());
+        if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) {
+            throw exception(KNOWLEDGE_NOT_EXISTS);
+        }
+        // 1.2 校验模型配置
+        AiChatModelDO model = chatModalService.validateChatModel(updateReqVO.getModelId());
+
+        // 2. 更新知识库
+        AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class);
+        updateDO.setModel(model.getModel());
+        knowledgeMapper.updateById(updateDO);
+    }
+
+    @Override
+    public AiKnowledgeDO validateKnowledgeExists(Long id) {
+        AiKnowledgeDO knowledgeBase = knowledgeMapper.selectById(id);
+        if (knowledgeBase == null) {
+            throw exception(KNOWLEDGE_NOT_EXISTS);
+        }
+        return knowledgeBase;
+    }
+
+    @Override
+    public PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO) {
+        return knowledgeMapper.selectPageByMy(userId, pageReqVO);
+    }
+
+}

+ 18 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java

@@ -1,7 +1,10 @@
 package cn.iocoder.yudao.module.ai.service.mindmap;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import reactor.core.publisher.Flux;
 
 /**
@@ -20,4 +23,19 @@ public interface AiMindMapService {
      */
     Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId);
 
+    /**
+     * 删除思维导图
+     *
+     * @param id 编号
+     */
+    void deleteMindMap(Long id);
+
+    /**
+     * 获得思维导图分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 思维导图分页
+     */
+    PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO);
+
 }

+ 25 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java

@@ -6,9 +6,11 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
 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.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
@@ -33,8 +35,10 @@ import reactor.core.publisher.Flux;
 import java.util.ArrayList;
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_EXISTS;
 
 /**
  * AI 思维导图 Service 实现类
@@ -57,10 +61,10 @@ public class AiMindMapServiceImpl implements AiMindMapService {
 
     @Override
     public Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId) {
-        // 1. 获取图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
+        // 1. 获取图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
         AiChatRoleDO role = CollUtil.getFirst(
                 chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName()));
-        // 1.1 获取图执行模型
+        // 1.1 获取图执行模型
         AiChatModelDO model = getModel(role);
         // 1.2 获取角色设定消息
         String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage())
@@ -131,4 +135,23 @@ public class AiMindMapServiceImpl implements AiMindMapService {
         return model;
     }
 
+    @Override
+    public void deleteMindMap(Long id) {
+        // 校验存在
+        validateMindMapExists(id);
+        // 删除
+        mindMapMapper.deleteById(id);
+    }
+
+    private void validateMindMapExists(Long id) {
+        if (mindMapMapper.selectById(id) == null) {
+            throw exception(MIND_MAP_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) {
+        return mindMapMapper.selectPage(pageReqVO);
+    }
+
 }

+ 18 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java

@@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
 import jakarta.validation.Valid;
 import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.embedding.EmbeddingModel;
 import org.springframework.ai.image.ImageModel;
+import org.springframework.ai.vectorstore.VectorStore;
 
 import java.util.List;
 
@@ -111,4 +113,20 @@ public interface AiApiKeyService {
      */
     SunoApi getSunoApi();
 
+    /**
+     * 获得 EmbeddingModel 对象
+     *
+     * @param id 编号
+     * @return EmbeddingModel 对象
+     */
+    EmbeddingModel getEmbeddingModel(Long id);
+
+    /**
+     * 获得 VectorStore 对象
+     *
+     * @param id 编号
+     * @return VectorStore 对象
+     */
+    VectorStore getOrCreateVectorStore(Long id);
+
 }

+ 20 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model;
 
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
+import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -13,7 +14,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
 import jakarta.annotation.Resource;
 import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.embedding.EmbeddingModel;
 import org.springframework.ai.image.ImageModel;
+import org.springframework.ai.vectorstore.VectorStore;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -36,6 +39,8 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
 
     @Resource
     private AiModelFactory modelFactory;
+    @Resource
+    private AiVectorStoreFactory vectorFactory;
 
     @Override
     public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
@@ -132,4 +137,19 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
         }
         return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
     }
+
+    @Override
+    public EmbeddingModel getEmbeddingModel(Long id) {
+        AiApiKeyDO apiKey = validateApiKey(id);
+        AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
+        return modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), apiKey.getUrl());
+    }
+
+    @Override
+    public VectorStore getOrCreateVectorStore(Long id) {
+        AiApiKeyDO apiKey = validateApiKey(id);
+        AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
+        return vectorFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl());
+    }
+
 }

+ 35 - 9
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml

@@ -14,42 +14,68 @@
     <name>${project.artifactId}</name>
     <description>AI 大模型拓展,接入国内外大模型</description>
     <properties>
-        <spring-ai.version>1.0.0-M1</spring-ai.version>
+        <spring-ai.groupId>group.springframework.ai</spring-ai.groupId>
+        <spring-ai.version>1.1.0</spring-ai.version>
     </properties>
 
     <dependencies>
         <dependency>
-            <groupId>org.springframework.ai</groupId>
+            <groupId>${spring-ai.groupId}</groupId>
             <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
             <version>${spring-ai.version}</version>
         </dependency>
-
         <dependency>
-            <groupId>org.springframework.ai</groupId>
+            <groupId>${spring-ai.groupId}</groupId>
             <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
             <version>${spring-ai.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.springframework.ai</groupId>
+            <groupId>${spring-ai.groupId}</groupId>
+            <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>${spring-ai.groupId}</groupId>
             <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
             <version>${spring-ai.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.springframework.ai</groupId>
+            <groupId>${spring-ai.groupId}</groupId>
             <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
             <version>${spring-ai.version}</version>
         </dependency>
 
+        <!-- 向量化,基于 Redis 存储,Tika 解析内容 -->
+        <dependency>
+            <groupId>${spring-ai.groupId}</groupId>
+            <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>${spring-ai.groupId}</groupId>
+            <artifactId>spring-ai-tika-document-reader</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>${spring-ai.groupId}</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>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-common</artifactId>
         </dependency>
 
-        <!-- TODO 芋艿:等 spring-ai 官方发布后,需要把 groupId 改下 -->
         <dependency>
-            <groupId>group.springframework.ai</groupId>
+            <groupId>${spring-ai.groupId}</groupId>
             <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
-            <version>1.1.0</version>
+            <version>${spring-ai.version}</version>
         </dependency>
 
         <!-- 阿里云 通义千问 -->

+ 51 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.ai.config;
 
 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
+import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
+import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactoryImpl;
 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
@@ -10,11 +12,15 @@ import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
+import org.springframework.ai.tokenizer.TokenCountEstimator;
+import org.springframework.ai.transformer.splitter.TokenTextSplitter;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Lazy;
 
 /**
  * 芋道 AI 自动配置
@@ -32,6 +38,12 @@ public class YudaoAiAutoConfiguration {
         return new AiModelFactoryImpl();
     }
 
+    @Bean
+    public AiVectorStoreFactory aiVectorFactory() {
+        return new AiVectorStoreFactoryImpl();
+    }
+
+
     // ========== 各种 AI Client 创建 ==========
 
     @Bean
@@ -73,4 +85,43 @@ public class YudaoAiAutoConfiguration {
         return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
     }
 
+    // ========== rag 相关 ==========
+    // TODO @xin 免费版本
+//    @Bean
+//    @Lazy // TODO 芋艿:临时注释,避免无法启动」
+//    public EmbeddingModel transformersEmbeddingClient() {
+//        return new TransformersEmbeddingModel(MetadataMode.EMBED);
+//    }
+
+    /**
+     * TODO @xin 默认版本先不弄,目前都先取对应的 EmbeddingModel
+     */
+//    @Bean
+//    @Lazy // TODO 芋艿:临时注释,避免无法启动
+//    public RedisVectorStore vectorStore(TongYiTextEmbeddingModel tongYiTextEmbeddingModel, RedisVectorStoreProperties properties,
+//                                        RedisProperties redisProperties) {
+//        var config = RedisVectorStore.RedisVectorStoreConfig.builder()
+//                .withIndexName(properties.getIndex())
+//                .withPrefix(properties.getPrefix())
+//                .build();
+//
+//        RedisVectorStore redisVectorStore = new RedisVectorStore(config, tongYiTextEmbeddingModel,
+//                new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
+//                properties.isInitializeSchema());
+//        redisVectorStore.afterPropertiesSet();
+//        return redisVectorStore;
+//    }
+
+    @Bean
+    @Lazy // TODO 芋艿:临时注释,避免无法启动
+    public TokenTextSplitter tokenTextSplitter() {
+        return new TokenTextSplitter(500, 100, 5, 10000, true);
+    }
+
+    @Bean
+    @Lazy // TODO 芋艿:临时注释,避免无法启动
+    public TokenCountEstimator tokenCountEstimator() {
+        return new JTokkitTokenCountEstimator();
+    }
+
 }

+ 2 - 1
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java

@@ -22,7 +22,8 @@ public enum AiPlatformEnum {
 
     // ========== 国外平台 ==========
 
-    OPENAI("OpenAI", "OpenAI"),
+    OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
+    AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
     OLLAMA("Ollama", "Ollama"),
 
     STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI

+ 13 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.embedding.EmbeddingModel;
 import org.springframework.ai.image.ImageModel;
 
 /**
@@ -79,4 +80,16 @@ public interface AiModelFactory {
      */
     SunoApi getOrCreateSunoApi(String apiKey, String url);
 
+    /**
+     * 基于指定配置,获得 EmbeddingModel 对象
+     *
+     * 如果不存在,则进行创建
+     *
+     * @param platform 平台
+     * @param apiKey   API KEY
+     * @param url      API URL
+     * @return ChatModel 对象
+     */
+    EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url);
+
 }

+ 53 - 4
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java

@@ -21,6 +21,11 @@ import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel;
 import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties;
 import com.alibaba.dashscope.aigc.generation.Generation;
 import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
+import com.alibaba.dashscope.embeddings.TextEmbedding;
+import com.azure.ai.openai.OpenAIClient;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
 import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
 import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
 import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
@@ -31,7 +36,9 @@ import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties;
 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties;
 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
 import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.embedding.EmbeddingModel;
 import org.springframework.ai.image.ImageModel;
 import org.springframework.ai.model.function.FunctionCallbackContext;
 import org.springframework.ai.ollama.OllamaChatModel;
@@ -82,6 +89,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
                     return buildXingHuoChatModel(apiKey);
                 case OPENAI:
                     return buildOpenAiChatModel(apiKey, url);
+                case AZURE_OPENAI:
+                    return buildAzureOpenAiChatModel(apiKey, url);
                 case OLLAMA:
                     return buildOllamaChatModel(url);
                 default:
@@ -106,6 +115,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
                 return SpringUtil.getBean(XingHuoChatModel.class);
             case OPENAI:
                 return SpringUtil.getBean(OpenAiChatModel.class);
+            case AZURE_OPENAI:
+                return SpringUtil.getBean(AzureOpenAiChatModel.class);
             case OLLAMA:
                 return SpringUtil.getBean(OllamaChatModel.class);
             default:
@@ -166,6 +177,20 @@ public class AiModelFactoryImpl implements AiModelFactory {
         return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
     }
 
+    @Override
+    public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url) {
+        String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url);
+        return Singleton.get(cacheKey, (Func0<EmbeddingModel>) () -> {
+            // TODO @xin 先测试一个
+            switch (platform) {
+                case TONG_YI:
+                    return buildTongYiEmbeddingModel(apiKey);
+                default:
+                    throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
+            }
+        });
+    }
+
     private static String buildClientCacheKey(Class<?> clazz, Object... params) {
         if (ArrayUtil.isEmpty(params)) {
             return clazz.getName();
@@ -229,8 +254,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
     }
 
     /**
-     * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(
-     * ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)}
+     * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)}
      */
     private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
         url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL);
@@ -239,8 +263,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
     }
 
     /**
-     * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(
-     * ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)}
+     * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)}
      */
     private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
         url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL);
@@ -268,6 +291,21 @@ public class AiModelFactoryImpl implements AiModelFactory {
         return new OpenAiChatModel(openAiApi);
     }
 
+    /**
+     * 可参考 {@link AzureOpenAiAutoConfiguration}
+     */
+    private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
+        AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
+        // 创建 OpenAIClient 对象
+        AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
+        connectionProperties.setApiKey(apiKey);
+        connectionProperties.setEndpoint(url);
+        OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties);
+        // 获取 AzureOpenAiChatProperties 对象
+        AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
+        return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null);
+    }
+
     /**
      * 可参考 {@link OpenAiAutoConfiguration}
      */
@@ -291,4 +329,15 @@ public class AiModelFactoryImpl implements AiModelFactory {
         return new StabilityAiImageModel(stabilityAiApi);
     }
 
+    // ========== 各种创建 EmbeddingModel 的方法 ==========
+
+    /**
+     * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)}
+     */
+    private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) {
+        TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties();
+        connectionProperties.setApiKey(apiKey);
+        return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties);
+    }
+
 }

+ 28 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.ai.core.factory;
+
+import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.VectorStore;
+
+// TODO @xin:也放到 AiModelFactory 里面好了,后续改成 AiFactory
+/**
+ * AI Vector 模型工厂的接口类
+ *
+ * @author xiaoxin
+ */
+public interface AiVectorStoreFactory {
+
+    /**
+     * 基于指定配置,获得 VectorStore 对象
+     * <p>
+     * 如果不存在,则进行创建
+     *
+     * @param embeddingModel 嵌入模型
+     * @param platform       平台
+     * @param apiKey         API KEY
+     * @param url            API URL
+     * @return VectorStore 对象
+     */
+    VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url);
+
+}

+ 52 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.framework.ai.core.factory;
+
+import cn.hutool.core.lang.Singleton;
+import cn.hutool.core.lang.func.Func0;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
+import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import redis.clients.jedis.JedisPooled;
+
+/**
+ * AI Vector 模型工厂的实现类
+ * 使用 redisVectorStore 实现 VectorStore
+ *
+ * @author xiaoxin
+ */
+public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory {
+
+    @Override
+    public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
+        String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
+        return Singleton.get(cacheKey, (Func0<VectorStore>) () -> {
+            // TODO 芋艿 @xin 这两个配置取哪好呢
+            // TODO 不同模型的向量维度可能会不一样,目前看貌似是以 index 来做区分的,维度不一样存不到一个 index 上
+            // TODO 回复:好的哈
+            String index = "default-index";
+            String prefix = "default:";
+            var config = RedisVectorStore.RedisVectorStoreConfig.builder()
+                    .withIndexName(index)
+                    .withPrefix(prefix)
+                    .build();
+            RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
+            RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
+                    new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
+                    true);
+            redisVectorStore.afterPropertiesSet();
+            return redisVectorStore;
+        });
+    }
+
+    private static String buildClientCacheKey(Class<?> clazz, Object... params) {
+        if (ArrayUtil.isEmpty(params)) {
+            return clazz.getName();
+        }
+        return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
+    }
+
+}

+ 3 - 2
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.ai.core.model.deepseek;
 
+import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.lang.Assert;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
@@ -70,12 +71,12 @@ public class DeepSeekChatModel implements ChatModel {
             OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
             if (chatCompletion == null) {
                 log.warn("No chat completion returned for prompt: {}", prompt);
-                return new ChatResponse(List.of());
+                return new ChatResponse(ListUtil.of());
             }
             List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
             if (choices == null) {
                 log.warn("No choices returned for prompt: {}", prompt);
-                return new ChatResponse(List.of());
+                return new ChatResponse(ListUtil.of());
             }
 
             // 2. 转换 ChatResponse 返回

+ 3 - 2
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
 
+import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.lang.Assert;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
@@ -72,12 +73,12 @@ public class XingHuoChatModel implements ChatModel {
             OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
             if (chatCompletion == null) {
                 log.warn("No chat completion returned for prompt: {}", prompt);
-                return new ChatResponse(List.of());
+                return new ChatResponse(ListUtil.of());
             }
             List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
             if (choices == null) {
                 log.warn("No choices returned for prompt: {}", prompt);
-                return new ChatResponse(List.of());
+                return new ChatResponse(ListUtil.of());
             }
 
             // 2. 转换 ChatResponse 返回

+ 4 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
 import org.springframework.ai.chat.messages.*;
 import org.springframework.ai.chat.prompt.ChatOptions;
 import org.springframework.ai.ollama.api.OllamaOptions;
@@ -35,6 +36,9 @@ public class AiUtils {
                 return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
             case OPENAI:
                 return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
+            case AZURE_OPENAI:
+                // TODO 芋艿:貌似没 model 字段???!
+                return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
             case OLLAMA:
                 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
             default:

+ 2 - 1
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java

@@ -16,6 +16,7 @@
 
 package com.alibaba.cloud.ai.tongyi.audio.transcription;
 
+import cn.hutool.core.collection.ListUtil;
 import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels;
 import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionPrompt;
 import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResponse;
@@ -82,7 +83,7 @@ public class TongYiAudioTranscriptionModel
 			try {
 				transcriptionParam = TranscriptionParam.builder()
 						.model(AudioTranscriptionModels.Paraformer_V1)
-						.fileUrls(List.of(String.valueOf(instructions.getURL())))
+						.fileUrls(ListUtil.of(String.valueOf(instructions.getURL())))
 						.build();
 			}
 			catch (IOException e) {

+ 2 - 1
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java

@@ -16,6 +16,7 @@
 
 package com.alibaba.cloud.ai.tongyi.chat;
 
+import cn.hutool.core.collection.ListUtil;
 import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
 import com.alibaba.dashscope.aigc.conversation.ConversationParam;
 import com.alibaba.dashscope.aigc.generation.Generation;
@@ -207,7 +208,7 @@ public class TongYiChatModel extends
 															.getChoices()
 															.get(0)
 											));
-									return new ChatResponse(List.of(gen));
+									return new ChatResponse(ListUtil.of(gen));
 								})
 				)
 				.publishOn(Schedulers.parallel());

+ 2 - 1
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java

@@ -16,6 +16,7 @@
 
 package com.alibaba.cloud.ai.tongyi.embedding;
 
+import cn.hutool.core.collection.ListUtil;
 import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
 import com.alibaba.cloud.ai.tongyi.metadata.TongYiTextEmbeddingResponseMetadata;
 import com.alibaba.dashscope.embeddings.TextEmbedding;
@@ -100,7 +101,7 @@ public class TongYiTextEmbeddingModel extends AbstractEmbeddingModel {
 
 		return this.call(
 						new EmbeddingRequest(
-								List.of(document.getFormattedContent(this.metadataMode)),
+								ListUtil.of(document.getFormattedContent(this.metadataMode)),
 								null)
 				).getResults().stream()
 				.map(Embedding::getOutput)

+ 61 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vectorstore.redis;
+
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import redis.clients.jedis.JedisPooled;
+
+/**
+ * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突
+ *
+ * TODO 这个官方,有说啥时候 fix 哇?
+ * TODO 看着是列在1.0.0-M2版本
+ *
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ */
+@AutoConfiguration(after = RedisAutoConfiguration.class)
+@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class})
+@ConditionalOnBean(JedisConnectionFactory.class)
+@EnableConfigurationProperties(RedisVectorStoreProperties.class)
+public class RedisVectorStoreAutoConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
+                                        JedisConnectionFactory jedisConnectionFactory) {
+
+        var config = RedisVectorStoreConfig.builder()
+                .withIndexName(properties.getIndex())
+                .withPrefix(properties.getPrefix())
+                .build();
+
+        return new RedisVectorStore(config, embeddingModel,
+                new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()),
+                properties.isInitializeSchema());
+    }
+
+}

+ 456 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java

@@ -0,0 +1,456 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vectorstore;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import redis.clients.jedis.JedisPooled;
+import redis.clients.jedis.Pipeline;
+import redis.clients.jedis.json.Path2;
+import redis.clients.jedis.search.*;
+import redis.clients.jedis.search.Schema.FieldType;
+import redis.clients.jedis.search.schemafields.*;
+import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
+
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * The RedisVectorStore is for managing and querying vector data in a Redis database. It
+ * offers functionalities like adding, deleting, and performing similarity searches on
+ * documents.
+ *
+ * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
+ * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
+ * efficient similarity searches. Additionally, it allows for custom metadata fields in
+ * the documents to be stored alongside the vector and content data.
+ *
+ * This class requires a RedisVectorStoreConfig configuration object for initialization,
+ * which includes settings like Redis URI, index name, field names, and vector algorithms.
+ * It also requires an EmbeddingModel to convert documents into embeddings before storing
+ * them.
+ *
+ * @author Julien Ruaux
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ * @see VectorStore
+ * @see RedisVectorStoreConfig
+ * @see EmbeddingModel
+ */
+public class RedisVectorStore implements VectorStore, InitializingBean {
+
+    public enum Algorithm {
+
+        FLAT, HSNW
+
+    }
+
+    public record MetadataField(String name, FieldType fieldType) {
+
+        public static MetadataField text(String name) {
+            return new MetadataField(name, FieldType.TEXT);
+        }
+
+        public static MetadataField numeric(String name) {
+            return new MetadataField(name, FieldType.NUMERIC);
+        }
+
+        public static MetadataField tag(String name) {
+            return new MetadataField(name, FieldType.TAG);
+        }
+
+    }
+
+    /**
+     * Configuration for the Redis vector store.
+     */
+    public static final class RedisVectorStoreConfig {
+
+        private final String indexName;
+
+        private final String prefix;
+
+        private final String contentFieldName;
+
+        private final String embeddingFieldName;
+
+        private final Algorithm vectorAlgorithm;
+
+        private final List<MetadataField> metadataFields;
+
+        private RedisVectorStoreConfig() {
+            this(builder());
+        }
+
+        private RedisVectorStoreConfig(Builder builder) {
+            this.indexName = builder.indexName;
+            this.prefix = builder.prefix;
+            this.contentFieldName = builder.contentFieldName;
+            this.embeddingFieldName = builder.embeddingFieldName;
+            this.vectorAlgorithm = builder.vectorAlgorithm;
+            this.metadataFields = builder.metadataFields;
+        }
+
+        /**
+         * Start building a new configuration.
+         * @return The entry point for creating a new configuration.
+         */
+        public static Builder builder() {
+
+            return new Builder();
+        }
+
+        /**
+         * {@return the default config}
+         */
+        public static RedisVectorStoreConfig defaultConfig() {
+
+            return builder().build();
+        }
+
+        public static class Builder {
+
+            private String indexName = DEFAULT_INDEX_NAME;
+
+            private String prefix = DEFAULT_PREFIX;
+
+            private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME;
+
+            private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME;
+
+            private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM;
+
+            private List<MetadataField> metadataFields = new ArrayList<>();
+
+            private Builder() {
+            }
+
+            /**
+             * Configures the Redis index name to use.
+             * @param name the index name to use
+             * @return this builder
+             */
+            public Builder withIndexName(String name) {
+                this.indexName = name;
+                return this;
+            }
+
+            /**
+             * Configures the Redis key prefix to use (default: "embedding:").
+             * @param prefix the prefix to use
+             * @return this builder
+             */
+            public Builder withPrefix(String prefix) {
+                this.prefix = prefix;
+                return this;
+            }
+
+            /**
+             * Configures the Redis content field name to use.
+             * @param name the content field name to use
+             * @return this builder
+             */
+            public Builder withContentFieldName(String name) {
+                this.contentFieldName = name;
+                return this;
+            }
+
+            /**
+             * Configures the Redis embedding field name to use.
+             * @param name the embedding field name to use
+             * @return this builder
+             */
+            public Builder withEmbeddingFieldName(String name) {
+                this.embeddingFieldName = name;
+                return this;
+            }
+
+            /**
+             * Configures the Redis vector algorithmto use.
+             * @param algorithm the vector algorithm to use
+             * @return this builder
+             */
+            public Builder withVectorAlgorithm(Algorithm algorithm) {
+                this.vectorAlgorithm = algorithm;
+                return this;
+            }
+
+            public Builder withMetadataFields(MetadataField... fields) {
+                return withMetadataFields(Arrays.asList(fields));
+            }
+
+            public Builder withMetadataFields(List<MetadataField> fields) {
+                this.metadataFields = fields;
+                return this;
+            }
+
+            /**
+             * {@return the immutable configuration}
+             */
+            public RedisVectorStoreConfig build() {
+
+                return new RedisVectorStoreConfig(this);
+            }
+
+        }
+
+    }
+
+    private final boolean initializeSchema;
+
+    public static final String DEFAULT_INDEX_NAME = "spring-ai-index";
+
+    public static final String DEFAULT_CONTENT_FIELD_NAME = "content";
+
+    public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding";
+
+    public static final String DEFAULT_PREFIX = "embedding:";
+
+    public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW;
+
+    private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]";
+
+    private static final Path2 JSON_SET_PATH = Path2.of("$");
+
+    private static final String JSON_PATH_PREFIX = "$.";
+
+    private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class);
+
+    private static final Predicate<Object> RESPONSE_OK = Predicate.isEqual("OK");
+
+    private static final Predicate<Object> RESPONSE_DEL_OK = Predicate.isEqual(1l);
+
+    private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32";
+
+    private static final String EMBEDDING_PARAM_NAME = "BLOB";
+
+    public static final String DISTANCE_FIELD_NAME = "vector_score";
+
+    private static final String DEFAULT_DISTANCE_METRIC = "COSINE";
+
+    private final JedisPooled jedis;
+
+    private final EmbeddingModel embeddingModel;
+
+    private final RedisVectorStoreConfig config;
+
+    private FilterExpressionConverter filterExpressionConverter;
+
+    public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
+                            boolean initializeSchema) {
+
+        Assert.notNull(config, "Config must not be null");
+        Assert.notNull(embeddingModel, "Embedding model must not be null");
+        this.initializeSchema = initializeSchema;
+
+        this.jedis = jedis;
+        this.embeddingModel = embeddingModel;
+        this.config = config;
+        this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields);
+    }
+
+    public JedisPooled getJedis() {
+        return this.jedis;
+    }
+
+    @Override
+    public void add(List<Document> documents) {
+        try (Pipeline pipeline = this.jedis.pipelined()) {
+            for (Document document : documents) {
+                var embedding = this.embeddingModel.embed(document);
+                document.setEmbedding(embedding);
+
+                var fields = new HashMap<String, Object>();
+                fields.put(this.config.embeddingFieldName, embedding);
+                fields.put(this.config.contentFieldName, document.getContent());
+                fields.putAll(document.getMetadata());
+                pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
+            }
+            List<Object> responses = pipeline.syncAndReturnAll();
+            Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny();
+            if (errResponse.isPresent()) {
+                String message = MessageFormat.format("Could not add document: {0}", errResponse.get());
+                if (logger.isErrorEnabled()) {
+                    logger.error(message);
+                }
+                throw new RuntimeException(message);
+            }
+        }
+    }
+
+    private String key(String id) {
+        return this.config.prefix + id;
+    }
+
+    @Override
+    public Optional<Boolean> delete(List<String> idList) {
+        try (Pipeline pipeline = this.jedis.pipelined()) {
+            for (String id : idList) {
+                pipeline.jsonDel(key(id));
+            }
+            List<Object> responses = pipeline.syncAndReturnAll();
+            Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny();
+            if (errResponse.isPresent()) {
+                if (logger.isErrorEnabled()) {
+                    logger.error("Could not delete document: {}", errResponse.get());
+                }
+                return Optional.of(false);
+            }
+            return Optional.of(true);
+        }
+    }
+
+    @Override
+    public List<Document> similaritySearch(SearchRequest request) {
+
+        Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero");
+        Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1,
+                "The similarity score is bounded between 0 and 1; least to most similar respectively.");
+
+        String filter = nativeExpressionFilter(request);
+
+        String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName,
+                EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME);
+
+        List<String> returnFields = new ArrayList<>();
+        this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
+        returnFields.add(this.config.embeddingFieldName);
+        returnFields.add(this.config.contentFieldName);
+        returnFields.add(DISTANCE_FIELD_NAME);
+        var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery()));
+        Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding))
+                .returnFields(returnFields.toArray(new String[0]))
+                .setSortBy(DISTANCE_FIELD_NAME, true)
+                .dialect(2);
+
+        SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
+        return result.getDocuments()
+                .stream()
+                .filter(d -> similarityScore(d) >= request.getSimilarityThreshold())
+                .map(this::toDocument)
+                .toList();
+    }
+
+    private Document toDocument(redis.clients.jedis.search.Document doc) {
+        var id = doc.getId().substring(this.config.prefix.length());
+        var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName)
+                : null;
+        Map<String, Object> metadata = this.config.metadataFields.stream()
+                .map(MetadataField::name)
+                .filter(doc::hasProperty)
+                .collect(Collectors.toMap(Function.identity(), doc::getString));
+        metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc));
+        return new Document(id, content, metadata);
+    }
+
+    private float similarityScore(redis.clients.jedis.search.Document doc) {
+        return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2;
+    }
+
+    private String nativeExpressionFilter(SearchRequest request) {
+        if (request.getFilterExpression() == null) {
+            return "*";
+        }
+        return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")";
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+
+        if (!this.initializeSchema) {
+            return;
+        }
+
+        // If index already exists don't do anything
+        if (this.jedis.ftList().contains(this.config.indexName)) {
+            return;
+        }
+
+        String response = this.jedis.ftCreate(this.config.indexName,
+                FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields());
+        if (!RESPONSE_OK.test(response)) {
+            String message = MessageFormat.format("Could not create index: {0}", response);
+            throw new RuntimeException(message);
+        }
+    }
+
+    private Iterable<SchemaField> schemaFields() {
+        Map<String, Object> vectorAttrs = new HashMap<>();
+        vectorAttrs.put("DIM", this.embeddingModel.dimensions());
+        vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC);
+        vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32);
+        List<SchemaField> fields = new ArrayList<>();
+        fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0));
+        fields.add(VectorField.builder()
+                .fieldName(jsonPath(this.config.embeddingFieldName))
+                .algorithm(vectorAlgorithm())
+                .attributes(vectorAttrs)
+                .as(this.config.embeddingFieldName)
+                .build());
+
+        if (!CollectionUtils.isEmpty(this.config.metadataFields)) {
+            for (MetadataField field : this.config.metadataFields) {
+                fields.add(schemaField(field));
+            }
+        }
+        return fields;
+    }
+
+    private SchemaField schemaField(MetadataField field) {
+        String fieldName = jsonPath(field.name);
+        switch (field.fieldType) {
+            case NUMERIC:
+                return NumericField.of(fieldName).as(field.name);
+            case TAG:
+                return TagField.of(fieldName).as(field.name);
+            case TEXT:
+                return TextField.of(fieldName).as(field.name);
+            default:
+                throw new IllegalArgumentException(
+                        MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType));
+        }
+    }
+
+    private VectorAlgorithm vectorAlgorithm() {
+        if (config.vectorAlgorithm == Algorithm.HSNW) {
+            return VectorAlgorithm.HNSW;
+        }
+        return VectorAlgorithm.FLAT;
+    }
+
+    private String jsonPath(String field) {
+        return JSON_PATH_PREFIX + field;
+    }
+
+    private static float[] toFloatArray(List<Double> embeddingDouble) {
+        float[] embeddingFloat = new float[embeddingDouble.size()];
+        int i = 0;
+        for (Double d : embeddingDouble) {
+            embeddingFloat[i++] = d.floatValue();
+        }
+        return embeddingFloat;
+    }
+
+}

+ 70 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.framework.ai.chat;
+
+import com.azure.ai.openai.OpenAIClient;
+import com.azure.ai.openai.OpenAIClientBuilder;
+import com.azure.core.credential.AzureKeyCredential;
+import com.azure.core.util.ClientOptions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import reactor.core.publisher.Flux;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
+
+/**
+ * {@link AzureOpenAiChatModel} 集成测试
+ *
+ * @author 芋道源码
+ */
+public class AzureOpenAIChatModelTests {
+
+    private final OpenAIClient openAiApi = (new OpenAIClientBuilder())
+            .endpoint("https://eastusprejade.openai.azure.com")
+            .credential(new AzureKeyCredential("xxx"))
+            .clientOptions((new ClientOptions()).setApplicationId("spring-ai"))
+            .buildClient();
+    private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi,
+            AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
+
+    @Test
+    @Disabled
+    public void testCall() {
+        // 准备参数
+        List<Message> messages = new ArrayList<>();
+        messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
+        messages.add(new UserMessage("1 + 1 = ?"));
+
+        // 调用
+        ChatResponse response = chatModel.call(new Prompt(messages));
+        // 打印结果
+        System.out.println(response);
+        System.out.println(response.getResult().getOutput());
+    }
+
+    @Test
+    @Disabled
+    public void testStream() {
+        // 准备参数
+        List<Message> messages = new ArrayList<>();
+        messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
+        messages.add(new UserMessage("1 + 1 = ?"));
+
+        // 调用
+        Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
+        // 打印结果
+        flux.doOnNext(response -> {
+//            System.out.println(response);
+            System.out.println(response.getResult().getOutput());
+        }).then().block();
+    }
+
+}

+ 1 - 2
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.framework.ai.chat;
 
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.ai.chat.messages.Message;
@@ -17,7 +16,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * {@link XingHuoChatModel} 集成测试
+ * {@link OpenAiChatModel} 集成测试
  *
  * @author 芋道源码
  */

+ 2 - 1
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.ai.music;
 
+import cn.hutool.core.collection.ListUtil;
 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -66,7 +67,7 @@ public class SunoApiTests {
         String id = "584729e5-0fe9-4157-86da-1b4803ff42bf";
 
         // 调用方法
-        List<SunoApi.MusicData> musicList = sunoApi.getMusicList(List.of(id));
+        List<SunoApi.MusicData> musicList = sunoApi.getMusicList(ListUtil.of(id));
         // 打印结果
         System.out.println(musicList);
     }

+ 3 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java

@@ -11,6 +11,8 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModel
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
 import org.flowable.common.engine.impl.db.SuspensionState;
 import org.flowable.engine.repository.Deployment;
@@ -52,7 +54,7 @@ public interface BpmModelConvert {
         BpmModelMetaInfoVO metaInfo = buildMetaInfo(model);
         BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null);
         if (ArrayUtil.isNotEmpty(bpmnBytes)) {
-            modelVO.setBpmnXml(new String(bpmnBytes));
+            modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes));
         }
         return modelVO;
     }

+ 12 - 8
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java

@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import lombok.Setter;
 import org.flowable.bpmn.model.Activity;
 import org.flowable.engine.delegate.DelegateExecution;
@@ -50,14 +50,18 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
         super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
 
         // 第二步,获取任务的所有处理人
-        Set<Long> assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
-        if (CollUtil.isEmpty(assigneeUserIds)) {
-            // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
-            // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
-            // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
-            assigneeUserIds = SetUtils.asSet((Long) null);
+        @SuppressWarnings("unchecked")
+        Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
+        if (assigneeUserIds == null) {
+            assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
+            execution.setVariable(super.collectionVariable, assigneeUserIds);
+            if (CollUtil.isEmpty(assigneeUserIds)) {
+                // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
+                // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
+                // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
+                assigneeUserIds = SetUtils.asSet((Long) null);
+            }
         }
-        execution.setVariable(super.collectionVariable, assigneeUserIds);
         return assigneeUserIds.size();
     }
 

+ 11 - 7
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java

@@ -43,14 +43,18 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
         super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
 
         // 第二步,获取任务的所有处理人
-        Set<Long> assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
-        if (CollUtil.isEmpty(assigneeUserIds)) {
-            // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
-            // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
-            // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
-            assigneeUserIds = SetUtils.asSet((Long) null);
+        @SuppressWarnings("unchecked")
+        Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
+        if (assigneeUserIds == null) {
+            assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
+            execution.setVariable(super.collectionVariable, assigneeUserIds);
+            if (CollUtil.isEmpty(assigneeUserIds)) {
+                // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
+                // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
+                // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
+                assigneeUserIds = SetUtils.asSet((Long) null);
+            }
         }
-        execution.setVariable(super.collectionVariable, assigneeUserIds);
         return assigneeUserIds.size();
     }
 

+ 8 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java

@@ -211,7 +211,14 @@ public class BpmnModelUtils {
             return null;
         }
         BpmnXMLConverter converter = new BpmnXMLConverter();
-        return new String(converter.convertToXML(model));
+        return StrUtil.utf8Str(converter.convertToXML(model));
+    }
+
+    public static String getBpmnXml(byte[] bpmnBytes) {
+        if (ArrayUtil.isEmpty(bpmnBytes)) {
+            return null;
+        }
+        return StrUtil.utf8Str(bpmnBytes);
     }
 
     // ========== 遍历相关的方法 ==========

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java

@@ -65,6 +65,7 @@ public class BpmModelServiceImpl implements BpmModelService {
     @Override
     public PageResult<Model> getModelPage(BpmModelPageReqVO pageVO) {
         ModelQuery modelQuery = repositoryService.createModelQuery();
+        modelQuery.modelTenantId(FlowableUtils.getTenantId());
         if (StrUtil.isNotBlank(pageVO.getKey())) {
             modelQuery.modelKey(pageVO.getKey());
         }
@@ -80,7 +81,6 @@ public class BpmModelServiceImpl implements BpmModelService {
             return PageResult.empty(count);
         }
         List<Model> models = modelQuery
-                .modelTenantId(FlowableUtils.getTenantId())
                 .orderByCreateTime().desc()
                 .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
         return new PageResult<>(models, count);

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java

@@ -106,7 +106,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
         if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
             taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
-            taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1]));
+            taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
         }
         long count = taskQuery.count();
         if (count == 0) {
@@ -128,7 +128,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
         if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
             taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
-            taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1]));
+            taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
         }
         // 执行查询
         long count = taskQuery.count();
@@ -150,7 +150,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
         if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
             taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
-            taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1]));
+            taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
         }
         // 执行查询
         long count = taskQuery.count();

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java

@@ -92,7 +92,7 @@ public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
         List<Map<String, Object>> result = selectMaps(new QueryWrapper<CrmReceivableDO>()
                 .select("contract_id, SUM(price) AS total_price")
                 .in("audit_status", CrmAuditStatusEnum.DRAFT.getStatus(), // 草稿 + 审批中 + 审批通过
-                        CrmAuditStatusEnum.PROCESS, CrmAuditStatusEnum.APPROVE.getStatus())
+                        CrmAuditStatusEnum.PROCESS.getStatus(), CrmAuditStatusEnum.APPROVE.getStatus())
                 .groupBy("contract_id")
                 .in("contract_id", contractIds));
         // 获得金额

+ 2 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.statistics;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
@@ -106,7 +107,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
     private List<Long> getUserIds(CrmStatisticsPerformanceReqVO reqVO) {
         // 情况一:选中某个用户
         if (ObjUtil.isNotNull(reqVO.getUserId())) {
-            return List.of(reqVO.getUserId());
+            return ListUtil.of(reqVO.getUserId());
         }
         // 情况二:选中某个部门
         // 2.1 获得部门列表

+ 11 - 0
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
 
 import jakarta.validation.Valid;
+import org.springframework.scheduling.annotation.Async;
 
 /**
  * API 访问日志的 API 接口
@@ -18,4 +19,14 @@ public interface ApiAccessLogApi {
      */
     void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO);
 
+    /**
+     * 【异步】创建 API 访问日志
+     *
+     * @param createDTO 访问日志 DTO
+     */
+    @Async
+    default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
+        createApiAccessLog(createDTO);
+    }
+
 }

+ 11 - 0
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
 
 import jakarta.validation.Valid;
+import org.springframework.scheduling.annotation.Async;
 
 /**
  * API 错误日志的 API 接口
@@ -18,4 +19,14 @@ public interface ApiErrorLogApi {
      */
     void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO);
 
+    /**
+     * 【异步】创建 API 异常日志
+     *
+     * @param createDTO 异常日志 DTO
+     */
+    @Async
+    default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
+        createApiErrorLog(createDTO);
+    }
+
 }

+ 1 - 1
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java

@@ -22,7 +22,7 @@ public interface ErrorCodeConstants {
     ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改");
     ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改");
     ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确");
-    ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在");
+    ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在,注意 Bean 默认首字母小写");
     ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口");
 
     // ========== API 错误日志 1-001-002-000 ==========

+ 0 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java

@@ -14,7 +14,6 @@ public enum CodegenFrontTypeEnum {
 
     VUE2(10), // Vue2 Element UI 标准模版
     VUE3(20), // Vue3 Element Plus 标准模版
-    VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版
     VUE3_VBEN(30), // Vue3 VBEN 模版
     ;
 

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java

@@ -91,7 +91,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
      * 开启 VirtualStyle 模式
      */
     private void enableVirtualStyleEndpoint() {
-        if (StrUtil.containsAll(config.getEndpoint(),
+        if (StrUtil.containsAny(config.getEndpoint(),
                 S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284
                 S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493
             client.enableVirtualStyleEndpoint();

+ 0 - 13
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java

@@ -135,15 +135,6 @@ public class CodegenEngine {
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
             .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"),
                     vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
-            // Vue3 Schema 模版
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"),
-                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"),
-                    vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"),
-                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"),
-                    vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
             // Vue3 vben 模版
             .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@@ -496,10 +487,6 @@ public class CodegenEngine {
                 "src/" + path;
     }
 
-    private static String vue3SchemaTemplatePath(String path) {
-        return "codegen/vue3_schema/" + path + ".vm";
-    }
-
     private static String vue3VbenTemplatePath(String path) {
         return "codegen/vue3_vben/" + path + ".vm";
     }

+ 8 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java

@@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.quartz.SchedulerException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -91,13 +92,15 @@ public class JobServiceImpl implements JobService {
     }
 
     private void validateJobHandlerExists(String handlerName) {
-        Object handler = SpringUtil.getBean(handlerName);
-        if (handler == null) {
+        try {
+            Object handler = SpringUtil.getBean(handlerName);
+            assert handler != null;
+            if (!(handler instanceof JobHandler)) {
+                throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
+            }
+        } catch (NoSuchBeanDefinitionException e) {
             throw exception(JOB_HANDLER_BEAN_NOT_EXISTS);
         }
-        if (!(handler instanceof JobHandler)) {
-            throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
-        }
     }
 
     @Override

+ 1 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm

@@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
         // 校验存在
         validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
         // 更新
+        ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
         ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
     }
 

+ 5 - 6
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm

@@ -64,12 +64,11 @@
           <el-checkbox
             v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
             :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-checkbox>
+            :label="dict.label"
+            :value="dict.value"
+          />
                 #else##没数据字典
-          <el-checkbox>请选择字典生成</el-checkbox>
+          <el-checkbox label="请选择字典生成" />
                 #end
         </el-checkbox-group>
       </el-form-item>
@@ -85,7 +84,7 @@
             {{ dict.label }}
           </el-radio>
                 #else##没数据字典
-          <el-radio label="1">请选择字典生成</el-radio>
+          <el-radio value="1">请选择字典生成</el-radio>
                 #end
         </el-radio-group>
       </el-form-item>

Деякі файли не було показано, через те що забагато файлів було змінено