Browse Source

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

# Conflicts:
#	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/task/BpmActivityController.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/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.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/BpmProcessDefinitionServiceImpl.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/BpmProcessInstanceServiceImpl.java
YunaiV 6 months ago
parent
commit
9da052a45a
100 changed files with 2947 additions and 1595 deletions
  1. BIN
      .image/工作流设计器-bpmn.jpg
  2. BIN
      .image/工作流设计器-simple.jpg
  3. 13 9
      README.md
  4. 10 9
      sql/mysql/ruoyi-vue-pro.sql
  5. 9 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  6. 4 2
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
  7. 4 4
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  8. 10 24
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java
  9. 8 5
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java
  10. 4 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java
  11. 9 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java
  12. 5 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java
  13. 9 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java
  14. 26 19
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
  15. 1 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
  16. 11 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
  17. 0 23
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java
  18. 0 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java
  19. 8 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java
  20. 0 40
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java
  21. 16 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http
  22. 8 12
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  23. 9 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java
  24. 15 16
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
  25. 16 15
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java
  26. 14 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java
  27. 45 26
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java
  28. 0 38
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java
  29. 43 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java
  30. 2 17
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java
  31. 1 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java
  32. 23 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java
  33. 13 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java
  34. 5 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java
  35. 18 11
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
  36. 7 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java
  37. 0 30
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmActivityConvert.java
  38. 176 9
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
  39. 62 46
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java
  40. 6 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
  41. 18 9
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java
  42. 6 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java
  43. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
  44. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
  45. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java
  46. 49 16
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java
  47. 20 23
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java
  48. 0 37
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java
  49. 20 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java
  50. 11 11
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java
  51. 5 9
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategy.java
  52. 7 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategy.java
  53. 16 36
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java
  54. 13 33
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java
  55. 35 34
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java
  56. 56 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormSDeptLeaderStrategy.java
  57. 47 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java
  58. 22 15
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java
  59. 13 11
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java
  60. 6 10
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategy.java
  61. 7 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategy.java
  62. 3 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java
  63. 11 16
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java
  64. 6 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategy.java
  65. 5 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java
  66. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java
  67. 1 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java
  68. 6 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
  69. 0 33
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/SimpleModelConstants.java
  70. 5 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java
  71. 2 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java
  72. 373 23
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  73. 62 9
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
  74. 533 478
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
  75. 7 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java
  76. 22 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java
  77. 15 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
  78. 53 35
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  79. 11 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java
  80. 9 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
  81. 0 30
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityService.java
  82. 0 37
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java
  83. 15 18
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java
  84. 9 15
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java
  85. 8 9
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
  86. 0 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  87. 52 15
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
  88. 149 36
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
  89. 0 36
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/bo/AlreadyRunApproveNodeRespBO.java
  90. 207 34
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java
  91. 43 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java
  92. 10 11
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java
  93. 15 13
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java
  94. 82 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java
  95. 82 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java
  96. 68 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java
  97. 88 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java
  98. 23 3
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java
  99. 3 3
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java
  100. 3 3
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java

BIN
.image/工作流设计器-bpmn.jpg


BIN
.image/工作流设计器-simple.jpg


+ 13 - 9
README.md

@@ -149,18 +149,22 @@
 
 ### 工作流程
 
-|     | 功能    | 描述                                     |
-|-----|-------|----------------------------------------|
-| 🚀  | 流程模型  | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 |
-| 🚀  | 流程表单  | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
-| 🚀  | 用户分组  | 自定义用户分组,可用于工作流的审批分组                    |
-| 🚀  | 我的流程  | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线    |
-| 🚀  | 待办任务  | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作    |
-| 🚀  | 已办任务  | 查看自己【已】审批的工作任务,未来会支持回退操作               |
-| 🚀  | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
+|    | 功能    | 描述                                      |
+|----|-------|-----------------------------------------|
+| 🚀 | 流程模型  | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器           |
+| 🚀 | 流程表单  | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件  |
+| 🚀 | 用户分组  | 自定义用户分组,可用于工作流的审批分组                     |
+| 🚀 | 我的流程  | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线     |
+| 🚀 | 待办任务  | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
+| 🚀 | 已办任务  | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息         |
+| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批  |
 
 ![功能图](/.image/common/bpm-feature.png)
 
+| BPMN 设计器                     | 钉钉/飞书设计器                       |
+|------------------------------|--------------------------------|
+| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
+
 ### 支付系统
 
 |     | 功能   | 描述                        |

+ 10 - 9
sql/mysql/ruoyi-vue-pro.sql

@@ -11,7 +11,7 @@
  Target Server Version : 80200 (8.2.0)
  File Encoding         : 65001
 
- Date: 09/11/2024 18:16:12
+ Date: 23/11/2024 10:16:46
 */
 
 SET NAMES utf8mb4;
@@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 21213 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 21220 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -250,7 +250,7 @@ CREATE TABLE `infra_file`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1558 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1561 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file
@@ -1044,7 +1044,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3370 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 3395 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -2165,7 +2165,7 @@ CREATE TABLE `system_oauth2_access_token`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
   INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 11308 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 11844 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -2287,7 +2287,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1676 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 1696 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -2308,6 +2308,7 @@ CREATE TABLE `system_operate_log`  (
   `sub_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作名',
   `biz_id` bigint NOT NULL COMMENT '操作数据模块编号',
   `action` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作内容',
+  `success` bit(1) NOT NULL DEFAULT b'1' COMMENT '操作结果',
   `extra` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '拓展字段',
   `request_method` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求方法名',
   `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求地址',
@@ -3303,7 +3304,7 @@ CREATE TABLE `system_sms_code`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号'
-) ENGINE = InnoDB AUTO_INCREMENT = 642 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
+) ENGINE = InnoDB AUTO_INCREMENT = 644 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
 
 -- ----------------------------
 -- Records of system_sms_code
@@ -3344,7 +3345,7 @@ CREATE TABLE `system_sms_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1234 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 1238 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
 
 -- ----------------------------
 -- Records of system_sms_log
@@ -3642,7 +3643,7 @@ CREATE TABLE `system_users`  (
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-11-08 19:27:07', 'admin', '2021-01-05 17:03:47', NULL, '2024-11-08 19:27:07', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-11-22 20:11:14', 'admin', '2021-01-05 17:03:47', NULL, '2024-11-22 20:11:14', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$IgUse/ibRzAZ3rngCThmtemJeoh15Ux1TQ2hIMe4iwt/K3LcFHEda', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-11-02 14:00:46', '', '2021-01-07 09:07:17', NULL, '2024-11-02 14:00:46', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', NULL, '2024-08-11 17:48:12', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-09-17 15:05:43', '', '2021-01-21 02:13:53', NULL, '2024-09-17 15:05:43', b'0', 1);

+ 9 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.collection;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import com.google.common.collect.ImmutableMap;
 
 import java.util.*;
@@ -73,6 +74,13 @@ public class CollectionUtils {
         return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
     }
 
+    public static <T, U> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) {
+        if (ArrayUtil.isEmpty(from)) {
+            return new PageResult<>(from.getTotal());
+        }
+        return new PageResult<>(convertList(from.getList(), func), from.getTotal());
+    }
+
     public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
                                                       Function<T, ? extends Stream<? extends U>> func) {
         if (CollUtil.isEmpty(from)) {
@@ -324,7 +332,7 @@ public class CollectionUtils {
     }
 
     public static <T> List<T> newArrayList(List<List<T>> list) {
-        return list.stream().flatMap(Collection::stream).collect(Collectors.toList());
+        return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
     }
 
 }

+ 4 - 2
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java

@@ -64,8 +64,10 @@ public abstract class AbstractWebSocketMessageSender implements WebSocketMessage
             sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
         }
         if (CollUtil.isEmpty(sessions)) {
-            log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
-                    sessionId, userType, userId, messageType, messageContent);
+            if (log.isDebugEnabled()) {
+                log.debug("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
+                        sessionId, userType, userId, messageType, messageContent);
+            }
         }
         // 2. 执行发送
         doSend(sessions, messageType, messageContent);

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

@@ -23,7 +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, "操作流程失败,原因:你不是该流程的管理员");
+    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 流程图");
@@ -35,8 +35,8 @@ public interface ErrorCodeConstants {
     ErrorCode PROCESS_INSTANCE_NOT_EXISTS = new ErrorCode(1_009_004_000, "流程实例不存在");
     ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS = new ErrorCode(1_009_004_001, "流程取消失败,流程不处于运行中");
     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_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 ==========
@@ -44,7 +44,7 @@ public interface ErrorCodeConstants {
     ErrorCode TASK_NOT_EXISTS = new ErrorCode(1_009_005_002, "流程任务不存在");
     ErrorCode TASK_IS_PENDING = new ErrorCode(1_009_005_003, "当前任务处于挂起状态,不能操作");
     ErrorCode TASK_TARGET_NODE_NOT_EXISTS = new ErrorCode(1_009_005_004, " 目标节点不存在");
-    ErrorCode TASK_RETURN_FAIL_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_006, "退任务失败,目标节点是在并行网关上或非同一路线上,不可跳转");
+    ErrorCode TASK_RETURN_FAIL_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_006, "退任务失败,目标节点是在并行网关上或非同一路线上,不可跳转");
     ErrorCode TASK_DELEGATE_FAIL_USER_REPEAT = new ErrorCode(1_009_005_007, "任务委派失败,委派人和当前审批人为同一人");
     ErrorCode TASK_DELEGATE_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_008, "任务委派失败,被委派人不存在");
     ErrorCode TASK_SIGN_CREATE_USER_NOT_EXIST = new ErrorCode(1_009_005_009, "任务加签:选择的用户不存在");

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

@@ -18,30 +18,26 @@ import java.util.Objects;
 public enum BpmSimpleModelNodeType implements IntArrayValuable {
 
     // 0 ~ 1 开始和结束
-    START_NODE(0, "startEvent", "开始节点"),
-    END_NODE(1, "endEvent", "结束节点"),
+    START_NODE(0, "开始", "startEvent"),
+    END_NODE(1, "结束", "endEvent"),
 
     // 10 ~ 49 各种节点
-    START_USER_NODE(10, "userTask", "发起人节点"), // 发起人节点。前端的开始节点,Id 固定
-    APPROVE_NODE(11, "userTask", "审批人节点"),
-    COPY_NODE(12, "serviceTask", "抄送人节点"),
+    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 @芋艿。 感觉还是分开好理解一点,也好处理一点。前端结构中把聚合节点显示并传过来。
+    CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式
+    CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"),
+    PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"),
+    INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"),
     ;
 
     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;
+    private final String bpmnType;
 
     /**
      * 判断是否为分支节点
@@ -54,16 +50,6 @@ public enum BpmSimpleModelNodeType implements IntArrayValuable {
                 || 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());
     }

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

@@ -16,20 +16,23 @@ import java.util.Arrays;
 @AllArgsConstructor
 public enum BpmUserTaskApproveMethodEnum implements IntArrayValuable {
 
-    RANDOM(1, "随机挑选一人审批"),
-    RATIO(2, "多人会签(按通过比例)"), // 会签(按通过比例)
-    ANY(3, "多人或签(一人通过或拒绝)"), // 或签(通过只需一人,拒绝只需一人)
-    SEQUENTIAL(4, "依次审批"); // 依次审批
+    RANDOM(1, "随机挑选一人审批", null),
+    RATIO(2, "多人会签(按通过比例)", "${ nrOfCompletedInstances/nrOfInstances >= %s}"), // 会签(按通过比例)
+    ANY(3, "多人或签(一人通过或拒绝)", "${ nrOfCompletedInstances > 0 }"), // 或签(通过只需一人,拒绝只需一人)
+    SEQUENTIAL(4, "依次审批", "${ nrOfCompletedInstances >= nrOfInstances }"); // 依次审批
 
     /**
      * 审批方式
      */
     private final Integer method;
-
     /**
      * 名字
      */
     private final String name;
+    /**
+     * 完成表达式
+     */
+    private final String completionCondition;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveMethodEnum::getMethod).toArray();
 

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

@@ -38,6 +38,10 @@ public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
+    public static boolean isRejectStatus(Integer status) {
+        return REJECT.getStatus().equals(status);
+    }
+
     public static boolean isProcessEndStatus(Integer status) {
         return ObjectUtils.equalsAny(status,
                 APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus());

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.enums.task;
 
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
@@ -20,7 +21,6 @@ public enum BpmTaskStatusEnum {
     CANCEL(4, "已取消"),
 
     RETURN(5, "已退回"),
-    DELEGATE(6, "委派中"),
 
     /**
      * 使用场景:
@@ -45,6 +45,10 @@ public enum BpmTaskStatusEnum {
      */
     private final String name;
 
+    public static boolean isRejectStatus(Integer status) {
+        return REJECT.getStatus().equals(status);
+    }
+
     /**
      * 判断该状态是否已经处于 End 最终状态
      * <p>
@@ -59,4 +63,8 @@ public enum BpmTaskStatusEnum {
                 RETURN.getStatus(), APPROVING.getStatus());
     }
 
+    public static boolean isCancelStatus(Integer status) {
+        return ObjUtil.equal(status, CANCEL.getStatus());
+    }
+
 }

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

@@ -9,11 +9,14 @@ 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;
 
+    @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long deptId;
+    @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部")
+    private String deptName;
+
 }

+ 9 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java

@@ -48,6 +48,15 @@ public class BpmCategoryController {
         return success(true);
     }
 
+    @PutMapping("/update-sort-batch")
+    @Operation(summary = "批量更新流程分类的排序")
+    @Parameter(name = "ids", description = "分类编号列表", required = true, example = "1,2,3")
+    @PreAuthorize("@ss.hasPermission('bpm:category:update')")
+    public CommonResult<Boolean> updateCategorySortBatch(@RequestParam("ids") List<Long> ids) {
+        categoryService.updateCategorySortBatch(ids);
+        return success(true);
+    }
+
     @DeleteMapping("/delete")
     @Operation(summary = "删除流程分类")
     @Parameter(name = "id", description = "编号", required = true)

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

@@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition;
 
 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.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;
@@ -19,6 +17,8 @@ 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,9 +26,7 @@ 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.HashSet;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -56,38 +54,38 @@ public class BpmModelController {
     @Resource
     private AdminUserApi adminUserApi;
 
-    @GetMapping("/page")
+    @GetMapping("/list")
     @Operation(summary = "获得模型分页")
-    public CommonResult<PageResult<BpmModelRespVO>> getModelPage(BpmModelPageReqVO pageVO) {
-        PageResult<Model> pageResult = modelService.getModelPage(pageVO);
-        if (CollUtil.isEmpty(pageResult.getList())) {
-            return success(PageResult.empty(pageResult.getTotal()));
+    @Parameter(name = "name", description = "模型名称", example = "芋艿")
+    public CommonResult<List<BpmModelRespVO>> getModelPage(@RequestParam(value = "name", required = false) String name) {
+        List<Model> list = modelService.getModelList(name);
+        if (CollUtil.isEmpty(list)) {
+            return success(Collections.emptyList());
         }
 
-        // 拼接数据
         // 获得 Form 表单
-        Set<Long> formIds = convertSet(pageResult.getList(), model -> {
+        Set<Long> formIds = convertSet(list, model -> {
             BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
             return metaInfo != null ? metaInfo.getFormId() : null;
         });
         Map<Long, BpmFormDO> formMap = formService.getFormMap(formIds);
         // 获得 Category Map
         Map<String, BpmCategoryDO> categoryMap = categoryService.getCategoryMap(
-                convertSet(pageResult.getList(), Model::getCategory));
+                convertSet(list, Model::getCategory));
         // 获得 Deployment Map
-        Set<String> deploymentIds = new HashSet<>();
-        pageResult.getList().forEach(model -> CollectionUtils.addIfNotNull(deploymentIds, model.getDeploymentId()));
-        Map<String, Deployment> deploymentMap = processDefinitionService.getDeploymentMap(deploymentIds);
+        Map<String, Deployment> deploymentMap = processDefinitionService.getDeploymentMap(
+                convertSet(list, Model::getDeploymentId));
         // 获得 ProcessDefinition Map
-        List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);
+        List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(
+                deploymentMap.keySet());
         Map<String, ProcessDefinition> processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId);
         // 获得 User Map
-        Set<Long> userIds = convertSetByFlatMap(pageResult.getList(), model -> {
+        Set<Long> userIds = convertSetByFlatMap(list, 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,
+        return success(BpmModelConvert.INSTANCE.buildModelList(list,
                 formMap, categoryMap, deploymentMap, processDefinitionMap, userMap));
     }
 
@@ -111,6 +109,7 @@ public class BpmModelController {
         return success(modelService.createModel(createRetVO));
     }
 
+
     @PutMapping("/update")
     @Operation(summary = "修改模型")
     @PreAuthorize("@ss.hasPermission('bpm:model:update')")
@@ -119,6 +118,14 @@ public class BpmModelController {
         return success(true);
     }
 
+    @PutMapping("/update-sort-batch")
+    @Operation(summary = "批量修改模型排序")
+    @Parameter(name = "ids", description = "编号数组", required = true, example = "1,2,3")
+    public CommonResult<Boolean> updateModelSortBatch(@RequestParam("ids") List<String> ids) {
+        modelService.updateModelSortBatch(getLoginUserId(), ids);
+        return success(true);
+    }
+
     @PostMapping("/deploy")
     @Operation(summary = "部署模型")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")

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

@@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConver
 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.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy;
 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.BpmProcessDefinitionService;
@@ -17,7 +16,6 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.flowable.bpmn.model.BpmnModel;
-import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -115,9 +113,8 @@ public class BpmProcessDefinitionController {
         }
         BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(processDefinition.getId());
         BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId());
-        List<UserTask> userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel);
         return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition(
-                processDefinition, null, processDefinitionInfo, null, null, bpmnModel, userTaskList));
+                processDefinition, null, processDefinitionInfo, null, null, bpmnModel));
     }
 
 }

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

@@ -15,7 +15,9 @@ import java.util.List;
  * BPM 流程 MetaInfo Response DTO
  * 主要用于 { Model#setMetaInfo(String)} 的存储
  *
- * 最终,它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的
+ * 最终,它的字段和
+ * {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO}
+ * 是一致的
  *
  * @author 芋道源码
  */
@@ -40,13 +42,11 @@ public class BpmModelMetaInfoVO {
     @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 时,必须非空
+    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 = "是否可见不能为空")
@@ -59,4 +59,7 @@ public class BpmModelMetaInfoVO {
     @NotEmpty(message = "可管理用户编号数组不能为空")
     private List<Long> managerUserIds;
 
+    @Schema(description = "排序", example = "1")
+    private Long sort; // 创建时,后端自动生成
+
 }

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

@@ -1,23 +0,0 @@
-package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
-
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-
-@Schema(description = "管理后台 - 流程模型分页 Request VO")
-@Data
-public class BpmModelPageReqVO extends PageParam {
-
-    @Schema(description = "标识,精准匹配", example = "process1641042089407")
-    private String key;
-
-    @Schema(description = "名字,模糊匹配", example = "芋道")
-    private String name;
-
-    @Schema(description = "流程分类", example = "1")
-    private String category;
-
-}

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

@@ -3,7 +3,6 @@ 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 lombok.Data;
@@ -31,7 +30,6 @@ public class BpmSimpleModelNodeVO {
     @Schema(description = "模型节点名称", example = "领导审批")
     private String name;
 
-    // TODO @jason:和 gpt 大模型对了下这个字段的命名,貌似叫 displayText 合适点。可以等最后我们全局替换下。(优先级:低)
     @Schema(description = "节点展示内容", example = "指定成员: 芋道源码")
     private String showText;
 
@@ -79,12 +77,6 @@ public class BpmSimpleModelNodeVO {
     @Schema(description = "操作按钮设置", example = "[]")
     private List<OperationButtonSetting> buttonsSetting;  // 用于审批节点
 
-    // TODO @jason:看看是不是可以简化;@芋艿: 暂时先放着。不知道后面是否会用到
-    /**
-     * 附加节点 Id, 该节点不从前端传入。 由程序生成. 由于当个节点无法完成功能。 需要附加节点来完成。
-     */
-    @JsonIgnore
-    private String attachNodeId;
     /**
      * 审批节点拒绝处理
      */

+ 8 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java

@@ -33,6 +33,9 @@ public class BpmProcessDefinitionRespVO {
     @Schema(description = "流程分类名字", example = "请假")
     private String categoryName;
 
+    @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Integer modelType; // 参见 BpmModelTypeEnum 枚举类
+
     @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
     private Integer formType;
     @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024")
@@ -59,9 +62,12 @@ public class BpmProcessDefinitionRespVO {
     @Schema(description = "BPMN XML")
     private String bpmnXml; // 需要从对应的 BpmnModel 读取,非必须返回
 
-    @Schema(description = "发起用户需要选择审批人的任务数组")
-    private List<UserTask> startUserSelectTasks; // 需要从对应的 BpmnModel 读取,非必须返回
+    @Schema(description = "SIMPLE 设计器模型数据 json 格式")
+    private String simpleModel; // 非必须返回
 
+    @Schema(description = "流程定义排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long sort;
+    
     @Schema(description = "BPMN UserTask 用户任务")
     @Data
     public static class UserTask {

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

@@ -1,40 +0,0 @@
-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;
-import io.swagger.v3.oas.annotations.Operation;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.annotation.Resource;
-import java.util.List;
-
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
-@Tag(name = "管理后台 - 流程活动实例")
-@RestController
-@RequestMapping("/bpm/activity")
-@Validated
-public class BpmActivityController {
-
-    @Resource
-    private BpmActivityService activityService;
-
-    @GetMapping("/list")
-    @Operation(summary = "生成指定流程实例的高亮流程图",
-            description = "只高亮进行中的任务。不过要注意,该接口暂时没用,通过前端的 ProcessViewer.vue 界面的 highlightDiagram 方法生成")
-    @Parameter(name = "processInstanceId", description = "流程实例的编号", required = true)
-    @PreAuthorize("@ss.hasPermission('bpm:task:query')")
-    public CommonResult<List<BpmActivityRespVO>> getActivityList(
-            @RequestParam("processInstanceId") String processInstanceId) {
-        return success(BpmActivityConvert.INSTANCE.convertList(activityService.getActivityListByProcessInstanceId(processInstanceId)));
-    }
-}

+ 16 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http

@@ -0,0 +1,16 @@
+### 请求 /bpm/process-instance/get-bpmn 接口 => 成功
+GET {{baseUrl}}/bpm/process-instance/get-bpmn-model-view?id=1d5fb5a6-85f8-11ef-b717-7e93075f94e3
+Content-Type: application/json
+tenant-id: 1
+Authorization: Bearer {{token}}
+
+### 请求 /bpm/process-instance/get-bpmn 接口 => 失败
+#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=1d5fb5a6-85f8-11ef-b717-7e93075f94e3
+#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=3ee5c5ba-904a-11ef-a76e-b2ed5d6ef911
+#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=f630dfa2-8f92-11ef-947c-ba5e239a6eb4
+#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=9de8bdbf-9133-11ef-ae97-eaf49df1f932
+#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=dd2188eb-9394-11ef-a039-7a9ac3d9eb6b
+GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processDefinitionId=test-auto:1:c70a799a-9394-11ef-a039-7a9ac3d9eb6b
+Content-Type: application/json
+tenant-id: 1
+Authorization: Bearer {{token}}

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

@@ -8,7 +8,6 @@ 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;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
@@ -128,15 +127,13 @@ public class BpmProcessInstanceController {
                 processInstance.getProcessDefinitionId());
         BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(
                 processInstance.getProcessDefinitionId());
-        String bpmnXml = BpmnModelUtils.getBpmnXml(
-                processDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId()));
         AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId()));
         DeptRespDTO dept = null;
         if (startUser != null && startUser.getDeptId() != null) {
             dept = deptApi.getDept(startUser.getDeptId());
         }
         return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance,
-                processDefinition, processDefinitionInfo, bpmnXml, startUser, dept));
+                processDefinition, processDefinitionInfo, startUser, dept));
     }
 
     @DeleteMapping("/cancel-by-start-user")
@@ -157,14 +154,6 @@ 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)
@@ -173,4 +162,11 @@ public class BpmProcessInstanceController {
         return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
     }
 
+    @GetMapping("/get-bpmn-model-view")
+    @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
+    @Parameter(name = "id", description = "流程实例的编号", required = true)
+    public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) {
+        return success(processInstanceService.getProcessInstanceBpmnModelView(id));
+    }
+
 }

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

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.cc.BpmProcessInstanceCopyRespVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
@@ -28,8 +29,7 @@ import java.util.Map;
 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.convertListByFlatMap;
-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 = "管理后台 - 流程实例抄送")
@@ -62,11 +62,15 @@ public class BpmProcessInstanceCopyController {
                 convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
                 copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator()))));
-        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()));
+        return success(convertPage(pageResult, copy -> {
+            BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class);
+            MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()),
+                    user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
+            MapUtils.findAndThen(userMap, copy.getStartUserId(),
+                    user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
             MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
                     processInstance -> copyVO.setProcessInstanceStartTime(DateUtils.of(processInstance.getStartTime())));
+            return copyVO;
         }));
     }
 

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

@@ -8,7 +8,6 @@ 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;
@@ -18,7 +17,8 @@ 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 org.flowable.bpmn.model.BpmnModel;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
@@ -28,8 +28,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;
@@ -52,8 +50,6 @@ public class BpmTaskController {
     private BpmProcessInstanceService processInstanceService;
     @Resource
     private BpmFormService formService;
-    @Resource
-    private BpmProcessDefinitionService bpmProcessDefinitionService;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -121,27 +117,22 @@ public class BpmTaskController {
     @PreAuthorize("@ss.hasPermission('bpm:task:query')")
     public CommonResult<List<BpmTaskRespVO>> getTaskListByProcessInstanceId(
             @RequestParam("processInstanceId") String processInstanceId) {
-        List<HistoricTaskInstance> taskList = taskService.getTaskListByProcessInstanceId(processInstanceId);
+        List<HistoricTaskInstance> taskList = taskService.getTaskListByProcessInstanceId(processInstanceId, true);
         if (CollUtil.isEmpty(taskList)) {
             return success(Collections.emptyList());
         }
 
         // 拼接数据
-        HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);
-        // 获得 User 和 Dept Map
         Set<Long> userIds = convertSetByFlatMap(taskList, task ->
                 Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner())));
-        userIds.add(NumberUtils.parseLong(processInstance.getStartUserId()));
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
                 convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
         // 获得 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, bpmnModel));
+        return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList,
+                formMap, userMap, deptMap));
     }
 
     @PutMapping("/approve")
@@ -161,7 +152,7 @@ public class BpmTaskController {
     }
 
     @GetMapping("/list-by-return")
-    @Operation(summary = "获取所有可退的节点", description = "用于【流程详情】的【退】按钮")
+    @Operation(summary = "获取所有可退的节点", description = "用于【流程详情】的【退】按钮")
     @Parameter(name = "taskId", description = "当前任务ID", required = true)
     @PreAuthorize("@ss.hasPermission('bpm:task:update')")
     public CommonResult<List<BpmTaskRespVO>> getTaskListByReturn(@RequestParam("id") String id) {
@@ -171,7 +162,7 @@ public class BpmTaskController {
     }
 
     @PutMapping("/return")
-    @Operation(summary = "退任务", description = "用于【流程详情】的【退】按钮")
+    @Operation(summary = "退任务", description = "用于【流程详情】的【退】按钮")
     @PreAuthorize("@ss.hasPermission('bpm:task:update')")
     public CommonResult<Boolean> returnTask(@Valid @RequestBody BpmTaskReturnReqVO reqVO) {
         taskService.returnTask(getLoginUserId(), reqVO);
@@ -210,6 +201,14 @@ public class BpmTaskController {
         return success(true);
     }
 
+    @PutMapping("/copy")
+    @Operation(summary = "抄送任务")
+    @PreAuthorize("@ss.hasPermission('bpm:task:update')")
+    public CommonResult<Boolean> copyTask(@Valid @RequestBody BpmTaskCopyReqVO reqVO) {
+        taskService.copyTask(getLoginUserId(), reqVO);
+        return success(true);
+    }
+
     @GetMapping("/list-by-parent-task-id")
     @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表
     @Parameter(name = "parentTaskId", description = "父级任务编号", required = true)

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.cc;
 
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -12,31 +13,31 @@ public class BpmProcessInstanceCopyRespVO {
     @Schema(description = "抄送主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long id;
 
-    @Schema(description = "发起人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888")
-    private Long startUserId;
-    @Schema(description = "发起人昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
-    private String startUserName;
+    @Schema(description = "发起人", requiredMode = Schema.RequiredMode.REQUIRED)
+    private UserSimpleBaseVO startUser;
 
     @Schema(description = "流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "A233")
     private String processInstanceId;
-    @Schema(description = "流程实例的名称")
+    @Schema(description = "流程实例的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试")
     private String processInstanceName;
-    @Schema(description = "流程实例的发起时间")
+    @Schema(description = "流程实例的发起时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime processInstanceStartTime;
 
-    @Schema(description = "抄送的节点的活动编号")
+    @Schema(description = "流程活动的编号", requiredMode = Schema.RequiredMode.REQUIRED)
     private String activityId;
-    @Schema(description = "发起抄送的任务编号")
+    @Schema(description = "流程活动的名字", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String activityName;
+
+    @Schema(description = "流程活动的编号")
     private String taskId;
-    @Schema(description = "发起抄送的任务名称")
-    private String taskName;
 
-    @Schema(description = "抄送人")
-    private String creator;
-    @Schema(description = "抄送人昵称")
-    private String creatorName;
+    @Schema(description = "抄送人意见")
+    private String reason;
+
+    @Schema(description = "创建人", requiredMode = Schema.RequiredMode.REQUIRED)
+    private UserSimpleBaseVO createUser;
 
-    @Schema(description = "抄送时间")
+    @Schema(description = "抄送时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
 }

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

@@ -3,20 +3,30 @@ 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;
 
-import javax.validation.constraints.AssertTrue;
+import java.util.Map;
 
-// TODO @jason:这个可以简化下,使用 @RequestParam。嘿嘿,主要 VO 项不要太多
 @Schema(description = "管理后台 - 审批详情 Request VO")
 @Data
 public class BpmApprovalDetailReqVO {
 
     @Schema(description = "流程定义的编号", example = "1024")
-    private String processDefinitionId;
+    private String processDefinitionId; // 使用场景:发起流程时,传流程定义 ID
+
+    @Schema(description = "流程变量")
+    private Map<String, Object> processVariables; // 使用场景:同 processDefinitionId,用于流程预测
 
     @Schema(description = "流程实例的编号", example = "1024")
-    private String processInstanceId;
+    private String processInstanceId;  // 使用场景:流程已发起时候传流程实例 ID
+
+    // TODO @芋艿:如果未来 BPMN 增加流程图,它没有发起人节点,会有问题。
+    @Schema(description = "流程活动编号", example = "StartUserNode")
+    private String activityId; // 用于获取表单权限。1)发起流程时,传“发起人节点” activityId 可获取发起人的表单权限;2)从抄送列表界面进来时,传抄送的 activityId 可获取抄送人的表单权限;
+
+    @Schema(description = "流程任务编号", example = "95f2f08b-621b-11ef-bf39-00ff4722db8b")
+    private String taskId; // 用于获取表单权限。1)从待审批/已审批界面进来时,传递 taskId 任务编号,可获取任务节点的变得权限
 
     @AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空")
     @JsonIgnore

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

@@ -1,10 +1,15 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
 
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 
 @Schema(description = "管理后台 - 审批详情 Response VO")
@@ -14,12 +19,28 @@ 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 = "活动节点列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<ActivityNode> activityNodes;
 
-    @Schema(description = "审批节点信息")
+    @Schema(description = "表单字段权限")
+    private Map<String, String> formFieldsPermission;
+
+    @Schema(description = "待办任务")
+    private BpmTaskRespVO todoTask;
+
+    /**
+     * 所属流程定义信息
+     */
+    private BpmProcessDefinitionRespVO processDefinition;
+
+    /**
+     * 所属流程实例信息
+     */
+    private BpmProcessInstanceRespVO processInstance;
+
+    @Schema(description = "活动节点信息")
     @Data
-    public static class ApprovalNodeInfo {
+    public static class ActivityNode {
 
         @Schema(description = "节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartUserNode")
         private String id;
@@ -39,42 +60,40 @@ public class BpmApprovalDetailRespVO {
         private LocalDateTime endTime;
 
         @Schema(description = "审批节点的任务信息")
-        private List<ApprovalTaskInfo> tasks;
+        private List<ActivityNodeTask> tasks;
 
-        @Schema(description = "候选人用户列表")
-        // TODO @jason:candidateUserList => candidateUsers,保持和 tasks 的命名风格一致哈
-        private List<User> candidateUserList; // 用于未运行任务节点
+        @Schema(description = "候选人策略", example = "35")
+        private Integer candidateStrategy; // 参见 BpmTaskCandidateStrategyEnum 枚举。主要用于发起时,审批节点、抄送节点自选
 
-    }
+        @Schema(description = "候选人用户 ID 列表", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1818")
+        @JsonIgnore // 不返回,只是方便后续读取,赋值给 candidateUsers
+        private List<Long> candidateUserIds;
 
-    // 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 = "候选人用户列表")
+        private List<UserSimpleBaseVO> candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表
 
     }
 
-    @Schema(description = "审批任务信息")
+    @Schema(description = "活动节点的任务信息")
     @Data
-    public static class ApprovalTaskInfo {
+    public static class ActivityNodeTask {
 
         @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
         private String id;
 
+        @Schema(description = "任务所属人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1818")
+        @JsonIgnore // 不返回,只是方便后续读取,赋值给 ownerUser
+        private Long owner;
+
         @Schema(description = "任务所属人", example = "1024")
-        private User ownerUser;
+        private UserSimpleBaseVO ownerUser;
+
+        @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048")
+        @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser
+        private Long assignee;
 
         @Schema(description = "任务分配人", example = "2048")
-        private User assigneeUser;
+        private UserSimpleBaseVO assigneeUser;
 
         @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
         private Integer status;  // 参见 BpmTaskStatusEnum 枚举

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

@@ -1,38 +0,0 @@
-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 lombok.Data;
-
-import javax.validation.constraints.AssertTrue;
-
-@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);
-    }
-
-}

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

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
+
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Set;
+
+@Schema(description = "管理后台 - 流程示例的 BPMN 视图 Response VO")
+@Data
+public class BpmProcessInstanceBpmnModelViewRespVO {
+
+    // ========== 基本信息 ==========
+
+    @Schema(description = "流程实例信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    private BpmProcessInstanceRespVO processInstance;
+
+    @Schema(description = "任务列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<BpmTaskRespVO> tasks;
+
+    @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String bpmnXml;
+
+    @Schema(description = "SIMPLE 模型")
+    private BpmSimpleModelNodeVO simpleModel;
+
+    // ========== 进度信息 ==========
+
+    @Schema(description = "进行中的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Set<String> unfinishedTaskActivityIds; // 只包括 UserTask
+
+    @Schema(description = "已经完成的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Set<String> finishedTaskActivityIds; // 包括 UserTask、Gateway 等,不包括 SequenceFlow
+
+    @Schema(description = "已经完成的连线节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Set<String> finishedSequenceFlowActivityIds; // 只包括 SequenceFlow
+
+    @Schema(description = "已经拒绝的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Set<String> rejectedTaskActivityIds; // 只包括 UserTask
+
+}

+ 2 - 17
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
 
+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;
@@ -44,7 +45,7 @@ public class BpmProcessInstanceRespVO {
     /**
      * 发起流程的用户
      */
-    private User startUser;
+    private UserSimpleBaseVO startUser;
 
     @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
     private String processDefinitionId;
@@ -58,22 +59,6 @@ public class BpmProcessInstanceRespVO {
      */
     private List<Task> tasks; // 仅在流程实例分页才返回
 
-    @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 = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-        private Long deptId;
-        @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部")
-        private String deptName;
-
-    }
-
     @Schema(description = "流程任务")
     @Data
     public static class Task {

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

@@ -1,10 +1,9 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
 import lombok.Data;
 
-import javax.validation.constraints.NotEmpty;
-import java.util.Collection;
 import java.util.Map;
 
 @Schema(description = "管理后台 - 通过流程任务的 Request VO")
@@ -19,9 +18,6 @@ public class BpmTaskApproveReqVO {
     @NotEmpty(message = "审批意见不能为空")
     private String reason;
 
-    @Schema(description = "抄送的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2")
-    private Collection<Long> copyUserIds;
-
     @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
     private Map<String, Object> variables;
 

+ 23 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.Collection;
+
+@Schema(description = "管理后台 - 抄送流程任务的 Request VO")
+@Data
+public class BpmTaskCopyReqVO {
+
+    @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotEmpty(message = "任务编号不能为空")
+    private String id;
+
+    @Schema(description = "抄送的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2]")
+    @NotEmpty(message = "抄送用户不能为空")
+    private Collection<Long> copyUserIds;
+
+    @Schema(description = "抄送意见", example = "帮忙看看!")
+    private String reason;
+}

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

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
 
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -33,14 +34,21 @@ public class BpmTaskRespVO {
     @Schema(description = "审批理由", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private String reason;
 
+    @Schema(description = "任务负责人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048")
+    @JsonIgnore // 不返回,只是方便后续读取,赋值给 ownerUser
+    private Long owner;
     /**
      * 负责人的用户信息
      */
-    private BpmProcessInstanceRespVO.User ownerUser;
+    private UserSimpleBaseVO ownerUser;
+
+    @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048")
+    @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser
+    private Long assignee;
     /**
      * 审核的用户信息
      */
-    private BpmProcessInstanceRespVO.User assigneeUser;
+    private UserSimpleBaseVO assigneeUser;
 
     @Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "Activity_one")
     private String taskDefinitionKey;
@@ -55,7 +63,7 @@ public class BpmTaskRespVO {
     @Schema(description = "父任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private String parentTaskId;
     @Schema(description = "子任务列表(由加签生成)", requiredMode = Schema.RequiredMode.REQUIRED, example = "childrenTask")
-    private List<BpmTaskRespVO> children;
+    private List<BpmTaskRespVO> children; // 由加签生成,包含多层子任务
 
     @Schema(description = "表单编号", example = "1024")
     private Long formId;
@@ -67,9 +75,6 @@ public class BpmTaskRespVO {
     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;
 
@@ -92,7 +97,7 @@ public class BpmTaskRespVO {
         /**
          * 发起人的用户信息
          */
-        private BpmProcessInstanceRespVO.User startUser;
+        private UserSimpleBaseVO startUser;
 
     }
 

+ 5 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java

@@ -5,7 +5,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotEmpty;
 
-@Schema(description = "管理后台 - 退流程任务的 Request VO")
+@Schema(description = "管理后台 - 退流程任务的 Request VO")
 @Data
 public class BpmTaskReturnReqVO {
 
@@ -13,12 +13,12 @@ public class BpmTaskReturnReqVO {
     @NotEmpty(message = "任务编号不能为空")
     private String id;
 
-    @Schema(description = "退到的任务 Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @NotEmpty(message = "退到的任务 Key 不能为空")
+    @Schema(description = "退到的任务 Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotEmpty(message = "退到的任务 Key 不能为空")
     private String targetTaskDefinitionKey;
 
-    @Schema(description = "退意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "我就是想驳回")
-    @NotEmpty(message = "退意见不能为空")
+    @Schema(description = "退意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "我就是想驳回")
+    @NotEmpty(message = "退意见不能为空")
     private String reason;
 
 }

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

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.bpm.convert.definition;
 
 import cn.hutool.core.util.ArrayUtil;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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;
@@ -22,6 +21,7 @@ import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 
@@ -37,25 +37,28 @@ public interface BpmModelConvert {
 
     BpmModelConvert INSTANCE = Mappers.getMapper(BpmModelConvert.class);
 
-    default PageResult<BpmModelRespVO> buildModelPage(PageResult<Model> pageResult,
-                                                      Map<Long, BpmFormDO> formMap,
-                                                      Map<String, BpmCategoryDO> categoryMap, Map<String, Deployment> deploymentMap,
-                                                      Map<String, ProcessDefinition> processDefinitionMap,
-                                                      Map<Long, AdminUserRespDTO> userMap) {
-        List<BpmModelRespVO> list = convertList(pageResult.getList(), model -> {
+    default List<BpmModelRespVO> buildModelList(List<Model> list,
+                                                Map<Long, BpmFormDO> formMap,
+                                                Map<String, BpmCategoryDO> categoryMap,
+                                                Map<String, Deployment> deploymentMap,
+                                                Map<String, ProcessDefinition> processDefinitionMap,
+                                                Map<Long, AdminUserRespDTO> userMap) {
+        List<BpmModelRespVO> result = convertList(list, 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;
+            ProcessDefinition processDefinition = model.getDeploymentId() != null ?
+                    processDefinitionMap.get(model.getDeploymentId()) : null;
             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());
+        // 排序
+        result.sort(Comparator.comparing(BpmModelMetaInfoVO::getSort));
+        return result;
     }
 
-    default BpmModelRespVO buildModel(Model model,
-                                     byte[] bpmnBytes) {
+    default BpmModelRespVO buildModel(Model model, byte[] bpmnBytes) {
         BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
         BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null, null);
         if (ArrayUtil.isNotEmpty(bpmnBytes)) {
@@ -112,6 +115,10 @@ public interface BpmModelConvert {
         if (vo.getStartUserIds() == null) {
             vo.setStartUserIds(Collections.emptyList());
         }
+        // 如果为空,兜底处理,使用 createTime 创建时间
+        if (vo.getSort() == null) {
+            vo.setSort(model.getCreateTime().getTime());
+        }
         return vo;
     }
 

+ 7 - 6
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java

@@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import org.flowable.bpmn.model.BpmnModel;
-import org.flowable.bpmn.model.UserTask;
 import org.flowable.common.engine.impl.db.SuspensionState;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.ProcessDefinition;
@@ -20,6 +19,7 @@ import org.mapstruct.Mapping;
 import org.mapstruct.MappingTarget;
 import org.mapstruct.factory.Mappers;
 
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 
@@ -47,7 +47,7 @@ public interface BpmProcessDefinitionConvert {
                                                                         Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap,
                                                                         Map<Long, BpmFormDO> formMap,
                                                                         Map<String, BpmCategoryDO> categoryMap) {
-        return CollectionUtils.convertList(list, definition -> {
+        List<BpmProcessDefinitionRespVO> result = CollectionUtils.convertList(list, definition -> {
             Deployment deployment = MapUtil.get(deploymentMap, definition.getDeploymentId(), Deployment.class);
             BpmProcessDefinitionInfoDO processDefinitionInfo = MapUtil.get(processDefinitionInfoMap, definition.getId(), BpmProcessDefinitionInfoDO.class);
             BpmFormDO form = null;
@@ -55,8 +55,11 @@ public interface BpmProcessDefinitionConvert {
                 form = MapUtil.get(formMap, processDefinitionInfo.getFormId(), BpmFormDO.class);
             }
             BpmCategoryDO category = MapUtil.get(categoryMap, definition.getCategory(), BpmCategoryDO.class);
-            return buildProcessDefinition(definition, deployment, processDefinitionInfo, form, category, null, null);
+            return buildProcessDefinition(definition, deployment, processDefinitionInfo, form, category, null);
         });
+        // 排序
+        result.sort(Comparator.comparing(BpmProcessDefinitionRespVO::getSort));
+        return result;
     }
 
     default BpmProcessDefinitionRespVO buildProcessDefinition(ProcessDefinition definition,
@@ -64,8 +67,7 @@ public interface BpmProcessDefinitionConvert {
                                                               BpmProcessDefinitionInfoDO processDefinitionInfo,
                                                               BpmFormDO form,
                                                               BpmCategoryDO category,
-                                                              BpmnModel bpmnModel,
-                                                              List<UserTask> startUserSelectUserTaskList) {
+                                                              BpmnModel bpmnModel) {
         BpmProcessDefinitionRespVO respVO = BeanUtils.toBean(definition, BpmProcessDefinitionRespVO.class);
         respVO.setSuspensionState(definition.isSuspended() ? SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode());
         // Deployment
@@ -87,7 +89,6 @@ public interface BpmProcessDefinitionConvert {
         // BpmnModel
         if (bpmnModel != null) {
             respVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnModel));
-            respVO.setStartUserSelectTasks(BeanUtils.toBean(startUserSelectUserTaskList, BpmProcessDefinitionRespVO.UserTask.class));
         }
         return respVO;
     }

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

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.bpm.convert.task;
-
-import cn.iocoder.yudao.framework.common.util.date.DateUtils;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
-import org.flowable.engine.history.HistoricActivityInstance;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.Mappings;
-import org.mapstruct.factory.Mappers;
-
-import java.util.List;
-
-/**
- * BPM 活动 Convert
- *
- * @author 芋道源码
- */
-@Mapper(uses = DateUtils.class)
-public interface BpmActivityConvert {
-
-    BpmActivityConvert INSTANCE = Mappers.getMapper(BpmActivityConvert.class);
-
-    List<BpmActivityRespVO> convertList(List<HistoricActivityInstance> list);
-
-    @Mappings({
-            @Mapping(source = "activityId", target = "key"),
-            @Mapping(source = "activityType", target = "type")
-    })
-    BpmActivityRespVO convert(HistoricActivityInstance bean);
-}

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

@@ -1,30 +1,47 @@
 package cn.iocoder.yudao.module.bpm.convert.task;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO;
 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.convert.definition.BpmProcessDefinitionConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceStatusEvent;
+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.BpmMessageSendWhenProcessInstanceApproveReqDTO;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
 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.repository.ProcessDefinition;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.Task;
+import org.flowable.task.api.history.HistoricTaskInstance;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.MappingTarget;
 import org.mapstruct.factory.Mappers;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
 /**
  * 流程实例 Convert
@@ -55,7 +72,7 @@ public interface BpmProcessInstanceConvert {
             if (userMap != null) {
                 AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(pageResult.getList().get(i).getStartUserId()));
                 if (startUser != null) {
-                    respVO.setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class));
+                    respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
                     MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName()));
                 }
             }
@@ -65,20 +82,18 @@ public interface BpmProcessInstanceConvert {
 
     default BpmProcessInstanceRespVO buildProcessInstance(HistoricProcessInstance processInstance,
                                                           ProcessDefinition processDefinition,
-                                                          BpmProcessDefinitionInfoDO processDefinitionExt,
-                                                          String bpmnXml,
+                                                          BpmProcessDefinitionInfoDO processDefinitionInfo,
                                                           AdminUserRespDTO startUser,
                                                           DeptRespDTO dept) {
         BpmProcessInstanceRespVO respVO = BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class);
-        respVO.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance));
-        respVO.setFormVariables(FlowableUtils.getProcessInstanceFormVariable(processInstance));
+        respVO.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance))
+                .setFormVariables(FlowableUtils.getProcessInstanceFormVariable(processInstance));
         // definition
         respVO.setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class));
-        copyTo(processDefinitionExt, respVO.getProcessDefinition());
-        respVO.getProcessDefinition().setBpmnXml(bpmnXml);
+        copyTo(processDefinitionInfo, respVO.getProcessDefinition());
         // user
         if (startUser != null) {
-            respVO.setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class));
+            respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
             if (dept != null) {
                 respVO.getStartUser().setDeptName(dept.getName());
             }
@@ -89,7 +104,7 @@ public interface BpmProcessInstanceConvert {
     @Mapping(source = "from.id", target = "to.id", ignore = true)
     void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to);
 
-    default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) {;
+    default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) {
         return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status)
                 .setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey());
     }
@@ -109,4 +124,156 @@ public interface BpmProcessInstanceConvert {
             .setStartUserId(NumberUtils.parseLong(instance.getStartUserId()));
     }
 
+    default BpmProcessInstanceBpmnModelViewRespVO buildProcessInstanceBpmnModelView(HistoricProcessInstance processInstance,
+                                                                                    List<HistoricTaskInstance> taskInstances,
+                                                                                    BpmnModel bpmnModel,
+                                                                                    BpmSimpleModelNodeVO simpleModel,
+                                                                                    Set<String> unfinishedTaskActivityIds,
+                                                                                    Set<String> finishedTaskActivityIds,
+                                                                                    Set<String> finishedSequenceFlowActivityIds,
+                                                                                    Set<String> rejectTaskActivityIds,
+                                                                                    Map<Long, AdminUserRespDTO> userMap,
+                                                                                    Map<Long, DeptRespDTO> deptMap) {
+        BpmProcessInstanceBpmnModelViewRespVO respVO = new BpmProcessInstanceBpmnModelViewRespVO();
+        // 基本信息
+        respVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class, o -> o
+                        .setStatus(FlowableUtils.getProcessInstanceStatus(processInstance)))
+                        .setStartUser(buildUser(processInstance.getStartUserId(), userMap, deptMap)));
+        respVO.setTasks(convertList(taskInstances, task -> BeanUtils.toBean(task, BpmTaskRespVO.class)
+                .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
+                .setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap))
+                .setOwnerUser(buildUser(task.getOwner(), userMap, deptMap))));
+        respVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnModel));
+        respVO.setSimpleModel(simpleModel);
+        // 进度信息
+        respVO.setUnfinishedTaskActivityIds(unfinishedTaskActivityIds)
+                .setFinishedTaskActivityIds(finishedTaskActivityIds)
+                .setFinishedSequenceFlowActivityIds(finishedSequenceFlowActivityIds)
+                .setRejectedTaskActivityIds(rejectTaskActivityIds);
+        return respVO;
+    }
+
+    default UserSimpleBaseVO buildUser(String userIdStr,
+                                       Map<Long, AdminUserRespDTO> userMap,
+                                       Map<Long, DeptRespDTO> deptMap) {
+        if (StrUtil.isEmpty(userIdStr)) {
+            return null;
+        }
+        Long userId = NumberUtils.parseLong(userIdStr);
+        return buildUser(userId, userMap, deptMap);
+    }
+
+    default UserSimpleBaseVO buildUser(Long userId,
+                                                    Map<Long, AdminUserRespDTO> userMap,
+                                                    Map<Long, DeptRespDTO> deptMap) {
+        if (userId == null) {
+            return null;
+        }
+        AdminUserRespDTO user = userMap.get(userId);
+        if (user == null) {
+            return null;
+        }
+        UserSimpleBaseVO userVO = BeanUtils.toBean(user, UserSimpleBaseVO.class);
+        DeptRespDTO dept = user.getDeptId() != null ? deptMap.get(user.getDeptId()) : null;
+        if (dept != null) {
+            userVO.setDeptName(dept.getName());
+        }
+        return userVO;
+    }
+
+    default BpmApprovalDetailRespVO.ActivityNodeTask buildApprovalTaskInfo(HistoricTaskInstance task) {
+        if (task == null) {
+            return null;
+        }
+        return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class)
+                .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task));
+    }
+
+    default Set<Long> parseUserIds(HistoricProcessInstance processInstance,
+                                   List<BpmApprovalDetailRespVO.ActivityNode> activityNodes,
+                                   BpmTaskRespVO todoTask) {
+        Set<Long> userIds = new HashSet<>();
+        if (processInstance != null) {
+            userIds.add(NumberUtils.parseLong(processInstance.getStartUserId()));
+        }
+        for (BpmApprovalDetailRespVO.ActivityNode activityNode : activityNodes) {
+            CollUtil.addAll(userIds, convertSet(activityNode.getTasks(), BpmApprovalDetailRespVO.ActivityNodeTask::getAssignee));
+            CollUtil.addAll(userIds, convertSet(activityNode.getTasks(), BpmApprovalDetailRespVO.ActivityNodeTask::getOwner));
+            CollUtil.addAll(userIds, activityNode.getCandidateUserIds());
+        }
+        if (todoTask != null) {
+            CollUtil.addIfAbsent(userIds, todoTask.getAssignee());
+            CollUtil.addIfAbsent(userIds, todoTask.getOwner());
+            if (CollUtil.isNotEmpty(todoTask.getChildren())) {
+                CollUtil.addAll(userIds, convertSet(todoTask.getChildren(), BpmTaskRespVO::getAssignee));
+                CollUtil.addAll(userIds, convertSet(todoTask.getChildren(), BpmTaskRespVO::getOwner));
+            }
+        }
+        return userIds;
+    }
+
+    default Set<Long> parseUserIds02(HistoricProcessInstance processInstance,
+                                     List<HistoricTaskInstance> tasks) {
+        Set<Long> userIds = SetUtils.asSet(Long.valueOf(processInstance.getStartUserId()));
+        tasks.forEach(task -> {
+            CollUtil.addIfAbsent(userIds, NumberUtils.parseLong((task.getAssignee())));
+            CollUtil.addIfAbsent(userIds, NumberUtils.parseLong((task.getOwner())));
+        });
+        return userIds;
+    }
+
+    default BpmApprovalDetailRespVO buildApprovalDetail(BpmnModel bpmnModel,
+                                                        ProcessDefinition processDefinition,
+                                                        BpmProcessDefinitionInfoDO processDefinitionInfo,
+                                                        HistoricProcessInstance processInstance,
+                                                        Integer processInstanceStatus,
+                                                        List<BpmApprovalDetailRespVO.ActivityNode> activityNodes,
+                                                        BpmTaskRespVO todoTask,
+                                                        Map<String, String> formFieldsPermission,
+                                                        Map<Long, AdminUserRespDTO> userMap,
+                                                        Map<Long, DeptRespDTO> deptMap) {
+        // 1.1 流程实例
+        BpmProcessInstanceRespVO processInstanceResp = null;
+        if (processInstance != null) {
+            AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
+            DeptRespDTO dept = startUser != null ? deptMap.get(startUser.getDeptId()) : null;
+            processInstanceResp = buildProcessInstance(processInstance, null, null, startUser, dept);
+        }
+
+        // 1.2 流程定义
+        BpmProcessDefinitionRespVO definitionResp = BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition(
+                processDefinition, null, processDefinitionInfo, null, null, bpmnModel);
+
+        // 1.3 流程节点
+        activityNodes.forEach(approveNode -> {
+            if (approveNode.getTasks() != null) {
+                approveNode.getTasks().forEach(task -> {
+                    task.setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap));
+                    task.setOwnerUser(buildUser(task.getOwner(), userMap, deptMap));
+                });
+            }
+            approveNode.setCandidateUsers(convertList(approveNode.getCandidateUserIds(), userId -> buildUser(userId, userMap, deptMap)));
+        });
+
+        // 1.4 待办任务
+        if (todoTask != null) {
+            todoTask.setAssigneeUser(buildUser(todoTask.getAssignee(), userMap, deptMap));
+            todoTask.setOwnerUser(buildUser(todoTask.getOwner(), userMap, deptMap));
+            if (CollUtil.isNotEmpty(todoTask.getChildren())) {
+                todoTask.getChildren().forEach(childTask -> {
+                    childTask.setAssigneeUser(buildUser(childTask.getAssignee(), userMap, deptMap));
+                    childTask.setOwnerUser(buildUser(childTask.getOwner(), userMap, deptMap));
+                });
+            }
+        }
+
+        // 2. 拼接起来
+        return new BpmApprovalDetailRespVO().setStatus(processInstanceStatus)
+                .setProcessDefinition(definitionResp)
+                .setProcessInstance(processInstanceResp)
+                .setFormFieldsPermission(formFieldsPermission)
+                .setTodoTask(todoTask)
+                .setActivityNodes(activityNodes);
+    }
+
 }

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

@@ -1,21 +1,19 @@
 package cn.iocoder.yudao.module.bpm.convert.task;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
-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.number.NumberUtils;
 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.base.user.UserSimpleBaseVO;
 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;
@@ -28,7 +26,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 
 /**
@@ -51,7 +49,7 @@ public interface BpmTaskConvert {
             }
             taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
             AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
-            taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class));
+            taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
         });
     }
 
@@ -65,7 +63,7 @@ public interface BpmTaskConvert {
             // 用户信息
             AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee()));
             if (assignUser != null) {
-                taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, BpmProcessInstanceRespVO.User.class));
+                taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class));
                 findAndThen(deptMap, assignUser.getDeptId(), dept -> taskVO.getAssigneeUser().setDeptName(dept.getName()));
             }
             // 流程实例
@@ -73,7 +71,7 @@ public interface BpmTaskConvert {
             if (processInstance != null) {
                 AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
                 taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
-                taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class));
+                taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
             }
             return taskVO;
         });
@@ -81,19 +79,17 @@ public interface BpmTaskConvert {
     }
 
     default List<BpmTaskRespVO> buildTaskListByProcessInstanceId(List<HistoricTaskInstance> taskList,
-                                                                 HistoricProcessInstance processInstance,
                                                                  Map<Long, BpmFormDO> formMap,
                                                                  Map<Long, AdminUserRespDTO> userMap,
-                                                                 Map<Long, DeptRespDTO> deptMap,
-                                                                 BpmnModel bpmnModel) {
-        List<BpmTaskRespVO> taskVOList = CollectionUtils.convertList(taskList, task -> {
+                                                                 Map<Long, DeptRespDTO> deptMap) {
+        return CollectionUtils.convertList(taskList, task -> {
+            // 特殊:已取消的任务,不返回
             BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class);
             Integer taskStatus = FlowableUtils.getTaskStatus(task);
+            if (BpmTaskStatusEnum.isCancelStatus(taskStatus)) {
+                return null;
+            }
             taskVO.setStatus(taskStatus).setReason(FlowableUtils.getTaskReason(task));
-            // 流程实例
-            AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
-            taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
-            taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class));
             // 表单信息
             BpmFormDO form = MapUtil.get(formMap, NumberUtils.parseLong(task.getFormKey()), BpmFormDO.class);
             if (form != null) {
@@ -101,35 +97,10 @@ public interface BpmTaskConvert {
                         .setFormFields(form.getFields()).setFormVariables(FlowableUtils.getTaskFormVariable(task));
             }
             // 用户信息
-            AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee()));
-            if (assignUser != null) {
-                taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, BpmProcessInstanceRespVO.User.class));
-                findAndThen(deptMap, assignUser.getDeptId(), dept -> taskVO.getAssigneeUser().setDeptName(dept.getName()));
-            }
-            AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(task.getOwner()));
-            if (ownerUser != null) {
-                taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, BpmProcessInstanceRespVO.User.class));
-                findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName()));
-            }
-            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;
+            buildTaskAssignee(taskVO, task.getAssignee(), userMap, deptMap);
+            buildTaskOwner(taskVO, task.getOwner(), userMap, deptMap);
+            return taskVO;
         });
-
-        // 拼接父子关系
-        Map<String, List<BpmTaskRespVO>> childrenTaskMap = convertMultiMap(
-                filterList(taskVOList, r -> StrUtil.isNotEmpty(r.getParentTaskId())),
-                BpmTaskRespVO::getParentTaskId);
-        for (BpmTaskRespVO taskVO : taskVOList) {
-            taskVO.setChildren(childrenTaskMap.get(taskVO.getId()));
-        }
-        return filterList(taskVOList, r -> StrUtil.isEmpty(r.getParentTaskId()));
     }
 
     default List<BpmTaskRespVO> buildTaskListByParentTaskId(List<Task> taskList,
@@ -138,7 +109,7 @@ public interface BpmTaskConvert {
         return convertList(taskList, task -> BeanUtils.toBean(task, BpmTaskRespVO.class, taskVO -> {
             AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee()));
             if (assignUser != null) {
-                taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, BpmProcessInstanceRespVO.User.class));
+                taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class));
                 DeptRespDTO dept = deptMap.get(assignUser.getDeptId());
                 if (dept != null) {
                     taskVO.getAssigneeUser().setDeptName(dept.getName());
@@ -146,12 +117,21 @@ public interface BpmTaskConvert {
             }
             AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(task.getOwner()));
             if (ownerUser != null) {
-                taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, BpmProcessInstanceRespVO.User.class));
+                taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, UserSimpleBaseVO.class));
                 findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName()));
             }
         }));
     }
 
+    default BpmTaskRespVO buildTodoTask(Task todoTask, List<Task> childrenTasks,
+                                              Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonsSetting) {
+        return BeanUtils.toBean(todoTask, BpmTaskRespVO.class)
+                .setStatus(FlowableUtils.getTaskStatus(todoTask)).setReason(FlowableUtils.getTaskReason(todoTask))
+                .setButtonsSetting(buttonsSetting)
+                .setChildren(convertList(childrenTasks, childTask -> BeanUtils.toBean(childTask, BpmTaskRespVO.class)
+                        .setStatus(FlowableUtils.getTaskStatus(childTask))));
+    }
+
     default BpmMessageSendWhenTaskCreatedReqDTO convert(ProcessInstance processInstance, AdminUserRespDTO startUser,
                                                         Task task) {
         BpmMessageSendWhenTaskCreatedReqDTO reqDTO = new BpmMessageSendWhenTaskCreatedReqDTO();
@@ -162,6 +142,42 @@ public interface BpmTaskConvert {
         return reqDTO;
     }
 
+    default void buildTaskOwner(BpmTaskRespVO task, String taskOwner,
+                                Map<Long, AdminUserRespDTO> userMap,
+                                Map<Long, DeptRespDTO> deptMap) {
+        AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(taskOwner));
+        if (ownerUser != null) {
+            task.setOwnerUser(BeanUtils.toBean(ownerUser, UserSimpleBaseVO.class));
+            findAndThen(deptMap, ownerUser.getDeptId(), dept -> task.getOwnerUser().setDeptName(dept.getName()));
+        }
+    }
+
+    default void buildTaskChildren(BpmTaskRespVO task, Map<String, List<Task>> childrenTaskMap,
+                                   Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+        List<Task> childTasks = childrenTaskMap.get(task.getId());
+        if (CollUtil.isNotEmpty(childTasks)) {
+            task.setChildren(
+                    convertList(childTasks, childTask -> {
+                        BpmTaskRespVO childTaskVO = BeanUtils.toBean(childTask, BpmTaskRespVO.class);
+                        childTaskVO.setStatus(FlowableUtils.getTaskStatus(childTask));
+                        buildTaskOwner(childTaskVO, childTask.getOwner(), userMap, deptMap);
+                        buildTaskAssignee(childTaskVO, childTask.getAssignee(), userMap, deptMap);
+                        return childTaskVO;
+                    })
+            );
+        }
+    }
+
+    default void buildTaskAssignee(BpmTaskRespVO task, String taskAssignee,
+                                   Map<Long, AdminUserRespDTO> userMap,
+                                   Map<Long, DeptRespDTO> deptMap) {
+        AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(taskAssignee));
+        if (assignUser != null) {
+            task.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class));
+            findAndThen(deptMap, assignUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName()));
+        }
+    }
+
     /**
      * 将父任务的属性,拷贝到子任务(加签任务)
      * <p>

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

@@ -1,6 +1,7 @@
 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.LongListTypeHandler;
 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;
@@ -122,6 +123,10 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
      * 目的:如果 false 不可见,则不展示在“发起流程”的列表里
      */
     private Boolean visible;
+    /**
+     * 排序值
+     */
+    private Long sort;
 
     /**
      * 可发起用户编号数组
@@ -134,7 +139,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
      * 1. {@link #visible} 只是决定是否可见。即使不可见,还是可以发起
      * 2. startUserIds 决定某个用户是否可以发起。如果该用户不可发起,则他也是不可见的
      */
-    @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
+    @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
     private List<Long> startUserIds;
 
     /**

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

@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import org.flowable.bpmn.model.FlowNode;
+import org.flowable.task.api.history.HistoricTaskInstance;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -54,23 +56,25 @@ public class BpmProcessInstanceCopyDO extends BaseDO {
      */
     private String category;
     /**
-     * 流程活动编号
+     * 流程活动编号
      * <p/>
-     * 对应 BPMN XML 节点编号,用于查询抄送节点的表单字段权限
-     * 这里冗余的原因:如果是钉钉易搭的抄送节点 (ServiceTask),使用 taskId 可能查不到对应的 activityId
+     *
+     * 冗余 {@link FlowNode#getId()},对应 BPMN XML 节点编号
+     * 原因:用于查询抄送节点的表单字段权限。因为仿钉钉/飞书的抄送节点 (ServiceTask),没有 taskId,只有 activityId
      */
     private String activityId;
     /**
-     * 任务主键
-     * 关联 Task 的 id 属性
+     * 流程活动的名字
+     *
+     * 冗余 {@link FlowNode#getName()}
      */
-    private String taskId;
+    private String activityName;
     /**
-     * 任务名称
+     * 流程活动的编号
      *
-     * 冗余 Task 的 name 属性
+     * 关联 {@link HistoricTaskInstance#getId()}
      */
-    private String taskName;
+    private String taskId;
 
     /**
      * 用户编号(被抄送的用户编号)
@@ -79,4 +83,9 @@ public class BpmProcessInstanceCopyDO extends BaseDO {
      */
     private Long userId;
 
+    /**
+     * 抄送意见
+     */
+    private String reason;
+
 }

+ 6 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.dal.mysql.definition;
 
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import org.apache.ibatis.annotations.Mapper;
@@ -18,4 +19,9 @@ public interface BpmProcessDefinitionInfoMapper extends BaseMapperX<BpmProcessDe
         return selectOne(BpmProcessDefinitionInfoDO::getProcessDefinitionId, processDefinitionId);
     }
 
+    default void updateByModelId(String modelId, BpmProcessDefinitionInfoDO updateObj) {
+        update(updateObj,
+                new LambdaQueryWrapperX<BpmProcessDefinitionInfoDO>().eq(BpmProcessDefinitionInfoDO::getModelId, modelId));
+    }
+
 }

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

@@ -53,7 +53,7 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
         @SuppressWarnings("unchecked")
         Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
         if (assigneeUserIds == null) {
-            assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
+            assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
             execution.setVariable(super.collectionVariable, assigneeUserIds);
             if (CollUtil.isEmpty(assigneeUserIds)) {
                 // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!

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

@@ -46,7 +46,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
         @SuppressWarnings("unchecked")
         Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
         if (assigneeUserIds == null) {
-            assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
+            assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
             execution.setVariable(super.collectionVariable, assigneeUserIds);
             if (CollUtil.isEmpty(assigneeUserIds)) {
                 // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!

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

@@ -57,7 +57,7 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
 
         // 情况二,如果非多实例的任务,则计算任务处理人
         // 第一步,先计算可处理该任务的处理人们
-        Set<Long> candidateUserIds = taskCandidateInvoker.calculateUsers(execution);
+        Set<Long> candidateUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
         if (CollUtil.isEmpty(candidateUserIds)) {
             return null;
         }

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

@@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.runtime.ProcessInstance;
@@ -89,32 +90,66 @@ public class BpmTaskCandidateInvoker {
      * @return 用户编号集合
      */
     @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人
-    public Set<Long> calculateUsers(DelegateExecution execution) {
+    public Set<Long> calculateUsersByTask(DelegateExecution execution) {
         // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过
-        Integer approveType = BpmnModelUtils.parseApproveType(execution.getCurrentFlowElement());
+        FlowElement flowElement = execution.getCurrentFlowElement();
+        Integer approveType = BpmnModelUtils.parseApproveType(flowElement);
         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);
+        Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement);
+        String param = BpmnModelUtils.parseCandidateParam(flowElement);
+        Set<Long> userIds = getCandidateStrategy(strategy).calculateUsersByTask(execution, param);
+        // 1.2 移除被禁用的用户
         removeDisableUsers(userIds);
+
+        // 2. 候选人为空时,根据“审批人为空”的配置补充
+        if (CollUtil.isEmpty(userIds)) {
+            userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
+                    .calculateUsersByTask(execution, param);
+            // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!!
+        }
+
+        // 3. 移除发起人的用户
+        ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class)
+                .getProcessInstance(execution.getProcessInstanceId());
+        Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId());
+        removeStartUserIfSkip(userIds, flowElement, Long.valueOf(processInstance.getStartUserId()));
+        return userIds;
+    }
+
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
+                                              Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过
+        FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId);
+        Integer approveType = BpmnModelUtils.parseApproveType(flowElement);
+        if (ObjectUtils.equalsAny(approveType,
+                BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
+                BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
+            return new HashSet<>();
+        }
+
+        // 1.1 计算任务的候选人
+        Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement);
+        String param = BpmnModelUtils.parseCandidateParam(flowElement);
+        Set<Long> userIds = getCandidateStrategy(strategy).calculateUsersByActivity(bpmnModel, activityId, param,
+                startUserId, processDefinitionId, processVariables);
         // 1.2 移除被禁用的用户
         removeDisableUsers(userIds);
 
         // 2. 候选人为空时,根据“审批人为空”的配置补充
         if (CollUtil.isEmpty(userIds)) {
             userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
-                    .calculateUsers(execution, param);
+                    .calculateUsersByActivity(bpmnModel, activityId, param, startUserId, processDefinitionId, processVariables);
             // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!!
         }
 
         // 3. 移除发起人的用户
-        removeStartUserIfSkip(execution, userIds);
+        removeStartUserIfSkip(userIds, flowElement, startUserId);
         return userIds;
     }
 
@@ -126,7 +161,7 @@ public class BpmTaskCandidateInvoker {
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(assigneeUserIds);
         assigneeUserIds.removeIf(id -> {
             AdminUserRespDTO user = userMap.get(id);
-            return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus());
+            return user == null || CommonStatusEnum.isDisable(user.getStatus());
         });
     }
 
@@ -135,25 +170,23 @@ public class BpmTaskCandidateInvoker {
      *
      * 注意:如果只有一个候选人,则不处理,避免无法审批
      *
-     * @param execution 执行中的任务
      * @param assigneeUserIds 当前分配的候选人
+     * @param flowElement 当前节点
+     * @param startUserId 发起人
      */
     @VisibleForTesting
-    void removeStartUserIfSkip(DelegateExecution execution, Set<Long> assigneeUserIds) {
+    void removeStartUserIfSkip(Set<Long> assigneeUserIds, FlowElement flowElement, Long startUserId) {
         if (CollUtil.size(assigneeUserIds) <= 1) {
             return;
         }
-        Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(execution.getCurrentFlowElement());
+        Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(flowElement);
         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()));
+        assigneeUserIds.remove(startUserId);
     }
 
-    public BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
+    private BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
         BpmTaskCandidateStrategyEnum strategyEnum = BpmTaskCandidateStrategyEnum.valueOf(strategy);
         Assert.notNull(strategyEnum, "策略(%s) 不存在", strategy);
         BpmTaskCandidateStrategy strategyObj = strategyMap.get(strategyEnum);

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

@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
 
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.engine.delegate.DelegateExecution;
-import org.flowable.engine.runtime.ProcessInstance;
 
-import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -42,47 +42,44 @@ public interface BpmTaskCandidateStrategy {
     /**
      * 基于候选人参数,获得任务的候选用户们
      *
+     * 注意:实现 calculateUsers 系列方法时,有两种选择:
+     * 1. 只重写 calculateUsers 默认方法
+     * 2. 都重写 calculateUsersByTask 和 calculateUsersByActivity 两个方法
+     *
      * @param param 执行任务
      * @return 用户编号集合
      */
     default Set<Long> calculateUsers(String param) {
-        return Collections.emptySet();
+        throw new UnsupportedOperationException("该分配方法未实现,请检查!");
     }
 
     /**
-     * 基于执行任务,获得任务的候选用户们
+     * 基于执行任务,获得任务的候选用户们
      *
      * @param execution 执行任务
      * @return 用户编号集合
      */
-    default Set<Long> calculateUsers(DelegateExecution execution, String param) {
-        Set<Long> users = calculateUsers(param);
-        removeDisableUsers(users);
-        return users;
+    default Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
+        return calculateUsers(param);
     }
 
     /**
-     * 基于流程实例,获得任务的候选用户们
+     * 基于【流程活动】,获得任务的候选用户们
      * <p>
      * 目的:用于获取未执行节点的候选用户们
      *
-     * @param startUserId  流程发起人编号
-     * @param processInstance 流程实例编号
-     * @param activityId 活动 Id (对应 Bpmn XML id)
+     * @param bpmnModel 流程图
+     * @param activityId 活动 ID (对应 Bpmn XML id)
      * @param param     节点的参数
+     * @param startUserId  流程发起人编号
+     * @param processDefinitionId 流程定义编号
+     * @param processVariables 流程变量
      * @return 用户编号集合
      */
-    default Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
-        Set<Long> users = calculateUsers(param);
-        removeDisableUsers(users);
-        return users;
+    @SuppressWarnings("unused")
+    default Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                               Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        return calculateUsers(param);
     }
 
-    /**
-     * 移除被禁用的用户
-     *
-     * @param users 用户 Ids
-     */
-    void removeDisableUsers(Set<Long> users);
-
 }

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

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

+ 20 - 7
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractDeptLeaderStrategy.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
 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 java.util.*;
 
@@ -14,14 +16,12 @@ import java.util.*;
  *
  * @author jason
  */
-public abstract class BpmTaskCandidateAbstractDeptLeaderStrategy extends  BpmTaskCandidateAbstractStrategy {
+public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy {
 
+    @Resource
     protected DeptApi deptApi;
-
-    public BpmTaskCandidateAbstractDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
-        super(adminUserApi);
-        this.deptApi = deptApi;
-    }
+    @Resource
+    protected AdminUserApi adminUserApi;
 
     /**
      * 获得指定层级的部门负责人,只有第 level 的负责人
@@ -75,4 +75,17 @@ public abstract class BpmTaskCandidateAbstractDeptLeaderStrategy extends  BpmTas
         return deptLeaderIds;
     }
 
+    /**
+     * 获取发起人的部门
+     *
+     * @param startUserId 发起人 Id
+     */
+    protected DeptRespDTO getStartUserDept(Long startUserId) {
+        AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
+        if (startUser.getDeptId() == null) { // 找不到部门
+            return null;
+        }
+        return deptApi.getDept(startUser.getDeptId());
+    }
+
 }

+ 11 - 11
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderMultiStrategy.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java

@@ -1,13 +1,12 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
 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.List;
 import java.util.Set;
 
 /**
@@ -16,11 +15,7 @@ import java.util.Set;
  * @author jason
  */
 @Component
-public class BpmTaskCandidateDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
-
-    public BpmTaskCandidateDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
-        super(adminUserApi, deptApi);
-    }
+public class BpmTaskCandidateDeptLeaderMultiStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -32,14 +27,19 @@ public class BpmTaskCandidateDeptLeaderMultiStrategy extends BpmTaskCandidateAbs
         // 参数格式: | 分隔: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");
+        List<Long> deptIds = StrUtils.splitToLong(params[0], ",");
+        int level = Integer.parseInt(params[1]);
+        // 校验部门存在
+        deptApi.validateDeptList(deptIds);
+        Assert.isTrue(level > 0, "部门层级必须大于 0");
     }
 
     @Override
     public Set<Long> calculateUsers(String param) {
         String[] params = param.split("\\|");
-        return getMultiLevelDeptLeaderIds(StrUtils.splitToLong(params[0], ","), Integer.valueOf(params[1]));
+        List<Long> deptIds = StrUtils.splitToLong(params[0], ",");
+        int level = Integer.parseInt(params[1]);
+        return super.getMultiLevelDeptLeaderIds(deptIds, level);
     }
 
 }

+ 5 - 9
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/dept/BpmTaskCandidateDeptLeaderStrategy.java

@@ -1,11 +1,11 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
 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.dept.dto.DeptRespDTO;
-import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Component;
 
 import java.util.List;
@@ -19,14 +19,10 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy {
 
-    private final DeptApi deptApi;
-
-    public BpmTaskCandidateDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
-        super(adminUserApi);
-        this.deptApi = deptApi;
-    }
+    @Resource
+    private DeptApi deptApi;
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {

+ 7 - 8
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/dept/BpmTaskCandidateDeptMemberStrategy.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
 import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
@@ -6,6 +6,7 @@ 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 jakarta.annotation.Resource;
 import org.springframework.stereotype.Component;
 
 import java.util.List;
@@ -19,14 +20,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateDeptMemberStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrategy {
 
-    private final DeptApi deptApi;
-
-    public BpmTaskCandidateDeptMemberStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
-        super(adminUserApi);
-        this.deptApi = deptApi;
-    }
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private AdminUserApi adminUserApi;
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {

+ 16 - 36
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java

@@ -1,21 +1,20 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
 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.bpmn.model.BpmnModel;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 import static cn.hutool.core.collection.ListUtil.toList;
@@ -26,16 +25,12 @@ import static cn.hutool.core.collection.ListUtil.toList;
  * @author jason
  */
 @Component
-public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
+public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
 
     @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;
@@ -43,48 +38,33 @@ public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends BpmTaskCan
 
     @Override
     public void validateParam(String param) {
-        // 参数是部门的层级
-        Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0");
+        int level = Integer.parseInt(param); // 参数是部门的层级
+        Assert.isTrue(level > 0, "部门的层级必须大于 0");
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
+        int level = Integer.parseInt(param); // 参数是部门的层级
         // 获得流程发起人
         ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
         Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
         // 获取发起人的 multi 部门负责人
-        DeptRespDTO dept = getStartUserDept(startUserId);
+        DeptRespDTO dept = super.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;
+        return super.getMultiLevelDeptLeaderIds(toList(dept.getId()), level);
     }
 
     @Override
-    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
-        DeptRespDTO dept = getStartUserDept(startUserId);
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                              Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        int level = Integer.parseInt(param); // 参数是部门的层级
+        DeptRespDTO dept = super.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());
+        return super.getMultiLevelDeptLeaderIds(toList(dept.getId()), level);
     }
 
 }

+ 13 - 33
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderStrategy.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java

@@ -1,21 +1,20 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
 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.bpmn.model.BpmnModel;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
@@ -26,7 +25,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
  * @author jason
  */
 @Component
-public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
+public class BpmTaskCandidateStartUserDeptLeaderStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
 
     @Resource
     @Lazy // 避免循环依赖
@@ -37,10 +36,6 @@ public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidat
         return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER;
     }
 
-    public BpmTaskCandidateStartUserDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
-        super(adminUserApi, deptApi);
-    }
-
     @Override
     public void validateParam(String param) {
         // 参数是部门的层级
@@ -48,44 +43,29 @@ public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidat
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsersByTask(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;
+        return getStartUserDeptLeader(startUserId, param);
     }
 
     @Override
-    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                              Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
         // 获取发起人的部门负责人
-        Set<Long> users =  getStartUserDeptLeader(startUserId, param);
-        removeDisableUsers(users);
-        return users;
+        return getStartUserDeptLeader(startUserId, param);
     }
 
     private Set<Long> getStartUserDeptLeader(Long startUserId, String param) {
-        DeptRespDTO dept = getStartUserDept(startUserId);
+        int level = Integer.parseInt(param); // 参数是部门的层级
+        DeptRespDTO dept = super.getStartUserDept(startUserId);
         if (dept == null) {
             return new HashSet<>();
         }
-        Long deptLeaderId = getAssignLevelDeptLeaderId(dept, Integer.valueOf(param)); // 参数是部门的层级
+        Long deptLeaderId = super.getAssignLevelDeptLeaderId(dept, level);
         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());
-    }
-
 }

+ 35 - 34
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/dept/BpmTaskCandidateStartUserSelectStrategy.java

@@ -1,20 +1,24 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
 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 com.google.common.collect.Sets;
+import jakarta.annotation.Resource;
 import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.ServiceTask;
+import org.flowable.bpmn.model.Task;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
 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,16 +27,12 @@ import java.util.*;
  * @author 芋道源码
  */
 @Component
-public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
 
     @Resource
     @Lazy // 延迟加载,避免循环依赖
     private BpmProcessInstanceService processInstanceService;
 
-    public BpmTaskCandidateStartUserSelectStrategy(AdminUserApi adminUserApi) {
-        super(adminUserApi);
-    }
-
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
         return BpmTaskCandidateStrategyEnum.START_USER_SELECT;
@@ -42,7 +42,12 @@ public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbs
     public void validateParam(String param) {}
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public boolean isParamRequired() {
+        return false;
+    }
+
+    @Override
+    public LinkedHashSet<Long> calculateUsersByTask(DelegateExecution execution, String param) {
         ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
         Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId());
         Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance);
@@ -50,47 +55,43 @@ public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbs
                 execution.getProcessInstanceId());
         // 获得审批人
         List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
-        Set<Long> users = new LinkedHashSet<>(assignees);
-        removeDisableUsers(users);
-        return users;
+        return new LinkedHashSet<>(assignees);
     }
 
     @Override
-    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
-        if (processInstance == null) {
-            return Collections.emptySet();
+    public LinkedHashSet<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                                        Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        if (processVariables == null) {
+            return Sets.newLinkedHashSet();
+        }
+        Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processVariables);
+        if (startUserSelectAssignees == null) {
+            return Sets.newLinkedHashSet();
         }
-        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
-    public boolean isParamRequired() {
-        return false;
+        return new LinkedHashSet<>(assignees);
     }
 
     /**
-     * 获得发起人自选审批人的 UserTask 列表
+     * 获得发起人自选审批人或抄送人的 Task 列表
      *
      * @param bpmnModel BPMN 模型
-     * @return UserTask 列表
+     * @return Task 列表
      */
-    public static List<UserTask> getStartUserSelectUserTaskList(BpmnModel bpmnModel) {
+    public static List<Task> getStartUserSelectTaskList(BpmnModel bpmnModel) {
         if (bpmnModel == null) {
-            return null;
+            return Collections.emptyList();
         }
-        List<UserTask> userTaskList = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
-        if (CollUtil.isEmpty(userTaskList)) {
-            return null;
+        List<Task> tasks = new ArrayList<>();
+        tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class));
+        tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, ServiceTask.class));
+        if (CollUtil.isEmpty(tasks)) {
+            return Collections.emptyList();
         }
-        userTaskList.removeIf(userTask -> !Objects.equals(BpmnModelUtils.parseCandidateStrategy(userTask),
+        tasks.removeIf(task -> ObjectUtil.notEqual(BpmnModelUtils.parseCandidateStrategy(task),
                 BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()));
-        return userTaskList;
+        return tasks;
     }
 
 }

+ 56 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormSDeptLeaderStrategy.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form;
+
+import cn.hutool.core.convert.Convert;
+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.candidate.strategy.dept.AbstractBpmTaskCandidateDeptLeaderStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 表单内部门负责人 {@link BpmTaskCandidateStrategy} 实现类
+ *
+ * @author jason
+ */
+@Component
+public class BpmTaskCandidateFormSDeptLeaderStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.FORM_DEPT_LEADER;
+    }
+
+    @Override
+    public void validateParam(String param) {
+        // 参数格式: | 分隔:1)左边为表单内部门字段。2)右边为部门层级
+        String[] params = param.split("\\|");
+        Assert.isTrue(params.length == 2, "参数格式不匹配");
+        Assert.notEmpty(param, "表单内部门字段不能为空");
+        int level = Integer.parseInt(params[1]);
+        Assert.isTrue(level > 0, "部门层级必须大于 0");
+    }
+
+    @Override
+    public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
+        String[] params = param.split("\\|");
+        Object result = execution.getVariable(params[0]);
+        int level = Integer.parseInt(params[1]);
+        return super.getMultiLevelDeptLeaderIds(Convert.toList(Long.class, result), level);
+    }
+
+    @Override
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
+                                              String param, Long startUserId, String processDefinitionId,
+                                              Map<String, Object> processVariables) {
+        String[] params = param.split("\\|");
+        Object result = processVariables == null ? null : processVariables.get(params[0]);
+        int level = Integer.parseInt(params[1]);
+        return super.getMultiLevelDeptLeaderIds(Convert.toList(Long.class, result), level);
+    }
+
+}

+ 47 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form;
+
+import cn.hutool.core.convert.Convert;
+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.candidate.strategy.user.BpmTaskCandidateUserStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 表单内用户字段 {@link BpmTaskCandidateUserStrategy} 实现类
+ *
+ * @author jason
+ */
+@Component
+public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrategy {
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.FORM_USER;
+    }
+
+    @Override
+    public void validateParam(String param) {
+        Assert.notEmpty(param, "表单内用户字段不能为空");
+    }
+
+    @Override
+    public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
+        Object result = execution.getVariable(param);
+        return Convert.toSet(Long.class, result);
+    }
+
+    @Override
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
+                                              String param, Long startUserId, String processDefinitionId,
+                                              Map<String, Object> processVariables) {
+        Object result = processVariables == null ? null : processVariables.get(param);
+        return Convert.toSet(Long.class, result);
+    }
+
+}

+ 22 - 15
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAssignEmptyStrategy.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other;
 
 import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
@@ -7,13 +7,15 @@ 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.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.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
@@ -23,16 +25,12 @@ import java.util.Set;
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateAssignEmptyStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateAssignEmptyStrategy implements BpmTaskCandidateStrategy {
 
     @Resource
     @Lazy // 延迟加载,避免循环依赖
     private BpmProcessDefinitionService processDefinitionService;
 
-    public BpmTaskCandidateAssignEmptyStrategy(AdminUserApi adminUserApi) {
-        super(adminUserApi);
-    }
-
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
         return BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY;
@@ -43,19 +41,28 @@ public class BpmTaskCandidateAssignEmptyStrategy extends BpmTaskCandidateAbstrac
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
+        return getCandidateUsers(execution.getProcessDefinitionId(), execution.getCurrentFlowElement());
+    }
+
+    @Override
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                              Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId);
+        return getCandidateUsers(processDefinitionId, flowElement);
+    }
+
+    private Set<Long> getCandidateUsers(String processDefinitionId, FlowElement flowElement) {
         // 情况一:指定人员审批
-        Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(execution.getCurrentFlowElement());
+        Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(flowElement);
         if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType())) {
-            Set<Long> users = new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(execution.getCurrentFlowElement()));
-            removeDisableUsers(users);
-            return users;
+            return new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(flowElement));
         }
 
         // 情况二:流程管理员
         if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType())) {
-            BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId());
-            Assert.notNull(processDefinition, "流程定义({})不存在", execution.getProcessDefinitionId());
+            BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processDefinitionId);
+            Assert.notNull(processDefinition, "流程定义({})不存在", processDefinitionId);
             return new HashSet<>(processDefinition.getManagerUserIds());
         }
 

+ 13 - 11
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategy.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java

@@ -1,13 +1,14 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other;
 
 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.bpmn.model.BpmnModel;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.springframework.stereotype.Component;
 
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -16,11 +17,7 @@ import java.util.Set;
  * @author 芋道源码
  */
 @Component
-public class BpmTaskCandidateExpressionStrategy extends BpmTaskCandidateAbstractStrategy {
-
-    public BpmTaskCandidateExpressionStrategy(AdminUserApi adminUserApi) {
-        super(adminUserApi);
-    }
+public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrategy {
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -33,11 +30,16 @@ public class BpmTaskCandidateExpressionStrategy extends BpmTaskCandidateAbstract
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
         Object result = FlowableUtils.getExpressionValue(execution, param);
-        Set<Long> users = Convert.toSet(Long.class, result);
-        removeDisableUsers(users);
-        return users;
+        return Convert.toSet(Long.class, result);
+    }
+
+    @Override
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                              Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        Object result = FlowableUtils.getExpressionValue(processVariables, param);
+        return Convert.toSet(Long.class, result);
     }
 
 }

+ 6 - 10
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/user/BpmTaskCandidateGroupStrategy.java

@@ -1,11 +1,11 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
 
 import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 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 cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Component;
 
 import java.util.Collection;
@@ -20,14 +20,10 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateGroupStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
 
-    private final BpmUserGroupService userGroupService;
-
-    public BpmTaskCandidateGroupStrategy(AdminUserApi adminUserApi, BpmUserGroupService userGroupService) {
-        super(adminUserApi);
-        this.userGroupService = userGroupService;
-    }
+    @Resource
+    private BpmUserGroupService userGroupService;
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -37,7 +33,7 @@ public class BpmTaskCandidateGroupStrategy extends BpmTaskCandidateAbstractStrat
     @Override
     public void validateParam(String param) {
         Set<Long> groupIds = StrUtils.splitToLongSet(param);
-        userGroupService.getUserGroupList(groupIds);
+        userGroupService.validUserGroups(groupIds);
     }
 
     @Override

+ 7 - 8
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/user/BpmTaskCandidatePostStrategy.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
 
 import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
@@ -6,6 +6,7 @@ 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 jakarta.annotation.Resource;
 import org.springframework.stereotype.Component;
 
 import java.util.List;
@@ -19,14 +20,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  * @author kyle
  */
 @Component
-public class BpmTaskCandidatePostStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
 
-    private final PostApi postApi;
-
-    public BpmTaskCandidatePostStrategy(AdminUserApi adminUserApi, PostApi postApi) {
-        super(adminUserApi);
-        this.postApi = postApi;
-    }
+    @Resource
+    private PostApi postApi;
+    @Resource
+    private AdminUserApi adminUserApi;
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {

+ 3 - 8
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/user/BpmTaskCandidateRoleStrategy.java

@@ -1,14 +1,13 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
 
 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.permission.PermissionApi;
 import cn.iocoder.yudao.module.system.api.permission.RoleApi;
-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,17 +16,13 @@ import java.util.Set;
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateRoleStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy {
 
     @Resource
     private RoleApi roleApi;
     @Resource
     private PermissionApi permissionApi;
 
-    public BpmTaskCandidateRoleStrategy(AdminUserApi adminUserApi) {
-        super(adminUserApi);
-    }
-
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
         return BpmTaskCandidateStrategyEnum.ROLE;

+ 11 - 16
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserStrategy.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java

@@ -1,15 +1,17 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
 
 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+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.user.AdminUserApi;
+import jakarta.annotation.Resource;
+import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -20,16 +22,12 @@ import java.util.Set;
  * @author jason
  */
 @Component
-public class BpmTaskCandidateStartUserStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateStartUserStrategy implements BpmTaskCandidateStrategy {
 
     @Resource
     @Lazy // 延迟加载,避免循环依赖
     private BpmProcessInstanceService processInstanceService;
 
-    public BpmTaskCandidateStartUserStrategy(AdminUserApi adminUserApi) {
-        super(adminUserApi);
-    }
-
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
         return BpmTaskCandidateStrategyEnum.START_USER;
@@ -45,18 +43,15 @@ public class BpmTaskCandidateStartUserStrategy extends BpmTaskCandidateAbstractS
     }
 
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+    public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
         ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
-        Set<Long> users =  SetUtils.asSet(Long.valueOf(processInstance.getStartUserId()));
-        removeDisableUsers(users);
-        return users;
+        return SetUtils.asSet(Long.valueOf(processInstance.getStartUserId()));
     }
 
     @Override
-    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
-        Set<Long> users = SetUtils.asSet(startUserId);
-        removeDisableUsers(users);
-        return users;
+    public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                              Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        return SetUtils.asSet(startUserId);
     }
 
 }

+ 6 - 7
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/candidate/strategy/user/BpmTaskCandidateUserStrategy.java

@@ -1,14 +1,14 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
 
 import cn.hutool.core.text.StrPool;
 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.user.AdminUserApi;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Component;
 
 import java.util.LinkedHashSet;
-import java.util.Set;
 
 /**
  * 用户 {@link BpmTaskCandidateStrategy} 实现类
@@ -16,11 +16,10 @@ import java.util.Set;
  * @author kyle
  */
 @Component
-public class BpmTaskCandidateUserStrategy extends BpmTaskCandidateAbstractStrategy {
+public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy {
 
-    public BpmTaskCandidateUserStrategy(AdminUserApi adminUserApi) {
-        super(adminUserApi);
-    }
+    @Resource
+    private AdminUserApi adminUserApi;
 
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -33,7 +32,7 @@ public class BpmTaskCandidateUserStrategy extends BpmTaskCandidateAbstractStrate
     }
 
     @Override
-    public Set<Long> calculateUsers(String param) {
+    public LinkedHashSet<Long> calculateUsers(String param) {
         return new LinkedHashSet<>(StrUtils.splitToLong(param, StrPool.COMMA));
     }
 

+ 5 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java

@@ -4,9 +4,10 @@ import org.flowable.common.engine.api.variable.VariableContainer;
 import org.flowable.common.engine.impl.el.function.AbstractFlowableVariableExpressionFunction;
 import org.springframework.stereotype.Component;
 
-// TODO @jason:这个自定义转换的原因是啥呀?
 /**
- * 根据流程变量 variable 的类型, 转换参数的值
+ * 根据流程变量 variable 的类型,转换参数的值
+ *
+ * 目前用于 ConditionNodeConvert 的 buildConditionExpression 方法中
  *
  * @author jason
  */
@@ -20,11 +21,12 @@ public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVar
     public static Object convertByType(VariableContainer variableContainer, String variableName, Object parmaValue) {
         Object variable = variableContainer.getVariable(variableName);
         if (variable != null && parmaValue != null) {
-            // 如果值不是字符串类型, 流程变量的类型是字符串。 把值转成字符串
+            // 如果值不是字符串类型,流程变量的类型是字符串,把值转成字符串
             if (!(parmaValue instanceof String) && variable instanceof String ) {
                 return parmaValue.toString();
             }
         }
         return parmaValue;
     }
+
 }

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java

@@ -29,6 +29,8 @@ public enum BpmTaskCandidateStrategyEnum implements IntArrayValuable {
     START_USER_DEPT_LEADER(37, "发起人部门负责人"),
     START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"),
     USER_GROUP(40, "用户组"),
+    FORM_USER(50, "表单内用户字段"),
+    FORM_DEPT_LEADER(51, "表单内部门负责人"),
     EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager
     ASSIGN_EMPTY(1, "审批人为空"),
     ;

+ 1 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java

@@ -52,7 +52,7 @@ public interface BpmnModelConstants {
      */
     String USER_TASK_REJECT_HANDLER_TYPE = "rejectHandlerType";
     /**
-     * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝后的退的任务 Id
+     * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝后的退的任务 Id
      */
     String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId";
 
@@ -104,10 +104,6 @@ public interface BpmnModelConstants {
      * BPMN Start Event Node Id
      */
     String START_EVENT_NODE_ID = "StartEvent";
-    /**
-     * BPMN Start Event Node Name
-     */
-    String START_EVENT_NODE_NAME = "开始";
 
     /**
      * 发起人节点 ID

+ 6 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java

@@ -29,7 +29,12 @@ public class BpmnVariableConstants {
      * @see ProcessInstance#getProcessVariables()
      */
     public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
-
+    /**
+     * 流程实例的变量 - 发起用户 ID
+     *
+     * @see ProcessInstance#getProcessVariables()
+     */
+    public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID";
     /**
      * 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id}
      *

+ 0 - 33
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/SimpleModelConstants.java

@@ -1,33 +0,0 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
-
-// TODO @jason:要不合并到 BpmnModelConstants 那
-/**
- * 仿钉钉快搭 JSON 常量信息
- *
- * @author jason
- */
-public interface SimpleModelConstants {
-
-    // TODO @芋艿:条件表达式的字段名
-
-    /**
-     * 网关节点默认序列流属性
-     */
-    String DEFAULT_FLOW_ATTRIBUTE = "defaultFlow";
-
-    /**
-     * 条件节点的条件类型属性
-     */
-    String CONDITION_TYPE_ATTRIBUTE = "conditionType";
-
-    /**
-     * 条件节点条件表达式属性
-     */
-    String CONDITION_EXPRESSION_ATTRIBUTE = "conditionExpression";
-
-    /**
-     * 条件规则的条件组属性
-     */
-    String CONDITION_GROUPS_ATTRIBUTE = "conditionGroups";
-
-}

+ 5 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java

@@ -15,8 +15,8 @@ import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCo
 
 /**
  * 处理抄送用户的 {@link JavaDelegate} 的实现类
- *
- * 目前只有快搭模式的【抄送节点】使用
+ * <p>
+ * 目前只有仿钉钉/飞书模式的【抄送节点】使用
  *
  * @author jason
  */
@@ -34,14 +34,14 @@ public class BpmCopyTaskDelegate implements JavaDelegate {
     @Override
     public void execute(DelegateExecution execution) {
         // 1. 获得抄送人
-        Set<Long> userIds = taskCandidateInvoker.calculateUsers(execution);
+        Set<Long> userIds = taskCandidateInvoker.calculateUsersByTask(execution);
         if (CollUtil.isEmpty(userIds)) {
             return;
         }
         // 2. 执行抄送
         FlowElement currentFlowElement = execution.getCurrentFlowElement();
-        processInstanceCopyService.createProcessInstanceCopy(userIds, execution.getProcessInstanceId(),
-                currentFlowElement.getId(), null, currentFlowElement.getName());
+        processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(),
+                currentFlowElement.getId(), currentFlowElement.getName(), null);
     }
 
 }

+ 2 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java

@@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
-import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import com.google.common.collect.ImmutableSet;
 import lombok.extern.slf4j.Slf4j;
@@ -44,9 +43,6 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
     @Resource
     @Lazy // 解决循环依赖
     private BpmTaskService taskService;
-    @Resource
-    @Lazy // 解决循环依赖
-    private BpmActivityService activityService;
 
     public static final Set<FlowableEngineEventType> TASK_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
             .add(FlowableEngineEventType.TASK_CREATED)
@@ -72,7 +68,7 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
 
     @Override
     protected void activityCancelled(FlowableActivityCancelledEvent event) {
-        List<HistoricActivityInstance> activityList = activityService.getHistoricActivityListByExecutionId(event.getExecutionId());
+        List<HistoricActivityInstance> activityList = taskService.getHistoricActivityListByExecutionId(event.getExecutionId());
         if (CollUtil.isEmpty(activityList)) {
             log.error("[activityCancelled][使用 executionId({}) 查找不到对应的活动实例]", event.getExecutionId());
             return;
@@ -87,6 +83,7 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
     }
 
     @Override
+    @SuppressWarnings("PatternVariableCanBeUsed")
     protected void timerFired(FlowableEngineEntityEvent event) {
         // 1.1 只处理 BoundaryEvent 边界计时时间
         String processDefinitionId = event.getProcessDefinitionId();

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

@@ -3,27 +3,116 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.string.StrUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
+import com.google.common.collect.Maps;
+import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.converter.BpmnXMLConverter;
 import org.flowable.bpmn.model.Process;
 import org.flowable.bpmn.model.*;
+import org.flowable.common.engine.api.FlowableException;
 import org.flowable.common.engine.impl.util.io.BytesStreamSource;
 
 import java.util.*;
 
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
 import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE;
+import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_PREFIX;
 
 /**
- * 流程模型转操作工具类
+ * BPMN Model 操作工具类。目前分成三部分:
+ *
+ * 1. BPMN 修改 + 解析元素相关的方法
+ * 2. BPMN 简单查找相关的方法
+ * 3. BPMN 复杂遍历相关的方法
+ * 4. BPMN 流程预测相关的方法
+ *
+ * @author 芋道源码
  */
+@Slf4j
 public class BpmnModelUtils {
 
+    // ========== BPMN 修改 + 解析元素相关的方法 ==========
+
+    public static void addExtensionElement(FlowElement element, String name, String value) {
+        if (value == null) {
+            return;
+        }
+        ExtensionElement extensionElement = new ExtensionElement();
+        extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
+        extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
+        extensionElement.setElementText(value);
+        extensionElement.setName(name);
+        element.addExtensionElement(extensionElement);
+    }
+
+    public static void addExtensionElement(FlowElement element, String name, Integer value) {
+        if (value == null) {
+            return;
+        }
+        addExtensionElement(element, name, String.valueOf(value));
+    }
+
+    public static void addExtensionElement(FlowElement element, String name, Map<String, String> attributes) {
+        if (attributes == null) {
+            return;
+        }
+        ExtensionElement extensionElement = new ExtensionElement();
+        extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
+        extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
+        extensionElement.setName(name);
+        attributes.forEach((key, value) -> {
+            ExtensionAttribute extensionAttribute = new ExtensionAttribute(key, value);
+            extensionAttribute.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
+            extensionElement.addAttribute(extensionAttribute);
+        });
+        element.addExtensionElement(extensionElement);
+    }
+
+    /**
+     * 解析扩展元素
+     *
+     * @param flowElement 节点
+     * @param elementName 元素名称
+     * @return 扩展元素
+     */
+    public static String parseExtensionElement(FlowElement flowElement, String elementName) {
+        if (flowElement == null) {
+            return null;
+        }
+        ExtensionElement element = CollUtil.getFirst(flowElement.getExtensionElements().get(elementName));
+        return element != null ? element.getElementText() : null;
+    }
+
+    /**
+     * 给节点添加候选人元素
+     *
+     * @param candidateStrategy 候选人策略
+     * @param candidateParam 候选人参数,允许空
+     * @param flowElement 节点
+     */
+    public static void addCandidateElements(Integer candidateStrategy, String candidateParam, FlowElement flowElement) {
+        addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY,
+                candidateStrategy == null ? null : candidateStrategy.toString());
+        addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM, candidateParam);
+    }
+
+    /**
+     * 解析候选人策略
+     *
+     * @param userTask 任务节点
+     * @return 候选人策略
+     */
     public static Integer parseCandidateStrategy(FlowElement userTask) {
         Integer candidateStrategy = NumberUtils.parseInt(userTask.getAttributeValue(
                 BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
@@ -35,6 +124,12 @@ public class BpmnModelUtils {
         return candidateStrategy;
     }
 
+    /**
+     * 解析候选人参数
+     *
+     * @param userTask 任务节点
+     * @return 候选人参数
+     */
     public static String parseCandidateParam(FlowElement userTask) {
         String candidateParam = userTask.getAttributeValue(
                 BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM);
@@ -45,39 +140,130 @@ public class BpmnModelUtils {
         return candidateParam;
     }
 
+    /**
+     * 解析审批类型
+     *
+     * @see BpmUserTaskApproveTypeEnum
+     * @param userTask 任务节点
+     * @return 审批类型
+     */
     public static Integer parseApproveType(FlowElement userTask) {
         return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE));
     }
 
+    /**
+     * 添加任务拒绝处理元素
+     *
+     * @param rejectHandler 任务拒绝处理
+     * @param userTask 任务节点
+     */
+    public static void addTaskRejectElements(BpmSimpleModelNodeVO.RejectHandler rejectHandler, UserTask userTask) {
+        if (rejectHandler == null) {
+            return;
+        }
+        addExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE, StrUtil.toStringOrNull(rejectHandler.getType()));
+        addExtensionElement(userTask, USER_TASK_REJECT_RETURN_TASK_ID, rejectHandler.getReturnNodeId());
+    }
+
+    /**
+     * 解析任务拒绝处理类型
+     *
+     * @param userTask 任务节点
+     * @return 任务拒绝处理类型
+     */
     public static BpmUserTaskRejectHandlerType parseRejectHandlerType(FlowElement userTask) {
         Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE));
         return BpmUserTaskRejectHandlerType.typeOf(rejectHandlerType);
     }
 
+    /**
+     * 解析任务拒绝返回任务节点 ID
+     *
+     * @param flowElement 任务节点
+     * @return 任务拒绝返回任务节点 ID
+     */
     public static String parseReturnTaskId(FlowElement flowElement) {
         return parseExtensionElement(flowElement, USER_TASK_REJECT_RETURN_TASK_ID);
     }
 
+    /**
+     * 给节点添加用户任务的审批人与发起人相同时,处理类型枚举
+     *
+     * @see BpmUserTaskAssignStartUserHandlerTypeEnum
+     * @param assignStartUserHandlerType 发起人处理类型
+     * @param userTask 任务节点
+     */
+    public static void addAssignStartUserHandlerType(Integer assignStartUserHandlerType, UserTask userTask) {
+        if (assignStartUserHandlerType == null) {
+            return;
+        }
+        addExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, assignStartUserHandlerType.toString());
+    }
+
+    /**
+     * 给节点添加用户任务的审批人为空时,处理类型枚举
+     *
+     * @see BpmUserTaskAssignEmptyHandlerTypeEnum
+     * @param emptyHandler 空处理
+     * @param userTask 任务节点
+     */
+    public static void addAssignEmptyHandlerType(BpmSimpleModelNodeVO.AssignEmptyHandler emptyHandler, UserTask userTask) {
+        if (emptyHandler == null) {
+            return;
+        }
+        addExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE, StrUtil.toStringOrNull(emptyHandler.getType()));
+        addExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS, StrUtil.join(",", emptyHandler.getUserIds()));
+    }
+
+    /**
+     * 解析用户任务的审批人与发起人相同时,处理类型枚举
+     *
+     * @param userTask 任务节点
+     * @return 处理类型枚举
+     */
     public static Integer parseAssignStartUserHandlerType(FlowElement userTask) {
         return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE));
     }
 
+    /**
+     * 解析用户任务的审批人为空时,处理类型枚举
+     *
+     * @param userTask 任务节点
+     * @return 处理类型枚举
+     */
     public static Integer parseAssignEmptyHandlerType(FlowElement userTask) {
         return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE));
     }
 
+    /**
+     * 解析用户任务的审批人为空时,处理用户 ID 数组
+     *
+     * @param userTask 任务节点
+     * @return 处理用户 ID 数组
+     */
     public static List<Long> parseAssignEmptyHandlerUserIds(FlowElement userTask) {
         return StrUtils.splitToLong(parseExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS), ",");
     }
 
-    public static String parseExtensionElement(FlowElement flowElement, String elementName) {
-        if (flowElement == null) {
-            return null;
+    /**
+     * 给节点添加表单字段权限元素
+     *
+     * @param fieldsPermissions 表单字段权限
+     * @param flowElement 节点
+     */
+    public static void addFormFieldsPermission(List<Map<String, String>> fieldsPermissions, FlowElement flowElement) {
+        if (CollUtil.isNotEmpty(fieldsPermissions)) {
+            fieldsPermissions.forEach(item -> addExtensionElement(flowElement, FORM_FIELD_PERMISSION_ELEMENT, item));
         }
-        ExtensionElement element = CollUtil.getFirst(flowElement.getExtensionElements().get(elementName));
-        return element != null ? element.getElementText() : null;
     }
 
+    /**
+     * 解析表单字段权限
+     *
+     * @param bpmnModel bpmnModel 对象
+     * @param flowElementId 元素 ID
+     * @return 表单字段权限
+     */
     public static Map<String, String> parseFormFieldsPermission(BpmnModel bpmnModel, String flowElementId) {
         if (bpmnModel == null || StrUtil.isEmpty(flowElementId)) {
             return null;
@@ -101,6 +287,29 @@ public class BpmnModelUtils {
         return fieldsPermission;
     }
 
+    /**
+     * 给节点添加操作按钮设置元素
+     */
+    public static void addButtonsSetting(List<BpmSimpleModelNodeVO.OperationButtonSetting> buttonsSetting, UserTask userTask) {
+        if (CollUtil.isNotEmpty(buttonsSetting)) {
+            List<Map<String, String>> list = CollectionUtils.convertList(buttonsSetting, item -> {
+                Map<String, String> settingMap = Maps.newHashMapWithExpectedSize(3);
+                settingMap.put(BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE, String.valueOf(item.getId()));
+                settingMap.put(BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE, item.getDisplayName());
+                settingMap.put(BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE, String.valueOf(item.getEnable()));
+                return settingMap;
+            });
+            list.forEach(item -> addExtensionElement(userTask, BUTTON_SETTING_ELEMENT, item));
+        }
+    }
+
+    /**
+     * 解析操作按钮设置
+     *
+     * @param bpmnModel bpmnModel 对象
+     * @param flowElementId 元素 ID
+     * @return 操作按钮设置
+     */
     public static Map<Integer, BpmTaskRespVO.OperationButtonSetting> parseButtonsSetting(BpmnModel bpmnModel, String flowElementId) {
         FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
         if (flowElement == null) {
@@ -110,7 +319,7 @@ public class BpmnModelUtils {
         if (CollUtil.isEmpty(extensionElements)) {
             return null;
         }
-        Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonSettings = MapUtil.newHashMap(16);
+        Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonSettings = Maps.newHashMapWithExpectedSize(extensionElements.size());
         extensionElements.forEach(element -> {
             String id = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE);
             String displayName = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE);
@@ -123,6 +332,23 @@ public class BpmnModelUtils {
         return buttonSettings;
     }
 
+    /**
+     * 解析边界事件扩展元素
+     *
+     * @param boundaryEvent 边界事件
+     * @param customElement 元素
+     * @return 扩展元素
+     */
+    public static String parseBoundaryEventExtensionElement(BoundaryEvent boundaryEvent, String customElement) {
+        if (boundaryEvent == null) {
+            return null;
+        }
+        ExtensionElement extensionElement = CollUtil.getFirst(boundaryEvent.getExtensionElements().get(customElement));
+        return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null);
+    }
+
+    // ========== BPM 简单查找相关的方法 ==========
+
     /**
      * 根据节点,获取入口连线
      *
@@ -168,15 +394,14 @@ public class BpmnModelUtils {
      * @param clazz 指定元素。例如说,{@link UserTask}、{@link Gateway} 等等
      * @return 元素们
      */
+    @SuppressWarnings("unchecked")
     public static <T extends FlowElement> List<T> getBpmnModelElements(BpmnModel model, Class<T> clazz) {
         List<T> result = new ArrayList<>();
-        model.getProcesses().forEach(process -> {
-            process.getFlowElements().forEach(flowElement -> {
-                if (flowElement.getClass().isAssignableFrom(clazz)) {
-                    result.add((T) flowElement);
-                }
-            });
-        });
+        model.getProcesses().forEach(process -> process.getFlowElements().forEach(flowElement -> {
+            if (flowElement.getClass().isAssignableFrom(clazz)) {
+                result.add((T) flowElement);
+            }
+        }));
         return result;
     }
 
@@ -193,7 +418,7 @@ public class BpmnModelUtils {
 
     public static EndEvent getEndEvent(BpmnModel model) {
         Process process = model.getMainProcess();
-        // 从 flowElementList 找 endEvent. TODO 多个 EndEvent 会有问题
+        // 从 flowElementList 找 endEvent
         return (EndEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof EndEvent);
     }
 
@@ -221,7 +446,7 @@ public class BpmnModelUtils {
         return StrUtil.utf8Str(bpmnBytes);
     }
 
-    // ========== 遍历相关的方法 ==========
+    // ========== BPMN 复杂遍历相关的方法 ==========
 
     /**
      * 找到 source 节点之前的所有用户任务节点
@@ -316,16 +541,16 @@ public class BpmnModelUtils {
         return userTaskList;
     }
 
-
     /**
      * 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行
-     * 不存在直接退到子流程中的情况,但存在从子流程出去到父流程情况
+     * 不存在直接退到子流程中的情况,但存在从子流程出去到父流程情况
      *
      * @param source          起始节点
      * @param target          目标节点
      * @param visitedElements 已经经过的连线的 ID,用于判断线路是否重复
      * @return 结果
      */
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
     public static boolean isSequentialReachable(FlowElement source, FlowElement target, Set<String> visitedElements) {
         visitedElements = visitedElements == null ? new HashSet<>() : visitedElements;
         // 不能是开始事件和子流程
@@ -436,11 +661,136 @@ public class BpmnModelUtils {
         return userTaskList;
     }
 
-    public static String parseBoundaryEventExtensionElement(BoundaryEvent boundaryEvent, String customElement) {
-        if (boundaryEvent == null) {
-            return null;
+    // ========== BPMN 流程预测相关的方法 ==========
+
+    /**
+     * 流程预测,返回 StartEvent、UserTask、ServiceTask、EndEvent 节点元素,最终是 List 串行结果
+     *
+     * @param bpmnModel BPMN 图
+     * @param variables 变量
+     * @return 节点元素数组
+     */
+    public static List<FlowElement> simulateProcess(BpmnModel bpmnModel, Map<String, Object> variables) {
+        List<FlowElement> resultElements = new ArrayList<>();
+        Set<FlowElement> visitElements = new HashSet<>();
+
+        // 从 StartEvent 开始遍历
+        StartEvent startEvent = getStartEvent(bpmnModel);
+        simulateNextFlowElements(startEvent, variables, resultElements, visitElements);
+
+        // 将 EndEvent 放在末尾。原因是,DFS 遍历,可能 EndEvent 在 resultElements 中
+        List<FlowElement> endEvents = CollUtil.removeWithAddIf(resultElements,
+                flowElement -> flowElement instanceof EndEvent);
+        resultElements.addAll(endEvents);
+        return resultElements;
+    }
+
+    @SuppressWarnings("PatternVariableCanBeUsed")
+    private static void simulateNextFlowElements(FlowElement currentElement, Map<String, Object> variables,
+                                                 List<FlowElement> resultElements, Set<FlowElement> visitElements) {
+        // 如果为空,或者已经遍历过,则直接结束
+        if (currentElement == null) {
+            return;
+        }
+        if (visitElements.contains(currentElement)) {
+            return;
+        }
+        visitElements.add(currentElement);
+
+        // 情况:StartEvent/EndEvent/UserTask/ServiceTask
+        if (currentElement instanceof StartEvent
+            || currentElement instanceof EndEvent
+            || currentElement instanceof UserTask
+            || currentElement instanceof ServiceTask) {
+            // 添加元素
+            FlowNode flowNode = (FlowNode) currentElement;
+            resultElements.add(flowNode);
+            // 遍历子节点
+            flowNode.getOutgoingFlows().forEach(
+                    nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements));
+            return;
+        }
+
+        // 情况:ExclusiveGateway 排它,只有一个满足条件的。如果没有,就走默认的
+        if (currentElement instanceof ExclusiveGateway) {
+            // 查找满足条件的 SequenceFlow 路径
+            Gateway gateway = (Gateway) currentElement;
+            SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
+                    flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
+                            && evalConditionExpress(variables, flow.getConditionExpression()));
+            if (matchSequenceFlow == null) {
+                matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
+                        flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()));
+                // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
+                if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) {
+                    matchSequenceFlow = gateway.getOutgoingFlows().get(0);
+                }
+            }
+            // 遍历满足条件的 SequenceFlow 路径
+            if (matchSequenceFlow != null) {
+                simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements);
+            }
+            return;
+        }
+
+        // 情况:InclusiveGateway 包容,多个满足条件的。如果没有,就走默认的
+        if (currentElement instanceof InclusiveGateway) {
+            // 查找满足条件的 SequenceFlow 路径
+            Gateway gateway = (Gateway) currentElement;
+            Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
+                    flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
+                            && evalConditionExpress(variables, flow.getConditionExpression()));
+            if (CollUtil.isEmpty(matchSequenceFlows)) {
+                matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
+                        flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()));
+                // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
+                if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) {
+                    matchSequenceFlows = gateway.getOutgoingFlows();
+                }
+            }
+            // 遍历满足条件的 SequenceFlow 路径
+            matchSequenceFlows.forEach(
+                    flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements));
+        }
+
+        // 情况:ParallelGateway 并行,都满足,都走
+        if (currentElement instanceof ParallelGateway) {
+            Gateway gateway = (Gateway) currentElement;
+            // 遍历子节点
+            gateway.getOutgoingFlows().forEach(
+                    nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements));
+            return;
+        }
+    }
+
+    /**
+     * 计算条件表达式是否为 true 满足条件
+     *
+     * @param variables 流程实例
+     * @param express 条件表达式
+     * @return 是否满足条件
+     */
+    public static boolean evalConditionExpress(Map<String, Object> variables, String express) {
+        if (express == null) {
+            return Boolean.FALSE;
+        }
+        try {
+            Object result = FlowableUtils.getExpressionValue(variables, express);
+            return Boolean.TRUE.equals(result);
+        } catch (FlowableException ex) {
+            log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错", express, variables, ex);
+            return Boolean.FALSE;
         }
-        ExtensionElement extensionElement = CollUtil.getFirst(boundaryEvent.getExtensionElements().get(customElement));
-        return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null);
     }
+
+    @SuppressWarnings("PatternVariableCanBeUsed")
+    public static boolean isSequentialUserTask(FlowElement flowElement) {
+        if (!(flowElement instanceof UserTask)) {
+            return false;
+        }
+        UserTask userTask = (UserTask) flowElement;
+        MultiInstanceLoopCharacteristics loopCharacteristics = userTask.getLoopCharacteristics();
+        return loopCharacteristics != null && loopCharacteristics.isSequential();
+    }
+
 }

+ 62 - 9
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
@@ -8,6 +9,8 @@ import org.flowable.common.engine.api.delegate.Expression;
 import org.flowable.common.engine.api.variable.VariableContainer;
 import org.flowable.common.engine.impl.el.ExpressionManager;
 import org.flowable.common.engine.impl.identity.Authentication;
+import org.flowable.common.engine.impl.variable.MapDelegateVariableContainer;
+import org.flowable.engine.ManagementService;
 import org.flowable.engine.ProcessEngineConfiguration;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
@@ -94,6 +97,27 @@ public class FlowableUtils {
         return (Integer) processVariables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
     }
 
+    /**
+     * 获得流程实例的审批原因
+     *
+     * @param processInstance 流程实例
+     * @return 审批原因
+     */
+    public static String getProcessInstanceReason(HistoricProcessInstance processInstance) {
+        return (String) processInstance.getProcessVariables().get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON);
+    }
+
+    /**
+     * 获得流程实例的表单
+     *
+     * @param processInstance 流程实例
+     * @return 表单
+     */
+    public static Map<String, Object> getProcessInstanceFormVariable(ProcessInstance processInstance) {
+        Map<String, Object> processVariables = new HashMap<>(processInstance.getProcessVariables());
+        return filterProcessInstanceFormVariable(processVariables);
+    }
+
     /**
      * 获得流程实例的表单
      *
@@ -101,9 +125,8 @@ public class FlowableUtils {
      * @return 表单
      */
     public static Map<String, Object> getProcessInstanceFormVariable(HistoricProcessInstance processInstance) {
-        Map<String, Object> formVariables = new HashMap<>(processInstance.getProcessVariables());
-        filterProcessInstanceFormVariable(formVariables);
-        return formVariables;
+        Map<String, Object> processVariables = new HashMap<>(processInstance.getProcessVariables());
+        return filterProcessInstanceFormVariable(processVariables);
     }
 
     /**
@@ -125,9 +148,22 @@ public class FlowableUtils {
      * @param processInstance 流程实例
      * @return 发起用户选择的审批人 Map
      */
-    @SuppressWarnings("unchecked")
     public static Map<String, List<Long>> getStartUserSelectAssignees(ProcessInstance processInstance) {
-        return (Map<String, List<Long>>) processInstance.getProcessVariables().get(
+        return processInstance != null ? getStartUserSelectAssignees(processInstance.getProcessVariables()) : null;
+    }
+
+    /**
+     * 获得流程实例的发起用户选择的审批人 Map
+     *
+     * @param processVariables 流程变量
+     * @return 发起用户选择的审批人 Map
+     */
+    @SuppressWarnings("unchecked")
+    public static Map<String, List<Long>> getStartUserSelectAssignees(Map<String, Object> processVariables) {
+        if (processVariables == null) {
+            return null;
+        }
+        return (Map<String, List<Long>>) processVariables.get(
                 BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
     }
 
@@ -181,13 +217,30 @@ public class FlowableUtils {
 
     // ========== Expression 相关的工具方法 ==========
 
-    public static Object getExpressionValue(VariableContainer variableContainer, String expressionString) {
-        ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration();
-        assert processEngineConfiguration != null;
+    private static Object getExpressionValue(VariableContainer variableContainer, String expressionString,
+                                             ProcessEngineConfigurationImpl processEngineConfiguration) {
+        assert processEngineConfiguration!= null;
         ExpressionManager expressionManager = processEngineConfiguration.getExpressionManager();
-        assert expressionManager != null;
+        assert expressionManager!= null;
         Expression expression = expressionManager.createExpression(expressionString);
         return expression.getValue(variableContainer);
     }
 
+    public static Object getExpressionValue(VariableContainer variableContainer, String expressionString) {
+        ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration();
+        if (processEngineConfiguration != null) {
+            return getExpressionValue(variableContainer, expressionString, processEngineConfiguration);
+        }
+        // 如果 ProcessEngineConfigurationImpl 获取不到,则需要通过 ManagementService 来获取
+        ManagementService managementService = SpringUtil.getBean(ManagementService.class);
+        assert managementService != null;
+        return managementService.executeCommand(context ->
+                getExpressionValue(variableContainer, expressionString, CommandContextUtil.getProcessEngineConfiguration()));
+    }
+
+    public static Object getExpressionValue(Map<String, Object> variable, String expressionString) {
+        VariableContainer variableContainer = new MapDelegateVariableContainer(variable, VariableContainer.empty());
+        return getExpressionValue(variableContainer, expressionString);
+    }
+
 }

+ 533 - 478
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java

@@ -7,65 +7,47 @@ import cn.hutool.core.util.*;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 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.BpmSimpleModelNodeVO.ConditionGroups;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.RejectHandler;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModeConditionType;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.*;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate;
 import org.flowable.bpmn.BpmnAutoLayout;
+import org.flowable.bpmn.constants.BpmnXMLConstants;
 import org.flowable.bpmn.model.Process;
 import org.flowable.bpmn.model.*;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import static cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.OperationButtonSetting;
-import static cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TimeoutHandler;
-import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.*;
-import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum.*;
-import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum.USER;
-import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP;
-import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum.REMINDER;
-import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum.START_USER;
+import java.util.*;
+
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
-import static org.flowable.bpmn.constants.BpmnXMLConstants.*;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
+import static java.util.Arrays.asList;
 
 /**
- * 仿钉钉快搭模型相关的工具方法
+ * 仿钉钉/飞书的模型相关的工具方法
+ * <p>
+ * 1. 核心的逻辑实现,可见 {@link #buildBpmnModel(String, String, BpmSimpleModelNodeVO)} 方法
+ * 2. 所有的 BpmSimpleModelNodeVO 转换成 BPMN FlowNode 元素,可见 {@link NodeConvert} 实现类
  *
  * @author jason
  */
 public class SimpleModelUtils {
 
-    /**
-     * 聚合网关节点 Id 后缀
-     */
-    public static final String JOIN_GATE_WAY_NODE_ID_SUFFIX = "_join";
-
-    /**
-     * 所有审批人同意的表达式
-     */
-    public static final String ALL_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances >= nrOfInstances }";
-
-    /**
-     * 任一一名审批人同意的表达式
-     */
-    public static final String ANY_OF_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances > 0 }";
-
-    /**
-     * 按通过比例完成表达式
-     */
-    public static final String APPROVE_BY_RATIO_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances/nrOfInstances >= %s}";
+    private static final Map<BpmSimpleModelNodeType, NodeConvert> NODE_CONVERTS = MapUtil.newHashMap();
 
-    // TODO @yunai:注释需要完善下;
+    static {
+        List<NodeConvert> converts = asList(new StartNodeConvert(), new EndNodeConvert(),
+                new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(),
+                new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert());
+        converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert));
+    }
 
     /**
-     * 仿钉钉流程设计模型数据结构(json) 转换成 Bpmn Model (待完善)
+     * 仿钉钉流程设计模型数据结构(json)转换成 Bpmn Model
+     * <p>
+     * 整体逻辑如下:
+     * 1. 创建:BpmnModel、Process 对象
+     * 2. 转换:将 BpmSimpleModelNodeVO 转换成 BPMN FlowNode 元素
+     * 3. 连接:构建并添加节点之间的连线 Sequence Flow
      *
      * @param processId       流程标识
      * @param processName     流程名称
@@ -73,42 +55,76 @@ public class SimpleModelUtils {
      * @return Bpmn Model
      */
     public static BpmnModel buildBpmnModel(String processId, String processName, BpmSimpleModelNodeVO simpleModelNode) {
+        // 1. 创建 BpmnModel
         BpmnModel bpmnModel = new BpmnModel();
-        // 不加这个 解析 Message 会报 NPE 异常 .
-        bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason:待定:是不是搞个自定义的 namespace;
-        // TODO 芋艿:后续在 review
-
+        bpmnModel.setTargetNamespace(BpmnXMLConstants.BPMN2_NAMESPACE); // 设置命名空间。不加这个,解析 Message 会报 NPE 异常
+        // 创建 Process 对象
         Process process = new Process();
         process.setId(processId);
         process.setName(processName);
-        process.setExecutable(Boolean.TRUE); // TODO @jason:这个是必须设置的么?
+        process.setExecutable(Boolean.TRUE);
         bpmnModel.addProcess(process);
 
-        // TODO 芋艿:这里可能纠结下,到底前端传递,还是后端创建出来。
-        // 目前前端的第一个节点是“发起人节点”这里构建一个 StartNode,用于创建 Bpmn 的 StartEvent 节点
-        BpmSimpleModelNodeVO startNode = buildStartSimpleModelNode();
+        // 2.1 创建 StartNode 节点
+        // 原因是:目前前端的第一个节点是“发起人节点”,所以这里构建一个 StartNode,用于创建 Bpmn 的 StartEvent 节点
+        BpmSimpleModelNodeVO startNode = buildStartNode();
         startNode.setChildNode(simpleModelNode);
-        // 从 前端模型数据结构 SimpleModel 构建 FlowNode 并添加到 Main Process
+        // 2.2 将前端传递的 simpleModelNode 数据结构(json),转换成从 BPMN FlowNode 元素,并添加到 Main Process 中
         traverseNodeToBuildFlowNode(startNode, process);
-        // 找到 end event
-        EndEvent endEvent = (EndEvent) CollUtil.findOne(process.getFlowElements(), item -> item instanceof EndEvent);
 
-        // 构建并添加节点之间的连线 Sequence Flow
+        // 3. 构建并添加节点之间的连线 Sequence Flow
+        EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel);
         traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId());
-        // 自动布局
+
+        // 4. 自动布局
         new BpmnAutoLayout(bpmnModel).execute();
         return bpmnModel;
     }
 
-    private static BpmSimpleModelNodeVO buildStartSimpleModelNode() {
-        BpmSimpleModelNodeVO startNode = new BpmSimpleModelNodeVO();
-        startNode.setId(START_EVENT_NODE_ID);
-        startNode.setName(START_EVENT_NODE_NAME);
-        startNode.setType(START_NODE.getType());
-        return startNode;
+    private static BpmSimpleModelNodeVO buildStartNode() {
+        return new BpmSimpleModelNodeVO().setId(START_EVENT_NODE_ID)
+                .setName(BpmSimpleModelNodeType.START_USER_NODE.getName())
+                .setType(BpmSimpleModelNodeType.START_NODE.getType());
+    }
+
+    /**
+     * 遍历节点,构建 FlowNode 元素
+     *
+     * @param node SIMPLE 节点
+     * @param process BPMN 流程
+     */
+    private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) {
+        // 1. 判断是否有效节点
+        if (!isValidNode(node)) {
+            return;
+        }
+        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
+        Assert.notNull(nodeType, "模型节点类型({})不支持", node.getType());
+
+        // 2. 处理当前节点
+        NodeConvert nodeConvert = NODE_CONVERTS.get(nodeType);
+        Assert.notNull(nodeConvert, "模型节点类型的转换器({})不存在", node.getType());
+        List<? extends FlowElement> flowElements = nodeConvert.convertList(node);
+        flowElements.forEach(process::addFlowElement);
+
+        // 3.1 情况一:如果当前是分支节点,并且存在条件节点,则处理每个条件的子节点
+        if (BpmSimpleModelNodeType.isBranchNode(node.getType())
+                && CollUtil.isNotEmpty(node.getConditionNodes())) {
+            // 注意:这里的 item.getChildNode() 处理的是每个条件的子节点,不是处理条件
+            node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process));
+        }
+
+        // 3.2 情况二:如果有“子”节点,则递归处理子节点
+        traverseNodeToBuildFlowNode(node.getChildNode(), process);
     }
 
-    // TODO @芋艿:在优化下这个注释
+    /**
+     * 遍历节点,构建 SequenceFlow 元素
+     *
+     * @param process Bpmn 流程
+     * @param node 当前节点
+     * @param targetNodeId 目标节点 ID
+     */
     private static void traverseNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
         // 1.1 无效节点返回
         if (!isValidNode(node)) {
@@ -117,516 +133,555 @@ public class SimpleModelUtils {
         // 1.2 END_NODE 直接返回
         BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
         Assert.notNull(nodeType, "模型节点类型不支持");
-        if (nodeType == END_NODE) {
+        if (nodeType == BpmSimpleModelNodeType.END_NODE) {
             return;
         }
+
         // 2.1 情况一:普通节点
-        BpmSimpleModelNodeVO childNode = node.getChildNode();
         if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) {
-            if (!isValidNode(childNode)) {
-                // 2.1.1 普通节点且无孩子节点。分两种情况
-                // a.结束节点  b. 条件分支的最后一个节点.与分支节点的孩子节点或聚合节点建立连线。
-                if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
-                    // 2.1.1.1 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线
-                    List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), targetNodeId);
-                    sequenceFlows.forEach(process::addFlowElement);
-                } else {
-                    SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null);
-                    process.addFlowElement(sequenceFlow);
-                }
-            } else {
-                // 2.1.2 普通节点且有孩子节点。建立连线
-                if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
-                    // 2.1.1.2 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线
-                    List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), childNode.getId());
-                    sequenceFlows.forEach(process::addFlowElement);
-                } else {
-                    SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null);
-                    process.addFlowElement(sequenceFlow);
-                }
-                // 递归调用后续节点
-                traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
-            }
+            traverseNormalNodeToBuildSequenceFlow(process, node, targetNodeId);
         } else {
             // 2.2 情况二:分支节点
-            List<BpmSimpleModelNodeVO> conditionNodes = node.getConditionNodes();
-            Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空");
-            // 分支终点节点 Id
-            String branchEndNodeId = null;
-            if (nodeType == CONDITION_BRANCH_NODE) { // 条件分支
-                // 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点Id
-                branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
-            } else if (nodeType == PARALLEL_BRANCH_NODE) {  // 并行分支
-                // 分支节点:分支终点节点 Id 为程序创建的网关集合节点。目前不会从前端传入。
-                branchEndNodeId = node.getId() + JOIN_GATE_WAY_NODE_ID_SUFFIX;
-            }
-            // TODO 包容网关待实现
-            Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空");
-            // 3.1 遍历分支节点. 如下情况:
-            // 分支1、A->B->C->D->E 和 分支2、A->D->E。 A为分支节点, D为A孩子节点
-            for (BpmSimpleModelNodeVO item : conditionNodes) {
-                // TODO @jason:条件分支的情况下,需要分 item 搞的条件,和 conditionNodes 搞的条件
-                // @芋艿 这个是啥意思。 这里的 item 的节点类型为 BpmSimpleModelNodeType.CONDITION_NODE 类型,没有对应的 bpmn 的节点。 仅仅用于构建条件表达式。
-                Assert.isTrue(Objects.equals(item.getType(), CONDITION_NODE.getType()), "条件节点类型不符合");
-                // 构建表达式,可以为空. 并行分支为空
-                String conditionExpression = buildConditionExpression(item);
-                BpmSimpleModelNodeVO nextNodeOnCondition = item.getChildNode();
-                // 3.2 分支有后续节点, 分支1: A->B->C->D
-                if (isValidNode(nextNodeOnCondition)) {
-                    // 3.2.1 建立 A->B
-                    SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), nextNodeOnCondition.getId(),
-                            item.getId(), item.getName(), conditionExpression);
-                    process.addFlowElement(sequenceFlow);
-                    // 3.2.2 递归调用后续节点连线。 建立 B->C->D 的连线
-                    traverseNodeToBuildSequenceFlow(process, nextNodeOnCondition, branchEndNodeId);
-                } else {
-                    // 3.3 分支无后续节点 建立 A->D
-                    SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), branchEndNodeId,
-                            item.getId(), item.getName(), conditionExpression);
-                    process.addFlowElement(sequenceFlow);
-                }
-            }
-            // 如果是并行分支。由于是程序创建的聚合网关。需要手工创建聚合网关和下一个节点的连线
-            if (nodeType == PARALLEL_BRANCH_NODE) {
-                String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
-                SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId, null, null, null);
-                process.addFlowElement(sequenceFlow);
-            }
-            // 4.递归调用后续节点 继续递归建立 D->E 的连线
-            traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
+            traverseBranchNodeToBuildSequenceFlow(process, node, targetNodeId);
         }
     }
 
     /**
-     * 构建有附加节点的连线
+     * 遍历普通(非条件)节点,构建 SequenceFlow 元素
      *
-     * @param nodeId       当前节点 Id
-     * @param attachNodeId 附属节点 Id
-     * @param targetNodeId 目标节点 Id
+     * @param process Bpmn 流程
+     * @param node 当前节点
+     * @param targetNodeId 目标节点 ID
      */
-    private static List<SequenceFlow> buildAttachNodeSequenceFlow(String nodeId, String attachNodeId, String targetNodeId) {
-        SequenceFlow sequenceFlow = buildBpmnSequenceFlow(nodeId, attachNodeId, null, null, null);
-        SequenceFlow attachSequenceFlow = buildBpmnSequenceFlow(attachNodeId, targetNodeId, null, null, null);
-        return CollUtil.newArrayList(sequenceFlow, attachSequenceFlow);
+    private static void traverseNormalNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
+        BpmSimpleModelNodeVO childNode = node.getChildNode();
+        boolean isChildNodeValid = isValidNode(childNode);
+        // 情况一:有“子”节点,则建立连线
+        // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点
+        String finalTargetNodeId = isChildNodeValid? childNode.getId() : targetNodeId;
+        SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId);
+        process.addFlowElement(sequenceFlow);
+
+        // 因为有子节点,递归调用后续子节点
+        if (isChildNodeValid) {
+            traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
+        }
     }
 
     /**
-     * 构造条件表达式
+     * 遍历条件节点,构建 SequenceFlow 元素
      *
-     * @param conditionNode 条件节点
+     * @param process Bpmn 流程
+     * @param node 当前节点
+     * @param targetNodeId 目标节点 ID
      */
-    public static String buildConditionExpression(BpmSimpleModelNodeVO conditionNode) {
-        BpmSimpleModeConditionType conditionTypeEnum = BpmSimpleModeConditionType.valueOf(conditionNode.getConditionType());
-        String conditionExpression = null;
-        if (conditionTypeEnum == BpmSimpleModeConditionType.EXPRESSION) {
-            conditionExpression = conditionNode.getConditionExpression();
-        } else if (conditionTypeEnum == BpmSimpleModeConditionType.RULE) {
-            ConditionGroups conditionGroups = conditionNode.getConditionGroups();
-            if (conditionGroups != null && CollUtil.isNotEmpty(conditionGroups.getConditions())) {
-                List<String> strConditionGroups = conditionGroups.getConditions().stream().map(item -> {
-                    if (CollUtil.isNotEmpty(item.getRules())) {
-                        Boolean and = item.getAnd();
-                        List<String> list = CollectionUtils.convertList(item.getRules(), (rule) -> {
-                            // 如果非数值类型加引号
-                            String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() : "\"" + rule.getRightSide() + "\"";
-                            return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide);
-                        });
-                        return "(" + CollUtil.join(list, and ? " && " : " || ") + ")";
-                    } else {
-                        return "";
-                    }
-                }).collect(Collectors.toList());
-                conditionExpression = String.format("${%s}", CollUtil.join(strConditionGroups, conditionGroups.getAnd() ? " && " : " || "));
+    private static void traverseBranchNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
+        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
+        BpmSimpleModelNodeVO childNode = node.getChildNode();
+        List<BpmSimpleModelNodeVO> conditionNodes = node.getConditionNodes();
+        Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空");
+        // 分支终点节点 ID
+        String branchEndNodeId = null;
+        if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) { // 条件分支
+            // 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点 ID
+            branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
+        } else if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE
+                || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) {  // 并行分支或包容分支
+            // 分支节点:分支终点节点 Id 为程序创建的网关集合节点。目前不会从前端传入。
+            branchEndNodeId = buildGatewayJoinId(node.getId());
+        }
+        Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空");
+
+        // 3. 遍历分支节点
+        // 下面的注释,以如下情况举例子。分支 1:A->B->C->D->E,分支 2:A->D->E。其中,A 为分支节点, D 为 A 孩子节点
+        for (BpmSimpleModelNodeVO item : conditionNodes) {
+            Assert.isTrue(Objects.equals(item.getType(), BpmSimpleModelNodeType.CONDITION_NODE.getType()),
+                    "条件节点类型({})不符合", item.getType());
+            BpmSimpleModelNodeVO conditionChildNode = item.getChildNode();
+            // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况
+            if (isValidNode(conditionChildNode)) {
+                // 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线
+                SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), item);
+                process.addFlowElement(sequenceFlow);
+                // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线
+                traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId);
+            } else {
+                // 3.2 分支没有后续节点。例如说,建立 A->D 的连线
+                SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item);
+                process.addFlowElement(sequenceFlow);
             }
         }
-        // TODO 待增加其它类型
-        return conditionExpression;
+
+        // 4. 如果是并行分支、包容分支,由于是程序创建的聚合网关,需要手工创建聚合网关和下一个节点的连线
+        if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE
+                || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE ) {
+            String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
+            SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId);
+            process.addFlowElement(sequenceFlow);
+        }
+
+        // 5. 递归调用后续节点 继续递归。例如说,建立 D->E 的连线
+        traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
+    }
+
+    private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId) {
+        return buildBpmnSequenceFlow(sourceId, targetId, null, null, null);
     }
 
-    private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId, String seqFlowId, String seqName, String conditionExpression) {
+    private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId,
+                                                      String sequenceFlowId, String sequenceFlowName,
+                                                      String conditionExpression) {
         Assert.notEmpty(sourceId, "sourceId 不能为空");
         Assert.notEmpty(targetId, "targetId 不能为空");
-        // TODO @jason:如果 seqFlowId 不存在的时候,是不是要生成一个默认的 seqFlowId? @芋艿: 貌似不需要,Flowable 会默认生成
-        // TODO @jason:如果 name 不存在的时候,是不是要生成一个默认的 name? @芋艿: 不需要生成默认的吧? 这个会在流程图展示的, 一般用户填写的。不好生成默认的吧
+        // TODO @jason:如果 sequenceFlowId 不存在的时候,是不是要生成一个默认的 sequenceFlowId? @芋艿: 貌似不需要,Flowable 会默认生成;TODO @jason:建议还是搞一个,主要是后续好排查问题。
+        // TODO @jason:如果 name 不存在的时候,是不是要生成一个默认的 name? @芋艿: 不需要生成默认的吧? 这个会在流程图展示的, 一般用户填写的。不好生成默认的吧;TODO @jason:建议还是搞一个,主要是后续好排查问题。
         SequenceFlow sequenceFlow = new SequenceFlow(sourceId, targetId);
-        if (StrUtil.isNotEmpty(conditionExpression)) {
-            sequenceFlow.setConditionExpression(conditionExpression);
+        if (StrUtil.isNotEmpty(sequenceFlowId)) {
+            sequenceFlow.setId(sequenceFlowId);
         }
-        if (StrUtil.isNotEmpty(seqFlowId)) {
-            sequenceFlow.setId(seqFlowId);
+        if (StrUtil.isNotEmpty(sequenceFlowName)) {
+            sequenceFlow.setName(sequenceFlowName);
         }
-        if (StrUtil.isNotEmpty(seqName)) {
-            sequenceFlow.setName(seqName);
+        if (StrUtil.isNotEmpty(conditionExpression)) {
+            sequenceFlow.setConditionExpression(conditionExpression);
         }
         return sequenceFlow;
     }
 
-    // TODO @芋艿 改成了 traverseNodeToBuildFlowNode, 连线的叫 traverseNodeToBuildSequenceFlow
-    private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) {
-        // 判断是否有效节点
-        if (!isValidNode(node)) {
-            return;
+    public static boolean isValidNode(BpmSimpleModelNodeVO node) {
+        return node != null && node.getId() != null;
+    }
+
+    public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) {
+        return BpmSimpleModelNodeType.APPROVE_NODE.getType().equals(node.getType())
+                && BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod().equals(node.getApproveMethod());
+    }
+
+    // ========== 各种 convert 节点的方法: BpmSimpleModelNodeVO => BPMN FlowElement ==========
+
+    private interface NodeConvert {
+
+        default List<? extends FlowElement> convertList(BpmSimpleModelNodeVO node) {
+            return Collections.singletonList(convert(node));
         }
-        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
-        Assert.notNull(nodeType, "模型节点类型不支持");
 
-        List<FlowElement> flowElements = buildFlowNode(node, nodeType);
-        flowElements.forEach(process::addFlowElement);
+        default FlowElement convert(BpmSimpleModelNodeVO node) {
+            throw new UnsupportedOperationException("请实现该方法");
+        }
 
-        // 如果不是网关类型的接口, 并且chileNode为空退出
-        // 如果是“分支”节点,则递归处理条件
-        if (BpmSimpleModelNodeType.isBranchNode(node.getType())
-                && ArrayUtil.isNotEmpty(node.getConditionNodes())) {
-            node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process));
+        BpmSimpleModelNodeType getType();
+
+    }
+
+    private static class StartNodeConvert implements NodeConvert {
+
+        @Override
+        public StartEvent convert(BpmSimpleModelNodeVO node) {
+            StartEvent startEvent = new StartEvent();
+            startEvent.setId(node.getId());
+            startEvent.setName(node.getName());
+            return startEvent;
+        }
+
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.START_NODE;
         }
 
-        // 如果有“子”节点,则递归处理子节点
-        traverseNodeToBuildFlowNode(node.getChildNode(), process);
     }
 
-    public static boolean isValidNode(BpmSimpleModelNodeVO node) {
-        return node != null && node.getId() != null;
+    private static class EndNodeConvert implements NodeConvert {
+
+        @Override
+        public EndEvent convert(BpmSimpleModelNodeVO node) {
+            EndEvent endEvent = new EndEvent();
+            endEvent.setId(node.getId());
+            endEvent.setName(node.getName());
+            // TODO @芋艿 + jason:要不要加一个终止定义?
+            return endEvent;
+        }
+
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.END_NODE;
+        }
+
     }
 
-    public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) {
-        return APPROVE_NODE.getType().equals(node.getType()) && SEQUENTIAL.getMethod().equals(node.getApproveMethod());
+    private static class StartUserNodeConvert implements NodeConvert {
+
+        @Override
+        public UserTask convert(BpmSimpleModelNodeVO node) {
+            UserTask userTask = new UserTask();
+            userTask.setId(node.getId());
+            userTask.setName(node.getName());
+
+            // 人工审批
+            addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType());
+            // 候选人策略为发起人自己
+            addCandidateElements(BpmTaskCandidateStrategyEnum.START_USER.getStrategy(), null, userTask);
+            // 添加表单字段权限属性元素
+            addFormFieldsPermission(node.getFieldsPermission(), userTask);
+            // 添加操作按钮配置属性元素
+            addButtonsSetting(node.getButtonsSetting(), userTask);
+            // 使用自动通过策略
+            // TODO @芋艿 复用了SKIP, 是否需要新加一个策略;TODO @芋艿:【回复】是不是应该类似飞书,搞个草稿状态。待定;还有一种策略,不标记自动通过,而是首次发起后,第一个节点,自动通过;
+            addAssignStartUserHandlerType(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType(), userTask);
+            return userTask;
+        }
+
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.START_USER_NODE;
+        }
+
     }
 
-    private static List<FlowElement> buildFlowNode(BpmSimpleModelNodeVO node, BpmSimpleModelNodeType nodeType) {
-        List<FlowElement> list = new ArrayList<>();
-        switch (nodeType) {
-            case START_NODE: { // 开始节点
-                StartEvent startEvent = convertStartNode(node);
-                list.add(startEvent);
-                break;
-            }
-            case END_NODE: { // 结束节点
-                EndEvent endEvent = convertEndNode(node);
-                list.add(endEvent);
-                break;
-            }
-            case START_USER_NODE: { // 发起人节点
-                UserTask userTask = convertStartUserNode(node);
-                list.add(userTask);
-                break;
-            }
-            case APPROVE_NODE: { // 审批节点
-                List<FlowElement> flowElements = convertApproveNode(node);
-                list.addAll(flowElements);
-                break;
+    private static class ApproveNodeConvert implements NodeConvert {
+
+        @Override
+        public List<FlowElement> convertList(BpmSimpleModelNodeVO node) {
+            List<FlowElement> flowElements = new ArrayList<>(2);
+            // 1. 构建用户任务
+            UserTask userTask = buildBpmnUserTask(node);
+            flowElements.add(userTask);
+
+            // 2. 添加用户任务的 Timer Boundary Event, 用于任务的审批超时处理
+            if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) {
+                BoundaryEvent boundaryEvent = buildUserTaskTimeoutBoundaryEvent(userTask, node.getTimeoutHandler());
+                flowElements.add(boundaryEvent);
             }
-            case COPY_NODE: { // 抄送节点
-                ServiceTask serviceTask = convertCopyNode(node);
-                list.add(serviceTask);
-                break;
+            return flowElements;
+        }
+
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.APPROVE_NODE;
+        }
+
+        /**
+         * 添加 UserTask 用户的审批超时 BoundaryEvent 事件
+         *
+         * @param userTask       审批任务
+         * @param timeoutHandler 超时处理器
+         * @return BoundaryEvent 超时事件
+         */
+        private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask,
+                                                                BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) {
+            // 1.1 定时器边界事件
+            BoundaryEvent boundaryEvent = new BoundaryEvent();
+            boundaryEvent.setId("Event-" + IdUtil.fastUUID());
+            boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断
+            boundaryEvent.setAttachedToRef(userTask);
+            // 1.2 定义超时时间、最大提醒次数
+            TimerEventDefinition eventDefinition = new TimerEventDefinition();
+            eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration());
+            if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) &&
+                    timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) {
+                eventDefinition.setTimeCycle(String.format("R%d/%s",
+                        timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration()));
             }
-            case CONDITION_BRANCH_NODE: {
-                ExclusiveGateway exclusiveGateway = convertConditionBranchNode(node);
-                list.add(exclusiveGateway);
-                break;
+            boundaryEvent.addEventDefinition(eventDefinition);
+
+            // 2.1 添加定时器边界事件类型
+            addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventType.USER_TASK_TIMEOUT.getType());
+            // 2.2 添加超时执行动作元素
+            addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType());
+            return boundaryEvent;
+        }
+
+        private UserTask buildBpmnUserTask(BpmSimpleModelNodeVO node) {
+            UserTask userTask = new UserTask();
+            userTask.setId(node.getId());
+            userTask.setName(node.getName());
+
+            // 如果不是审批人节点,则直接返回
+            addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, node.getApproveType());
+            if (ObjectUtil.notEqual(node.getApproveType(), BpmUserTaskApproveTypeEnum.USER.getType())) {
+                return userTask;
             }
-            case PARALLEL_BRANCH_NODE: {
-                List<ParallelGateway> parallelGateways = convertParallelBranchNode(node);
-                list.addAll(parallelGateways);
-                break;
+
+            // 添加候选人元素
+            addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), userTask);
+            // 添加表单字段权限属性元素
+            addFormFieldsPermission(node.getFieldsPermission(), userTask);
+            // 添加操作按钮配置属性元素
+            addButtonsSetting(node.getButtonsSetting(), userTask);
+            // 处理多实例(审批方式)
+            processMultiInstanceLoopCharacteristics(node.getApproveMethod(), node.getApproveRatio(), userTask);
+            // 添加任务被拒绝的处理元素
+            addTaskRejectElements(node.getRejectHandler(), userTask);
+            // 添加用户任务的审批人与发起人相同时的处理元素
+            addAssignStartUserHandlerType(node.getAssignStartUserHandlerType(), userTask);
+            // 添加用户任务的空处理元素
+            addAssignEmptyHandlerType(node.getAssignEmptyHandler(), userTask);
+            //  设置审批任务的截止时间
+            if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) {
+                userTask.setDueDate(node.getTimeoutHandler().getTimeDuration());
             }
+            return userTask;
+        }
 
-            case INCLUSIVE_BRANCH_NODE: {
-                // TODO jason 待实现
-                break;
+        private void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) {
+            BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod);
+            Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum);
+            // 添加审批方式的扩展属性
+            addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD, approveMethod);
+            if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) {
+                // 随机审批,不需要设置多实例属性
+                return;
             }
-            default: {
-                // TODO 其它节点类型的实现
+
+            // 处理多实例审批方式
+            MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
+            // 设置 collectionVariable。本系统用不到,仅仅为了 Flowable 校验不报错
+            multiInstanceCharacteristics.setInputDataItem("${coll_userList}");
+            if (approveMethodEnum == BpmUserTaskApproveMethodEnum.ANY) {
+                multiInstanceCharacteristics.setCompletionCondition(approveMethodEnum.getCompletionCondition());
+                multiInstanceCharacteristics.setSequential(false);
+            } else if (approveMethodEnum == BpmUserTaskApproveMethodEnum.SEQUENTIAL) {
+                multiInstanceCharacteristics.setCompletionCondition(approveMethodEnum.getCompletionCondition());
+                multiInstanceCharacteristics.setSequential(true);
+                multiInstanceCharacteristics.setLoopCardinality("1");
+            } else if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RATIO) {
+                Assert.notNull(approveRatio, "通过比例不能为空");
+                multiInstanceCharacteristics.setCompletionCondition(
+                        String.format(approveMethodEnum.getCompletionCondition(), String.format("%.2f", approveRatio / 100D)));
+                multiInstanceCharacteristics.setSequential(false);
             }
+            userTask.setLoopCharacteristics(multiInstanceCharacteristics);
         }
-        return list;
-    }
 
-    private static UserTask convertStartUserNode(BpmSimpleModelNodeVO node) {
-        return buildBpmnStartUserTask(node);
     }
 
-    private static List<FlowElement> convertApproveNode(BpmSimpleModelNodeVO node) {
-        List<FlowElement> flowElements = new ArrayList<>();
-        UserTask userTask = buildBpmnUserTask(node);
-        flowElements.add(userTask);
-
-        // 添加用户任务的 Timer Boundary Event, 用于任务的审批超时处理
-        if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) {
-            BoundaryEvent boundaryEvent = buildUserTaskTimeoutBoundaryEvent(userTask, node.getTimeoutHandler());
-            flowElements.add(boundaryEvent);
+    private static class CopyNodeConvert implements NodeConvert {
+
+        @Override
+        public ServiceTask convert(BpmSimpleModelNodeVO node) {
+            ServiceTask serviceTask = new ServiceTask();
+            serviceTask.setId(node.getId());
+            serviceTask.setName(node.getName());
+            serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
+            serviceTask.setImplementation("${" + BpmCopyTaskDelegate.BEAN_NAME + "}");
+
+            // 添加抄送候选人元素
+            addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), serviceTask);
+            // 添加表单字段权限属性元素
+            addFormFieldsPermission(node.getFieldsPermission(), serviceTask);
+            return serviceTask;
         }
-        return flowElements;
-    }
 
-    /**
-     * 添加 UserTask 用户的审批超时 BoundaryEvent 事件
-     *
-     * @param userTask       审批任务
-     * @param timeoutHandler 超时处理器
-     * @return BoundaryEvent 超时事件
-     */
-    private static BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, TimeoutHandler timeoutHandler) {
-        // 1.1 定时器边界事件
-        BoundaryEvent boundaryEvent = new BoundaryEvent();
-        boundaryEvent.setId("Event-" + IdUtil.fastUUID());
-        boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断
-        boundaryEvent.setAttachedToRef(userTask);
-        // 1.2 定义超时时间、最大提醒次数
-        TimerEventDefinition eventDefinition = new TimerEventDefinition();
-        eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration());
-        if (Objects.equals(REMINDER.getType(), timeoutHandler.getType()) &&
-                timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) {
-            eventDefinition.setTimeCycle(String.format("R%d/%s",
-                    timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration()));
-        }
-        boundaryEvent.addEventDefinition(eventDefinition);
-
-        // 2.1 添加定时器边界事件类型
-        addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventType.USER_TASK_TIMEOUT.getType().toString());
-        // 2.2 添加超时执行动作元素
-        addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, StrUtil.toStringOrNull(timeoutHandler.getType()));
-        return boundaryEvent;
-    }
-
-    private static List<ParallelGateway> convertParallelBranchNode(BpmSimpleModelNodeVO node) {
-        ParallelGateway parallelGateway = new ParallelGateway();
-        parallelGateway.setId(node.getId());
-        // TODO @jason:setName
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.COPY_NODE;
+        }
 
-        // TODO @芋艿 + jason:合并网关;是不是要有条件啥的。微信讨论
-        // 并行聚合网关有程序创建。前端不需要传入
-        ParallelGateway joinParallelGateway = new ParallelGateway();
-        joinParallelGateway.setId(node.getId() + JOIN_GATE_WAY_NODE_ID_SUFFIX);
-        return CollUtil.newArrayList(parallelGateway, joinParallelGateway);
     }
 
-    private static ServiceTask convertCopyNode(BpmSimpleModelNodeVO node) {
-        ServiceTask serviceTask = new ServiceTask();
-        serviceTask.setId(node.getId());
-        serviceTask.setName(node.getName());
-        serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
-        serviceTask.setImplementation("${" + BpmCopyTaskDelegate.BEAN_NAME + "}");
-
-        // 添加抄送候选人元素
-        addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), serviceTask);
-        // 添加表单字段权限属性元素
-        addFormFieldsPermission(node.getFieldsPermission(), serviceTask);
-        return serviceTask;
-    }
+    private static class ConditionBranchNodeConvert implements NodeConvert {
 
-    /**
-     * 给节点添加候选人元素
-     */
-    private static void addCandidateElements(Integer candidateStrategy, String candidateParam, FlowElement flowElement) {
-        addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY,
-                candidateStrategy == null ? null : candidateStrategy.toString());
-        addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM, candidateParam);
-    }
+        @Override
+        public ExclusiveGateway convert(BpmSimpleModelNodeVO node) {
+            ExclusiveGateway exclusiveGateway = new ExclusiveGateway();
+            exclusiveGateway.setId(node.getId());
+            // TODO @jason:setName
 
-    private static ExclusiveGateway convertConditionBranchNode(BpmSimpleModelNodeVO node) {
-        Assert.notEmpty(node.getConditionNodes(), "条件分支节点不能为空");
-        ExclusiveGateway exclusiveGateway = new ExclusiveGateway();
-        exclusiveGateway.setId(node.getId());
-        // 寻找默认的序列流
-        BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(),
-                item -> BooleanUtil.isTrue(item.getDefaultFlow()));
-        if (defaultSeqFlow != null) {
+            // 设置默认的序列流(条件)
+            BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(),
+                    item -> BooleanUtil.isTrue(item.getDefaultFlow()));
+            Assert.notNull(defaultSeqFlow, "条件分支节点({})的默认序列流不能为空", node.getId());
             exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
+            return exclusiveGateway;
         }
-        return exclusiveGateway;
-    }
 
-    private static InclusiveGateway convertInclusiveBranchNode(BpmSimpleModelNodeVO node, Boolean isFork) {
-        InclusiveGateway inclusiveGateway = new InclusiveGateway();
-        inclusiveGateway.setId(node.getId());
-        // TODO @jason:这里是不是 setName 哈;
-
-        // TODO @芋艿 + jason:是不是搞个合并网关;这里微信讨论下,有点奇怪;
-        // @芋艿 isFork 为 false 就是合并网关。由前端传入。这个前端暂时还未实现
-        if (isFork) {
-            Assert.notEmpty(node.getConditionNodes(), "条件节点不能为空");
-            // 寻找默认的序列流
-            BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(
-                    node.getConditionNodes(), item -> BooleanUtil.isTrue(item.getDefaultFlow()));
-            if (defaultSeqFlow != null) {
-                inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
-            }
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.CONDITION_BRANCH_NODE;
         }
-        return inclusiveGateway;
+
     }
 
-    private static UserTask buildBpmnUserTask(BpmSimpleModelNodeVO node) {
-        UserTask userTask = new UserTask();
-        userTask.setId(node.getId());
-        userTask.setName(node.getName());
+    private static class ParallelBranchNodeConvert implements NodeConvert {
 
-        // 如果不是审批人节点,则直接返回
-        addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, StrUtil.toStringOrNull(node.getApproveType()));
-        if (ObjectUtil.notEqual(node.getApproveType(), USER.getType())) {
-            return userTask;
+        @Override
+        public List<ParallelGateway> convertList(BpmSimpleModelNodeVO node) {
+            ParallelGateway parallelGateway = new ParallelGateway();
+            parallelGateway.setId(node.getId());
+            // TODO @jason:setName
+
+            // 并行聚合网关由程序创建,前端不需要传入
+            ParallelGateway joinParallelGateway = new ParallelGateway();
+            joinParallelGateway.setId(buildGatewayJoinId(node.getId()));
+            // TODO @jason:setName
+            return CollUtil.newArrayList(parallelGateway, joinParallelGateway);
+        }
+
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE;
         }
 
-        // 添加候选人元素
-        addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), userTask);
-        // 添加表单字段权限属性元素
-        addFormFieldsPermission(node.getFieldsPermission(), userTask);
-        // 添加操作按钮配置属性元素
-        addButtonsSetting(node.getButtonsSetting(), userTask);
-        // 处理多实例(审批方式)
-        processMultiInstanceLoopCharacteristics(node.getApproveMethod(), node.getApproveRatio(), userTask);
-        // 添加任务被拒绝的处理元素
-        addTaskRejectElements(node.getRejectHandler(), userTask);
-        // 添加用户任务的审批人与发起人相同时的处理元素
-        addAssignStartUserHandlerType(node.getAssignStartUserHandlerType(), userTask);
-        // 添加用户任务的空处理元素
-        addAssignEmptyHandlerType(node.getAssignEmptyHandler(), userTask);
-        //  设置审批任务的截止时间
-        if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) {
-            userTask.setDueDate(node.getTimeoutHandler().getTimeDuration());
-        }
-        return userTask;
     }
 
-    private static void addTaskRejectElements(RejectHandler rejectHandler, UserTask userTask) {
-        if (rejectHandler == null) {
-            return;
+    private static class InclusiveBranchNodeConvert implements NodeConvert {
+
+        @Override
+        public List<InclusiveGateway> convertList(BpmSimpleModelNodeVO node) {
+            InclusiveGateway inclusiveGateway = new InclusiveGateway();
+            inclusiveGateway.setId(node.getId());
+            // 设置默认的序列流(条件)
+            BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(),
+                    item -> BooleanUtil.isTrue(item.getDefaultFlow()));
+            Assert.notNull(defaultSeqFlow, "包容分支节点({})的默认序列流不能为空", node.getId());
+            inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
+            // TODO @jason:setName
+
+            // 并行聚合网关由程序创建,前端不需要传入
+            InclusiveGateway joinInclusiveGateway = new InclusiveGateway();
+            joinInclusiveGateway.setId(buildGatewayJoinId(node.getId()));
+            // TODO @jason:setName
+            return CollUtil.newArrayList(inclusiveGateway, joinInclusiveGateway);
         }
-        addExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE, StrUtil.toStringOrNull(rejectHandler.getType()));
-        addExtensionElement(userTask, USER_TASK_REJECT_RETURN_TASK_ID, rejectHandler.getReturnNodeId());
-    }
 
-    private static void addAssignStartUserHandlerType(Integer assignStartUserHandlerType, UserTask userTask) {
-        if (assignStartUserHandlerType == null) {
-            return;
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE;
         }
-        addExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, assignStartUserHandlerType.toString());
+
     }
 
-    private static void addAssignEmptyHandlerType(BpmSimpleModelNodeVO.AssignEmptyHandler emptyHandler, UserTask userTask) {
-        if (emptyHandler == null) {
-            return;
+    public static class ConditionNodeConvert implements NodeConvert {
+
+        @Override
+        public List<? extends FlowElement> convertList(BpmSimpleModelNodeVO node) {
+            // 原因是:正常情况下,它不会被调用到
+            throw new UnsupportedOperationException("条件节点不支持转换");
         }
-        addExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE, StrUtil.toStringOrNull(emptyHandler.getType()));
-        addExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS, StrUtil.join(",", emptyHandler.getUserIds()));
-    }
 
-    private static void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) {
-        BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod);
-        // TODO @jason:这种枚举,最终不要去掉哈 BpmUserTaskApproveMethodEnum。因为容易不经意重叠
-        if (approveMethodEnum == null || approveMethodEnum == RANDOM) {
-            return;
+        @Override
+        public BpmSimpleModelNodeType getType() {
+            return BpmSimpleModelNodeType.CONDITION_NODE;
         }
-        // 添加审批方式的扩展属性
-        addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD,
-                approveMethod == null ? null : approveMethod.toString());
-        MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
-        // 设置 collectionVariable。本系统用不到。仅仅为了 Flowable 校验不报错。
-        multiInstanceCharacteristics.setInputDataItem("${coll_userList}");
-        if (approveMethodEnum == BpmUserTaskApproveMethodEnum.ANY) {
-            multiInstanceCharacteristics.setCompletionCondition(ANY_OF_APPROVE_COMPLETE_EXPRESSION);
-            multiInstanceCharacteristics.setSequential(false);
-            userTask.setLoopCharacteristics(multiInstanceCharacteristics);
-        } else if (approveMethodEnum == SEQUENTIAL) {
-            multiInstanceCharacteristics.setCompletionCondition(ALL_APPROVE_COMPLETE_EXPRESSION);
-            multiInstanceCharacteristics.setSequential(true);
-            multiInstanceCharacteristics.setLoopCardinality("1");
-            userTask.setLoopCharacteristics(multiInstanceCharacteristics);
-        } else if (approveMethodEnum == RATIO) {
-            Assert.notNull(approveRatio, "通过比例不能为空");
-            multiInstanceCharacteristics.setCompletionCondition(
-                    String.format(APPROVE_BY_RATIO_COMPLETE_EXPRESSION, String.format("%.2f", approveRatio / (double) 100)));
-            multiInstanceCharacteristics.setSequential(false);
+
+        public static SequenceFlow buildSequenceFlow(String sourceId, String targetId,
+                                                     BpmSimpleModelNodeVO node) {
+            String conditionExpression = buildConditionExpression(node);
+            return buildBpmnSequenceFlow(sourceId, targetId, node.getId(), node.getName(), conditionExpression);
         }
-        userTask.setLoopCharacteristics(multiInstanceCharacteristics);
-    }
 
-    /**
-     * 给节点添加操作按钮设置元素
-     */
-    private static void addButtonsSetting(List<OperationButtonSetting> buttonsSetting, UserTask userTask) {
-        if (CollUtil.isNotEmpty(buttonsSetting)) {
-            List<Map<String, String>> list = CollectionUtils.convertList(buttonsSetting, item -> {
-                Map<String, String> settingMap = MapUtil.newHashMap(16);
-                settingMap.put(BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE, String.valueOf(item.getId()));
-                settingMap.put(BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE, item.getDisplayName());
-                settingMap.put(BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE, String.valueOf(item.getEnable()));
-                return settingMap;
-            });
-            list.forEach(item -> addExtensionElement(userTask, BUTTON_SETTING_ELEMENT, item));
+        /**
+         * 构造条件表达式
+         *
+         * @param node 条件节点
+         */
+        public static String buildConditionExpression(BpmSimpleModelNodeVO node) {
+            BpmSimpleModeConditionType conditionTypeEnum = BpmSimpleModeConditionType.valueOf(node.getConditionType());
+            if (conditionTypeEnum == BpmSimpleModeConditionType.EXPRESSION) {
+                return node.getConditionExpression();
+            }
+            if (conditionTypeEnum == BpmSimpleModeConditionType.RULE) {
+                ConditionGroups conditionGroups = node.getConditionGroups();
+                if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) {
+                    return null;
+                }
+                List<String> strConditionGroups = CollectionUtils.convertList(conditionGroups.getConditions(), item -> {
+                    if (CollUtil.isEmpty(item.getRules())) {
+                        return "";
+                    }
+                    // 构造规则表达式
+                    List<String> list = CollectionUtils.convertList(item.getRules(), (rule) -> {
+                        String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide()
+                                : "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号
+                        return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide);
+                    });
+                    // 构造条件组的表达式
+                    Boolean and = item.getAnd();
+                    return "(" + CollUtil.join(list, and ? " && " : " || ") + ")";
+                });
+                return String.format("${%s}", CollUtil.join(strConditionGroups, conditionGroups.getAnd() ? " && " : " || "));
+            }
+            return null;
         }
+
     }
 
-    /**
-     * 给节点添加表单字段权限元素
-     */
-    private static void addFormFieldsPermission(List<Map<String, String>> fieldsPermissions, FlowElement flowElement) {
-        if (CollUtil.isNotEmpty(fieldsPermissions)) {
-            fieldsPermissions.forEach(item -> addExtensionElement(flowElement, FORM_FIELD_PERMISSION_ELEMENT, item));
-        }
+    private static String buildGatewayJoinId(String id) {
+        return id + "_join";
     }
 
-    private static void addExtensionElement(FlowElement element, String name, Map<String, String> attributes) {
-        if (attributes == null) {
-            return;
-        }
-        ExtensionElement extensionElement = new ExtensionElement();
-        extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
-        extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
-        extensionElement.setName(name);
-        attributes.forEach((key, value) -> {
-            ExtensionAttribute extensionAttribute = new ExtensionAttribute(key, value);
-            extensionAttribute.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
-            extensionElement.addAttribute(extensionAttribute);
-        });
-        element.addExtensionElement(extensionElement);
+    // ========== SIMPLE 流程预测相关的方法 ==========
+
+    public static List<BpmSimpleModelNodeVO> simulateProcess(BpmSimpleModelNodeVO rootNode, Map<String, Object> variables) {
+        List<BpmSimpleModelNodeVO> resultNodes = new ArrayList<>();
+
+        // 从头开始遍历
+        simulateNextNode(rootNode, variables, resultNodes);
+        return resultNodes;
     }
 
-    private static void addExtensionElement(FlowElement element, String name, String value) {
-        if (value == null) {
+    private static void simulateNextNode(BpmSimpleModelNodeVO currentNode, Map<String, Object> variables,
+                                  List<BpmSimpleModelNodeVO> resultNodes) {
+        // 如果不合法(包括为空),则直接结束
+        if (!isValidNode(currentNode)) {
             return;
         }
-        ExtensionElement extensionElement = new ExtensionElement();
-        extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
-        extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
-        extensionElement.setElementText(value);
-        extensionElement.setName(name);
-        element.addExtensionElement(extensionElement);
-    }
+        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(currentNode.getType());
+        Assert.notNull(nodeType, "模型节点类型不支持");
 
-    // ========== 各种 build 节点的方法 ==========
+        // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE
+        if (nodeType == BpmSimpleModelNodeType.START_NODE
+            || nodeType == BpmSimpleModelNodeType.START_USER_NODE
+            || nodeType == BpmSimpleModelNodeType.APPROVE_NODE
+            || nodeType == BpmSimpleModelNodeType.COPY_NODE
+            || nodeType == BpmSimpleModelNodeType.END_NODE) {
+            // 添加元素
+            resultNodes.add(currentNode);
+        }
 
-    private static StartEvent convertStartNode(BpmSimpleModelNodeVO node) {
-        StartEvent startEvent = new StartEvent();
-        startEvent.setId(node.getId());
-        startEvent.setName(node.getName());
-        return startEvent;
-    }
+        // 情况:CONDITION_BRANCH_NODE 排它,只有一个满足条件的。如果没有,就走默认的
+        if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) {
+            // 查找满足条件的 BpmSimpleModelNodeVO 节点
+            BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
+                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow())
+                        && evalConditionExpress(variables, conditionNode));
+            if (matchConditionNode == null) {
+                matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
+                        conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow()));
+            }
+            Assert.notNull(matchConditionNode, "找不到条件节点({})", currentNode);
+            // 遍历满足条件的 BpmSimpleModelNodeVO 节点
+            simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes);
+        }
 
-    private static UserTask buildBpmnStartUserTask(BpmSimpleModelNodeVO node) {
-        UserTask userTask = new UserTask();
-        userTask.setId(node.getId());
-        userTask.setName(node.getName());
-        // 人工审批
-        addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, USER.getType().toString());
-        // 候选人策略为发起人自己
-        addCandidateElements(START_USER.getStrategy(), null, userTask);
-        // 添加表单字段权限属性元素
-        addFormFieldsPermission(node.getFieldsPermission(), userTask);
-        // 添加操作按钮配置属性元素
-        addButtonsSetting(node.getButtonsSetting(), userTask);
-        // 使用自动通过策略 TODO @芋艿 复用了SKIP, 是否需要新加一个策略;TODO @芋艿:【回复】是不是应该类似飞书,搞个草稿状态。待定;还有一种策略,不标记自动通过,而是首次发起后,第一个节点,自动通过;
-        addAssignStartUserHandlerType(SKIP.getType(), userTask);
-        return userTask;
-    }
+        // 情况:INCLUSIVE_BRANCH_NODE 包容,多个满足条件的。如果没有,就走默认的
+        if (nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) {
+            // 查找满足条件的 BpmSimpleModelNodeVO 节点
+            Collection<BpmSimpleModelNodeVO> matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
+                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow())
+                            && evalConditionExpress(variables, conditionNode));
+            if (CollUtil.isEmpty(matchConditionNodes)) {
+                matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
+                        conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow()));
+            }
+            Assert.isTrue(!matchConditionNodes.isEmpty(), "找不到条件节点({})", currentNode);
+            // 遍历满足条件的 BpmSimpleModelNodeVO 节点
+            matchConditionNodes.forEach(matchConditionNode ->
+                    simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes));
+        }
 
-    private static EndEvent convertEndNode(BpmSimpleModelNodeVO node) {
-        EndEvent endEvent = new EndEvent();
-        endEvent.setId(node.getId());
-        endEvent.setName(node.getName());
+        // 情况:PARALLEL_BRANCH_NODE 并行,都满足,都走
+        if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE) {
+            // 遍历所有 BpmSimpleModelNodeVO 节点
+            currentNode.getConditionNodes().forEach(matchConditionNode ->
+                    simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes));
+        }
+
+        // 遍历子节点
+        simulateNextNode(currentNode.getChildNode(), variables, resultNodes);
+    }
 
-        // TODO @芋艿 + jason:要不要加一个终止定义?
-        return endEvent;
+    public static boolean evalConditionExpress(Map<String, Object> variables, BpmSimpleModelNodeVO conditionNode) {
+        return BpmnModelUtils.evalConditionExpress(variables, ConditionNodeConvert.buildConditionExpression(conditionNode));
     }
 
 }

+ 7 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java

@@ -82,4 +82,11 @@ public interface BpmCategoryService {
      */
     List<BpmCategoryDO> getCategoryListByStatus(Integer status);
 
+    /**
+     * 批量更新流程分类的排序:每个分类的 sort 值,从 0 开始递增
+     *
+     * @param ids 分类编号列表
+     */
+    void updateCategorySortBatch(List<Long> ids);
+
 }

+ 22 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java

@@ -8,13 +8,16 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCa
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategorySaveReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
@@ -56,7 +59,7 @@ public class BpmCategoryServiceImpl implements BpmCategoryService {
     private void validateCategoryNameUnique(BpmCategorySaveReqVO updateReqVO) {
         BpmCategoryDO category = bpmCategoryMapper.selectByName(updateReqVO.getName());
         if (category == null
-            || ObjUtil.equal(category.getId(), updateReqVO.getId())) {
+                || ObjUtil.equal(category.getId(), updateReqVO.getId())) {
             return;
         }
         throw exception(CATEGORY_NAME_DUPLICATE, updateReqVO.getName());
@@ -65,7 +68,7 @@ public class BpmCategoryServiceImpl implements BpmCategoryService {
     private void validateCategoryCodeUnique(BpmCategorySaveReqVO updateReqVO) {
         BpmCategoryDO category = bpmCategoryMapper.selectByCode(updateReqVO.getCode());
         if (category == null
-            || ObjUtil.equal(category.getId(), updateReqVO.getId())) {
+                || ObjUtil.equal(category.getId(), updateReqVO.getId())) {
             return;
         }
         throw exception(CATEGORY_CODE_DUPLICATE, updateReqVO.getCode());
@@ -108,4 +111,20 @@ public class BpmCategoryServiceImpl implements BpmCategoryService {
         return bpmCategoryMapper.selectListByStatus(status);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateCategorySortBatch(List<Long> ids) {
+        // 校验分类都存在
+        List<BpmCategoryDO> categories = bpmCategoryMapper.selectByIds(ids);
+        if (categories.size() != ids.size()) {
+            throw exception(CATEGORY_NOT_EXISTS);
+        }
+
+        // 批量更新排序
+        List<BpmCategoryDO> updateList = IntStream.range(0, ids.size())
+                .mapToObj(index -> new BpmCategoryDO().setId(ids.get(index)).setSort(index))
+                .collect(Collectors.toList());
+        bpmCategoryMapper.updateBatch(updateList);
+    }
+
 }

+ 15 - 8
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java

@@ -1,29 +1,28 @@
 package cn.iocoder.yudao.module.bpm.service.definition;
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
 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 jakarta.validation.Valid;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.engine.repository.Model;
 
-import javax.validation.Valid;
+import java.util.List;
 
 /**
- * Flowable流程模型接口
+ * 流程模型接口
  *
  * @author yunlongn
  */
 public interface BpmModelService {
 
     /**
-     * 获得流程模型分页
+     * 获得流程模型列表
      *
-     * @param pageVO 分页查询
-     * @return 流程模型分页
+     * @param name 模型名称
+     * @return 流程模型列表
      */
-    PageResult<Model> getModelPage(BpmModelPageReqVO pageVO);
+    List<Model> getModelList(String name);
 
     /**
      * 创建流程模型
@@ -65,6 +64,14 @@ public interface BpmModelService {
      */
     void updateModel(Long userId, @Valid BpmModelSaveReqVO updateReqVO);
 
+    /**
+     * 批量更新模型排序
+     *
+     * @param userId 用户编号
+     * @param ids 编号列表
+     */
+    void updateModelSortBatch(Long userId, List<String> ids);
+
     /**
      * 将流程模型,部署成一个流程定义
      *

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

@@ -3,12 +3,9 @@ package cn.iocoder.yudao.module.bpm.service.definition;
 import cn.hutool.core.collection.CollUtil;
 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.json.JsonUtils;
-import cn.iocoder.yudao.framework.common.util.object.PageUtils;
 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
 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;
@@ -35,14 +32,15 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.util.List;
+import java.util.Map;
 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.convertMap;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
 
 /**
- * Flowable流程模型实现
- * 主要进行 Flowable {@link Model} 的维护
+ * 流程模型实现:主要进行 Flowable {@link Model} 的维护
  *
  * @author yunlongn
  * @author 芋道源码
@@ -64,27 +62,12 @@ public class BpmModelServiceImpl implements BpmModelService {
     private BpmTaskCandidateInvoker taskCandidateInvoker;
 
     @Override
-    public PageResult<Model> getModelPage(BpmModelPageReqVO pageVO) {
+    public List<Model> getModelList(String name) {
         ModelQuery modelQuery = repositoryService.createModelQuery();
-        modelQuery.modelTenantId(FlowableUtils.getTenantId());
-        if (StrUtil.isNotBlank(pageVO.getKey())) {
-            modelQuery.modelKey(pageVO.getKey());
+        if (StrUtil.isNotEmpty(name)) {
+            modelQuery.modelNameLike(name);
         }
-        if (StrUtil.isNotBlank(pageVO.getName())) {
-            modelQuery.modelNameLike("%" + pageVO.getName() + "%"); // 模糊匹配
-        }
-        if (StrUtil.isNotBlank(pageVO.getCategory())) {
-            modelQuery.modelCategory(pageVO.getCategory());
-        }
-        // 执行查询
-        long count = modelQuery.count();
-        if (count == 0) {
-            return PageResult.empty(count);
-        }
-        List<Model> models = modelQuery
-                .orderByCreateTime().desc()
-                .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
-        return new PageResult<>(models, count);
+        return modelQuery.list();
     }
 
     @Override
@@ -100,6 +83,7 @@ public class BpmModelServiceImpl implements BpmModelService {
         }
 
         // 2.1 创建流程定义
+        createReqVO.setSort(System.currentTimeMillis()); // 使用当前时间,作为排序
         Model model = repositoryService.newModel();
         BpmModelConvert.INSTANCE.copyToModel(model, createReqVO);
         model.setTenantId(FlowableUtils.getTenantId());
@@ -120,6 +104,34 @@ public class BpmModelServiceImpl implements BpmModelService {
         repositoryService.saveModel(model);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateModelSortBatch(Long userId, List<String> ids) {
+        // 1.1 校验流程模型存在
+        List<Model> models = repositoryService.createModelQuery()
+                .modelTenantId(FlowableUtils.getTenantId()).list();
+        models.removeIf(model ->!ids.contains(model.getId()));
+        if (ids.size() != models.size()) {
+            throw exception(MODEL_NOT_EXISTS);
+        }
+        Map<String, Model> modelMap = convertMap(models, Model::getId);
+        // 1.2 校验是否为管理员
+        ids.forEach(id -> validateModelManager(id, userId));
+
+        // 保存排序
+        long sort = System.currentTimeMillis(); // 使用时间戳 - i 作为排序
+        for (int i = ids.size() - 1; i > 0; i--) {
+            Model model = modelMap.get(ids.get(i));
+            // 更新模型
+            BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model).setSort(sort);
+            model.setMetaInfo(JsonUtils.toJsonString(metaInfo));
+            repositoryService.saveModel(model);
+            // 更新排序
+            processDefinitionService.updateProcessDefinitionSortByModelId(model.getId(), sort);
+            sort--;
+        }
+    }
+
     private Model validateModelExists(String id) {
         Model model = repositoryService.getModel(id);
         if (model == null) {
@@ -139,7 +151,7 @@ public class BpmModelServiceImpl implements BpmModelService {
         Model model = validateModelExists(id);
         BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
         if (metaInfo == null || !CollUtil.contains(metaInfo.getManagerUserIds(), userId)) {
-            throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER);
+            throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER, model.getName());
         }
         return model;
     }
@@ -158,10 +170,10 @@ public class BpmModelServiceImpl implements BpmModelService {
         // 1.4 校验任务分配规则已配置
         taskCandidateInvoker.validateBpmnConfig(bpmnBytes);
         // 1.5 获取仿钉钉流程设计器模型数据
-        byte[] simpleBytes = getModelSimpleJson(model.getId());
+        String simpleJson = getModelSimpleJson(model.getId());
 
         // 2.1 创建流程定义
-        String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, simpleBytes, form);
+        String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, simpleJson, form);
 
         // 2.2 将老的流程定义进行挂起。也就是说,只有最新部署的流程定义,才可以发起任务。
         updateProcessDefinitionSuspended(model.getDeploymentId());
@@ -226,8 +238,8 @@ public class BpmModelServiceImpl implements BpmModelService {
     public BpmSimpleModelNodeVO getSimpleModel(String modelId) {
         Model model = validateModelExists(modelId);
         // 通过 ACT_RE_MODEL 表 EDITOR_SOURCE_EXTRA_VALUE_ID_ ,获取仿钉钉快搭模型的 JSON 数据
-        byte[] jsonBytes = getModelSimpleJson(model.getId());
-        return JsonUtils.parseObject(jsonBytes, BpmSimpleModelNodeVO.class);
+        String json = getModelSimpleJson(model.getId());
+        return JsonUtils.parseObject(json, BpmSimpleModelNodeVO.class);
     }
 
     @Override
@@ -240,7 +252,7 @@ public class BpmModelServiceImpl implements BpmModelService {
         // 2.2 保存 Bpmn XML
         updateModelBpmnXml(model.getId(), BpmnModelUtils.getBpmnXml(bpmnModel));
         // 2.3 保存 JSON 数据
-        saveModelSimpleJson(model.getId(), JsonUtils.toJsonByte(reqVO.getSimpleModel()));
+        updateModelSimpleJson(model.getId(), reqVO.getSimpleModel());
     }
 
     /**
@@ -279,15 +291,21 @@ public class BpmModelServiceImpl implements BpmModelService {
         repositoryService.addModelEditorSource(id, StrUtil.utf8Bytes(bpmnXml));
     }
 
-    private byte[] getModelSimpleJson(String id) {
-        return repositoryService.getModelEditorSourceExtra(id);
+    @SuppressWarnings("JavaExistingMethodCanBeUsed")
+    private String getModelSimpleJson(String id) {
+        byte[] bytes = repositoryService.getModelEditorSourceExtra(id);
+        if (ArrayUtil.isEmpty(bytes)) {
+            return null;
+        }
+        return StrUtil.utf8Str(bytes);
     }
 
-    private void saveModelSimpleJson(String id, byte[] jsonBytes) {
-        if (ArrayUtil.isEmpty(jsonBytes)) {
+    private void updateModelSimpleJson(String id, BpmSimpleModelNodeVO node) {
+        if (node == null) {
             return;
         }
-        repositoryService.addModelEditorSourceExtra(id, jsonBytes);
+        byte[] bytes = JsonUtils.toJsonByte(node);
+        repositoryService.addModelEditorSourceExtra(id, bytes);
     }
 
     /**

+ 11 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java

@@ -18,7 +18,7 @@ import java.util.Set;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
 /**
- * Flowable流程定义接口
+ * 流程定义接口
  *
  * @author yunlong.li
  * @author ZJQ
@@ -48,12 +48,12 @@ public interface BpmProcessDefinitionService {
      * @param model 流程模型
      * @param modelMetaInfo 流程模型元信息
      * @param bpmnBytes BPMN XML 字节数组
-     * @param simpleBytes SIMPLE Model JSON 字节数组
+     * @param simpleJson SIMPLE Model JSON
      * @param form 表单
      * @return 流程编号
      */
     String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo,
-                                   byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form);
+                                   byte[] bpmnBytes, String simpleJson, BpmFormDO form);
 
     /**
      * 更新流程定义状态
@@ -63,6 +63,14 @@ public interface BpmProcessDefinitionService {
      */
     void updateProcessDefinitionState(String id, Integer state);
 
+    /**
+     * 更新模型编号
+     *
+     * @param modelId 流程定义编号
+     * @param sort 排序
+     */
+    void updateProcessDefinitionSortByModelId(String modelId, Long sort);
+
     /**
      * 获得流程定义对应的 BPMN
      *

+ 9 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java

@@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitio
 import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.common.engine.impl.db.SuspensionState;
@@ -23,8 +24,6 @@ import org.flowable.engine.repository.ProcessDefinitionQuery;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
-import java.nio.charset.StandardCharsets;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -120,7 +119,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
 
     @Override
     public String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo,
-                                          byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form) {
+                                          byte[] bpmnBytes, String simpleJson, BpmFormDO form) {
         // 创建 Deployment 部署
         Deployment deploy = repositoryService.createDeployment()
                 .key(model.getKey()).name(model.getName()).category(model.getCategory())
@@ -145,8 +144,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
 
         // 插入拓展表
         BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class)
-                .setModelId(model.getId()).setProcessDefinitionId(definition.getId()).setModelType(modelMetaInfo.getType())
-                .setSimpleModel(StrUtil.str(simpleBytes, StandardCharsets.UTF_8));
+                .setModelId(model.getId()).setProcessDefinitionId(definition.getId())
+                .setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson);
 
         if (form != null) {
             definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf());
@@ -172,6 +171,11 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
         log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state);
     }
 
+    @Override
+    public void updateProcessDefinitionSortByModelId(String modelId, Long sort) {
+        processDefinitionMapper.updateByModelId(modelId, new BpmProcessDefinitionInfoDO().setSort(sort));
+    }
+
     @Override
     public BpmnModel getProcessDefinitionBpmnModel(String id) {
         return repositoryService.getBpmnModel(id);

+ 0 - 30
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityService.java

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.bpm.service.task;
-
-import org.flowable.engine.history.HistoricActivityInstance;
-
-import java.util.List;
-
-/**
- * BPM 活动实例 Service 接口
- *
- * @author 芋道源码
- */
-public interface BpmActivityService {
-
-    /**
-     * 获得指定流程实例的活动实例列表
-     *
-     * @param processInstanceId 流程实例的编号
-     * @return 活动实例列表
-     */
-    List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId);
-
-    /**
-     * 获得执行编号对应的活动实例
-     *
-     * @param executionId 执行编号
-     * @return 活动实例
-     */
-    List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId);
-
-}

+ 0 - 37
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java

@@ -1,37 +0,0 @@
-package cn.iocoder.yudao.module.bpm.service.task;
-
-import lombok.extern.slf4j.Slf4j;
-import org.flowable.engine.HistoryService;
-import org.flowable.engine.history.HistoricActivityInstance;
-import org.springframework.stereotype.Service;
-import org.springframework.validation.annotation.Validated;
-
-import javax.annotation.Resource;
-import java.util.List;
-
-
-/**
- * BPM 活动实例 Service 实现类
- *
- * @author 芋道源码
- */
-@Service
-@Slf4j
-@Validated
-public class BpmActivityServiceImpl implements BpmActivityService {
-
-    @Resource
-    private HistoryService historyService;
-
-    @Override
-    public List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId) {
-        return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId)
-                .orderByHistoricActivityInstanceStartTime().asc().list();
-    }
-
-    @Override
-    public List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId) {
-        return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list();
-    }
-
-}

+ 15 - 18
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java

@@ -3,9 +3,10 @@ package cn.iocoder.yudao.module.bpm.service.task;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
+import jakarta.validation.constraints.NotEmpty;
+import org.flowable.bpmn.model.FlowNode;
 
 import java.util.Collection;
-import java.util.Set;
 
 /**
  * 流程抄送 Service 接口
@@ -15,24 +16,29 @@ import java.util.Set;
 public interface BpmProcessInstanceCopyService {
 
     /**
-     * 流程实例的抄送
+     * 【管理员】流程实例的抄送
      *
      * @param userIds 抄送的用户编号
+     * @param reason 抄送意见
      * @param taskId 流程任务编号
      */
-    void createProcessInstanceCopy(Collection<Long> userIds, String taskId);
+    void createProcessInstanceCopy(Collection<Long> userIds, String reason, String taskId);
 
     /**
-     * 流程实例的抄送
+     * 【自动抄送】流程实例的抄送
      *
      * @param userIds 抄送的用户编号
+     * @param reason 抄送意见
      * @param processInstanceId 流程编号
-     * @param activityId 流程活动编号 id (对应 BPMN XML 节点 Id)
-     * // TODO 芋艿这个 taskId 是不是可以不要了
-     * @param taskId 任务编号
-     * @param taskName 任务名称
+     * @param activityId 流程活动编号(对应 {@link FlowNode#getId()})
+     * @param activityName 任务编号(对应 {@link FlowNode#getName()})
+     * @param taskId 任务编号,允许空
      */
-    void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String activityId, String taskId, String taskName);
+    void createProcessInstanceCopy(Collection<Long> userIds, String reason,
+                                   @NotEmpty(message = "流程实例编号不能为空") String processInstanceId,
+                                   @NotEmpty(message = "流程活动编号不能为空") String activityId,
+                                   @NotEmpty(message = "流程活动名字不能为空") String activityName,
+                                   String taskId);
 
     /**
      * 获得抄送的流程的分页
@@ -43,14 +49,5 @@ public interface BpmProcessInstanceCopyService {
      */
     PageResult<BpmProcessInstanceCopyDO> getProcessInstanceCopyPage(Long userId,
                                                                     BpmProcessInstanceCopyPageReqVO pageReqVO);
-    // TODO @芋艿:重点在 review 下
-    /**
-     * 通过流程实例和流程活动编号获取抄送人的 Id
-     *
-     * @param processInstanceId 流程实例 Id
-     * @param activityId 流程活动编号 Id
-     * @return 抄送人 Ids
-     */
-    Set<Long> getCopyUserIds(String processInstanceId, String activityId);
 
 }

+ 9 - 15
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.service.task;
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper;
@@ -19,7 +18,6 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
-import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -49,17 +47,19 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
     private BpmProcessDefinitionService processDefinitionService;
 
     @Override
-    public void createProcessInstanceCopy(Collection<Long> userIds, String taskId) {
+    public void createProcessInstanceCopy(Collection<Long> userIds, String reason, String taskId) {
         Task task = taskService.getTask(taskId);
         if (ObjectUtil.isNull(task)) {
             throw exception(ErrorCodeConstants.TASK_NOT_EXISTS);
         }
-        String processInstanceId = task.getProcessInstanceId();
-        createProcessInstanceCopy(userIds, processInstanceId, task.getTaskDefinitionKey(), task.getId(), task.getName());
+        // 执行抄送
+        createProcessInstanceCopy(userIds, reason,
+                task.getProcessInstanceId(), task.getTaskDefinitionKey(), task.getId(), task.getName());
     }
 
     @Override
-    public void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String activityId, String taskId, String taskName) {
+    public void createProcessInstanceCopy(Collection<Long> userIds, String reason, String processInstanceId,
+                                          String activityId, String activityName, String taskId) {
         // 1.1 校验流程实例存在
         ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
         if (processInstance == null) {
@@ -74,10 +74,10 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
 
         // 2. 创建抄送流程
         List<BpmProcessInstanceCopyDO> copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO()
-                .setUserId(userId).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
+                .setUserId(userId).setReason(reason).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
                 .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
-                .setCategory(processDefinition.getCategory()).setActivityId(activityId)
-                .setTaskId(taskId).setTaskName(taskName));
+                .setCategory(processDefinition.getCategory()).setTaskId(taskId)
+                .setActivityId(activityId).setActivityName(activityName));
         processInstanceCopyMapper.insertBatch(copyList);
     }
 
@@ -87,10 +87,4 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
         return processInstanceCopyMapper.selectPage(userId, pageReqVO);
     }
 
-    @Override
-    public Set<Long> getCopyUserIds(String processInstanceId, String activityId) {
-        return CollectionUtils.convertSet(processInstanceCopyMapper.selectListByProcessInstanceIdAndActivityId(processInstanceId, activityId),
-                BpmProcessInstanceCopyDO::getUserId);
-    }
-
 }

+ 8 - 9
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java

@@ -84,14 +84,6 @@ public interface BpmProcessInstanceService {
     PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
                                                                @Valid BpmProcessInstancePageReqVO pageReqVO);
 
-    /**
-     * 获得表单字段权限
-     *
-     * @param reqVO 请求消息
-     * @return 表单字段权限
-     */
-    Map<String, String> getFormFieldsPermission(@Valid BpmFormFieldsPermissionReqVO reqVO);
-
     // TODO @芋艿:重点在 review 下
     /**
      * 获取审批详情。
@@ -104,6 +96,14 @@ public interface BpmProcessInstanceService {
      */
     BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
 
+    /**
+     * 获取流程实例的 BPMN 模型视图
+     *
+     * @param id 流程实例的编号
+     * @return BPMN 模型视图
+     */
+    BpmProcessInstanceBpmnModelViewRespVO getProcessInstanceBpmnModelView(String id);
+
     // ========== Update 写入相关方法 ==========
 
     /**
@@ -157,5 +157,4 @@ public interface BpmProcessInstanceService {
      */
     void processProcessInstanceCompleted(ProcessInstance instance);
 
-
 }

File diff suppressed because it is too large
+ 0 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java


+ 52 - 15
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java

@@ -5,7 +5,9 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
 import org.flowable.bpmn.model.UserTask;
+import org.flowable.engine.history.HistoricActivityInstance;
 import org.flowable.task.api.Task;
+import org.flowable.task.api.TaskInfo;
 import org.flowable.task.api.history.HistoricTaskInstance;
 
 import javax.validation.Valid;
@@ -32,6 +34,15 @@ public interface BpmTaskService {
      */
     PageResult<Task> getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO);
 
+    /**
+     * 获得用户在指定流程下,首个需要处理(待办)的任务
+     *
+     * @param userId 用户编号
+     * @param processInstanceId 流程实例编号
+     * @return 待办任务
+     */
+    BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId);
+
     /**
      * 获得已办的流程任务分页
      *
@@ -73,9 +84,10 @@ public interface BpmTaskService {
      * 获得指定流程实例的流程任务列表,包括所有状态的
      *
      * @param processInstanceId 流程实例的编号
+     * @param asc               是否升序
      * @return 流程任务列表
      */
-    List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId);
+    List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId, Boolean asc);
 
     /**
      * 获取任务
@@ -105,21 +117,30 @@ public interface BpmTaskService {
      * 根据条件查询正在进行中的任务
      *
      * @param processInstanceId 流程实例编号,不允许为空
-     * @param assigned 是否分配了审批人,允许空
-     * @param taskDefineKey 任务定义 Key,允许空
+     * @param assigned          是否分配了审批人,允许空
+     * @param taskDefineKey     任务定义 Key,允许空
      */
     List<Task> getRunningTaskListByProcessInstanceId(String processInstanceId,
                                                      Boolean assigned,
                                                      String taskDefineKey);
 
     /**
-     * 获取当前任务的可退的 UserTask 集合
+     * 获取当前任务的可退的 UserTask 集合
      *
      * @param id 当前的任务 ID
-     * @return 可以退的节点列表
+     * @return 可以退的节点列表
      */
     List<UserTask> getUserTaskListByReturn(String id);
 
+    /**
+     * 获取指定任务的子任务列表(多层)
+     *
+     * @param parentTaskId 父任务 ID
+     * @param tasks 任务列表
+     * @return 子任务列表
+     */
+    <T extends TaskInfo> List<T> getAllChildrenTaskListByParentTaskId(String parentTaskId, List<T> tasks);
+
     /**
      * 获取指定任务的子任务列表
      *
@@ -129,12 +150,20 @@ public interface BpmTaskService {
     List<Task> getTaskListByParentTaskId(String parentTaskId);
 
     /**
-     * 通过任务 ID,查询任务名 Map
+     * 获得指定流程实例的活动实例列表
      *
-     * @param taskIds 任务 ID
-     * @return 任务 ID 与名字的 Map
+     * @param processInstanceId 流程实例的编号
+     * @return 活动实例列表
      */
-    Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds);
+    List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId);
+
+    /**
+     * 获得执行编号对应的活动实例
+     *
+     * @param executionId 执行编号
+     * @return 活动实例
+     */
+    List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId);
 
     // ========== Update 写入相关方法 ==========
 
@@ -170,10 +199,10 @@ public interface BpmTaskService {
     void moveTaskToEnd(String processInstanceId);
 
     /**
-     * 将任务退到指定的 targetDefinitionKey 位置
+     * 将任务退到指定的 targetDefinitionKey 位置
      *
      * @param userId 用户编号
-     * @param reqVO  退的任务key和当前所在的任务ID
+     * @param reqVO  退的任务key和当前所在的任务ID
      */
     void returnTask(Long userId, BpmTaskReturnReqVO reqVO);
 
@@ -201,14 +230,22 @@ public interface BpmTaskService {
      */
     void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO);
 
+    /**
+     * 抄送任务
+     *
+     * @param userId 用户编号
+     * @param reqVO  通过请求
+     */
+    void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO);
+
     // ========== Event 事件相关方法 ==========
 
     /**
      * 处理 Task 创建事件,目前是
-     *
+     * <p>
      * 1. 更新它的状态为审批中
      * 2. 处理自动通过的情况,例如说:1)无审批人时,是否自动通过、不通过;2)非【人工审核】时,是否自动通过、不通过
-     *
+     * <p>
      * 注意:它的触发时机,晚于 {@link #processTaskAssigned(Task)} 之后
      *
      * @param task 任务实体
@@ -233,8 +270,8 @@ public interface BpmTaskService {
      * 处理 Task 审批超时事件,可能会处理多个当前审批中的任务
      *
      * @param processInstanceId 流程示例编号
-     * @param taskDefineKey 任务 Key
-     * @param handlerType 处理类型,参见 {@link BpmUserTaskTimeoutHandlerTypeEnum}
+     * @param taskDefineKey     任务 Key
+     * @param handlerType       处理类型,参见 {@link BpmUserTaskTimeoutHandlerTypeEnum}
      */
     void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType);
 

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

@@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableCon
 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.definition.BpmModelService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
 import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -36,9 +37,11 @@ import org.flowable.engine.HistoryService;
 import org.flowable.engine.ManagementService;
 import org.flowable.engine.RuntimeService;
 import org.flowable.engine.TaskService;
+import org.flowable.engine.history.HistoricActivityInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.DelegationState;
 import org.flowable.task.api.Task;
+import org.flowable.task.api.TaskInfo;
 import org.flowable.task.api.TaskQuery;
 import org.flowable.task.api.history.HistoricTaskInstance;
 import org.flowable.task.api.history.HistoricTaskInstanceQuery;
@@ -81,6 +84,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     @Resource
     private BpmProcessInstanceService processInstanceService;
     @Resource
+    private BpmProcessDefinitionService bpmProcessDefinitionService;
+    @Resource
     private BpmProcessInstanceCopyService processInstanceCopyService;
     @Resource
     private BpmModelService modelService;
@@ -116,6 +121,41 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         return new PageResult<>(tasks, count);
     }
 
+    @Override
+    public BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId) {
+        if (processInstanceId == null) {
+            return null;
+        }
+        // 1. 查询所有任务
+        List<Task> tasks = taskService.createTaskQuery()
+                .active()
+                .processInstanceId(processInstanceId)
+                .includeTaskLocalVariables()
+                .includeProcessVariables()
+                .orderByTaskCreateTime().asc() // 按创建时间升序
+                .list();
+        if (CollUtil.isEmpty(tasks)) {
+            return null;
+        }
+
+        // 2.1 查询我的首个任务
+        Task todoTask = CollUtil.findOne(tasks, task -> {
+            return isAssignUserTask(userId, task) // 当前用户为审批人
+                    || isAddSignUserTask(userId, task); // 当前用户为加签人(为了减签)
+        });
+        if (todoTask == null) {
+            return null;
+        }
+        // 2.2 查询该任务的子任务
+        List<Task> childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), tasks);
+
+        // 3. 转换返回
+        BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId());
+        Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonsSetting = BpmnModelUtils.parseButtonsSetting(
+                bpmnModel, todoTask.getTaskDefinitionKey());
+        return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting);
+    }
+
     @Override
     public PageResult<HistoricTaskInstance> getTaskDonePage(Long userId, BpmTaskPageReqVO pageVO) {
         HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery()
@@ -170,16 +210,16 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     }
 
     @Override
-    public List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId) {
-        List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery()
+    public List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId, Boolean asc) {
+        HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery()
                 .includeTaskLocalVariables()
-                .processInstanceId(processInstanceId)
-                .orderByHistoricTaskInstanceStartTime().desc() // 创建时间倒序
-                .list();
-        if (CollUtil.isEmpty(tasks)) {
-            return Collections.emptyList();
+                .processInstanceId(processInstanceId);
+        if (Boolean.TRUE.equals(asc)) {
+            query.orderByHistoricTaskInstanceStartTime().asc();
+        } else {
+            query.orderByHistoricTaskInstanceStartTime().desc();
         }
-        return tasks;
+        return query.list();
     }
 
     /**
@@ -254,11 +294,43 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         if (CollUtil.isEmpty(previousUserList)) {
             return Collections.emptyList();
         }
-        // 2.2 过滤:只有串行可到达的节点,才可以退。类似非串行、子流程无法退回
+        // 2.2 过滤:只有串行可到达的节点,才可以退。类似非串行、子流程无法退回
         previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null));
         return previousUserList;
     }
 
+    @Override
+    public <T extends TaskInfo> List<T> getAllChildrenTaskListByParentTaskId(String parentTaskId, List<T> tasks) {
+        if (CollUtil.isEmpty(tasks)) {
+            return Collections.emptyList();
+        }
+        Map<String, List<T>> parentTaskMap = convertMultiMap(
+                filterList(tasks, task -> StrUtil.isNotEmpty(task.getParentTaskId())), TaskInfo::getParentTaskId);
+        if (CollUtil.isEmpty(parentTaskMap)) {
+            return Collections.emptyList();
+        }
+
+        List<T> result = new ArrayList<>();
+        // 1. 递归获取子级
+        Stack<String> stack = new Stack<>();
+        stack.push(parentTaskId);
+        // 2. 递归遍历
+        for (int i = 0; i < Short.MAX_VALUE; i++) {
+            if (stack.isEmpty()) {
+                break;
+            }
+            // 2.1 获取子任务们
+            String taskId = stack.pop();
+            List<T> childTaskList = filterList(tasks, task -> StrUtil.equals(task.getParentTaskId(), taskId));
+            // 2.2 如果非空,则添加到 stack 进一步递归
+            if (CollUtil.isNotEmpty(childTaskList)) {
+                stack.addAll(convertList(childTaskList, TaskInfo::getId));
+                result.addAll(childTaskList);
+            }
+        }
+        return result;
+    }
+
     /**
      * 获得所有子任务列表
      *
@@ -331,12 +403,50 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     }
 
     @Override
-    public Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds) {
-        if (CollUtil.isEmpty(taskIds)) {
-            return Collections.emptyMap();
-        }
-        List<Task> tasks = taskService.createTaskQuery().taskIds(taskIds).list();
-        return convertMap(tasks, Task::getId, Task::getName);
+    public List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId) {
+        return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId)
+                .orderByHistoricActivityInstanceStartTime().asc().list();
+    }
+
+    @Override
+    public List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId) {
+        return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list();
+    }
+
+    /**
+     * 判断指定用户,是否是当前任务的审批人
+     *
+     * @param userId 用户编号
+     * @param task   任务
+     * @return 是否
+     */
+    private boolean isAssignUserTask(Long userId, Task task) {
+        Long assignee = NumberUtil.parseLong(task.getAssignee(), null);
+        return ObjectUtil.equals(userId, assignee);
+    }
+
+    /**
+     * 判断指定用户,是否是当前任务的拥有人
+     *
+     * @param userId 用户编号
+     * @param task   任务
+     * @return 是否
+     */
+    private boolean isOwnerUserTask(Long userId, Task task) {
+        Long assignee = NumberUtil.parseLong(task.getOwner(), null);
+        return ObjectUtil.equal(userId, assignee);
+    }
+
+    /**
+     * 判断指定用户,是否是当前任务的加签人
+     *
+     * @param userId 用户 Id
+     * @param task 任务
+     * @return 是否
+     */
+    private boolean isAddSignUserTask(Long userId, Task task) {
+        return (isAssignUserTask(userId, task) || isOwnerUserTask(userId, task))
+                && BpmTaskSignTypeEnum.of(task.getScopeType()) != null;
     }
 
     // ========== Update 写入相关方法 ==========
@@ -352,11 +462,6 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             throw exception(PROCESS_INSTANCE_NOT_EXISTS);
         }
 
-        // 2. 抄送用户
-        if (CollUtil.isNotEmpty(reqVO.getCopyUserIds())) {
-            processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getId());
-        }
-
         // 情况一:被委派的任务,不调用 complete 去完成任务
         if (DelegationState.PENDING.equals(task.getDelegationState())) {
             approveDelegateTask(reqVO, task);
@@ -370,12 +475,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
 
         // 情况三:审批普通的任务。大多数情况下,都是这样
-        // 3.1 更新 task 状态、原因
+        // 2.1 更新 task 状态、原因
         updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), reqVO.getReason());
-        // 3.2 添加评论
+        // 2.2 添加评论
         taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
                 BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
-        // 3.3 调用 BPM complete 去完成任务
+        // 2.3 调用 BPM complete 去完成任务
         // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
         if (CollUtil.isNotEmpty(reqVO.getVariables())) {
             Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
@@ -519,7 +624,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         BpmUserTaskRejectHandlerType userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement);
         if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.RETURN_USER_TASK) {
             String returnTaskId = BpmnModelUtils.parseReturnTaskId(userTaskElement);
-            Assert.notNull(returnTaskId, "退的节点不能为空");
+            Assert.notNull(returnTaskId, "退的节点不能为空");
             returnTask(userId, new BpmTaskReturnReqVO().setId(task.getId())
                     .setTargetTaskDefinitionKey(returnTaskId).setReason(reqVO.getReason()));
             return;
@@ -563,12 +668,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(),
                 reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId());
 
-        // 2. 调用 Flowable 框架的退逻辑
+        // 2. 调用 Flowable 框架的退逻辑
         returnTask(task, targetElement, reqVO);
     }
 
     /**
-     * 退流程节点时,校验目标任务节点是否可退
+     * 退流程节点时,校验目标任务节点是否可退
      *
      * @param sourceKey           当前任务节点 Key
      * @param targetKey           目标任务节点 key
@@ -586,7 +691,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             throw exception(TASK_TARGET_NODE_NOT_EXISTS);
         }
 
-        // 2.2 只有串行可到达的节点,才可以退。类似非串行、子流程无法退回
+        // 2.2 只有串行可到达的节点,才可以退。类似非串行、子流程无法退回
         if (!BpmnModelUtils.isSequentialReachable(source, target, null)) {
             throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
         }
@@ -594,10 +699,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     }
 
     /**
-     * 执行退逻辑
+     * 执行退逻辑
      *
-     * @param currentTask   当前退的任务
-     * @param targetElement 需要退到的目标任务
+     * @param currentTask   当前退的任务
+     * @param targetElement 需要退到的目标任务
      * @param reqVO         前端参数封装
      */
     public void returnTask(Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
@@ -610,9 +715,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
         List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
 
-        // 2. 给当前要被退的 task 数组,设置退意见
+        // 2. 给当前要被退的 task 数组,设置退意见
         taskList.forEach(task -> {
-            // 需要排除掉,不需要设置退意见的任务
+            // 需要排除掉,不需要设置退意见的任务
             if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
                 return;
             }
@@ -659,9 +764,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         taskService.setOwner(taskId, task.getAssignee());
         // 3.2 执行委派,将任务委派给 delegateUser
         taskService.delegateTask(taskId, reqVO.getDelegateUserId().toString());
-        // 3.3 更新 task 状态。
-        // 为什么不更新原因?因为原因目前主要给审批通过、不通过时使用
-        updateTaskStatus(taskId, BpmTaskStatusEnum.DELEGATE.getStatus());
+        // 补充说明:委托不单独设置状态。如果需要,可通过 Task 的 DelegationState 字段,判断是否为 DelegationState.PENDING 委托中
     }
 
     @Override
@@ -868,6 +971,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         handleParentTaskIfSign(task.getParentTaskId());
     }
 
+    @Override
+    public void copyTask(Long userId, BpmTaskCopyReqVO reqVO) {
+        processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId());
+    }
+
     /**
      * 校验任务是否能被减签
      *
@@ -920,8 +1028,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                 if (ObjectUtil.notEqual(transactionStatus, TransactionSynchronization.STATUS_COMMITTED)) {
                     return;
                 }
+                // TODO 芋艿:可以后续优化成 getSelf();
                 // 特殊情况一:【人工审核】审批人为空,根据配置是否要自动通过、自动拒绝
                 if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.USER.getType())) {
+                    // 如果有审批人、或者拥有人,则说明不满足情况一,不自动通过、不自动拒绝
+                    if (!ObjectUtil.isAllEmpty(task.getAssignee(), task.getOwner())) {
+                        return;
+                    }
                     if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.APPROVE.getType())) {
                         SpringUtil.getBean(BpmTaskService.class).approveTask(null, new BpmTaskApproveReqVO()
                                 .setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_APPROVE.getReason()));
@@ -987,7 +1100,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                 }
                 // 审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
                 if (StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
-                    // 判断是否为退或者驳回:如果是退或者驳回不走这个策略
+                    // 判断是否为退或者驳回:如果是退或者驳回不走这个策略
                     // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识
                     Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
                             String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);

+ 0 - 36
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/bo/AlreadyRunApproveNodeRespBO.java

@@ -1,36 +0,0 @@
-package cn.iocoder.yudao.module.bpm.service.task.bo;
-
-import lombok.Data;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ApprovalNodeInfo;
-
-/**
- * 已经进行中的审批节点 Response BO
- *
- * @author jason
- */
-@Data
-public class AlreadyRunApproveNodeRespBO {
-
-    /**
-     * 审批节点信息数组
-     */
-    private List<ApprovalNodeInfo> approveNodes;
-
-    /**
-     * 已运行的节点 ID 数组 (对应 Bpmn XML 节点 id)
-     */
-    private Set<String> runNodeIds;
-
-    /**
-     * 正在运行的节点的审批信息(key: activityId, value: 审批信息)
-     * <p>
-     * 用于依次审批,需要加上候选人信息
-     */
-    private Map<String, ApprovalNodeInfo> runningApprovalNodes;
-
-}

+ 207 - 34
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java

@@ -1,31 +1,41 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
 
 import cn.hutool.core.map.MapUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateUserStrategy;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other.BpmTaskCandidateAssignEmptyStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
+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 org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.ExtensionElement;
+import org.flowable.bpmn.model.FlowElement;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
+import org.mockito.MockedStatic;
 import org.mockito.Spy;
+import org.mockito.internal.util.collections.Sets;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE;
+import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_PREFIX;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 /**
  * {@link BpmTaskCandidateInvoker} 的单元测试
@@ -39,44 +49,207 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
     @Mock
     private AdminUserApi adminUserApi;
 
+    @Mock
+    private BpmProcessInstanceService processInstanceService;
+
     @Spy
-    private BpmTaskCandidateStrategy strategy ;
+    private BpmTaskCandidateStrategy userStrategy;
+    @Mock
+    private BpmTaskCandidateAssignEmptyStrategy emptyStrategy;
 
     @Spy
-    private List<BpmTaskCandidateStrategy> strategyList ;
+    private List<BpmTaskCandidateStrategy> strategyList;
 
     @BeforeEach
     public void setUp() {
-        strategy = new BpmTaskCandidateUserStrategy(adminUserApi); // 创建strategy实例
-        strategyList = Collections.singletonList(strategy); // 创建strategyList
+        userStrategy = new BpmTaskCandidateUserStrategy(); // 创建 strategy 实例
+        when(emptyStrategy.getStrategy()).thenReturn(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY);
+        strategyList = List.of(userStrategy, emptyStrategy); // 创建 strategyList
         taskCandidateInvoker = new BpmTaskCandidateInvoker(strategyList, adminUserApi);
     }
 
+    /**
+     * 场景:成功计算到候选人,但是移除了发起人的用户
+     */
     @Test
-    public void testCalculateUsers() {
-        // 准备参数
-        String param = "1,2";
-        DelegateExecution execution = mock(DelegateExecution.class);
-        // mock 方法(DelegateExecution)
-        UserTask userTask = mock(UserTask.class);
-        when(execution.getCurrentFlowElement()).thenReturn(userTask);
-        when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)))
-                .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString());
-        when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM)))
-                .thenReturn(param);
-        // mock 方法(adminUserApi)
-        AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
-                .setStatus(CommonStatusEnum.ENABLE.getStatus()));
-        AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
-                .setStatus(CommonStatusEnum.ENABLE.getStatus()));
-        Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
-                .put(user2.getId(), user2).build();
-        when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
+    public void testCalculateUsersByTask_some() {
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            // 准备参数
+            String param = "1,2";
+            DelegateExecution execution = mock(DelegateExecution.class);
+            // mock 方法(DelegateExecution)
+            UserTask userTask = mock(UserTask.class);
+            String processInstanceId = randomString();
+            when(execution.getProcessInstanceId()).thenReturn(processInstanceId);
+            when(execution.getCurrentFlowElement()).thenReturn(userTask);
+            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)))
+                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString());
+            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM)))
+                    .thenReturn(param);
+            // mock 方法(adminUserApi)
+            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
+                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
+            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
+                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
+            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
+                    .put(user2.getId(), user2).build();
+            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
+            // mock 移除发起人的用户
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class))
+                    .thenReturn(processInstanceService);
+            ProcessInstance processInstance = mock(ProcessInstance.class);
+            when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance);
+            when(processInstance.getStartUserId()).thenReturn("1");
+            mockFlowElementExtensionElement(userTask, BpmnModelConstants.USER_TASK_ASSIGN_START_USER_HANDLER_TYPE,
+                    String.valueOf(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType()));
 
-        // 调用
-        Set<Long> results = taskCandidateInvoker.calculateUsers(execution);
-        // 断言
-        assertEquals(asSet(1L, 2L), results);
+            // 调用
+            Set<Long> results = taskCandidateInvoker.calculateUsersByTask(execution);
+            // 断言
+            assertEquals(asSet(2L), results);
+        }
+    }
+
+    /**
+     * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配
+     */
+    @Test
+    public void testCalculateUsersByTask_none() {
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            // 准备参数
+            String param = "1,2";
+            DelegateExecution execution = mock(DelegateExecution.class);
+            // mock 方法(DelegateExecution)
+            UserTask userTask = mock(UserTask.class);
+            String processInstanceId = randomString();
+            when(execution.getProcessInstanceId()).thenReturn(processInstanceId);
+            when(execution.getCurrentFlowElement()).thenReturn(userTask);
+            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)))
+                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString());
+            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM)))
+                    .thenReturn(param);
+            // mock 方法(adminUserApi)
+            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
+                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
+            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
+                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
+            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
+                    .put(user2.getId(), user2).build();
+            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
+            // mock 方法(empty)
+            when(emptyStrategy.calculateUsersByTask(same(execution), same(param)))
+                    .thenReturn(Sets.newSet(2L));
+            // mock 移除发起人的用户
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class))
+                    .thenReturn(processInstanceService);
+            ProcessInstance processInstance = mock(ProcessInstance.class);
+            when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance);
+            when(processInstance.getStartUserId()).thenReturn("1");
+
+            // 调用
+            Set<Long> results = taskCandidateInvoker.calculateUsersByTask(execution);
+            // 断言
+            assertEquals(asSet(2L), results);
+        }
+    }
+
+    /**
+     * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配
+     */
+    @Test
+    public void testCalculateUsersByActivity_some() {
+        try (MockedStatic<BpmnModelUtils> bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) {
+            // 准备参数
+            String param = "1,2";
+            BpmnModel bpmnModel = mock(BpmnModel.class);
+            String activityId = randomString();
+            Long startUserId = 1L;
+            String processDefinitionId = randomString();
+            Map<String, Object> processVariables = new HashMap<>();
+            // mock 方法(DelegateExecution)
+            UserTask userTask = mock(UserTask.class);
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask)))
+                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy());
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask)))
+                    .thenReturn(param);
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask);
+            // mock 方法(adminUserApi)
+            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
+                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
+            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
+                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
+            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
+                    .put(user2.getId(), user2).build();
+            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
+            // mock 移除发起人的用户
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignStartUserHandlerType(same(userTask)))
+                    .thenReturn(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType());
+
+            // 调用
+            Set<Long> results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId,
+                    startUserId, processDefinitionId, processVariables);
+            // 断言
+            assertEquals(asSet(2L), results);
+        }
+    }
+
+    /**
+     * 场景:成功计算到候选人,但是移除了发起人的用户
+     */
+    @Test
+    public void testCalculateUsersByActivity_none() {
+        try (MockedStatic<BpmnModelUtils> bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) {
+            // 准备参数
+            String param = "1,2";
+            BpmnModel bpmnModel = mock(BpmnModel.class);
+            String activityId = randomString();
+            Long startUserId = 1L;
+            String processDefinitionId = randomString();
+            Map<String, Object> processVariables = new HashMap<>();
+            // mock 方法(DelegateExecution)
+            UserTask userTask = mock(UserTask.class);
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask)))
+                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy());
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask)))
+                    .thenReturn(param);
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask);
+            // mock 方法(adminUserApi)
+            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
+                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
+            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
+                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
+            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
+                    .put(user2.getId(), user2).build();
+            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
+            // mock 方法(empty)
+            when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId),
+                            eq(param), same(startUserId), same(processDefinitionId), same(processVariables)))
+                    .thenReturn(Sets.newSet(2L));
+
+            // 调用
+            Set<Long> results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId,
+                    startUserId, processDefinitionId, processVariables);
+            // 断言
+            assertEquals(asSet(2L), results);
+        }
+    }
+
+    private static void mockFlowElementExtensionElement(FlowElement element, String name, String value) {
+        if (value == null) {
+            return;
+        }
+        ExtensionElement extensionElement = new ExtensionElement();
+        extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
+        extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
+        extensionElement.setElementText(value);
+        extensionElement.setName(name);
+        // mock
+        Map<String, List<ExtensionElement>> extensionElements = element.getExtensionElements();
+        if (extensionElements == null) {
+            extensionElements = new LinkedHashMap<>();
+        }
+        extensionElements.put(name, Collections.singletonList(extensionElement));
+        when(element.getExtensionElements()).thenReturn(extensionElements);
     }
 
     @Test

+ 43 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import org.assertj.core.util.Sets;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+public class BpmTaskCandidateDeptLeaderMultiStrategyTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private BpmTaskCandidateDeptLeaderMultiStrategy strategy;
+
+    @Mock
+    private DeptApi deptApi;
+
+    @Test
+    public void testCalculateUsers() {
+        // 准备参数
+        String param = "10,20|2";
+        // mock 方法
+        when(deptApi.getDept(any())).thenAnswer((Answer<DeptRespDTO>) invocationOnMock -> {
+            Long deptId = invocationOnMock.getArgument(0);
+            return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1));
+        });
+
+        // 调用
+        Set<Long> userIds = strategy.calculateUsers(param);
+        // 断言结果
+        assertEquals(Sets.newLinkedHashSet(11L, 1001L, 21L, 2001L), userIds);
+    }
+
+}

+ 10 - 11
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategyTest.java → yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java

@@ -1,23 +1,22 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
-import org.junit.jupiter.api.Disabled;
+import org.assertj.core.util.Sets;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
 import java.util.Set;
 
-import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static java.util.Arrays.asList;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
-@Disabled // TODO 芋艿:临时注释
 public class BpmTaskCandidateDeptLeaderStrategyTest extends BaseMockitoUnitTest {
 
     @InjectMocks
@@ -29,16 +28,16 @@ public class BpmTaskCandidateDeptLeaderStrategyTest extends BaseMockitoUnitTest
     @Test
     public void testCalculateUsers() {
         // 准备参数
-        String param = "1,2";
+        String param = "10,20";
         // mock 方法
-        DeptRespDTO dept1 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(11L));
-        DeptRespDTO dept2 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(22L));
-        when(deptApi.getDeptList(eq(asSet(1L, 2L)))).thenReturn(asList(dept1, dept2));
+        when(deptApi.getDeptList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList(
+                randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(10L).setLeaderUserId(11L)),
+                randomPojo(DeptRespDTO.class, o -> o.setId(20L).setParentId(20L).setLeaderUserId(21L))));
 
         // 调用
-        Set<Long> results = strategy.calculateUsers(null, param);
-        // 断言
-        assertEquals(asSet(11L, 22L), results);
+        Set<Long> userIds = strategy.calculateUsers(param);
+        // 断言结果
+        assertEquals(Sets.newLinkedHashSet(11L, 21L), userIds);
     }
 
 }

+ 15 - 13
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategyTest.java → yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java

@@ -1,44 +1,46 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
 
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+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.junit.jupiter.api.Disabled;
+import org.assertj.core.util.Sets;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
-import java.util.List;
 import java.util.Set;
 
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static java.util.Arrays.asList;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
-@Disabled // TODO 芋艿:临时注释
 public class BpmTaskCandidateDeptMemberStrategyTest extends BaseMockitoUnitTest {
 
     @InjectMocks
     private BpmTaskCandidateDeptMemberStrategy strategy;
 
+    @Mock
+    private DeptApi deptApi;
     @Mock
     private AdminUserApi adminUserApi;
 
     @Test
     public void testCalculateUsers() {
         // 准备参数
-        String param = "11,22";
+        String param = "10,20";
         // mock 方法
-        List<AdminUserRespDTO> users = convertList(asSet(11L, 22L),
-                id -> new AdminUserRespDTO().setId(id));
-        when(adminUserApi.getUserListByDeptIds(eq(asSet(11L, 22L)))).thenReturn(users);
+        when(adminUserApi.getUserListByDeptIds(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList(
+                randomPojo(AdminUserRespDTO.class, o -> o.setId(11L)),
+                randomPojo(AdminUserRespDTO.class, o -> o.setId(21L))));
 
         // 调用
-        Set<Long> results = strategy.calculateUsers(null, param);
-        // 断言
-        assertEquals(asSet(11L, 22L), results);
+        Set<Long> userIds = strategy.calculateUsers(param);
+        // 断言结果
+        assertEquals(Sets.newLinkedHashSet(11L, 21L), userIds);
     }
 
 }

+ 82 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java

@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+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 org.assertj.core.util.Sets;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private BpmTaskCandidateStartUserDeptLeaderMultiStrategy strategy;
+
+    @Mock
+    private BpmProcessInstanceService processInstanceService;
+
+    @Mock
+    private AdminUserApi adminUserApi;
+    @Mock
+    private DeptApi deptApi;
+
+    @Test
+    public void testCalculateUsersByTask() {
+        // 准备参数
+        String param = "2";
+        // mock 方法(获得流程发起人)
+        Long startUserId = 1L;
+        ProcessInstance processInstance = mock(ProcessInstance.class);
+        DelegateExecution execution = mock(DelegateExecution.class);
+        when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance);
+        when(processInstance.getStartUserId()).thenReturn(startUserId.toString());
+        // mock 方法(获取发起人的 multi 部门负责人)
+        mockGetStartUserDept(startUserId);
+
+        // 调用
+        Set<Long> userIds = strategy.calculateUsersByTask(execution, param);
+        // 断言
+        assertEquals(Sets.newLinkedHashSet(11L, 1001L), userIds);
+    }
+
+    @Test
+    public void testCalculateUsersByActivity() {
+        // 准备参数
+        String param = "2";
+        // mock 方法
+        Long startUserId = 1L;
+        mockGetStartUserDept(startUserId);
+
+        // 调用
+        Set<Long> userIds = strategy.calculateUsersByActivity(null, null, param,
+                startUserId, null, null);
+        // 断言
+        assertEquals(Sets.newLinkedHashSet(11L, 1001L), userIds);
+    }
+
+    private void mockGetStartUserDept(Long startUserId) {
+        when(adminUserApi.getUser(eq(startUserId))).thenReturn(
+                randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)));
+        when(deptApi.getDept(any())).thenAnswer((Answer<DeptRespDTO>) invocationOnMock -> {
+            Long deptId = invocationOnMock.getArgument(0);
+            return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1));
+        });
+    }
+
+}

+ 82 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java

@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+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 org.assertj.core.util.Sets;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BpmTaskCandidateStartUserDeptLeaderStrategyTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private BpmTaskCandidateStartUserDeptLeaderStrategy strategy;
+
+    @Mock
+    private BpmProcessInstanceService processInstanceService;
+
+    @Mock
+    private AdminUserApi adminUserApi;
+    @Mock
+    private DeptApi deptApi;
+
+    @Test
+    public void testCalculateUsersByTask() {
+        // 准备参数
+        String param = "2";
+        // mock 方法(获得流程发起人)
+        Long startUserId = 1L;
+        ProcessInstance processInstance = mock(ProcessInstance.class);
+        DelegateExecution execution = mock(DelegateExecution.class);
+        when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance);
+        when(processInstance.getStartUserId()).thenReturn(startUserId.toString());
+        // mock 方法(获取发起人的部门负责人)
+        mockGetStartUserDeptLeader(startUserId);
+
+        // 调用
+        Set<Long> userIds = strategy.calculateUsersByTask(execution, param);
+        // 断言
+        assertEquals(Sets.newLinkedHashSet(1001L), userIds);
+    }
+
+    @Test
+    public void testGetStartUserDeptLeader() {
+        // 准备参数
+        String param = "2";
+        // mock 方法
+        Long startUserId = 1L;
+        mockGetStartUserDeptLeader(startUserId);
+
+        // 调用
+        Set<Long> userIds = strategy.calculateUsersByActivity(null, null, param,
+                startUserId, null, null);
+        // 断言
+        assertEquals(Sets.newLinkedHashSet(1001L), userIds);
+    }
+
+    private void mockGetStartUserDeptLeader(Long startUserId) {
+        when(adminUserApi.getUser(eq(startUserId))).thenReturn(
+                randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)));
+        when(deptApi.getDept(any())).thenAnswer((Answer<DeptRespDTO>) invocationOnMock -> {
+            Long deptId = invocationOnMock.getArgument(0);
+            return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1));
+        });
+    }
+
+}

+ 68 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java

@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
+
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import org.assertj.core.util.Sets;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BpmTaskCandidateStartUserSelectStrategyTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private BpmTaskCandidateStartUserSelectStrategy strategy;
+
+    @Mock
+    private BpmProcessInstanceService processInstanceService;
+
+    @Test
+    public void testCalculateUsersByTask() {
+        // 准备参数
+        String param = "2";
+        // mock 方法(获得流程发起人)
+        ProcessInstance processInstance = mock(ProcessInstance.class);
+        DelegateExecution execution = mock(DelegateExecution.class);
+        when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance);
+        when(execution.getCurrentActivityId()).thenReturn("activity_001");
+        // mock 方法(FlowableUtils)
+        Map<String, Object> processVariables = new HashMap<>();
+        processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES,
+                MapUtil.of("activity_001", List.of(1L, 2L)));
+        when(processInstance.getProcessVariables()).thenReturn(processVariables);
+
+        // 调用
+        Set<Long> userIds = strategy.calculateUsersByTask(execution, param);
+        // 断言
+        assertEquals(Sets.newLinkedHashSet(1L, 2L), userIds);
+    }
+
+    @Test
+    public void testCalculateUsersByActivity() {
+        // 准备参数
+        String activityId = "activity_001";
+        Map<String, Object> processVariables = new HashMap<>();
+        processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES,
+                MapUtil.of("activity_001", List.of(1L, 2L)));
+
+        // 调用
+        Set<Long> userIds = strategy.calculateUsersByActivity(null, activityId, null,
+                null, null, processVariables);
+        // 断言
+        assertEquals(Sets.newLinkedHashSet(1L, 2L), userIds);
+    }
+
+}

+ 88 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java

@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+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.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+public class BpmTaskCandidateAssignEmptyStrategyTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private BpmTaskCandidateAssignEmptyStrategy strategy;
+
+    @Mock
+    private BpmProcessDefinitionService processDefinitionService;
+
+    @Test
+    public void testCalculateUsersByTask() {
+        try (MockedStatic<FlowableUtils> flowableUtilMockedStatic = mockStatic(FlowableUtils.class);
+             MockedStatic<BpmnModelUtils> bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) {
+            // 准备参数
+            DelegateExecution execution = mock(DelegateExecution.class);
+            String param = randomString();
+            // mock 方法(execution)
+            String processDefinitionId = randomString();
+            when(execution.getProcessDefinitionId()).thenReturn(processDefinitionId);
+            FlowElement flowElement = mock(FlowElement.class);
+            when(execution.getCurrentFlowElement()).thenReturn(flowElement);
+            // mock 方法(parseAssignEmptyHandlerType)
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerType(same(flowElement)))
+                    .thenReturn(BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType());
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerUserIds(same(flowElement)))
+                    .thenReturn(ListUtil.of(1L, 2L));
+
+            // 调用
+            Set<Long> userIds = strategy.calculateUsersByTask(execution, param);
+            // 断言
+            assertEquals(SetUtils.asSet(1L, 2L), userIds);
+        }
+
+    }
+
+    @Test
+    public void testCalculateUsersByActivity() {
+        try (MockedStatic<BpmnModelUtils> bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) {
+            // 准备参数
+            String processDefinitionId = randomString();
+            String activityId = randomString();
+            String param = randomString();
+            // mock 方法(getFlowElementById)
+            FlowElement flowElement = mock(FlowElement.class);
+            BpmnModel bpmnModel = mock(BpmnModel.class);
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId)))
+                    .thenReturn(flowElement);
+            // mock 方法(parseAssignEmptyHandlerType)
+            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerType(same(flowElement)))
+                 .thenReturn(BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType());
+            // mock 方法(getProcessDefinitionInfo)
+            BpmProcessDefinitionInfoDO processDefinition = randomPojo(BpmProcessDefinitionInfoDO.class,
+                    o -> o.setManagerUserIds(ListUtil.of(1L, 2L)));
+            when(processDefinitionService.getProcessDefinitionInfo(eq(processDefinitionId))).thenReturn(processDefinition);
+
+            // 调用
+            Set<Long> userIds = strategy.calculateUsersByActivity(bpmnModel, activityId, param,
+                    null, processDefinitionId, null);
+            // 断言
+            assertEquals(SetUtils.asSet(1L, 2L), userIds);
+        }
+    }
+
+}

+ 23 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategyTest.java → yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other;
 
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
@@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.MockedStatic;
 
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
@@ -22,7 +24,7 @@ public class BpmTaskCandidateExpressionStrategyTest extends BaseMockitoUnitTest
     private BpmTaskCandidateExpressionStrategy strategy;
 
     @Test
-    public void testCalculateUsers() {
+    public void testCalculateUsersByTask() {
         try (MockedStatic<FlowableUtils> flowableUtilMockedStatic = mockStatic(FlowableUtils.class)) {
             // 准备参数
             String param = "1,2";
@@ -32,7 +34,25 @@ public class BpmTaskCandidateExpressionStrategyTest extends BaseMockitoUnitTest
                     .thenReturn(asSet(1L, 2L));
 
             // 调用
-            Set<Long> results = strategy.calculateUsers(execution, param);
+            Set<Long> results = strategy.calculateUsersByTask(execution, param);
+            // 断言
+            assertEquals(asSet(1L, 2L), results);
+        }
+    }
+
+    @Test
+    public void testCalculateUsersByActivity() {
+        try (MockedStatic<FlowableUtils> flowableUtilMockedStatic = mockStatic(FlowableUtils.class)) {
+            // 准备参数
+            String param = "1,2";
+            Map<String, Object> processVariables = new HashMap<>();
+            // mock 方法
+            flowableUtilMockedStatic.when(() -> FlowableUtils.getExpressionValue(same(processVariables), eq(param)))
+                    .thenReturn(asSet(1L, 2L));
+
+            // 调用
+            Set<Long> results = strategy.calculateUsersByActivity(null, null, param,
+                    null, null, processVariables);
             // 断言
             assertEquals(asSet(1L, 2L), results);
         }

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategyTest.java → yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
 
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
@@ -36,9 +36,9 @@ public class BpmTaskCandidateGroupStrategyTest extends BaseMockitoUnitTest {
         when(userGroupService.getUserGroupList(eq(asSet(1L, 2L)))).thenReturn(Arrays.asList(userGroup1, userGroup2));
 
         // 调用
-        Set<Long> results = strategy.calculateUsers(null, param);
+        Set<Long> userIds = strategy.calculateUsersByTask(null, param);
         // 断言
-        assertEquals(asSet(11L, 12L, 21L, 22L), results);
+        assertEquals(asSet(11L, 12L, 21L, 22L), userIds);
     }
 
 }

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategyTest.java → yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
 
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.system.api.dept.PostApi;
@@ -39,9 +39,9 @@ public class BpmTaskCandidatePostStrategyTest extends BaseMockitoUnitTest {
         when(adminUserApi.getUserListByPostIds(eq(asSet(1L, 2L)))).thenReturn(users);
 
         // 调用
-        Set<Long> results = strategy.calculateUsers(null, param);
+        Set<Long> userIds = strategy.calculateUsersByTask(null, param);
         // 断言
-        assertEquals(asSet(11L, 22L), results);
+        assertEquals(asSet(11L, 22L), userIds);
     }
 
 }

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