Sfoglia il codice sorgente

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

# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
#	yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java
#	yudao-server/pom.xml
YunaiV 8 mesi fa
parent
commit
eaa22e7a56
100 ha cambiato i file con 2288 aggiunte e 480 eliminazioni
  1. 2 1
      pom.xml
  2. 11 0
      sql/mysql/bpm_update.sql
  3. 8 0
      yudao-dependencies/pom.xml
  4. 7 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java
  5. 4 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java
  6. 6 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
  7. 6 1
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
  8. 3 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java
  9. 3 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java
  10. 15 16
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java
  11. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java
  12. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java
  13. 11 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java
  14. 20 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java
  15. 14 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java
  16. 2 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java
  17. 17 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java
  18. 8 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java
  19. 16 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
  20. 17 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java
  21. 29 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java
  22. 3 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java
  23. 4 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java
  24. 9 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java
  25. 15 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java
  26. 52 17
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
  27. 6 17
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java
  28. 11 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java
  29. 96 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
  30. 21 13
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java
  31. 23 11
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java
  32. 2 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
  33. 7 5
      yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
  34. 5 11
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
  35. 14 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java
  36. 25 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java
  37. 0 28
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java
  38. 0 52
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java
  39. 2 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  40. 25 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventType.java
  41. 33 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmFieldPermissionEnum.java
  42. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelTypeEnum.java
  43. 36 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionType.java
  44. 76 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java
  45. 44 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java
  46. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java
  47. 33 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java
  48. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java
  49. 35 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java
  50. 32 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java
  51. 2 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java
  52. 7 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java
  53. 12 8
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java
  54. 1 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java
  55. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/package-info.java
  56. 19 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java
  57. 53 28
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
  58. 12 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
  59. 0 19
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java
  60. 19 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java
  61. 62 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
  62. 6 14
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java
  63. 8 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java
  64. 0 47
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java
  65. 220 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java
  66. 23 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelUpdateReqVO.java
  67. 2 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java
  68. 17 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  69. 0 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java
  70. 9 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
  71. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java
  72. 26 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java
  73. 87 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java
  74. 37 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java
  75. 17 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java
  76. 36 56
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
  77. 0 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
  78. 18 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java
  79. 59 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
  80. 7 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java
  81. 7 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java
  82. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
  83. 9 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
  84. 8 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
  85. 8 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java
  86. 56 11
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java
  87. 46 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java
  88. 78 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractDeptLeaderStrategy.java
  89. 37 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java
  90. 66 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAssignEmptyStrategy.java
  91. 45 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderMultiStrategy.java
  92. 9 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java
  93. 8 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java
  94. 9 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategy.java
  95. 9 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java
  96. 8 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java
  97. 8 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java
  98. 90 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java
  99. 91 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderStrategy.java
  100. 24 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java

+ 2 - 1
pom.xml

@@ -23,7 +23,8 @@
 <!--        <module>yudao-module-mall</module>-->
 <!--        <module>yudao-module-crm</module>-->
 <!--        <module>yudao-module-erp</module>-->
-<!-- AI 大模型的开启,请参考 https://doc.iocoder.cn/ai/build/ 文档,对 JDK 版本要要求! -->
+<!--        <module>yudao-module-iot</module>-->
+        <!-- AI 大模型的开启,请参考 https://doc.iocoder.cn/ai/build/ 文档,对 JDK 版本要要求! -->
 <!--        <module>yudao-module-ai</module>-->
     </modules>
 

+ 11 - 0
sql/mysql/bpm_update.sql

@@ -0,0 +1,11 @@
+-- ----------------------------
+-- 流程抄送表新加流程活动编号
+-- ----------------------------
+ALTER TABLE `pro-test`.`bpm_process_instance_copy`
+    ADD COLUMN `activity_id` varchar(64) NULL COMMENT '流程活动编号' AFTER `category`,
+    MODIFY COLUMN `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '任务编号' AFTER `category`;
+
+ALTER TABLE `pro-test`.`bpm_process_definition_info`
+    ADD COLUMN `model_type` tinyint NOT NULL DEFAULT 10 COMMENT '流程模型的类型' AFTER `model_id`,
+    ADD COLUMN `simple_model` json NULL COMMENT 'SIMPLE 设计器模型数据' AFTER `form_custom_view_path`,
+    ADD COLUMN `visible` bit(1) NOT NULL DEFAULT 1 COMMENT '是否可见' AFTER `simple_model`;

+ 8 - 0
yudao-dependencies/pom.xml

@@ -66,6 +66,7 @@
         <ip2region.version>2.7.0</ip2region.version>
         <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
         <netty.version>4.1.113.Final</netty.version>
+        <mqtt.version>1.2.5</mqtt.version>
         <!-- 三方云服务相关 -->
         <okio.version>3.5.0</okio.version>
         <okhttp3.version>4.11.0</okhttp3.version>
@@ -626,6 +627,13 @@
                 </exclusions>
             </dependency>
 
+            <!-- MQTT -->
+            <dependency>
+                <groupId>org.eclipse.paho</groupId>
+                <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+                <version>${mqtt.version}</version>
+            </dependency>
+
             <!-- 专属于 JDK8 安全漏洞升级 -->
             <dependency>
                 <groupId>ch.qos.logback</groupId>

+ 7 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java

@@ -59,4 +59,11 @@ public class BeanUtils {
         return new PageResult<>(list, source.getTotal());
     }
 
+    public static void copyProperties(Object source, Object target) {
+        if (source == null || target == null) {
+            return;
+        }
+        BeanUtil.copyProperties(source, target, false);
+    }
+
 }

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java

@@ -62,6 +62,10 @@ public class BannerApplicationRunner implements ApplicationRunner {
             if (isNotPresent("cn.iocoder.yudao.module.ai.framework.web.config.AiWebConfiguration")) {
                 System.out.println("[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]");
             }
+            // IOT 物联网
+            if (isNotPresent("cn.iocoder.yudao.module.iot.framework.web.config.IotWebConfiguration")) {
+                System.out.println("[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]");
+            }
         });
     }
 

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

@@ -376,6 +376,12 @@ public class GlobalExceptionHandler {
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                     "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
         }
+        // 9. IOT 物联网
+        if (message.contains("iot_")) {
+            log.error("[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
+        }
         return null;
     }
 

+ 6 - 1
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java

@@ -34,7 +34,12 @@ public enum AiChatRoleEnum {
              ### 支付宝
              ### 微信
             除此之外不要任何解释性语句。
-            """);
+            """),
+
+    AI_KNOWLEDGE_ROLE("知识库助手", """
+                给你提供一些数据参考:{info},请回答我的问题。
+                请你跟进数据参考与工具返回结果回复用户的请求。
+                """);
 
     /**
      * 角色名

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

@@ -10,4 +10,7 @@ public class AiChatConversationCreateMyReqVO {
     @Schema(description = "聊天角色编号", example = "666")
     private Long roleId;
 
+    @Schema(description = "知识库编号", example = "1204")
+    private Long knowledgeId;
+
 }

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

@@ -21,6 +21,9 @@ public class AiChatConversationUpdateMyReqVO {
     @Schema(description = "模型编号", example = "1")
     private Long modelId;
 
+    @Schema(description = "知识库编号", example = "1")
+    private Long knowledgeId;
+
     @Schema(description = "角色设定", example = "一个快乐的程序员")
     private String systemMessage;
 

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

@@ -1,12 +1,12 @@
 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.AiKnowledgeCreateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
-import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
 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;
@@ -28,24 +28,23 @@ 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);
+    @GetMapping("/page")
+    @Operation(summary = "获取知识库分页")
+    public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) {
+        PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePage(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()));
+    @PostMapping("/create")
+    @Operation(summary = "创建知识库")
+    public CommonResult<Long> createKnowledge(@RequestBody @Valid AiKnowledgeCreateReqVO createReqVO) {
+        return success(knowledgeService.createKnowledge(createReqVO, getLoginUserId()));
     }
 
-    @PutMapping("/update-my")
-    @Operation(summary = "更新【我的】知识库")
-    public CommonResult<Boolean> updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) {
-        knowledgeService.updateKnowledgeMy(updateReqVO, getLoginUserId());
+    @PutMapping("/update")
+    @Operation(summary = "更新知识库")
+    public CommonResult<Boolean> updateKnowledge(@RequestBody @Valid AiKnowledgeUpdateReqVO updateReqVO) {
+        knowledgeService.updateKnowledge(updateReqVO, getLoginUserId());
         return success(true);
     }
-
 }

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

@@ -36,7 +36,7 @@ public class AiKnowledgeDocumentController {
 
     @GetMapping("/page")
     @Operation(summary = "获取文档分页")
-    public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPageMy(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
+    public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPage(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
         PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO);
         return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
     }

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

@@ -29,7 +29,7 @@ public class AiKnowledgeSegmentController {
 
     @GetMapping("/page")
     @Operation(summary = "获取段落分页")
-    public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPageMy(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
+    public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
         PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
         return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
     }

+ 11 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateMyReqVO.java → yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java

@@ -7,9 +7,9 @@ import lombok.Data;
 
 import java.util.List;
 
-@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO")
+@Schema(description = "管理后台 - AI 知识库创建 Request VO")
 @Data
-public class AiKnowledgeCreateMyReqVO {
+public class AiKnowledgeCreateReqVO {
 
     @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
     @NotBlank(message = "知识库名称不能为空")
@@ -18,11 +18,19 @@ public class AiKnowledgeCreateMyReqVO {
     @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档")
     private String description;
 
-    @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
+    @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]")
     private List<Long> visibilityPermissions;
 
     @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     @NotNull(message = "嵌入模型不能为空")
     private Long modelId;
 
+    @Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5")
+    @NotNull(message = "相似性阈值不能为空")
+    private Double similarityThreshold;
+
+    @Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
+    @NotNull(message = "topK 不能为空")
+    private Integer topK;
+
 }

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

@@ -23,4 +23,24 @@ public class AiKnowledgeDocumentCreateReqVO {
     @URL(message = "文档 URL 格式不正确")
     private String url;
 
+    @Schema(description = "每个段落的目标 token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800")
+    @NotNull(message = "每个段落的目标 token 数不能为空")
+    private Integer defaultSegmentTokens;
+
+    @Schema(description = "每个段落的最小字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "350")
+    @NotNull(message = "每个段落的最小字符数不能为空")
+    private Integer minSegmentWordCount;
+
+    @Schema(description = "丢弃阈值:低于此阈值的段落会被丢弃", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
+    @NotNull(message = "丢弃阈值不能为空")
+    private Integer minChunkLengthToEmbed;
+
+    @Schema(description = "最大段落数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+    @NotNull(message = "最大段落数不能为空")
+    private Integer maxNumSegments;
+
+    @Schema(description = "分块是否保留分隔符", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "分块是否保留分隔符不能为空")
+    private Boolean keepSeparator;
+
 }

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
+
+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 AiKnowledgePageReqVO extends PageParam {
+
+    @Schema(description = "知识库名称", example = "Java 开发手册")
+    private String name;
+
+}

+ 2 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateMyReqVO.java → yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java

@@ -9,7 +9,7 @@ import java.util.List;
 
 @Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO")
 @Data
-public class AiKnowledgeUpdateMyReqVO {
+public class AiKnowledgeUpdateReqVO {
 
     @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
     @NotNull(message = "知识库编号不能为空")
@@ -22,7 +22,7 @@ public class AiKnowledgeUpdateMyReqVO {
     @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
     private String description;
 
-    @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
+    @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
     private List<Long> visibilityPermissions;
 
     @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")

+ 17 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.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 AiKnowledgeSegmentSearchReqVO {
+
+    @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+    private Long knowledgeId;
+
+    @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
+    private String content;
+
+}

+ 8 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+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.dataobject.model.AiChatRoleDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -64,6 +65,13 @@ public class AiChatConversationDO extends BaseDO {
      */
     private Long roleId;
 
+    /**
+     * 知识库编号
+     * <p>
+     * 关联 {@link AiKnowledgeDO#getId()}
+     */
+    private Long knowledgeId;
+
     /**
      * 模型编号
      *

+ 16 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java

@@ -1,13 +1,18 @@
 package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
 
-import com.baomidou.mybatisplus.annotation.TableId;
-import org.springframework.ai.chat.messages.MessageType;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+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.dataobject.model.AiChatRoleDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
+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.*;
+import org.springframework.ai.chat.messages.MessageType;
+
+import java.util.List;
 
 /**
  * AI Chat 消息 DO
@@ -66,6 +71,15 @@ public class AiChatMessageDO extends BaseDO {
      */
     private Long roleId;
 
+
+    /**
+     * 段落编号数组
+     *
+     * 关联 {@link AiKnowledgeSegmentDO#getId()} 字段
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<Long> segmentIds;
+
     /**
      * 模型标志
      */

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

@@ -2,10 +2,10 @@ 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.framework.mybatis.core.type.LongListTypeHandler;
 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;
@@ -38,11 +38,13 @@ public class AiKnowledgeDO extends BaseDO {
      * 知识库描述
      */
     private String description;
-    // TODO @新:如果全部可见,需要怎么设置?
+
     /**
-     * 可见权限,只能选择哪些人可见
+     * 可见权限,选择哪些人可见
+     * <p>
+     * -1 所有人可见,其他为各自用户编号
      */
-    @TableField(typeHandler = JacksonTypeHandler.class)
+    @TableField(typeHandler = LongListTypeHandler.class)
     private List<Long> visibilityPermissions;
     /**
      * 嵌入模型编号
@@ -52,10 +54,21 @@ public class AiKnowledgeDO extends BaseDO {
      * 模型标识
      */
     private String model;
+
+    /**
+     * topK
+     */
+    private Integer topK;
+    /**
+     * 相似度阈值
+     */
+    private Double similarityThreshold;
+
     /**
      * 状态
      * <p>
      * 枚举 {@link CommonStatusEnum}
      */
     private Integer status;
+
 }

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

@@ -23,7 +23,7 @@ public class AiKnowledgeDocumentDO extends BaseDO {
     private Long id;
     /**
      * 知识库编号
-     *
+     * <p>
      * 关联 {@link AiKnowledgeDO#getId()}
      */
     private Long knowledgeId;
@@ -40,13 +40,39 @@ public class AiKnowledgeDocumentDO extends BaseDO {
      */
     private String url;
     /**
-     * token 数量
+     * 文档 token 数量
      */
     private Integer tokens;
     /**
-     * 字符数
+     * 文档字符数
      */
     private Integer wordCount;
+
+
+    // ========== 自定义分段所用参数 ==========
+    // TODO @新:3)defaultChunkSize、defaultChunkSize、minChunkSizeChars、maxNumChunks 这几个字段的命名,可能要微信一起讨论下。尽量命名保持风格统一哈。
+    /**
+     * 每个文本块的目标 token 数
+     */
+    private Integer defaultSegmentTokens;
+    /**
+     * 每个文本块的最小字符数
+     */
+    private Integer minSegmentWordCount;
+    /**
+     * 低于此值的块会被丢弃
+     */
+    private Integer minChunkLengthToEmbed;
+    /**
+     * 最大块数
+     */
+    private Integer maxNumSegments;
+    /**
+     * 分块是否保留分隔符
+     */
+    private Boolean keepSeparator;
+    // ===================================
+
     /**
      * 切片状态
      * <p>

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

@@ -28,13 +28,13 @@ public class AiKnowledgeSegmentDO extends BaseDO {
     private String vectorId;
     /**
      * 知识库编号
-     *
+     * <p>
      * 关联 {@link AiKnowledgeDO#getId()}
      */
     private Long knowledgeId;
     /**
      * 文档编号
-     *
+     * <p>
      * 关联 {@link AiKnowledgeDocumentDO#getId()}
      */
     private Long documentId;
@@ -52,7 +52,7 @@ public class AiKnowledgeSegmentDO extends BaseDO {
     private Integer tokens;
     /**
      * 状态
-     *
+     * <p>
      * 枚举 {@link CommonStatusEnum}
      */
     private Integer status;

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

@@ -1,10 +1,10 @@
 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.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -16,10 +16,11 @@ import org.apache.ibatis.annotations.Mapper;
 @Mapper
 public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> {
 
-    default PageResult<AiKnowledgeDO> selectPageByMy(Long userId, PageParam pageReqVO) {
+    default PageResult<AiKnowledgeDO> selectPage(Long userId, AiKnowledgePageReqVO pageReqVO) {
         return selectPage(pageReqVO, new LambdaQueryWrapperX<AiKnowledgeDO>()
-                .eq(AiKnowledgeDO::getUserId, userId)
                 .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
+                .likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName())
+                .and(e -> e.apply("FIND_IN_SET(" + userId + ",visibility_permissions)").or(m -> m.apply("FIND_IN_SET(-1,visibility_permissions)")))
                 .orderByDesc(AiKnowledgeDO::getId));
     }
 }

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

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle
 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.List;
+
 /**
  * AI 知识库-分片 Mapper
  *
@@ -22,4 +24,11 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegment
                 .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword())
                 .orderByDesc(AiKnowledgeSegmentDO::getId));
     }
+
+    default List<AiKnowledgeSegmentDO> selectListByVectorIds(List<String> vectorIdList) {
+        return selectList(new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
+                .in(AiKnowledgeSegmentDO::getVectorId, vectorIdList)
+                .orderByDesc(AiKnowledgeSegmentDO::getId));
+    }
+
 }

+ 15 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java

@@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
 import jakarta.annotation.Resource;
@@ -22,6 +23,7 @@ import org.springframework.validation.annotation.Validated;
 
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -45,6 +47,8 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
     private AiChatModelService chatModalService;
     @Resource
     private AiChatRoleService chatRoleService;
+    @Resource
+    private AiKnowledgeService knowledgeService;
 
     @Override
     public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) {
@@ -56,9 +60,14 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
         Assert.notNull(model, "必须找到默认模型");
         validateChatModel(model);
 
+        // 1.3 校验知识库
+        if (Objects.nonNull(createReqVO.getKnowledgeId())) {
+            knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
+        }
+
         // 2. 创建 AiChatConversationDO 聊天对话
         AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false)
-                .setModelId(model.getId()).setModel(model.getModel())
+                .setModelId(model.getId()).setModel(model.getModel()).setKnowledgeId(createReqVO.getKnowledgeId())
                 .setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts());
         if (role != null) {
             conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
@@ -82,6 +91,11 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
             model = chatModalService.validateChatModel(updateReqVO.getModelId());
         }
 
+        // 1.3 校验知识库是否存在
+        if (updateReqVO.getKnowledgeId() != null) {
+            knowledgeService.validateKnowledgeExists(updateReqVO.getKnowledgeId());
+        }
+
         // 2. 更新对话信息
         AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class);
         if (Boolean.TRUE.equals(updateReqVO.getPinned())) {

+ 52 - 17
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java

@@ -12,21 +12,29 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
+import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum;
 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
 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.chat.messages.*;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.MessageType;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
 import org.springframework.ai.chat.model.ChatModel;
 import org.springframework.ai.chat.model.ChatResponse;
 import org.springframework.ai.chat.model.StreamingChatModel;
 import org.springframework.ai.chat.prompt.ChatOptions;
 import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.chat.prompt.PromptTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import reactor.core.publisher.Flux;
@@ -59,6 +67,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
     private AiChatModelService chatModalService;
     @Resource
     private AiApiKeyService apiKeyService;
+    @Resource
+    private AiKnowledgeSegmentService knowledgeSegmentService;
 
     @Transactional(rollbackFor = Exception.class)
     public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
@@ -80,13 +90,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
                 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
 
-        // 3.2 创建 chat 需要的 Prompt
-        Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
+        // 3.2 召回段落
+        List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
+
+        // 3.3 创建 chat 需要的 Prompt
+        Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
         ChatResponse chatResponse = chatModel.call(prompt);
 
-        // 3.3 段式返回
+        // 3.4 段式返回
         String newContent = chatResponse.getResult().getOutput().getContent();
-        chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
+        chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent));
         return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
                 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
     }
@@ -111,11 +124,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
                 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
 
-        // 3.2 构建 Prompt,并进行调用
-        Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
+
+        // 3.2 召回段落
+        List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
+
+        // 3.3 构建 Prompt,并进行调用
+        Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
         Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
 
-        // 3.3 流式返回
+        // 3.4 流式返回
         // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
         StringBuffer contentBuffer = new StringBuffer();
         return streamResponse.map(chunk -> {
@@ -128,7 +145,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         }).doOnComplete(() -> {
             // 忽略租户,因为 Flux 异步无法透传租户
             TenantUtils.executeIgnore(() ->
-                    chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())));
+                    chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId))
+                            .setContent(contentBuffer.toString())));
         }).doOnError(throwable -> {
             log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
             // 忽略租户,因为 Flux 异步无法透传租户
@@ -137,18 +155,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
     }
 
-    private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
+    private List<AiKnowledgeSegmentDO> recallSegment(String content, Long knowledgeId) {
+        if (Objects.isNull(knowledgeId)) {
+            return Collections.emptyList();
+        }
+        return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content));
+    }
+
+    private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,List<AiKnowledgeSegmentDO> segmentList,
                                AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
         // 1. 构建 Prompt Message 列表
         List<Message> chatMessages = new ArrayList<>();
-        // 1.1 system context 角色设定
+
+        // 1.1 召回内容消息构建
+        if (CollUtil.isNotEmpty(segmentList)) {
+            PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage());
+            StringBuilder infoBuilder = StrUtil.builder();
+            segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent()));
+            Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString()));
+            chatMessages.add(message);
+        }
+
+        // 1.2 system context 角色设定
         if (StrUtil.isNotBlank(conversation.getSystemMessage())) {
             chatMessages.add(new SystemMessage(conversation.getSystemMessage()));
         }
-        // 1.2 history message 历史消息
+        // 1.3 history message 历史消息
         List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
         contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
-        // 1.3 user message 新发送消息
+        // 1.4 user message 新发送消息
         chatMessages.add(new UserMessage(sendReqVO.getContent()));
 
         // 2. 构建 ChatOptions 对象
@@ -160,12 +195,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
 
     /**
      * 从历史消息中,获得倒序的 n 组消息作为消息上下文
-     *
+     * <p>
      * n 组:指的是 user + assistant 形成一组
      *
-     * @param messages 消息列表
+     * @param messages     消息列表
      * @param conversation 对话
-     * @param sendReqVO 发送请求
+     * @param sendReqVO    发送请求
      * @return 消息上下文
      */
     private List<AiChatMessageDO> filterContextMessages(List<AiChatMessageDO> messages,
@@ -182,7 +217,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
             }
             AiChatMessageDO userMessage = CollUtil.get(messages, i - 1);
             if (userMessage == null || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId())
-                || StrUtil.isEmpty(assistantMessage.getContent())) {
+                    || StrUtil.isEmpty(assistantMessage.getContent())) {
                 continue;
             }
             // 由于后续要 reverse 反转,所以先添加 assistantMessage

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

@@ -9,15 +9,11 @@ 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;
@@ -48,24 +44,16 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
     @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());
+        // 0. 校验并获取向量存储实例
+        VectorStore vectorStore = knowledgeService.getVectorStoreById(createReqVO.getKnowledgeId());
 
         // 1.1 下载文档
         TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl()));
@@ -82,6 +70,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
             return documentId;
         }
 
+        // 2 构造文本分段器
+        TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(createReqVO.getDefaultSegmentTokens(), createReqVO.getMinSegmentWordCount(), createReqVO.getMinChunkLengthToEmbed(),
+                createReqVO.getMaxNumSegments(), createReqVO.getKeepSeparator());
         // 2.1 文档分段
         List<Document> segments = tokenTextSplitter.apply(documents);
         // 2.2 分段内容入库
@@ -92,9 +83,7 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
                         .setStatus(CommonStatusEnum.ENABLE.getStatus()));
         segmentMapper.insertBatch(segmentDOList);
 
-        // 3.1 获取向量存储实例
-        VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
-        // 3.2 向量化并存储
+        // 3. 向量化并存储
         segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId()));
         vectorStore.add(segments);
         return documentId;

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

@@ -2,10 +2,13 @@ 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.AiKnowledgeSegmentSearchReqVO;
 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 java.util.List;
+
 /**
  * AI 知识库段落 Service 接口
  *
@@ -35,4 +38,12 @@ public interface AiKnowledgeSegmentService {
      */
     void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
 
+    /**
+     * 召回段落
+     *
+     * @param reqVO 召回请求信息
+     * @return 召回的段落
+     */
+    List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO);
+
 }

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

@@ -1,16 +1,34 @@
 package cn.iocoder.yudao.module.ai.service.knowledge;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 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.AiKnowledgeSegmentSearchReqVO;
 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.AiKnowledgeDO;
 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.AiKnowledgeSegmentMapper;
+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.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
+
 /**
  * AI 知识库分片 Service 实现类
  *
@@ -23,6 +41,13 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
     @Resource
     private AiKnowledgeSegmentMapper segmentMapper;
 
+    @Resource
+    private AiKnowledgeService knowledgeService;
+    @Resource
+    private AiChatModelService chatModelService;
+    @Resource
+    private AiApiKeyService apiKeyService;
+
     @Override
     public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
         return segmentMapper.selectPage(pageReqVO);
@@ -30,13 +55,80 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
 
     @Override
     public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) {
-        segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
-        // TODO @xin 重新向量化
+        // 1. 校验
+        AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId());
+
+        // 2.1 获取知识库向量实例
+        VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId());
+        // 2.2 删除原向量
+        vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId()));
+        // 2.3 重新向量化
+        Document document = new Document(reqVO.getContent());
+        document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId());
+        vectorStore.add(List.of(document));
+
+        // 3. 更新段落内容
+        AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class);
+        knowledgeSegment.setVectorId(document.getId());
+        segmentMapper.updateById(knowledgeSegment);
     }
 
     @Override
     public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
-        segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
-        // TODO @xin 1.禁用删除向量 2.启用重新向量化
+        // 0 校验
+        AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId());
+        // 1 获取知识库向量实例
+        VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId());
+        AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class);
+
+        if (Objects.equals(reqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
+            // 2.1 启用重新向量化
+            Document document = new Document(oldKnowledgeSegment.getContent());
+            document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId());
+            vectorStore.add(List.of(document));
+            knowledgeSegment.setVectorId(document.getId());
+        } else {
+            // 2.2 禁用删除向量
+            vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId()));
+            knowledgeSegment.setVectorId("");
+        }
+        // 3 更新段落状态
+        segmentMapper.updateById(knowledgeSegment);
     }
+
+    @Override
+    public List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO) {
+        // 1. 校验
+        AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqVO.getKnowledgeId());
+        AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
+
+        // 2. 获取向量存储实例
+        VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
+
+        // 3.1 向量检索
+        List<Document> documentList = vectorStore.similaritySearch(SearchRequest.query(reqVO.getContent())
+                .withTopK(knowledge.getTopK())
+                .withSimilarityThreshold(knowledge.getSimilarityThreshold())
+                .withFilterExpression(new FilterExpressionBuilder().eq(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build()));
+        if (CollUtil.isEmpty(documentList)) {
+            return ListUtil.empty();
+        }
+        // 3.2 段落召回
+        return segmentMapper.selectListByVectorIds(CollUtil.getFieldValues(documentList, "id", String.class));
+    }
+
+    /**
+     * 校验段落是否存在
+     *
+     * @param id 文档编号
+     * @return 段落信息
+     */
+    private AiKnowledgeSegmentDO validateKnowledgeSegmentExists(Long id) {
+        AiKnowledgeSegmentDO knowledgeSegment = segmentMapper.selectById(id);
+        if (knowledgeSegment == null) {
+            throw exception(KNOWLEDGE_SEGMENT_NOT_EXISTS);
+        }
+        return knowledgeSegment;
+    }
+
 }

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

@@ -1,10 +1,11 @@
 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.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
+import org.springframework.ai.vectorstore.VectorStore;
 
 /**
  * AI 知识库-基础信息 Service 接口
@@ -14,23 +15,21 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
 public interface AiKnowledgeService {
 
     /**
-     * 创建【我的】知识库
+     * 创建知识库
      *
      * @param createReqVO 创建信息
      * @param userId      用户编号
      * @return 编号
      */
-    Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId);
-
+    Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId);
 
     /**
-     * 创建【我的】知识库
+     * 更新知识库
      *
      * @param updateReqVO 更新信息
      * @param userId      用户编号
      */
-    void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId);
-
+    void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId);
 
     /**
      * 校验知识库是否存在
@@ -40,11 +39,20 @@ public interface AiKnowledgeService {
     AiKnowledgeDO validateKnowledgeExists(Long id);
 
     /**
-     * 获得【我的】知识库分页
+     * 获得知识库分页
      *
-     * @param userId 用户编号
-     * @param pageReqVO   分页查询
+     * @param userId    用户编号
+     * @param pageReqVO 分页查询
      * @return 知识库分页
      */
-    PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO);
+    PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO);
+
+    /**
+     * 根据知识库编号获取向量存储实例
+     *
+     * @param id 知识库编号
+     * @return 向量存储实例
+     */
+    VectorStore getVectorStoreById(Long id);
+
 }

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

@@ -2,17 +2,19 @@ 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.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
 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.AiApiKeyService;
 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.vectorstore.VectorStore;
 import org.springframework.stereotype.Service;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -28,15 +30,17 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_
 public class AiKnowledgeServiceImpl implements AiKnowledgeService {
 
     @Resource
-    private AiChatModelService chatModalService;
+    private AiKnowledgeMapper knowledgeMapper;
 
     @Resource
-    private AiKnowledgeMapper knowledgeMapper;
+    private AiChatModelService chatModelService;
+    @Resource
+    private AiApiKeyService apiKeyService;
 
     @Override
-    public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) {
+    public Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId) {
         // 1. 校验模型配置
-        AiChatModelDO model = chatModalService.validateChatModel(createReqVO.getModelId());
+        AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getModelId());
 
         // 2. 插入知识库
         AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class)
@@ -46,14 +50,14 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
     }
 
     @Override
-    public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) {
+    public void updateKnowledge(AiKnowledgeUpdateReqVO 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());
+        AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getModelId());
 
         // 2. 更新知识库
         AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class);
@@ -71,8 +75,16 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
     }
 
     @Override
-    public PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO) {
-        return knowledgeMapper.selectPageByMy(userId, pageReqVO);
+    public PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO) {
+        return knowledgeMapper.selectPage(userId, pageReqVO);
+    }
+
+    @Override
+    public VectorStore getVectorStoreById(Long id) {
+        AiKnowledgeDO knowledge = validateKnowledgeExists(id);
+        AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
+        // 创建或获取 VectorStore 对象
+        return apiKeyService.getOrCreateVectorStore(model.getKeyId());
     }
 
 }

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

@@ -2,7 +2,6 @@ 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;
@@ -39,8 +38,6 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
 
     @Resource
     private AiModelFactory modelFactory;
-    @Resource
-    private AiVectorStoreFactory vectorFactory;
 
     @Override
     public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
@@ -149,7 +146,8 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
     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());
+        // 创建或获取 VectorStore 对象
+        return modelFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl());
     }
 
 }

+ 7 - 5
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml

@@ -46,11 +46,13 @@
         </dependency>
 
         <!-- 向量化,基于 Redis 存储,Tika 解析内容 -->
-        <dependency>
-            <groupId>${spring-ai.groupId}</groupId>
-            <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
-            <version>${spring-ai.version}</version>
-        </dependency>
+
+        <!-- 暂不做经济型,先注释 TODO 经济型是啥呀? -->
+        <!--        <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>

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

@@ -2,8 +2,6 @@ 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;
@@ -38,11 +36,6 @@ public class YudaoAiAutoConfiguration {
         return new AiModelFactoryImpl();
     }
 
-    @Bean
-    public AiVectorStoreFactory aiVectorFactory() {
-        return new AiVectorStoreFactoryImpl();
-    }
-
 
     // ========== 各种 AI Client 创建 ==========
 
@@ -89,7 +82,7 @@ public class YudaoAiAutoConfiguration {
     // TODO @xin 免费版本
 //    @Bean
 //    @Lazy // TODO 芋艿:临时注释,避免无法启动」
-//    public EmbeddingModel transformersEmbeddingClient() {
+//    public TransformersEmbeddingModel transformersEmbeddingClient() {
 //        return new TransformersEmbeddingModel(MetadataMode.EMBED);
 //    }
 
@@ -98,23 +91,24 @@ public class YudaoAiAutoConfiguration {
      */
 //    @Bean
 //    @Lazy // TODO 芋艿:临时注释,避免无法启动
-//    public RedisVectorStore vectorStore(TongYiTextEmbeddingModel tongYiTextEmbeddingModel, RedisVectorStoreProperties properties,
+//    public RedisVectorStore vectorStore(TransformersEmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
 //                                        RedisProperties redisProperties) {
 //        var config = RedisVectorStore.RedisVectorStoreConfig.builder()
 //                .withIndexName(properties.getIndex())
 //                .withPrefix(properties.getPrefix())
+//                .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC))
 //                .build();
 //
-//        RedisVectorStore redisVectorStore = new RedisVectorStore(config, tongYiTextEmbeddingModel,
+//        RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
 //                new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
 //                properties.isInitializeSchema());
 //        redisVectorStore.afterPropertiesSet();
 //        return redisVectorStore;
 //    }
-
     @Bean
     @Lazy // TODO 芋艿:临时注释,避免无法启动
     public TokenTextSplitter tokenTextSplitter() {
+        //TODO  @xin 配置提取
         return new TokenTextSplitter(500, 100, 5, 10000, true);
     }
 

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

@@ -6,6 +6,7 @@ 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;
+import org.springframework.ai.vectorstore.VectorStore;
 
 /**
  * AI Model 模型工厂的接口类
@@ -92,4 +93,17 @@ public interface AiModelFactory {
      */
     EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url);
 
+    /**
+     * 基于指定配置,获得 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);
+
 }

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

@@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
 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.ai.core.model.xinghuo.XingHuoChatModel;
+import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
 import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties;
 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel;
@@ -54,13 +55,18 @@ import org.springframework.ai.qianfan.api.QianFanApi;
 import org.springframework.ai.qianfan.api.QianFanImageApi;
 import org.springframework.ai.stabilityai.StabilityAiImageModel;
 import org.springframework.ai.stabilityai.api.StabilityAiApi;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.ai.vectorstore.VectorStore;
 import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
 import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
 import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
 import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
 import org.springframework.retry.support.RetryTemplate;
 import org.springframework.web.client.ResponseErrorHandler;
 import org.springframework.web.client.RestClient;
+import redis.clients.jedis.JedisPooled;
+import redis.clients.jedis.search.Schema;
 
 import java.util.List;
 
@@ -191,6 +197,25 @@ public class AiModelFactoryImpl implements AiModelFactory {
         });
     }
 
+    @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>) () -> {
+            String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey);
+            var config = RedisVectorStore.RedisVectorStoreConfig.builder()
+                    .withIndexName(cacheKey)
+                    .withPrefix(prefix)
+                    .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC))
+                    .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();

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

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

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

@@ -1,52 +0,0 @@
-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, "_"));
-    }
-
-}

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java

@@ -23,6 +23,7 @@ public interface ErrorCodeConstants {
             "原因:用户任务({})未配置审批人,请点击【流程设计】按钮,选择该它的【任务(审批人)】进行配置");
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败,原因:BPMN 流程图中,没有开始事件");
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在");
+    ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程的管理员");
 
     // ========== 流程定义 1-009-003-000 ==========
     ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
@@ -36,6 +37,7 @@ public interface ErrorCodeConstants {
     ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1_009_004_002, "流程取消失败,该流程不是你发起的");
     ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "审批任务({})的审批人未配置");
     ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "审批任务({})的审批人({})不存在");
+    ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
 
     // ========== 流程任务 1-009-005-000 ==========
     ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");

+ 25 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventType.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * BPM 边界事件 (boundary event) 自定义类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmBoundaryEventType {
+
+    USER_TASK_TIMEOUT(1,"用户任务超时");
+
+    private final Integer type;
+    private final String name;
+
+    public static BpmBoundaryEventType typeOf(Integer type) {
+        return ArrayUtil.firstMatch(eventType -> eventType.getType().equals(type), values());
+    }
+
+}

+ 33 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmFieldPermissionEnum.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * BPM 表单权限的枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmFieldPermissionEnum {
+
+    READ(1, "只读"),
+    WRITE(2, "可编辑"),
+    NONE(3, "隐藏");
+
+    /**
+     * 权限
+     */
+    private final Integer permission;
+    /**
+     * 名字
+     */
+    private final String name;
+
+    public static BpmFieldPermissionEnum valueOf(Integer permission) {
+        return ArrayUtil.firstMatch(item -> item.getPermission().equals(permission), values());
+    }
+
+}

+ 31 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelTypeEnum.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 模型的类型的枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmModelTypeEnum implements IntArrayValuable {
+
+    BPMN(10, "BPMN 设计器"), // https://bpmn.io/toolkit/bpmn-js/
+    SIMPLE(20, "SIMPLE 设计器"); // 参考钉钉、飞书工作流的设计器
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmModelTypeEnum::getType).toArray();
+
+    private final Integer type;
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 36 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionType.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 仿钉钉的流程器设计器条件节点的条件类型
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmSimpleModeConditionType implements IntArrayValuable {
+
+    EXPRESSION(1, "条件表达式"),
+    RULE(2, "条件规则");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModeConditionType::getType).toArray();
+
+    private final Integer type;
+
+    private final String name;
+
+    public static BpmSimpleModeConditionType valueOf(Integer type) {
+        return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 76 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java

@@ -0,0 +1,76 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * 仿钉钉的流程器设计器的模型节点类型
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmSimpleModelNodeType implements IntArrayValuable {
+
+    // 0 ~ 1 开始和结束
+    START_NODE(0, "startEvent", "开始节点"),
+    END_NODE(1, "endEvent", "结束节点"),
+
+    // 10 ~ 49 各种节点
+    START_USER_NODE(10, "userTask", "发起人节点"), // 发起人节点。前端的开始节点,Id 固定
+    APPROVE_NODE(11, "userTask", "审批人节点"),
+    COPY_NODE(12, "serviceTask", "抄送人节点"),
+
+    // 50 ~ 条件分支
+    CONDITION_NODE(50, "sequenceFlow", "条件节点"), // 用于构建流转条件的表达式
+    CONDITION_BRANCH_NODE(51, " “parallelGateway”", "条件分支节点"), // TODO @jason:是不是改成叫 条件分支?
+    PARALLEL_BRANCH_NODE(52, "exclusiveGateway", "并行分支节点"), // TODO @jason:是不是一个 并行分支 ?就可以啦? 后面是否去掉并行网关。只用包容网关
+    INCLUSIVE_BRANCH_NODE(53, "inclusiveGateway", "包容分支节点"),
+    // TODO @jason:建议整合 join,最终只有 条件分支、并行分支、包容分支,三种~
+    // TODO @芋艿。 感觉还是分开好理解一点,也好处理一点。前端结构中把聚合节点显示并传过来。
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModelNodeType::getType).toArray();
+
+    public static final String BPMN_USER_TASK_TYPE = "userTask";
+
+    private final Integer type;
+    private final String bpmnType;
+    private final String name;
+
+    /**
+     * 判断是否为分支节点
+     *
+     * @param type 节点类型
+     */
+    public static boolean isBranchNode(Integer type) {
+        return Objects.equals(CONDITION_BRANCH_NODE.getType(), type)
+                || Objects.equals(PARALLEL_BRANCH_NODE.getType(), type)
+                || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type);
+    }
+
+    /**
+     * 判断是否需要记录的节点
+     *
+     * @param bpmnType bpmn节点类型
+     */
+    public static boolean isRecordNode(String bpmnType) {
+        return Objects.equals(APPROVE_NODE.getBpmnType(), bpmnType)
+                || Objects.equals(END_NODE.getBpmnType(), bpmnType);
+    }
+
+    public static BpmSimpleModelNodeType valueOf(Integer type) {
+        return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 44 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 多人审批方式的枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskApproveMethodEnum implements IntArrayValuable {
+
+    RANDOM(1, "随机挑选一人审批"),
+    RATIO(2, "多人会签(按通过比例)"), // 会签(按通过比例)
+    ANY(3, "多人或签(一人通过或拒绝)"), // 或签(通过只需一人,拒绝只需一人)
+    SEQUENTIAL(4, "依次审批"); // 依次审批
+
+    /**
+     * 审批方式
+     */
+    private final Integer method;
+
+    /**
+     * 名字
+     */
+    private final String name;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveMethodEnum::getMethod).toArray();
+
+    public static BpmUserTaskApproveMethodEnum valueOf(Integer method) {
+        return ArrayUtil.firstMatch(item -> item.getMethod().equals(method), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 31 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 用户任务的审批类型枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskApproveTypeEnum implements IntArrayValuable {
+
+    USER(1), // 人工审批
+    AUTO_APPROVE(2), // 自动通过
+    AUTO_REJECT(3); // 自动拒绝
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveTypeEnum::getType).toArray();
+
+    private final Integer type;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 33 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * BPM 用户任务的审批人为空时,处理类型枚举
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Getter
+public enum BpmUserTaskAssignEmptyHandlerTypeEnum implements IntArrayValuable {
+
+    APPROVE(1), // 自动通过
+    REJECT(2), // 自动拒绝
+    ASSIGN_USER(3), // 指定人员审批
+    ASSIGN_ADMIN(4), // 转交给流程管理员
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskAssignEmptyHandlerTypeEnum::getType).toArray();
+
+    private final Integer type;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 31 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * BPM 用户任务的审批人与发起人相同时,处理类型枚举
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Getter
+public enum BpmUserTaskAssignStartUserHandlerTypeEnum implements IntArrayValuable {
+
+    START_USER_AUDIT(1), // 由发起人对自己审批
+    SKIP(2), // 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过
+    TRANSFER_DEPT_LEADER(3); // 转交给部门负责人审批【参考飞书】:若部门负责人为空,则自动通过
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskAssignStartUserHandlerTypeEnum::getType).toArray();
+
+    private final Integer type;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 35 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 用户任务拒绝处理类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskRejectHandlerType implements IntArrayValuable {
+
+    FINISH_PROCESS_INSTANCE(1, "终止流程"),
+    RETURN_USER_TASK(2, "驳回到指定任务节点");
+
+    private final Integer type;
+    private final String name;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskRejectHandlerType::getType).toArray();
+
+    public static BpmUserTaskRejectHandlerType typeOf(Integer type) {
+        return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 32 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 用户任务超时处理类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskTimeoutHandlerTypeEnum implements IntArrayValuable {
+
+    REMINDER(1,"自动提醒"),
+    APPROVE(2, "自动同意"),
+    REJECT(3, "自动拒绝");
+
+    private final Integer type;
+    private final String name;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskTimeoutHandlerTypeEnum::getType).toArray();
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 2 - 1
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java

@@ -14,7 +14,8 @@ public enum BpmMessageEnum {
 
     PROCESS_INSTANCE_APPROVE("bpm_process_instance_approve"), // 流程任务被审批通过时,发送给申请人
     PROCESS_INSTANCE_REJECT("bpm_process_instance_reject"), // 流程任务被审批不通过时,发送给申请人
-    TASK_ASSIGNED("bpm_task_assigned"); // 任务被分配时,发送给审批人
+    TASK_ASSIGNED("bpm_task_assigned"), // 任务被分配时,发送给审批人
+    TASK_TIMEOUT("bpm_task_timeout"); // 任务审批超时时,发送给审批人
 
     /**
      * 短信模板的标识

+ 7 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.bpm.enums.task;
 
 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
@@ -15,6 +16,7 @@ import java.util.Arrays;
 @AllArgsConstructor
 public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
 
+    NOT_START(-1, "未开始"),
     RUNNING(1, "审批中"),
     APPROVE(2, "审批通过"),
     REJECT(3, "审批不通过"),
@@ -36,4 +38,9 @@ public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
+    public static boolean isProcessEndStatus(Integer status) {
+        return ObjectUtils.equalsAny(status,
+                APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus());
+    }
+
 }

+ 12 - 8
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmDeleteReasonEnum.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java

@@ -5,13 +5,13 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 
 /**
- * 流程实例/任务的删除原因枚举
+ * 流程实例/任务的的处理原因枚举
  *
  * @author 芋道源码
  */
 @Getter
 @AllArgsConstructor
-public enum BpmDeleteReasonEnum {
+public enum BpmReasonEnum {
 
     // ========== 流程实例的独有原因 ==========
 
@@ -22,6 +22,16 @@ public enum BpmDeleteReasonEnum {
     // ========== 流程任务的独有原因 ==========
 
     CANCEL_BY_SYSTEM("系统自动取消"), // 场景:非常多,比如说:1)多任务审批已经满足条件,无需审批该任务;2)流程实例被取消,无需审批该任务;等等
+    TIMEOUT_APPROVE("审批超时,系统自动通过"),
+    TIMEOUT_REJECT("审批超时,系统自动不通过"),
+    ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"),
+    ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"),
+    ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"),
+    ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"),
+    ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"),
+    ASSIGN_EMPTY_REJECT("审批人为空,自动不通过"),
+    APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"),
+    APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"),
     ;
 
     private final String reason;
@@ -36,10 +46,4 @@ public enum BpmDeleteReasonEnum {
         return StrUtil.format(reason, args);
     }
 
-    // ========== 逻辑 ==========
-
-    public static boolean isRejectReason(String reason) {
-        return StrUtil.startWith(reason, "审批不通过任务,原因:");
-    }
-
 }

+ 1 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java

@@ -13,6 +13,7 @@ import lombok.Getter;
 @AllArgsConstructor
 public enum BpmTaskStatusEnum {
 
+    NOT_START(-1, "未开始"),
     RUNNING(1, "审批中"),
     APPROVE(2, "审批通过"),
     REJECT(3, "审批不通过"),

+ 4 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 基础包,放一些通用的 VO 类
+ */
+package cn.iocoder.yudao.module.bpm.controller.admin.base;

+ 19 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.base.user;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "用户精简信息 VO")
+@Data
+public class UserSimpleBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long id;
+
+    @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    private String nickname;
+
+    @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
+    private String avatar;
+
+}

+ 53 - 28
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java

@@ -4,10 +4,9 @@ import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
-import cn.iocoder.yudao.framework.common.util.io.IoUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
 import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
@@ -15,10 +14,13 @@ import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
-import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.Model;
 import org.flowable.engine.repository.ProcessDefinition;
@@ -26,17 +28,15 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
-import java.io.IOException;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 流程模型")
 @RestController
@@ -53,6 +53,9 @@ public class BpmModelController {
     @Resource
     private BpmProcessDefinitionService processDefinitionService;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @GetMapping("/page")
     @Operation(summary = "获得模型分页")
     public CommonResult<PageResult<BpmModelRespVO>> getModelPage(BpmModelPageReqVO pageVO) {
@@ -64,7 +67,7 @@ public class BpmModelController {
         // 拼接数据
         // 获得 Form 表单
         Set<Long> formIds = convertSet(pageResult.getList(), model -> {
-            BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
+            BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
             return metaInfo != null ? metaInfo.getFormId() : null;
         });
         Map<Long, BpmFormDO> formMap = formService.getFormMap(formIds);
@@ -78,7 +81,14 @@ public class BpmModelController {
         // 获得 ProcessDefinition Map
         List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);
         Map<String, ProcessDefinition> processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId);
-        return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult, formMap, categoryMap, deploymentMap, processDefinitionMap));
+        // 获得 User Map
+        Set<Long> userIds = convertSetByFlatMap(pageResult.getList(), model -> {
+            BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
+            return metaInfo != null ? metaInfo.getStartUserIds().stream() : Stream.empty();
+        });
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+        return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult,
+                formMap, categoryMap, deploymentMap, processDefinitionMap, userMap));
     }
 
     @GetMapping("/get")
@@ -97,34 +107,24 @@ public class BpmModelController {
     @PostMapping("/create")
     @Operation(summary = "新建模型")
     @PreAuthorize("@ss.hasPermission('bpm:model:create')")
-    public CommonResult<String> createModel(@Valid @RequestBody BpmModelCreateReqVO createRetVO) {
-        return success(modelService.createModel(createRetVO, null));
+    public CommonResult<String> createModel(@Valid @RequestBody BpmModelSaveReqVO createRetVO) {
+        return success(modelService.createModel(createRetVO));
     }
 
     @PutMapping("/update")
     @Operation(summary = "修改模型")
     @PreAuthorize("@ss.hasPermission('bpm:model:update')")
-    public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelUpdateReqVO modelVO) {
-        modelService.updateModel(modelVO);
+    public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelSaveReqVO modelVO) {
+        modelService.updateModel(getLoginUserId(), modelVO);
         return success(true);
     }
 
-    @PostMapping("/import")
-    @Operation(summary = "导入模型")
-    @PreAuthorize("@ss.hasPermission('bpm:model:import')")
-    public CommonResult<String> importModel(@Valid BpmModeImportReqVO importReqVO) throws IOException {
-        BpmModelCreateReqVO createReqVO = BeanUtils.toBean(importReqVO, BpmModelCreateReqVO.class);
-        // 读取文件
-        String bpmnXml = IoUtils.readUtf8(importReqVO.getBpmnFile().getInputStream(), false);
-        return success(modelService.createModel(createReqVO, bpmnXml));
-    }
-
     @PostMapping("/deploy")
     @Operation(summary = "部署模型")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('bpm:model:deploy')")
     public CommonResult<Boolean> deployModel(@RequestParam("id") String id) {
-        modelService.deployModel(id);
+        modelService.deployModel(getLoginUserId(), id);
         return success(true);
     }
 
@@ -132,7 +132,15 @@ public class BpmModelController {
     @Operation(summary = "修改模型的状态", description = "实际更新的部署的流程定义的状态")
     @PreAuthorize("@ss.hasPermission('bpm:model:update')")
     public CommonResult<Boolean> updateModelState(@Valid @RequestBody BpmModelUpdateStateReqVO reqVO) {
-        modelService.updateModelState(reqVO.getId(), reqVO.getState());
+        modelService.updateModelState(getLoginUserId(), reqVO.getId(), reqVO.getState());
+        return success(true);
+    }
+
+    @PutMapping("/update-bpmn")
+    @Operation(summary = "修改模型的 BPMN")
+    @PreAuthorize("@ss.hasPermission('bpm:model:update')")
+    public CommonResult<Boolean> updateModelBpmn(@Valid @RequestBody BpmModeUpdateBpmnReqVO reqVO) {
+        modelService.updateModelBpmnXml(reqVO.getId(), reqVO.getBpmnXml());
         return success(true);
     }
 
@@ -141,8 +149,25 @@ public class BpmModelController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('bpm:model:delete')")
     public CommonResult<Boolean> deleteModel(@RequestParam("id") String id) {
-        modelService.deleteModel(id);
+        modelService.deleteModel(getLoginUserId(), id);
         return success(true);
     }
 
+    // ========== 仿钉钉/飞书的精简模型 =========
+
+    @GetMapping("/simple/get")
+    @Operation(summary = "获得仿钉钉流程设计模型")
+    @Parameter(name = "modelId", description = "流程模型编号", required = true, example = "a2c5eee0-eb6c-11ee-abf4-0c37967c420a")
+    public CommonResult<BpmSimpleModelNodeVO> getSimpleModel(@RequestParam("id") String modelId){
+        return success(modelService.getSimpleModel(modelId));
+    }
+
+    @PostMapping("/simple/update")
+    @Operation(summary = "保存仿钉钉流程设计模型")
+    @PreAuthorize("@ss.hasPermission('bpm:model:update')")
+    public CommonResult<Boolean> updateSimpleModel(@Valid @RequestBody BpmSimpleModelUpdateReqVO reqVO) {
+        modelService.updateSimpleModel(getLoginUserId(), reqVO);
+        return success(Boolean.TRUE);
+    }
+
 }

+ 12 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java

@@ -34,6 +34,7 @@ import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 流程定义")
 @RestController
@@ -79,14 +80,23 @@ public class BpmProcessDefinitionController {
     @Parameter(name = "suspensionState", description = "挂起状态", required = true, example = "1") // 参见 Flowable SuspensionState 枚举
     public CommonResult<List<BpmProcessDefinitionRespVO>> getProcessDefinitionList(
             @RequestParam("suspensionState") Integer suspensionState) {
+        // 1.1 获得开启的流程定义
         List<ProcessDefinition> list = processDefinitionService.getProcessDefinitionListBySuspensionState(suspensionState);
         if (CollUtil.isEmpty(list)) {
             return success(Collections.emptyList());
         }
-
-        // 获得 BpmProcessDefinitionInfoDO Map
+        // 1.2 移除不可见的流程定义
         Map<String, BpmProcessDefinitionInfoDO> processDefinitionMap = processDefinitionService.getProcessDefinitionInfoMap(
                 convertSet(list, ProcessDefinition::getId));
+        Long userId = getLoginUserId();
+        list.removeIf(processDefinition -> {
+            BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionMap.get(processDefinition.getId());
+            return processDefinitionInfo == null // 不存在
+                    || Boolean.FALSE.equals(processDefinitionInfo.getVisible()) // visible 不可见
+                    || !processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId); // 无权限发起
+        });
+
+        // 2. 拼接 VO 返回
         return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinitionList(
                 list, null, processDefinitionMap, null, null));
     }

+ 0 - 19
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java

@@ -1,19 +0,0 @@
-package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-import org.springframework.web.multipart.MultipartFile;
-
-import javax.validation.constraints.NotNull;
-
-@Schema(description = "管理后台 - 流程模型的导入 Request VO 相比流程模型的新建来说,只是多了一个 bpmnFile 文件")
-@Data
-public class BpmModeImportReqVO extends BpmModelCreateReqVO {
-
-    @Schema(description = "BPMN 文件", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "BPMN 文件不能为空")
-    private MultipartFile bpmnFile;
-
-}

+ 19 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 流程模型的更新 BPMN XML Request VO")
+@Data
+public class BpmModeUpdateBpmnReqVO {
+
+    @Schema(description = "流程编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotEmpty(message = "流程编号不能为空")
+    private String id;
+
+    @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "BPMN XML 不能为空")
+    private String bpmnXml;
+
+}

+ 62 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.hibernate.validator.constraints.URL;
+
+import java.util.List;
+
+/**
+ * BPM 流程 MetaInfo Response DTO
+ * 主要用于 { Model#setMetaInfo(String)} 的存储
+ *
+ * 最终,它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的
+ *
+ * @author 芋道源码
+ */
+@Data
+public class BpmModelMetaInfoVO {
+
+    @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
+    @NotEmpty(message = "流程图标不能为空")
+    @URL(message = "流程图标格式不正确")
+    private String icon;
+
+    @Schema(description = "流程描述", example = "我是描述")
+    private String description;
+
+    @Schema(description = "流程类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @InEnum(BpmModelTypeEnum.class)
+    @NotNull(message = "流程类型不能为空")
+    private Integer type;
+
+    @Schema(description = "表单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @InEnum(BpmModelFormTypeEnum.class)
+    @NotNull(message = "表单类型不能为空")
+    private Integer formType;
+    @Schema(description = "表单编号", example = "1024")
+    private Long formId;  // formType 为 NORMAL 使用,必须非空
+    @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址",
+            example = "/bpm/oa/leave/create")
+    private String formCustomCreatePath;  // 表单类型为 CUSTOM 时,必须非空
+    @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址",
+            example = "/bpm/oa/leave/view")
+    private String formCustomViewPath;  // 表单类型为 CUSTOM 时,必须非空
+
+    @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否可见不能为空")
+    private Boolean visible;
+
+    @Schema(description = "可发起用户编号数组", example = "[1,2,3]")
+    private List<Long> startUserIds;
+
+    @Schema(description = "可管理用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2,4,6]")
+    @NotEmpty(message = "可管理用户编号数组不能为空")
+    private List<Long> managerUserIds;
+
+}

+ 6 - 14
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java

@@ -1,14 +1,16 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
 
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 @Schema(description = "管理后台 - 流程模型 Response VO")
 @Data
-public class BpmModelRespVO {
+public class BpmModelRespVO extends BpmModelMetaInfoVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private String id;
@@ -22,33 +24,23 @@ public class BpmModelRespVO {
     @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
     private String icon;
 
-    @Schema(description = "流程描述", example = "我是描述")
-    private String description;
-
     @Schema(description = "流程分类编码", example = "1")
     private String category;
     @Schema(description = "流程分类名字", example = "请假")
     private String categoryName;
 
-    @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
-    private Integer formType;
-
-    @Schema(description = "表单编号", example = "1024")
-    private Long formId; // 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空
     @Schema(description = "表单名字", example = "请假表单")
     private String formName;
 
-    @Schema(description = "自定义表单的提交路径", example = "/bpm/oa/leave/create")
-    private String formCustomCreatePath; // 使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空
-    @Schema(description = "自定义表单的查看路径", example = "/bpm/oa/leave/view")
-    private String formCustomViewPath; // ,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空
-
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
     @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
     private String bpmnXml;
 
+    @Schema(description = "可发起的用户数组")
+    private List<UserSimpleBaseVO> startUsers;
+
     /**
      * 最新部署的流程定义
      */

+ 8 - 8
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelCreateReqVO.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java

@@ -1,15 +1,15 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
 import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
 
-import javax.validation.constraints.NotEmpty;
-
-@Schema(description = "管理后台 - 流程模型的创建 Request VO")
+@Schema(description = "管理后台 - 流程模型的保存 Request VO")
 @Data
-public class BpmModelCreateReqVO {
+public class BpmModelSaveReqVO extends BpmModelMetaInfoVO {
+
+    @Schema(description = "编号", example = "1024")
+    private String id;
 
     @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yudao")
     @NotEmpty(message = "流程标识不能为空")
@@ -19,7 +19,7 @@ public class BpmModelCreateReqVO {
     @NotEmpty(message = "流程名称不能为空")
     private String name;
 
-    @Schema(description = "流程描述", example = "我是描述")
-    private String description;
+    @Schema(description = "流程分类", example = "1")
+    private String category;
 
 }

+ 0 - 47
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java

@@ -1,47 +0,0 @@
-package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
-
-import cn.iocoder.yudao.framework.common.validation.InEnum;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import org.hibernate.validator.constraints.URL;
-
-import javax.validation.constraints.NotEmpty;
-
-@Schema(description = "管理后台 - 流程模型的更新 Request VO")
-@Data
-public class BpmModelUpdateReqVO {
-
-    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
-    @NotEmpty(message = "编号不能为空")
-    private String id;
-
-    @Schema(description = "流程名称", example = "芋道")
-    private String name;
-
-    @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
-    @URL(message = "流程图标格式不正确")
-    private String icon;
-
-    @Schema(description = "流程描述", example = "我是描述")
-    private String description;
-
-    @Schema(description = "流程分类", example = "1")
-    private String category;
-
-    @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
-    private String bpmnXml;
-
-    @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
-    @InEnum(BpmModelFormTypeEnum.class)
-    private Integer formType;
-    @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024")
-    private Long formId;
-    @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空",
-            example = "/bpm/oa/leave/create")
-    private String formCustomCreatePath;
-    @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空",
-            example = "/bpm/oa/leave/view")
-    private String formCustomViewPath;
-
-}

+ 220 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java

@@ -0,0 +1,220 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.*;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO")
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class BpmSimpleModelNodeVO {
+
+    @Schema(description = "模型节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartEvent_1")
+    @NotEmpty(message = "模型节点编号不能为空")
+    private String id;
+
+    @Schema(description = "模型节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "模型节点类型不能为空")
+    @InEnum(BpmSimpleModelNodeType.class)
+    private Integer type;
+
+    @Schema(description = "模型节点名称", example = "领导审批")
+    private String name;
+
+    // TODO @jason:和 gpt 大模型对了下这个字段的命名,貌似叫 displayText 合适点。可以等最后我们全局替换下。(优先级:低)
+    @Schema(description = "节点展示内容", example = "指定成员: 芋道源码")
+    private String showText;
+
+    @Schema(description = "子节点")
+    private BpmSimpleModelNodeVO childNode; // 补充说明:在该模型下,子节点有且仅有一个,不会有多个
+
+    @Schema(description = "条件节点")
+    private List<BpmSimpleModelNodeVO> conditionNodes; // 补充说明:有且仅有条件、并行、包容等分支会使用
+
+    @Schema(description = "条件类型", example = "1")
+    @InEnum(BpmSimpleModeConditionType.class)
+    private Integer conditionType; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
+
+    @Schema(description = "条件表达式", example = "${day>3}")
+    private String conditionExpression; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
+
+    @Schema(description = "是否默认条件", example = "true")
+    private Boolean defaultFlow; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
+    /**
+     * 条件组
+     */
+    private ConditionGroups conditionGroups; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
+
+    @Schema(description = "候选人策略", example = "30")
+    @InEnum(BpmTaskCandidateStrategyEnum.class)
+    private Integer candidateStrategy; // 用于审批,抄送节点
+
+    @Schema(description = "候选人参数")
+    private String candidateParam; // 用于审批,抄送节点
+
+    @Schema(description = "审批节点类型", example = "1")
+    @InEnum(BpmUserTaskApproveTypeEnum.class)
+    private Integer approveType; // 用于审批节点
+
+    @Schema(description = "多人审批方式", example = "1")
+    @InEnum(BpmUserTaskApproveMethodEnum.class)
+    private Integer approveMethod; // 用于审批节点
+
+    @Schema(description = "通过比例", example = "100")
+    private Integer approveRatio; // 通过比例,当多人审批方式为:多人会签(按通过比例) 需要设置
+
+    @Schema(description = "表单权限", example = "[]")
+    private List<Map<String, String>> fieldsPermission;
+
+    @Schema(description = "操作按钮设置", example = "[]")
+    private List<OperationButtonSetting> buttonsSetting;  // 用于审批节点
+
+    // TODO @jason:看看是不是可以简化;@芋艿: 暂时先放着。不知道后面是否会用到
+    /**
+     * 附加节点 Id, 该节点不从前端传入。 由程序生成. 由于当个节点无法完成功能。 需要附加节点来完成。
+     */
+    @JsonIgnore
+    private String attachNodeId;
+    /**
+     * 审批节点拒绝处理
+     */
+    private RejectHandler rejectHandler;
+
+    /**
+     * 审批节点超时处理
+     */
+    private TimeoutHandler timeoutHandler;
+
+    @Schema(description = "审批节点的审批人与发起人相同时,对应的处理类型", example = "1")
+    @InEnum(BpmUserTaskAssignStartUserHandlerTypeEnum.class)
+    private Integer assignStartUserHandlerType;
+
+    /**
+     * 空处理策略
+     */
+    private AssignEmptyHandler assignEmptyHandler;
+
+    @Schema(description = "审批节点拒绝处理策略")
+    @Data
+    public static class RejectHandler {
+
+        @Schema(description = "拒绝处理类型", example = "1")
+        @InEnum(BpmUserTaskRejectHandlerType.class)
+        private Integer type;
+
+        @Schema(description = "任务拒绝后驳回的节点 Id", example = "Activity_1")
+        private String returnNodeId;
+    }
+
+    @Schema(description = "审批节点超时处理策略")
+    @Valid
+    @Data
+    public static class TimeoutHandler {
+
+        @Schema(description = "是否开启超时处理", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+        @NotNull(message = "是否开启超时处理不能为空")
+        private Boolean enable;
+
+        @Schema(description = "任务超时未处理的行为", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        @NotNull(message = "任务超时未处理的行为不能为空")
+        @InEnum(BpmUserTaskTimeoutHandlerTypeEnum.class)
+        private Integer type;
+
+        @Schema(description = "超时时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "PT6H")
+        @NotEmpty(message = "超时时间不能为空")
+        private String timeDuration;
+
+        @Schema(description = "最大提醒次数", example = "1")
+        private Integer maxRemindCount;
+
+    }
+
+    @Schema(description = "空处理策略")
+    @Data
+    @Valid
+    public static class AssignEmptyHandler {
+
+        @Schema(description = "空处理类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        @NotNull(message = "空处理类型不能为空")
+        @InEnum(BpmUserTaskAssignEmptyHandlerTypeEnum.class)
+        private Integer type;
+
+        @Schema(description = "指定人员审批的用户编号数组", example = "1")
+        private List<Long> userIds;
+
+    }
+
+    @Schema(description = "操作按钮设置")
+    @Data
+    @Valid
+    public static class OperationButtonSetting {
+
+        // TODO @jason:是不是按钮的标识?id 会和数据库的 id 自增有点模糊,key 标识会更合理一点点哈。
+        @Schema(description = "按钮 Id", example = "1")
+        private Integer id;
+
+        @Schema(description = "显示名称", example = "审批")
+        private String displayName;
+
+        @Schema(description = "是否启用", example = "true")
+        private Boolean enable;
+    }
+
+    @Schema(description = "条件组")
+    @Data
+    @Valid
+    public static class ConditionGroups {
+
+        @Schema(description = "条件组下的条件关系是否为与关系", example = "true")
+        @NotNull(message = "条件关系不能为空")
+        private Boolean and;
+
+        @Schema(description = "条件组下的条件", example = "[]")
+        @NotEmpty(message = "条件不能为空")
+        private List<Condition> conditions;
+    }
+
+    @Schema(description = "条件")
+    @Data
+    @Valid
+    public static class Condition {
+
+        @Schema(description = "条件下的规则关系是否为与关系", example = "true")
+        @NotNull(message = "规则关系不能为空")
+        private Boolean and;
+
+        @Schema(description = "条件下的规则", example = "[]")
+        @NotEmpty(message = "规则不能为空")
+        private List<ConditionRule> rules;
+    }
+
+    @Schema(description = "条件规则")
+    @Data
+    @Valid
+    public static class ConditionRule {
+
+        @Schema(description = "运行符号", example = "==")
+        @NotEmpty(message = "运行符号不能为空")
+        private String opCode;
+
+        @Schema(description = "运算符左边的值,例如某个流程变量", example = "startUserId")
+        @NotEmpty(message = "运算符左边的值不能为空")
+        private String leftSide;
+
+        @Schema(description = "运算符右边的值", example = "1")
+        @NotEmpty(message = "运算符右边的值不能为空")
+        private String rightSide;
+    }
+
+    // TODO @芋艿:条件;建议可以固化的一些选项;然后有个表达式兜底;要支持
+}

+ 23 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelUpdateReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+// TODO @jason:需要考虑,如果某个节点的配置不正确,需要有提示;具体怎么实现,可以讨论下;
+@Schema(description = "管理后台 - 仿钉钉流程设计模型的新增/修改 Request VO")
+@Data
+public class BpmSimpleModelUpdateReqVO {
+
+    @Schema(description = "流程模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotEmpty(message = "流程模型编号不能为空")
+    private String id; // 对应 Flowable act_re_model 表 ID_ 字段
+
+    @Schema(description = "仿钉钉流程设计模型对象", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "仿钉钉流程设计模型对象不能为空")
+    @Valid
+    private BpmSimpleModelNodeVO simpleModel;
+
+}

+ 2 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
+import cn.iocoder.yudao.module.bpm.convert.task.BpmActivityConvert;
 import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -34,6 +35,6 @@ public class BpmActivityController {
     @PreAuthorize("@ss.hasPermission('bpm:task:query')")
     public CommonResult<List<BpmActivityRespVO>> getActivityList(
             @RequestParam("processInstanceId") String processInstanceId) {
-        return success(activityService.getActivityListByProcessInstanceId(processInstanceId));
+        return success(BpmActivityConvert.INSTANCE.convertList(activityService.getActivityListByProcessInstanceId(processInstanceId)));
     }
 }

+ 17 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java

@@ -4,10 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
@@ -160,4 +157,20 @@ public class BpmProcessInstanceController {
         return success(true);
     }
 
+    @GetMapping("/get-form-fields-permission")
+    @Operation(summary = "获得表单字段权限")
+    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
+    public CommonResult<Map<String, String>> getFormFieldsPermission(
+            @Valid BpmFormFieldsPermissionReqVO reqVO) {
+        return success(processInstanceService.getFormFieldsPermission(reqVO));
+    }
+
+    @GetMapping("/get-approval-detail")
+    @Operation(summary = "获得审批详情")
+    @Parameter(name = "id", description = "流程实例的编号", required = true)
+    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
+    public CommonResult<BpmApprovalDetailRespVO> getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) {
+        return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
+    }
+
 }

+ 0 - 6
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java

@@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
-import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -43,8 +42,6 @@ public class BpmProcessInstanceCopyController {
     private BpmProcessInstanceCopyService processInstanceCopyService;
     @Resource
     private BpmProcessInstanceService processInstanceService;
-    @Resource
-    private BpmTaskService taskService;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -61,8 +58,6 @@ public class BpmProcessInstanceCopyController {
         }
 
         // 拼接返回
-        Map<String, String> taskNameMap = taskService.getTaskNameByTaskIds(
-                convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getTaskId));
         Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
                 convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
@@ -70,7 +65,6 @@ public class BpmProcessInstanceCopyController {
         return success(BeanUtils.toBean(pageResult, BpmProcessInstanceCopyRespVO.class, copyVO -> {
             MapUtils.findAndThen(userMap, Long.valueOf(copyVO.getCreator()), user -> copyVO.setCreatorName(user.getNickname()));
             MapUtils.findAndThen(userMap, copyVO.getStartUserId(), user -> copyVO.setStartUserName(user.getNickname()));
-            MapUtils.findAndThen(taskNameMap, copyVO.getTaskId(), copyVO::setTaskName);
             MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
                     processInstance -> copyVO.setProcessInstanceStartTime(DateUtils.of(processInstance.getStartTime())));
         }));

+ 9 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -17,6 +18,9 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
@@ -26,8 +30,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -50,6 +52,8 @@ public class BpmTaskController {
     private BpmProcessInstanceService processInstanceService;
     @Resource
     private BpmFormService formService;
+    @Resource
+    private BpmProcessDefinitionService bpmProcessDefinitionService;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -134,8 +138,10 @@ public class BpmTaskController {
         // 获得 Form Map
         Map<Long, BpmFormDO> formMap = formService.getFormMap(
                 convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey())));
+        // 获得 BpmnModel
+        BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId());
         return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, processInstance,
-                formMap, userMap, deptMap));
+                formMap, userMap, deptMap, bpmnModel));
     }
 
     @PutMapping("/approve")

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java

@@ -24,6 +24,8 @@ public class BpmProcessInstanceCopyRespVO {
     @Schema(description = "流程实例的发起时间")
     private LocalDateTime processInstanceStartTime;
 
+    @Schema(description = "抄送的节点的活动编号")
+    private String activityId;
     @Schema(description = "发起抄送的任务编号")
     private String taskId;
     @Schema(description = "发起抄送的任务名称")

+ 26 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
+import lombok.Data;
+
+// TODO @jason:这个可以简化下,使用 @RequestParam。嘿嘿,主要 VO 项不要太多
+@Schema(description = "管理后台 - 审批详情 Request VO")
+@Data
+public class BpmApprovalDetailReqVO {
+
+    @Schema(description = "流程定义的编号", example = "1024")
+    private String processDefinitionId;
+
+    @Schema(description = "流程实例的编号", example = "1024")
+    private String processInstanceId;
+
+    @AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空")
+    @JsonIgnore
+    public boolean isValidProcessParam() {
+        return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId);
+    }
+
+}

+ 87 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java

@@ -0,0 +1,87 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+@Schema(description = "管理后台 - 审批详情 Response VO")
+@Data
+public class BpmApprovalDetailRespVO {
+
+    @Schema(description = "流程实例的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status; // 参见 BpmProcessInstanceStatusEnum 枚举
+
+    @Schema(description = "审批信息列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<ApprovalNodeInfo> approveNodes;
+
+    @Schema(description = "审批节点信息")
+    @Data
+    public static class ApprovalNodeInfo {
+
+        @Schema(description = "节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartUserNode")
+        private String id;
+
+        @Schema(description = "节点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "发起人")
+        private String name;
+
+        @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer nodeType; // 参见 BpmSimpleModelNodeType 枚举
+
+        @Schema(description = "节点状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+        private Integer status; // 参见 BpmTaskStatusEnum 枚举
+
+        @Schema(description = "节点的开始时间")
+        private LocalDateTime startTime;
+        @Schema(description = "节点的结束时间")
+        private LocalDateTime endTime;
+
+        @Schema(description = "审批节点的任务信息")
+        private List<ApprovalTaskInfo> tasks;
+
+        @Schema(description = "候选人用户列表")
+        // TODO @jason:candidateUserList => candidateUsers,保持和 tasks 的命名风格一致哈
+        private List<User> candidateUserList; // 用于未运行任务节点
+
+    }
+
+    // TODO @jason:可以替换成 UserSimpleBaseVO。简化下
+    @Schema(description = "用户信息")
+    @Data
+    public static class User {
+
+        @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Long id;
+
+        @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+        private String nickname;
+
+        @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
+        private String avatar;
+
+    }
+
+    @Schema(description = "审批任务信息")
+    @Data
+    public static class ApprovalTaskInfo {
+
+        @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private String id;
+
+        @Schema(description = "任务所属人", example = "1024")
+        private User ownerUser;
+
+        @Schema(description = "任务分配人", example = "2048")
+        private User assigneeUser;
+
+        @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer status;  // 参见 BpmTaskStatusEnum 枚举
+
+        @Schema(description = "审批意见", example = "同意")
+        private String reason;
+
+    }
+
+}

+ 37 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 表单字段权限 Request VO")
+@Data
+public class BpmFormFieldsPermissionReqVO {
+
+    @Schema(description = "流程定义的编号", example = "1024")
+    private String processDefinitionId;
+
+    @Schema(description = "流程实例的编号", example = "1024")
+    private String processInstanceId;
+
+    @Schema(description = "流程活动编号",  example = "StartUserNode")
+    private String activityId; // 对应 BPMN XML 节点 Id
+
+    @Schema(description = "流程任务编号", example = "95f2f08b-621b-11ef-bf39-00ff4722db8b")
+    private String taskId; // UserTask 对应的Id
+
+    @AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空")
+    @JsonIgnore
+    public boolean isValidProcessParam() {
+        return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId);
+    }
+
+    @AssertTrue(message = "流程活动编号和流程任务编号编号不能同时为空")
+    @JsonIgnore
+    public boolean isValidActivityParam() {
+        return StrUtil.isNotEmpty(activityId) || StrUtil.isNotEmpty(taskId);
+    }
+
+}

+ 17 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java

@@ -61,12 +61,17 @@ public class BpmTaskRespVO {
     private Long formId;
     @Schema(description = "表单名字", example = "请假表单")
     private String formName;
-    @Schema(description = "表单的配置-JSON 字符串")
+    @Schema(description = "表单的配置JSON 字符串")
     private String formConf;
     @Schema(description = "表单项的数组")
     private List<String> formFields;
     @Schema(description = "提交的表单值", requiredMode = Schema.RequiredMode.REQUIRED)
     private Map<String, Object> formVariables;
+    // @芋艿 都改成了 fieldsPermission。 buttonsSetting。和 BpmSimpleModelNodeVO 统一
+    @Schema(description = "表单字段权限值")
+    private Map<String, String> fieldsPermission;
+    @Schema(description = "操作按钮设置值")
+    private Map<Integer, OperationButtonSetting> buttonsSetting;
 
     @Data
     @Schema(description = "流程实例")
@@ -91,4 +96,15 @@ public class BpmTaskRespVO {
 
     }
 
+    @Data
+    @Schema(description = "操作按钮设置")
+    public static class OperationButtonSetting {
+
+        @Schema(description = "显示名称", example = "审批")
+        private String displayName;
+
+        @Schema(description = "是否启用", example = "true")
+        private Boolean enable;
+    }
+
 }

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

@@ -1,20 +1,19 @@
 package cn.iocoder.yudao.module.bpm.convert.definition;
 
 import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelCreateReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelRespVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelUpdateReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
 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.system.api.user.dto.AdminUserRespDTO;
 import org.flowable.common.engine.impl.db.SuspensionState;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.Model;
@@ -22,9 +21,11 @@ import org.flowable.engine.repository.ProcessDefinition;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
 /**
  * 流程模型 Convert
@@ -39,22 +40,24 @@ public interface BpmModelConvert {
     default PageResult<BpmModelRespVO> buildModelPage(PageResult<Model> pageResult,
                                                       Map<Long, BpmFormDO> formMap,
                                                       Map<String, BpmCategoryDO> categoryMap, Map<String, Deployment> deploymentMap,
-                                                      Map<String, ProcessDefinition> processDefinitionMap) {
-        List<BpmModelRespVO> list = CollectionUtils.convertList(pageResult.getList(), model -> {
-            BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model);
+                                                      Map<String, ProcessDefinition> processDefinitionMap,
+                                                      Map<Long, AdminUserRespDTO> userMap) {
+        List<BpmModelRespVO> list = convertList(pageResult.getList(), model -> {
+            BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
             BpmFormDO form = metaInfo != null ? formMap.get(metaInfo.getFormId()) : null;
             BpmCategoryDO category = categoryMap.get(model.getCategory());
             Deployment deployment = model.getDeploymentId() != null ? deploymentMap.get(model.getDeploymentId()) : null;
             ProcessDefinition processDefinition = model.getDeploymentId() != null ? processDefinitionMap.get(model.getDeploymentId()) : null;
-            return buildModel0(model, metaInfo, form, category, deployment, processDefinition);
+            List<AdminUserRespDTO> startUsers = metaInfo != null ? convertList(metaInfo.getStartUserIds(), userMap::get) : null;
+            return buildModel0(model, metaInfo, form, category, deployment, processDefinition, startUsers);
         });
         return new PageResult<>(list, pageResult.getTotal());
     }
 
     default BpmModelRespVO buildModel(Model model,
                                      byte[] bpmnBytes) {
-        BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model);
-        BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null);
+        BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
+        BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null, null);
         if (ArrayUtil.isNotEmpty(bpmnBytes)) {
             modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes));
         }
@@ -62,20 +65,16 @@ public interface BpmModelConvert {
     }
 
     default BpmModelRespVO buildModel0(Model model,
-                                      BpmModelMetaInfoRespDTO metaInfo, BpmFormDO form, BpmCategoryDO category,
-                                      Deployment deployment, ProcessDefinition processDefinition) {
+                                       BpmModelMetaInfoVO metaInfo, BpmFormDO form, BpmCategoryDO category,
+                                       Deployment deployment, ProcessDefinition processDefinition,
+                                       List<AdminUserRespDTO> startUsers) {
         BpmModelRespVO modelRespVO = new BpmModelRespVO().setId(model.getId()).setName(model.getName())
                 .setKey(model.getKey()).setCategory(model.getCategory())
                 .setCreateTime(DateUtils.of(model.getCreateTime()));
         // Form
-        if (metaInfo != null) {
-            modelRespVO.setFormType(metaInfo.getFormType()).setFormId(metaInfo.getFormId())
-                    .setFormCustomCreatePath(metaInfo.getFormCustomCreatePath())
-                    .setFormCustomViewPath(metaInfo.getFormCustomViewPath());
-            modelRespVO.setIcon(metaInfo.getIcon()).setDescription(metaInfo.getDescription());
-        }
+        BeanUtils.copyProperties(metaInfo, modelRespVO);
         if (form != null) {
-            modelRespVO.setFormId(form.getId()).setFormName(form.getName());
+            modelRespVO.setFormName(form.getName());
         }
         // Category
         if (category != null) {
@@ -90,49 +89,30 @@ public interface BpmModelConvert {
                 modelRespVO.getProcessDefinition().setDeploymentTime(DateUtils.of(deployment.getDeploymentTime()));
             }
         }
+        // User
+        modelRespVO.setStartUsers(BeanUtils.toBean(startUsers, UserSimpleBaseVO.class));
         return modelRespVO;
     }
 
-    default void copyToCreateModel(Model model, BpmModelCreateReqVO bean) {
-        model.setName(bean.getName());
-        model.setKey(bean.getKey());
-        model.setMetaInfo(buildMetaInfoStr(null,
-                null, bean.getDescription(),
-                null, null, null, null));
-    }
-
-    default void copyToUpdateModel(Model model, BpmModelUpdateReqVO bean) {
-        model.setName(bean.getName());
-        model.setCategory(bean.getCategory());
-        model.setMetaInfo(buildMetaInfoStr(buildMetaInfo(model),
-                bean.getIcon(), bean.getDescription(),
-                bean.getFormType(), bean.getFormId(), bean.getFormCustomCreatePath(), bean.getFormCustomViewPath()));
+    default void copyToModel(Model model, BpmModelSaveReqVO reqVO) {
+        model.setName(reqVO.getName());
+        model.setKey(reqVO.getKey());
+        model.setCategory(reqVO.getCategory());
+        model.setMetaInfo(JsonUtils.toJsonString(BeanUtils.toBean(reqVO, BpmModelMetaInfoVO.class)));
     }
 
-    default String buildMetaInfoStr(BpmModelMetaInfoRespDTO metaInfo,
-                                    String icon, String description,
-                                    Integer formType, Long formId, String formCustomCreatePath, String formCustomViewPath) {
-        if (metaInfo == null) {
-            metaInfo = new BpmModelMetaInfoRespDTO();
+    default BpmModelMetaInfoVO parseMetaInfo(Model model) {
+        BpmModelMetaInfoVO vo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoVO.class);
+        if (vo == null) {
+            return null;
         }
-        // 只有非空,才进行设置,避免更新时的覆盖
-        if (StrUtil.isNotEmpty(icon)) {
-            metaInfo.setIcon(icon);
+        if (vo.getManagerUserIds() == null) {
+            vo.setManagerUserIds(Collections.emptyList());
         }
-        if (StrUtil.isNotEmpty(description)) {
-            metaInfo.setDescription(description);
+        if (vo.getStartUserIds() == null) {
+            vo.setStartUserIds(Collections.emptyList());
         }
-        if (Objects.nonNull(formType)) {
-            metaInfo.setFormType(formType);
-            metaInfo.setFormId(formId);
-            metaInfo.setFormCustomCreatePath(formCustomCreatePath);
-            metaInfo.setFormCustomViewPath(formCustomViewPath);
-        }
-        return JsonUtils.toJsonString(metaInfo);
-    }
-
-    default BpmModelMetaInfoRespDTO buildMetaInfo(Model model) {
-        return JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
+        return vo;
     }
 
 }

+ 0 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java

@@ -89,11 +89,6 @@ public interface BpmProcessInstanceConvert {
     @Mapping(source = "from.id", target = "to.id", ignore = true)
     void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to);
 
-    default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, HistoricProcessInstance instance, Integer status) {
-        return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status)
-                .setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey());
-    }
-
     default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) {;
         return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status)
                 .setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey());

+ 18 - 6
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java

@@ -9,10 +9,13 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.Task;
@@ -81,10 +84,12 @@ public interface BpmTaskConvert {
                                                                  HistoricProcessInstance processInstance,
                                                                  Map<Long, BpmFormDO> formMap,
                                                                  Map<Long, AdminUserRespDTO> userMap,
-                                                                 Map<Long, DeptRespDTO> deptMap) {
+                                                                 Map<Long, DeptRespDTO> deptMap,
+                                                                 BpmnModel bpmnModel) {
         List<BpmTaskRespVO> taskVOList = CollectionUtils.convertList(taskList, task -> {
             BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class);
-            taskVO.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task));
+            Integer taskStatus = FlowableUtils.getTaskStatus(task);
+            taskVO.setStatus(taskStatus).setReason(FlowableUtils.getTaskReason(task));
             // 流程实例
             AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
             taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
@@ -106,7 +111,15 @@ public interface BpmTaskConvert {
                 taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, BpmProcessInstanceRespVO.User.class));
                 findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName()));
             }
-            return taskVO;
+            if (BpmTaskStatusEnum.RUNNING.getStatus().equals(taskStatus)){
+                // 设置表单权限 TODO @芋艿 是不是还要加一个全局的权限 基于 processInstance 的权限;回复:可能不需要,但是发起人,需要有个权限配置
+                // TODO @jason:貌似这么返回,主要解决当前审批 task 的表单权限,但是不同抄送人的表单权限,可能不太对。例如说,对 A 抄送人是隐藏某个字段。
+                // @芋艿 表单权限需要分离开。单独的接口来获取了 BpmProcessInstanceService.getProcessInstanceFormFieldsPermission
+                taskVO.setFieldsPermission(BpmnModelUtils.parseFormFieldsPermission(bpmnModel, task.getTaskDefinitionKey()));
+                // 操作按钮设置
+                taskVO.setButtonsSetting(BpmnModelUtils.parseButtonsSetting(bpmnModel, task.getTaskDefinitionKey()));
+            }
+           return taskVO;
         });
 
         // 拼接父子关系
@@ -151,12 +164,12 @@ public interface BpmTaskConvert {
 
     /**
      * 将父任务的属性,拷贝到子任务(加签任务)
-     *
+     * <p>
      * 为什么不使用 mapstruct 映射?因为 TaskEntityImpl 还有很多其他属性,这里我们只设置我们需要的。
      * 使用 mapstruct 会将里面嵌套的各个属性值都设置进去,会出现意想不到的问题。
      *
      * @param parentTask 父任务
-     * @param childTask 加签任务
+     * @param childTask  加签任务
      */
     default void copyTo(TaskEntityImpl parentTask, TaskEntityImpl childTask) {
         childTask.setName(parentTask.getName());
@@ -165,7 +178,6 @@ public interface BpmTaskConvert {
         childTask.setParentTaskId(parentTask.getId());
         childTask.setProcessDefinitionId(parentTask.getProcessDefinitionId());
         childTask.setProcessInstanceId(parentTask.getProcessInstanceId());
-//        childTask.setExecutionId(parentTask.getExecutionId()); // TODO 芋艿:新加的,不太确定;尴尬,不加时,子任务不通过会失败(报错);加了,子任务审批通过会失败(报错)
         childTask.setTaskDefinitionKey(parentTask.getTaskDefinitionKey());
         childTask.setTaskDefinitionId(parentTask.getTaskDefinitionId());
         childTask.setPriority(parentTask.getPriority());

+ 59 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java

@@ -1,12 +1,20 @@
 package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 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.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.flowable.engine.repository.Model;
+import org.flowable.engine.repository.ProcessDefinition;
 
 import java.util.List;
 
@@ -31,15 +39,21 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
     /**
      * 流程定义的编号
      *
-     * 关联 ProcessDefinition 的 id 属性
+     * 关联 {@link ProcessDefinition#getId()} 属性
      */
     private String processDefinitionId;
     /**
      * 流程模型的编号
      *
-     * 关联 Model 的 id 属性
+     * 关联 {@link Model#getId()} 属性
      */
     private String modelId;
+    /**
+     * 流程模型的类型
+     *
+     * 枚举 {@link BpmModelTypeEnum}
+     */
+    private Integer modelType;
 
     /**
      * 图标
@@ -53,11 +67,12 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
     /**
      * 表单类型
      *
-     * 关联 {@link BpmModelFormTypeEnum}
+     * 枚举 {@link BpmModelFormTypeEnum}
      */
     private Integer formType;
     /**
      * 动态表单编号
+     *
      * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时
      *
      * 关联 {@link BpmFormDO#getId()}
@@ -65,6 +80,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
     private Long formId;
     /**
      * 表单的配置
+     *
      * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时
      *
      * 冗余 {@link BpmFormDO#getConf()}
@@ -72,21 +88,59 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
     private String formConf;
     /**
      * 表单项的数组
+     *
      * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时
      *
-     * 冗余 {@link BpmFormDO#getFields()} ()}
+     * 冗余 {@link BpmFormDO#getFields()}
      */
     @TableField(typeHandler = JacksonTypeHandler.class)
     private List<String> formFields;
     /**
      * 自定义表单的提交路径,使用 Vue 的路由地址
+     *
      * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时
      */
     private String formCustomCreatePath;
     /**
      * 自定义表单的查看路径,使用 Vue 的路由地址
+     *
      * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时
      */
     private String formCustomViewPath;
 
+    /**
+     * SIMPLE 设计器模型数据 json 格式
+     *
+     * 目的:当使用仿钉钉设计器时。流程模型发布的时候,需要保存流程模型设计器的快照数据。
+     */
+    private String simpleModel;
+    /**
+     * 是否可见
+     *
+     * 目的:如果 false 不可见,则不展示在“发起流程”的列表里
+     */
+    private Boolean visible;
+
+    /**
+     * 可发起用户编号数组
+     *
+     * 关联 {@link AdminUserRespDTO#getId()} 字段的数组
+     *
+     * 如果为空,则表示“全部可以发起”!
+     *
+     * 它和 {@link #visible} 的区别在于:
+     * 1. {@link #visible} 只是决定是否可见。即使不可见,还是可以发起
+     * 2. startUserIds 决定某个用户是否可以发起。如果该用户不可发起,则他也是不可见的
+     */
+    @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
+    private List<Long> startUserIds;
+
+    /**
+     * 可管理用户编号数组
+     *
+     * 关联 {@link AdminUserRespDTO#getId()} 字段的数组
+     */
+    @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
+    private List<Long> managerUserIds;
+
 }

+ 7 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java

@@ -48,10 +48,15 @@ public class BpmProcessInstanceCopyDO extends BaseDO {
      * 冗余 ProcessInstance 的 category 字段
      */
     private String category;
-
+    /**
+     * 流程活动编号
+     * <p/>
+     * 对应 BPMN XML 节点编号,用于查询抄送节点的表单字段权限
+     * 这里冗余的原因:如果是钉钉易搭的抄送节点 (ServiceTask),使用 taskId 可能查不到对应的 activityId
+     */
+    private String activityId;
     /**
      * 任务主键
-     *
      * 关联 Task 的 id 属性
      */
     private String taskId;

+ 7 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.List;
+
 @Mapper
 public interface BpmProcessInstanceCopyMapper extends BaseMapperX<BpmProcessInstanceCopyDO> {
 
@@ -18,4 +20,9 @@ public interface BpmProcessInstanceCopyMapper extends BaseMapperX<BpmProcessInst
                 .orderByDesc(BpmProcessInstanceCopyDO::getId));
     }
 
+    default List<BpmProcessInstanceCopyDO> selectListByProcessInstanceIdAndActivityId(String processInstanceId, String activityId) {
+        return selectList(BpmProcessInstanceCopyDO::getProcessInstanceId, processInstanceId,
+                BpmProcessInstanceCopyDO::getActivityId, activityId);
+    }
+
 }

+ 4 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate;
 import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
 import org.flowable.spring.SpringProcessEngineConfiguration;
 import org.flowable.spring.boot.EngineConfigurationConfigurer;
@@ -56,12 +57,15 @@ public class BpmFlowableConfiguration {
     @Bean
     public EngineConfigurationConfigurer<SpringProcessEngineConfiguration> bpmProcessEngineConfigurationConfigurer(
             ObjectProvider<FlowableEventListener> listeners,
+            ObjectProvider<FlowableFunctionDelegate> customFlowableFunctionDelegates,
             BpmActivityBehaviorFactory bpmActivityBehaviorFactory) {
         return configuration -> {
             // 注册监听器,例如说 BpmActivityEventListener
             configuration.setEventListeners(ListUtil.toList(listeners.iterator()));
             // 设置 ActivityBehaviorFactory 实现类,用于流程任务的审核人的自定义
             configuration.setActivityBehaviorFactory(bpmActivityBehaviorFactory);
+            // 设置自定义的函数
+            configuration.setCustomFlowableFunctionDelegates(ListUtil.toList(customFlowableFunctionDelegates.stream().iterator()));
         };
     }
 

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

@@ -1,7 +1,9 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
 
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.hutool.core.collection.CollUtil;
 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;
@@ -48,12 +50,17 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
         super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
 
         // 第二步,获取任务的所有处理人
-        // 由于每次审批(会签、或签等情况)后都会执行一次,所以 variable 已经有结果,不重复计算
         @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);
+            }
         }
         return assigneeUserIds.size();
     }

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

@@ -1,5 +1,7 @@
 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 lombok.Setter;
@@ -41,12 +43,17 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
         super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
 
         // 第二步,获取任务的所有处理人
-        // 由于每次审批(会签、或签等情况)后都会执行一次,所以 variable 已经有结果,不重复计算
         @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);
+            }
         }
         return assigneeUserIds.size();
     }

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

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
 import lombok.Setter;
@@ -14,6 +13,7 @@ import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
 import org.flowable.engine.impl.util.TaskHelper;
 import org.flowable.task.service.TaskService;
 import org.flowable.task.service.impl.persistence.entity.TaskEntity;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 import java.util.Set;
@@ -36,14 +36,16 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     protected void handleAssignments(TaskService taskService, String assignee, String owner,
         List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager,
         DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) {
         // 第一步,获得任务的候选用户
         Long assigneeUserId = calculateTaskCandidateUsers(execution);
-        Assert.notNull(assigneeUserId, "任务处理人不能为空");
         // 第二步,设置作为负责人
-        TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId));
+        if (assigneeUserId != null) {
+            TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId));
+        }
     }
 
     private Long calculateTaskCandidateUsers(DelegateExecution execution) {
@@ -56,6 +58,9 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
         // 情况二,如果非多实例的任务,则计算任务处理人
         // 第一步,先计算可处理该任务的处理人们
         Set<Long> candidateUserIds = taskCandidateInvoker.calculateUsers(execution);
+        if (CollUtil.isEmpty(candidateUserIds)) {
+            return null;
+        }
         // 第二步,后随机选择一个任务的处理人
         // 疑问:为什么一定要选择一个任务处理人?
         // 解答:项目对 bpm 的任务是责任到人,所以每个任务有且仅有一个处理人。

+ 56 - 11
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java

@@ -2,11 +2,17 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import com.google.common.annotations.VisibleForTesting;
@@ -14,15 +20,12 @@ import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG;
-import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.TASK_CREATE_FAIL_NO_CANDIDATE_USER;
 
 /**
  * {@link BpmTaskCandidateStrategy} 的调用者,用于调用对应的策略,实现任务的候选人的计算
@@ -57,7 +60,14 @@ public class BpmTaskCandidateInvoker {
         List<UserTask> userTaskList = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
         // 遍历所有的 UserTask,校验审批人配置
         userTaskList.forEach(userTask -> {
-            // 1. 非空校验
+            // 1.1 非人工审批,无需校验审批人配置
+            Integer approveType = BpmnModelUtils.parseApproveType(userTask);
+            if (ObjectUtils.equalsAny(approveType,
+                    BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
+                    BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
+                return;
+            }
+            // 1.2 非空校验
             Integer strategy = BpmnModelUtils.parseCandidateStrategy(userTask);
             String param = BpmnModelUtils.parseCandidateParam(userTask);
             if (strategy == null) {
@@ -80,19 +90,31 @@ public class BpmTaskCandidateInvoker {
      */
     @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人
     public Set<Long> calculateUsers(DelegateExecution execution) {
+        // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过
+        Integer approveType = BpmnModelUtils.parseApproveType(execution.getCurrentFlowElement());
+        if (ObjectUtils.equalsAny(approveType,
+                BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
+                BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
+            return new HashSet<>();
+        }
+
         Integer strategy = BpmnModelUtils.parseCandidateStrategy(execution.getCurrentFlowElement());
         String param = BpmnModelUtils.parseCandidateParam(execution.getCurrentFlowElement());
         // 1.1 计算任务的候选人
         Set<Long> userIds = getCandidateStrategy(strategy).calculateUsers(execution, param);
+        removeDisableUsers(userIds);
         // 1.2 移除被禁用的用户
         removeDisableUsers(userIds);
 
-        // 2. 校验是否有候选人
+        // 2. 候选人为空时,根据“审批人为空”的配置补充
         if (CollUtil.isEmpty(userIds)) {
-            log.error("[calculateUsers][流程任务({}/{}/{}) 任务规则({}/{}) 找不到候选人]", execution.getId(),
-                    execution.getProcessDefinitionId(), execution.getCurrentActivityId(), strategy, param);
-            throw exception(TASK_CREATE_FAIL_NO_CANDIDATE_USER);
+            userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
+                    .calculateUsers(execution, param);
+            // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!!
         }
+
+        // 3. 移除发起人的用户
+        removeStartUserIfSkip(execution, userIds);
         return userIds;
     }
 
@@ -108,7 +130,30 @@ public class BpmTaskCandidateInvoker {
         });
     }
 
-    private BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
+    /**
+     * 如果“审批人与发起人相同时”,配置了 SKIP 跳过,则移除发起人
+     *
+     * 注意:如果只有一个候选人,则不处理,避免无法审批
+     *
+     * @param execution 执行中的任务
+     * @param assigneeUserIds 当前分配的候选人
+     */
+    @VisibleForTesting
+    void removeStartUserIfSkip(DelegateExecution execution, Set<Long> assigneeUserIds) {
+        if (CollUtil.size(assigneeUserIds) <= 1) {
+            return;
+        }
+        Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(execution.getCurrentFlowElement());
+        if (ObjectUtil.notEqual(assignStartUserHandlerType, BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
+            return;
+        }
+        ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class)
+                .getProcessInstance(execution.getProcessInstanceId());
+        Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId());
+        assigneeUserIds.remove(Long.valueOf(processInstance.getStartUserId()));
+    }
+
+    public BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
         BpmTaskCandidateStrategyEnum strategyEnum = BpmTaskCandidateStrategyEnum.valueOf(strategy);
         Assert.notNull(strategyEnum, "策略(%s) 不存在", strategy);
         BpmTaskCandidateStrategy strategyObj = strategyMap.get(strategyEnum);

+ 46 - 6
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java

@@ -2,12 +2,14 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
 
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
 
+import java.util.Collections;
 import java.util.Set;
 
 /**
  * BPM 任务的候选人的策略接口
- *
+ * <p>
  * 例如说:分配审批人
  *
  * @author 芋道源码
@@ -28,21 +30,59 @@ public interface BpmTaskCandidateStrategy {
      */
     void validateParam(String param);
 
+    /**
+     * 是否一定要输入参数
+     *
+     * @return 是否
+     */
+    default boolean isParamRequired() {
+        return true;
+    }
+
+    /**
+     * 基于候选人参数,获得任务的候选用户们
+     *
+     * @param param 执行任务
+     * @return 用户编号集合
+     */
+    default Set<Long> calculateUsers(String param) {
+        return Collections.emptySet();
+    }
+
     /**
      * 基于执行任务,获得任务的候选用户们
      *
      * @param execution 执行任务
      * @return 用户编号集合
      */
-    Set<Long> calculateUsers(DelegateExecution execution, String param);
+    default Set<Long> calculateUsers(DelegateExecution execution, String param) {
+        Set<Long> users = calculateUsers(param);
+        removeDisableUsers(users);
+        return users;
+    }
 
     /**
-     * 是否一定要输入参数
+     * 基于流程实例,获得任务的候选用户们
+     * <p>
+     * 目的:用于获取未执行节点的候选用户们
      *
-     * @return 是否
+     * @param startUserId  流程发起人编号
+     * @param processInstance 流程实例编号
+     * @param activityId 活动 Id (对应 Bpmn XML id)
+     * @param param     节点的参数
+     * @return 用户编号集合
      */
-    default boolean isParamRequired() {
-        return true;
+    default Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
+        Set<Long> users = calculateUsers(param);
+        removeDisableUsers(users);
+        return users;
     }
 
+    /**
+     * 移除被禁用的用户
+     *
+     * @param users 用户 Ids
+     */
+    void removeDisableUsers(Set<Long> users);
+
 }

+ 78 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractDeptLeaderStrategy.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+
+import java.util.*;
+
+/**
+ * 部门的负责人 {@link BpmTaskCandidateStrategy} 抽象类
+ *
+ * @author jason
+ */
+public abstract class BpmTaskCandidateAbstractDeptLeaderStrategy extends  BpmTaskCandidateAbstractStrategy {
+
+    protected DeptApi deptApi;
+
+    public BpmTaskCandidateAbstractDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
+        super(adminUserApi);
+        this.deptApi = deptApi;
+    }
+
+    /**
+     * 获得指定层级的部门负责人,只有第 level 的负责人
+     *
+     * @param dept 指定部门
+     * @param level 第几级
+     * @return 部门负责人的编号
+     */
+    protected Long getAssignLevelDeptLeaderId(DeptRespDTO dept, Integer level) {
+        Assert.isTrue(level > 0, "level 必须大于 0");
+        if (dept == null) {
+            return null;
+        }
+        DeptRespDTO currentDept = dept;
+        for (int i = 1; i < level; i++) {
+            DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId());
+            if (parentDept == null) { // 找不到父级部门,到了最高级。返回最高级的部门负责人
+                break;
+            }
+            currentDept = parentDept;
+        }
+        return currentDept.getLeaderUserId();
+    }
+
+    /**
+     * 获得连续层级的部门负责人,包含 [1, level] 的负责人
+     *
+     * @param deptIds 指定部门编号数组
+     * @param level 最大层级
+     * @return 连续部门负责人 Id
+     */
+    protected Set<Long> getMultiLevelDeptLeaderIds(List<Long> deptIds, Integer level) {
+        Assert.isTrue(level > 0, "level 必须大于 0");
+        if (CollUtil.isEmpty(deptIds)) {
+            return new HashSet<>();
+        }
+        Set<Long> deptLeaderIds = new LinkedHashSet<>(); // 保证有序
+        for (Long deptId : deptIds) {
+            DeptRespDTO dept = deptApi.getDept(deptId);
+            for (int i = 0; i < level; i++) {
+                if (dept.getLeaderUserId() != null) {
+                    deptLeaderIds.add(dept.getLeaderUserId());
+                }
+                DeptRespDTO parentDept = deptApi.getDept(dept.getParentId());
+                if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了
+                    break;
+                }
+                dept = parentDept;
+            }
+        }
+        return deptLeaderIds;
+    }
+
+}

+ 37 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * {@link BpmTaskCandidateStrategy} 抽象类
+ *
+ * @author jason
+ */
+public abstract class BpmTaskCandidateAbstractStrategy implements BpmTaskCandidateStrategy {
+
+    protected AdminUserApi adminUserApi;
+
+    public BpmTaskCandidateAbstractStrategy(AdminUserApi adminUserApi) {
+        this.adminUserApi = adminUserApi;
+    }
+
+    @Override
+    public void removeDisableUsers(Set<Long> users) {
+        if (CollUtil.isEmpty(users)) {
+            return;
+        }
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(users);
+        users.removeIf(id -> {
+            AdminUserRespDTO user = userMap.get(id);
+            return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus());
+        });
+    }
+
+}

+ 66 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAssignEmptyStrategy.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * 审批人为空 {@link BpmTaskCandidateStrategy} 实现类
+ *
+ * @author kyle
+ */
+@Component
+public class BpmTaskCandidateAssignEmptyStrategy extends BpmTaskCandidateAbstractStrategy {
+
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private BpmProcessDefinitionService processDefinitionService;
+
+    public BpmTaskCandidateAssignEmptyStrategy(AdminUserApi adminUserApi) {
+        super(adminUserApi);
+    }
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY;
+    }
+
+    @Override
+    public void validateParam(String param) {
+    }
+
+    @Override
+    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+        // 情况一:指定人员审批
+        Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(execution.getCurrentFlowElement());
+        if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType())) {
+            Set<Long> users = new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(execution.getCurrentFlowElement()));
+            removeDisableUsers(users);
+            return users;
+        }
+
+        // 情况二:流程管理员
+        if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType())) {
+            BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId());
+            Assert.notNull(processDefinition, "流程定义({})不存在", execution.getProcessDefinitionId());
+            return new HashSet<>(processDefinition.getManagerUserIds());
+        }
+
+        // 都不满足,还是返回空
+        return new HashSet<>();
+    }
+
+}

+ 45 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderMultiStrategy.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * 连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类
+ *
+ * @author jason
+ */
+@Component
+public class BpmTaskCandidateDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
+
+    public BpmTaskCandidateDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
+        super(adminUserApi, deptApi);
+    }
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.MULTI_DEPT_LEADER_MULTI;
+    }
+
+    @Override
+    public void validateParam(String param) {
+        // 参数格式: | 分隔:1)左边为部门(多个部门用 , 分隔)。2)右边为部门层级
+        String[] params = param.split("\\|");
+        Assert.isTrue(params.length == 2, "参数格式不匹配");
+        deptApi.validateDeptList(StrUtils.splitToLong(params[0], ","));
+        Assert.isTrue(Integer.parseInt(params[1]) > 0, "部门层级必须大于 0");
+    }
+
+    @Override
+    public Set<Long> calculateUsers(String param) {
+        String[] params = param.split("\\|");
+        return getMultiLevelDeptLeaderIds(StrUtils.splitToLong(params[0], ","), Integer.valueOf(params[1]));
+    }
+
+}

+ 9 - 6
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java

@@ -5,10 +5,9 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
-import org.flowable.engine.delegate.DelegateExecution;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.List;
 import java.util.Set;
 
@@ -20,10 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidateDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy {
 
-    @Resource
-    private DeptApi deptApi;
+    private final DeptApi deptApi;
+
+    public BpmTaskCandidateDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
+        super(adminUserApi);
+        this.deptApi = deptApi;
+    }
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -37,7 +40,7 @@ public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrat
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsers(String param) {
         Set<Long> deptIds = StrUtils.splitToLongSet(param);
         List<DeptRespDTO> depts = deptApi.getDeptList(deptIds);
         return convertSet(depts, DeptRespDTO::getLeaderUserId);

+ 8 - 8
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java

@@ -6,10 +6,8 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import org.flowable.engine.delegate.DelegateExecution;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.List;
 import java.util.Set;
 
@@ -21,12 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidateDeptMemberStrategy extends BpmTaskCandidateAbstractStrategy {
 
-    @Resource
-    private DeptApi deptApi;
-    @Resource
-    private AdminUserApi adminUserApi;
+    private final DeptApi deptApi;
+
+    public BpmTaskCandidateDeptMemberStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
+        super(adminUserApi);
+        this.deptApi = deptApi;
+    }
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -40,7 +40,7 @@ public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrat
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsers(String param) {
         Set<Long> deptIds = StrUtils.splitToLongSet(param);
         List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(deptIds);
         return convertSet(users, AdminUserRespDTO::getId);

+ 9 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategy.java

@@ -4,6 +4,7 @@ import cn.hutool.core.convert.Convert;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.springframework.stereotype.Component;
 
@@ -15,7 +16,11 @@ import java.util.Set;
  * @author 芋道源码
  */
 @Component
-public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidateExpressionStrategy extends BpmTaskCandidateAbstractStrategy {
+
+    public BpmTaskCandidateExpressionStrategy(AdminUserApi adminUserApi) {
+        super(adminUserApi);
+    }
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -30,7 +35,9 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
     @Override
     public Set<Long> calculateUsers(DelegateExecution execution, String param) {
         Object result = FlowableUtils.getExpressionValue(execution, param);
-        return Convert.toSet(Long.class, result);
+        Set<Long> users = Convert.toSet(Long.class, result);
+        removeDisableUsers(users);
+        return users;
     }
 
 }

+ 9 - 6
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java

@@ -5,10 +5,9 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
-import org.flowable.engine.delegate.DelegateExecution;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
@@ -21,10 +20,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidateGroupStrategy extends BpmTaskCandidateAbstractStrategy {
 
-    @Resource
-    private BpmUserGroupService userGroupService;
+    private final BpmUserGroupService userGroupService;
+
+    public BpmTaskCandidateGroupStrategy(AdminUserApi adminUserApi, BpmUserGroupService userGroupService) {
+        super(adminUserApi);
+        this.userGroupService = userGroupService;
+    }
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -38,7 +41,7 @@ public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsers(String param) {
         Set<Long> groupIds = StrUtils.splitToLongSet(param);
         List<BpmUserGroupDO> groups = userGroupService.getUserGroupList(groupIds);
         return convertSetByFlatMap(groups, BpmUserGroupDO::getUserIds, Collection::stream);

+ 8 - 8
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java

@@ -6,10 +6,8 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat
 import cn.iocoder.yudao.module.system.api.dept.PostApi;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import org.flowable.engine.delegate.DelegateExecution;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.List;
 import java.util.Set;
 
@@ -21,12 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidatePostStrategy extends BpmTaskCandidateAbstractStrategy {
 
-    @Resource
-    private PostApi postApi;
-    @Resource
-    private AdminUserApi adminUserApi;
+    private final PostApi postApi;
+
+    public BpmTaskCandidatePostStrategy(AdminUserApi adminUserApi, PostApi postApi) {
+        super(adminUserApi);
+        this.postApi = postApi;
+    }
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -40,7 +40,7 @@ public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsers(String param) {
         Set<Long> postIds = StrUtils.splitToLongSet(param);
         List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(postIds);
         return convertSet(users, AdminUserRespDTO::getId);

+ 8 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java

@@ -5,10 +5,10 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
 import cn.iocoder.yudao.module.system.api.permission.RoleApi;
-import org.flowable.engine.delegate.DelegateExecution;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.Set;
 
 /**
@@ -17,13 +17,17 @@ import java.util.Set;
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidateRoleStrategy extends BpmTaskCandidateAbstractStrategy {
 
     @Resource
     private RoleApi roleApi;
     @Resource
     private PermissionApi permissionApi;
 
+    public BpmTaskCandidateRoleStrategy(AdminUserApi adminUserApi) {
+        super(adminUserApi);
+    }
+
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
         return BpmTaskCandidateStrategyEnum.ROLE;
@@ -36,7 +40,7 @@ public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy {
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsers(String param) {
         Set<Long> roleIds = StrUtils.splitToLongSet(param);
         return permissionApi.getUserRoleIdListByRoleIds(roleIds);
     }

+ 90 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java

@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static cn.hutool.core.collection.ListUtil.toList;
+
+/**
+ * 发起人连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类
+ *
+ * @author jason
+ */
+@Component
+public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
+
+    @Resource
+    @Lazy
+    private BpmProcessInstanceService processInstanceService;
+
+    public BpmTaskCandidateStartUserDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
+        super(adminUserApi, deptApi);
+    }
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER_MULTI;
+    }
+
+    @Override
+    public void validateParam(String param) {
+        // 参数是部门的层级
+        Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0");
+    }
+
+    @Override
+    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+        // 获得流程发起人
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
+        Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
+        // 获取发起人的 multi 部门负责人
+        DeptRespDTO dept = getStartUserDept(startUserId);
+        if (dept == null) {
+            return new HashSet<>();
+        }
+        Set<Long> users = getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级
+        // TODO @jason:这里 removeDisableUsers 的原因是啥呀?
+        removeDisableUsers(users);
+        return users;
+    }
+
+    @Override
+    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
+        DeptRespDTO dept = getStartUserDept(startUserId);
+        if (dept == null) {
+            return new HashSet<>();
+        }
+        Set<Long> users =  getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级
+        removeDisableUsers(users);
+        return users;
+    }
+
+    /**
+     * 获取发起人的部门
+     *
+     * @param startUserId 发起人 Id
+     */
+    protected DeptRespDTO getStartUserDept(Long startUserId) {
+        AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
+        if (startUser.getDeptId() == null) { // 找不到部门
+            return null;
+        }
+        return deptApi.getDept(startUser.getDeptId());
+    }
+
+}

+ 91 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderStrategy.java

@@ -0,0 +1,91 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
+
+/**
+ * 发起人的部门负责人, 可以是上级部门负责人 {@link BpmTaskCandidateStrategy} 实现类
+ *
+ * @author jason
+ */
+@Component
+public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
+
+    @Resource
+    @Lazy // 避免循环依赖
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER;
+    }
+
+    public BpmTaskCandidateStartUserDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
+        super(adminUserApi, deptApi);
+    }
+
+    @Override
+    public void validateParam(String param) {
+        // 参数是部门的层级
+        Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0");
+    }
+
+    @Override
+    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+        // 获得流程发起人
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
+        Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
+        // 获取发起人的部门负责人
+        Set<Long> users = getStartUserDeptLeader(startUserId, param);
+        removeDisableUsers(users);
+        return users;
+    }
+
+    @Override
+    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
+        // 获取发起人的部门负责人
+        Set<Long> users =  getStartUserDeptLeader(startUserId, param);
+        removeDisableUsers(users);
+        return users;
+    }
+
+    private Set<Long> getStartUserDeptLeader(Long startUserId, String param) {
+        DeptRespDTO dept = getStartUserDept(startUserId);
+        if (dept == null) {
+            return new HashSet<>();
+        }
+        Long deptLeaderId = getAssignLevelDeptLeaderId(dept, Integer.valueOf(param)); // 参数是部门的层级
+        return deptLeaderId != null ? asSet(deptLeaderId) : new HashSet<>();
+    }
+
+    /**
+     * 获取发起人的部门
+     *
+     * @param startUserId 发起人 Id
+     */
+    protected DeptRespDTO getStartUserDept(Long startUserId) {
+        AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
+        if (startUser.getDeptId() == null) { // 找不到部门
+            return null;
+        }
+        return deptApi.getDept(startUser.getDeptId());
+    }
+
+}

+ 24 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java

@@ -2,11 +2,12 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
@@ -14,7 +15,6 @@ import org.flowable.engine.runtime.ProcessInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.*;
 
 /**
@@ -23,12 +23,16 @@ import java.util.*;
  * @author 芋道源码
  */
 @Component
-public class BpmTaskCandidateStartUserSelectStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbstractStrategy {
 
     @Resource
     @Lazy // 延迟加载,避免循环依赖
     private BpmProcessInstanceService processInstanceService;
 
+    public BpmTaskCandidateStartUserSelectStrategy(AdminUserApi adminUserApi) {
+        super(adminUserApi);
+    }
+
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
         return BpmTaskCandidateStrategyEnum.START_USER_SELECT;
@@ -46,7 +50,23 @@ public class BpmTaskCandidateStartUserSelectStrategy implements BpmTaskCandidate
                 execution.getProcessInstanceId());
         // 获得审批人
         List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
-        return new LinkedHashSet<>(assignees);
+        Set<Long> users = new LinkedHashSet<>(assignees);
+        removeDisableUsers(users);
+        return users;
+    }
+
+    @Override
+    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
+        if (processInstance == null) {
+            return Collections.emptySet();
+        }
+        Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance);
+        Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空", processInstance.getId());
+        // 获得审批人
+        List<Long> assignees = startUserSelectAssignees.get(activityId);
+        Set<Long> users = new LinkedHashSet<>(assignees);
+        removeDisableUsers(users);
+        return users;
     }
 
     @Override

Some files were not shown because too many files changed in this diff