Browse Source

!978 BPM 仿钉钉/飞书模式的草稿 PR
Merge pull request !978 from 芋道源码/feature/bpm

芋道源码 10 months ago
parent
commit
76e1d0e6f7
97 changed files with 3809 additions and 656 deletions
  1. 1 1
      pom.xml
  2. 11 0
      sql/mysql/bpm_update.sql
  3. 7 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java
  4. 2 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  5. 25 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventType.java
  6. 33 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmFieldPermissionEnum.java
  7. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelTypeEnum.java
  8. 36 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionType.java
  9. 76 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java
  10. 44 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java
  11. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java
  12. 33 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java
  13. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java
  14. 35 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java
  15. 32 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java
  16. 2 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java
  17. 7 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java
  18. 12 8
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java
  19. 1 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java
  20. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/package-info.java
  21. 19 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java
  22. 51 26
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
  23. 12 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
  24. 0 19
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java
  25. 19 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java
  26. 62 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
  27. 6 14
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java
  28. 8 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java
  29. 0 46
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java
  30. 220 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java
  31. 23 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelUpdateReqVO.java
  32. 2 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java
  33. 17 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  34. 0 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java
  35. 7 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
  36. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java
  37. 26 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java
  38. 87 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java
  39. 37 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java
  40. 17 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java
  41. 36 56
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
  42. 0 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
  43. 18 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java
  44. 59 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
  45. 7 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java
  46. 7 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java
  47. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
  48. 9 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
  49. 8 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
  50. 8 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java
  51. 56 11
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java
  52. 46 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java
  53. 78 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractDeptLeaderStrategy.java
  54. 37 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java
  55. 66 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAssignEmptyStrategy.java
  56. 45 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderMultiStrategy.java
  57. 9 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java
  58. 8 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java
  59. 9 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategy.java
  60. 9 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java
  61. 8 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java
  62. 7 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java
  63. 90 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java
  64. 91 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderStrategy.java
  65. 23 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java
  66. 62 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserStrategy.java
  67. 8 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategy.java
  68. 30 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java
  69. 16 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java
  70. 91 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java
  71. 19 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
  72. 33 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/SimpleModelConstants.java
  73. 47 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java
  74. 6 13
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java
  75. 45 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java
  76. 111 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  77. 21 8
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
  78. 631 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
  79. 39 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
  80. 88 41
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  81. 13 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java
  82. 20 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
  83. 0 46
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmModelMetaInfoRespDTO.java
  84. 8 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java
  85. 11 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java
  86. 41 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskTimeoutReqDTO.java
  87. 1 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityService.java
  88. 4 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java
  89. 22 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java
  90. 17 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java
  91. 34 17
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
  92. 0 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  93. 92 37
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
  94. 439 170
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
  95. 36 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/bo/AlreadyRunApproveNodeRespBO.java
  96. 12 4
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java
  97. 5 5
      yudao-server/pom.xml

+ 1 - 1
pom.xml

@@ -16,7 +16,7 @@
         <module>yudao-module-system</module>
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
         <module>yudao-module-infra</module>
 <!--        <module>yudao-module-member</module>-->
 <!--        <module>yudao-module-member</module>-->
-<!--        <module>yudao-module-bpm</module>-->
+        <module>yudao-module-bpm</module>
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-pay</module>-->
 <!--        <module>yudao-module-pay</module>-->

+ 11 - 0
sql/mysql/bpm_update.sql

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 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.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.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -19,6 +20,7 @@ import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
 import jakarta.validation.Valid;
+import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
@@ -50,6 +52,8 @@ public class BpmTaskController {
     private BpmProcessInstanceService processInstanceService;
     private BpmProcessInstanceService processInstanceService;
     @Resource
     @Resource
     private BpmFormService formService;
     private BpmFormService formService;
+    @Resource
+    private BpmProcessDefinitionService bpmProcessDefinitionService;
 
 
     @Resource
     @Resource
     private AdminUserApi adminUserApi;
     private AdminUserApi adminUserApi;
@@ -134,8 +138,10 @@ public class BpmTaskController {
         // 获得 Form Map
         // 获得 Form Map
         Map<Long, BpmFormDO> formMap = formService.getFormMap(
         Map<Long, BpmFormDO> formMap = formService.getFormMap(
                 convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey())));
                 convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey())));
+        // 获得 BpmnModel
+        BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId());
         return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, processInstance,
         return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, processInstance,
-                formMap, userMap, deptMap));
+                formMap, userMap, deptMap, bpmnModel));
     }
     }
 
 
     @PutMapping("/approve")
     @PutMapping("/approve")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
 
 
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import lombok.Setter;
 import lombok.Setter;
@@ -41,12 +43,17 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
         super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
         super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
 
 
         // 第二步,获取任务的所有处理人
         // 第二步,获取任务的所有处理人
-        // 由于每次审批(会签、或签等情况)后都会执行一次,所以 variable 已经有结果,不重复计算
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
         Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
         Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
         if (assigneeUserIds == null) {
         if (assigneeUserIds == null) {
             assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
             assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
             execution.setVariable(super.collectionVariable, assigneeUserIds);
             execution.setVariable(super.collectionVariable, assigneeUserIds);
+            if (CollUtil.isEmpty(assigneeUserIds)) {
+                // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
+                // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
+                // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
+                assigneeUserIds = SetUtils.asSet((Long) null);
+            }
         }
         }
         return assigneeUserIds.size();
         return assigneeUserIds.size();
     }
     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+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.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * 发起人自己 {@link BpmTaskCandidateUserStrategy} 实现类
+ * <p>
+ * 适合场景:用于需要发起人信息复核等场景
+ *
+ * @author jason
+ */
+@Component
+public class BpmTaskCandidateStartUserStrategy extends BpmTaskCandidateAbstractStrategy {
+
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private BpmProcessInstanceService processInstanceService;
+
+    public BpmTaskCandidateStartUserStrategy(AdminUserApi adminUserApi) {
+        super(adminUserApi);
+    }
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.START_USER;
+    }
+
+    @Override
+    public void validateParam(String param) {
+    }
+
+    @Override
+    public boolean isParamRequired() {
+        return false;
+    }
+
+    @Override
+    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
+        Set<Long> users =  SetUtils.asSet(Long.valueOf(processInstance.getStartUserId()));
+        removeDisableUsers(users);
+        return users;
+    }
+
+    @Override
+    public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
+        Set<Long> users = SetUtils.asSet(startUserId);
+        removeDisableUsers(users);
+        return users;
+    }
+
+}

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

@@ -1,13 +1,13 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
 
 
+import cn.hutool.core.text.StrPool;
 import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 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.candidate.BpmTaskCandidateStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
-import jakarta.annotation.Resource;
-import org.flowable.engine.delegate.DelegateExecution;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 
 
+import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.Set;
 
 
 /**
 /**
@@ -16,10 +16,11 @@ import java.util.Set;
  * @author kyle
  * @author kyle
  */
  */
 @Component
 @Component
-public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy {
+public class BpmTaskCandidateUserStrategy extends BpmTaskCandidateAbstractStrategy {
 
 
-    @Resource
-    private AdminUserApi adminUserApi;
+    public BpmTaskCandidateUserStrategy(AdminUserApi adminUserApi) {
+        super(adminUserApi);
+    }
 
 
     @Override
     @Override
     public BpmTaskCandidateStrategyEnum getStrategy() {
     public BpmTaskCandidateStrategyEnum getStrategy() {
@@ -32,8 +33,8 @@ public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy {
     }
     }
 
 
     @Override
     @Override
-    public Set<Long> calculateUsers(DelegateExecution execution, String param) {
-        return StrUtils.splitToLongSet(param);
+    public Set<Long> calculateUsers(String param) {
+        return new LinkedHashSet<>(StrUtils.splitToLong(param, StrPool.COMMA));
     }
     }
 
 
 }
 }

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

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.el;
+
+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 的类型, 转换参数的值
+ *
+ * @author jason
+ */
+@Component
+public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {
+
+    public VariableConvertByTypeExpressionFunction() {
+        super("convertByType");
+    }
+
+    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;
+    }
+}

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

@@ -1,9 +1,12 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
 
 
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Getter;
 
 
+import java.util.Arrays;
+
 /**
 /**
  * BPM 任务的候选人策略枚举
  * BPM 任务的候选人策略枚举
  *
  *
@@ -13,18 +16,25 @@ import lombok.Getter;
  */
  */
 @Getter
 @Getter
 @AllArgsConstructor
 @AllArgsConstructor
-public enum BpmTaskCandidateStrategyEnum {
+public enum BpmTaskCandidateStrategyEnum implements IntArrayValuable {
 
 
     ROLE(10, "角色"),
     ROLE(10, "角色"),
     DEPT_MEMBER(20, "部门的成员"), // 包括负责人
     DEPT_MEMBER(20, "部门的成员"), // 包括负责人
     DEPT_LEADER(21, "部门的负责人"),
     DEPT_LEADER(21, "部门的负责人"),
+    MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"),
     POST(22, "岗位"),
     POST(22, "岗位"),
     USER(30, "用户"),
     USER(30, "用户"),
     START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时选择此节点的审批人
     START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时选择此节点的审批人
+    START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点,常用于发起人信息审核场景
+    START_USER_DEPT_LEADER(37, "发起人部门负责人"),
+    START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"),
     USER_GROUP(40, "用户组"),
     USER_GROUP(40, "用户组"),
     EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager
     EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager
+    ASSIGN_EMPTY(1, "审批人为空"),
     ;
     ;
 
 
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmTaskCandidateStrategyEnum::getStrategy).toArray();
+
     /**
     /**
      * 类型
      * 类型
      */
      */
@@ -38,4 +48,9 @@ public enum BpmTaskCandidateStrategyEnum {
         return ArrayUtil.firstMatch(o -> o.getStrategy().equals(strategy), values());
         return ArrayUtil.firstMatch(o -> o.getStrategy().equals(strategy), values());
     }
     }
 
 
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
 }
 }

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

@@ -23,4 +23,95 @@ public interface BpmnModelConstants {
      */
      */
     String USER_TASK_CANDIDATE_PARAM = "candidateParam";
     String USER_TASK_CANDIDATE_PARAM = "candidateParam";
 
 
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记边界事件类型
+     */
+    String BOUNDARY_EVENT_TYPE = "boundaryEventType";
+
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记用户任务超时执行动作
+     */
+    String USER_TASK_TIMEOUT_HANDLER_TYPE = "timeoutHandlerType";
+
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记用户任务的审批人与发起人相同时,对应的处理类型
+     */
+    String USER_TASK_ASSIGN_START_USER_HANDLER_TYPE = "assignStartUserHandlerType";
+
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记用户任务的空处理类型
+     */
+    String USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE = "assignEmptyHandlerType";
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记用户任务的空处理的指定用户编号数组
+     */
+    String USER_TASK_ASSIGN_USER_IDS = "assignEmptyUserIds";
+
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝处理类型
+     */
+    String USER_TASK_REJECT_HANDLER_TYPE = "rejectHandlerType";
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝后的回退的任务 Id
+     */
+    String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId";
+
+    /**
+     * BPMN UserTask 的扩展属性,用于标记用户任务的审批类型
+     */
+    String USER_TASK_APPROVE_TYPE = "approveType";
+
+    /**
+     * BPMN UserTask 的扩展属性,用于标记用户任务的审批方式
+     */
+    String USER_TASK_APPROVE_METHOD = "approveMethod";
+
+    /**
+     * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
+     */
+    String FORM_FIELD_PERMISSION_ELEMENT = "fieldsPermission";
+
+    /**
+     * BPMN ExtensionElement Attribute, 用于标记表单字段
+     */
+    String FORM_FIELD_PERMISSION_ELEMENT_FIELD_ATTRIBUTE = "field";
+    /**
+     * BPMN ExtensionElement Attribute, 用于标记表单权限
+     */
+    String FORM_FIELD_PERMISSION_ELEMENT_PERMISSION_ATTRIBUTE = "permission";
+
+    /**
+     * BPMN ExtensionElement 操作按钮设置元素, 用于审批节点操作按钮设置
+     */
+    String BUTTON_SETTING_ELEMENT = "buttonsSetting";
+
+    /**
+     * BPMN ExtensionElement Attribute, 用于标记按钮编号
+     */
+    String BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE = "id";
+
+    /**
+     * BPMN ExtensionElement Attribute, 用于标记按钮显示名称
+     */
+    String BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE = "displayName";
+
+    /**
+     * BPMN ExtensionElement Attribute, 用于标记按钮是否启用
+     */
+    String BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE = "enable";
+
+    /**
+     * BPMN Start Event Node Id
+     */
+    String START_EVENT_NODE_ID = "StartEvent";
+    /**
+     * BPMN Start Event Node Name
+     */
+    String START_EVENT_NODE_NAME = "开始";
+
+    /**
+     * 发起人节点 ID
+     */
+    String START_USER_NODE_ID = "StartUserNode";
+
 }
 }

+ 19 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmConstants.java → yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java

@@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 
 
 /**
 /**
- * BPM 通用常量
+ * BPM Variable 通用常量
  *
  *
  * @author 芋道源码
  * @author 芋道源码
  */
  */
-public class BpmConstants {
+public class BpmnVariableConstants {
 
 
     /**
     /**
      * 流程实例的变量 - 状态
      * 流程实例的变量 - 状态
@@ -15,6 +15,14 @@ public class BpmConstants {
      * @see ProcessInstance#getProcessVariables()
      * @see ProcessInstance#getProcessVariables()
      */
      */
     public static final String PROCESS_INSTANCE_VARIABLE_STATUS = "PROCESS_STATUS";
     public static final String PROCESS_INSTANCE_VARIABLE_STATUS = "PROCESS_STATUS";
+    /**
+     * 流程实例的变量 - 理由
+     *
+     * 例如说:审批不通过的理由(目前审核通过暂时不会记录)
+     *
+     * @see ProcessInstance#getProcessVariables()
+     */
+    public static final String PROCESS_INSTANCE_VARIABLE_REASON = "PROCESS_REASON";
     /**
     /**
      * 流程实例的变量 - 发起用户选择的审批人 Map
      * 流程实例的变量 - 发起用户选择的审批人 Map
      *
      *
@@ -22,6 +30,15 @@ public class BpmConstants {
      */
      */
     public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
     public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
 
 
+    /**
+     * 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id}
+     *
+     * 目的是:驳回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
+     *
+     * @see ProcessInstance#getProcessVariables()
+     */
+    public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
+
     /**
     /**
      * 任务的变量 - 状态
      * 任务的变量 - 状态
      *
      *

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

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

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

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
+import jakarta.annotation.Resource;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.delegate.JavaDelegate;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate.BEAN_NAME;
+
+/**
+ * 处理抄送用户的 {@link JavaDelegate} 的实现类
+ *
+ * 目前只有快搭模式的【抄送节点】使用
+ *
+ * @author jason
+ */
+@Component(BEAN_NAME)
+public class BpmCopyTaskDelegate implements JavaDelegate {
+
+    public static final String BEAN_NAME = "bpmCopyTaskDelegate";
+
+    @Resource
+    private BpmTaskCandidateInvoker taskCandidateInvoker;
+
+    @Resource
+    private BpmProcessInstanceCopyService processInstanceCopyService;
+
+    @Override
+    public void execute(DelegateExecution execution) {
+        // 1. 获得抄送人
+        Set<Long> userIds = taskCandidateInvoker.calculateUsers(execution);
+        if (CollUtil.isEmpty(userIds)) {
+            return;
+        }
+        // 2. 执行抄送
+        FlowElement currentFlowElement = execution.getCurrentFlowElement();
+        processInstanceCopyService.createProcessInstanceCopy(userIds, execution.getProcessInstanceId(),
+                currentFlowElement.getId(), null, currentFlowElement.getName());
+    }
+
+}

+ 6 - 13
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java

@@ -6,7 +6,6 @@ import jakarta.annotation.Resource;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
 import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
 import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
-import org.flowable.engine.delegate.event.FlowableCancelledEvent;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
@@ -21,27 +20,21 @@ import java.util.Set;
 @Component
 @Component
 public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener {
 public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener {
 
 
+    public static final Set<FlowableEngineEventType> PROCESS_INSTANCE_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
+            .add(FlowableEngineEventType.PROCESS_COMPLETED)
+            .build();
+
     @Resource
     @Resource
-    @Lazy
+    @Lazy // 延迟加载,避免循环依赖
     private BpmProcessInstanceService processInstanceService;
     private BpmProcessInstanceService processInstanceService;
 
 
-    public static final Set<FlowableEngineEventType> PROCESS_INSTANCE_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
-                     .add(FlowableEngineEventType.PROCESS_CANCELLED)
-                     .add(FlowableEngineEventType.PROCESS_COMPLETED)
-                     .build();
-
     public BpmProcessInstanceEventListener(){
     public BpmProcessInstanceEventListener(){
         super(PROCESS_INSTANCE_EVENTS);
         super(PROCESS_INSTANCE_EVENTS);
     }
     }
 
 
-    @Override
-    protected void processCancelled(FlowableCancelledEvent event) {
-        processInstanceService.updateProcessInstanceWhenCancel(event);
-    }
-
     @Override
     @Override
     protected void processCompleted(FlowableEngineEntityEvent event) {
     protected void processCompleted(FlowableEngineEntityEvent event) {
-        processInstanceService.updateProcessInstanceWhenApprove((ProcessInstance)event.getEntity());
+        processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity());
     }
     }
 
 
 }
 }

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

@@ -1,21 +1,31 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+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.BpmActivityService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
+import org.flowable.bpmn.model.BoundaryEvent;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
 import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
 import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
 import org.flowable.engine.delegate.event.FlowableActivityCancelledEvent;
 import org.flowable.engine.delegate.event.FlowableActivityCancelledEvent;
 import org.flowable.engine.history.HistoricActivityInstance;
 import org.flowable.engine.history.HistoricActivityInstance;
+import org.flowable.job.api.Job;
 import org.flowable.task.api.Task;
 import org.flowable.task.api.Task;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 
 
-import jakarta.annotation.Resource;
 import java.util.List;
 import java.util.List;
 import java.util.Set;
 import java.util.Set;
 
 
@@ -28,6 +38,9 @@ import java.util.Set;
 @Slf4j
 @Slf4j
 public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
 public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
 
 
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private BpmModelService modelService;
     @Resource
     @Resource
     @Lazy // 解决循环依赖
     @Lazy // 解决循环依赖
     private BpmTaskService taskService;
     private BpmTaskService taskService;
@@ -40,20 +53,21 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
             .add(FlowableEngineEventType.TASK_ASSIGNED)
             .add(FlowableEngineEventType.TASK_ASSIGNED)
 //            .add(FlowableEngineEventType.TASK_COMPLETED) // 由于审批通过时,已经记录了 task 的 status 为通过,所以不需要监听了。
 //            .add(FlowableEngineEventType.TASK_COMPLETED) // 由于审批通过时,已经记录了 task 的 status 为通过,所以不需要监听了。
             .add(FlowableEngineEventType.ACTIVITY_CANCELLED)
             .add(FlowableEngineEventType.ACTIVITY_CANCELLED)
+            .add(FlowableEngineEventType.TIMER_FIRED) // 监听审批超时
             .build();
             .build();
 
 
-    public BpmTaskEventListener(){
+    public BpmTaskEventListener() {
         super(TASK_EVENTS);
         super(TASK_EVENTS);
     }
     }
 
 
     @Override
     @Override
     protected void taskCreated(FlowableEngineEntityEvent event) {
     protected void taskCreated(FlowableEngineEntityEvent event) {
-        taskService.updateTaskStatusWhenCreated((Task) event.getEntity());
+        taskService.processTaskCreated((Task) event.getEntity());
     }
     }
 
 
     @Override
     @Override
     protected void taskAssigned(FlowableEngineEntityEvent event) {
     protected void taskAssigned(FlowableEngineEntityEvent event) {
-        taskService.updateTaskExtAssign((Task)event.getEntity());
+        taskService.processTaskAssigned((Task) event.getEntity());
     }
     }
 
 
     @Override
     @Override
@@ -68,8 +82,34 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
             if (StrUtil.isEmpty(activity.getTaskId())) {
             if (StrUtil.isEmpty(activity.getTaskId())) {
                 return;
                 return;
             }
             }
-            taskService.updateTaskStatusWhenCanceled(activity.getTaskId());
+            taskService.processTaskCanceled(activity.getTaskId());
         });
         });
     }
     }
 
 
+    @Override
+    protected void timerFired(FlowableEngineEntityEvent event) {
+        // 1.1 只处理 BoundaryEvent 边界计时时间
+        String processDefinitionId = event.getProcessDefinitionId();
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
+        Job entity = (Job) event.getEntity();
+        FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId());
+        if (!(element instanceof BoundaryEvent)) {
+            return;
+        }
+        // 1.2 判断是否为超时处理
+        BoundaryEvent boundaryEvent = (BoundaryEvent) element;
+        String boundaryEventType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent,
+                BpmnModelConstants.BOUNDARY_EVENT_TYPE);
+        BpmBoundaryEventType bpmTimerBoundaryEventType = BpmBoundaryEventType.typeOf(NumberUtils.parseInt(boundaryEventType));
+        if (ObjectUtil.notEqual(bpmTimerBoundaryEventType, BpmBoundaryEventType.USER_TASK_TIMEOUT)) {
+            return;
+        }
+
+        // 2. 处理超时
+        String timeoutHandlerType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent,
+                BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE);
+        String taskKey = boundaryEvent.getAttachedToRefId();
+        taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutHandlerType));
+    }
+
 }
 }

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

@@ -1,9 +1,13 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 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.task.vo.task.BpmTaskRespVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import org.flowable.bpmn.converter.BpmnXMLConverter;
 import org.flowable.bpmn.converter.BpmnXMLConverter;
 import org.flowable.bpmn.model.Process;
 import org.flowable.bpmn.model.Process;
@@ -12,19 +16,111 @@ import org.flowable.common.engine.impl.util.io.BytesStreamSource;
 
 
 import java.util.*;
 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;
+
 /**
 /**
  * 流程模型转操作工具类
  * 流程模型转操作工具类
  */
  */
 public class BpmnModelUtils {
 public class BpmnModelUtils {
 
 
     public static Integer parseCandidateStrategy(FlowElement userTask) {
     public static Integer parseCandidateStrategy(FlowElement userTask) {
-        return NumberUtils.parseInt(userTask.getAttributeValue(
+        Integer candidateStrategy = NumberUtils.parseInt(userTask.getAttributeValue(
                 BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
                 BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
+        // TODO @芋艿 尝试从 ExtensionElement 取. 后续相关扩展是否都可以 存 extensionElement。 如表单权限。 按钮权限
+        if (candidateStrategy == null) {
+            ExtensionElement element = CollUtil.getFirst(userTask.getExtensionElements().get(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
+            candidateStrategy = element != null ? NumberUtils.parseInt(element.getElementText()) : null;
+        }
+        return candidateStrategy;
     }
     }
 
 
     public static String parseCandidateParam(FlowElement userTask) {
     public static String parseCandidateParam(FlowElement userTask) {
-        return userTask.getAttributeValue(
+        String candidateParam = userTask.getAttributeValue(
                 BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM);
                 BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM);
+        if (candidateParam == null) {
+            ExtensionElement element = CollUtil.getFirst(userTask.getExtensionElements().get(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM));
+            candidateParam = element != null ? element.getElementText() : null;
+        }
+        return candidateParam;
+    }
+
+    public static Integer parseApproveType(FlowElement userTask) {
+        return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE));
+    }
+
+    public static BpmUserTaskRejectHandlerType parseRejectHandlerType(FlowElement userTask) {
+        Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE));
+        return BpmUserTaskRejectHandlerType.typeOf(rejectHandlerType);
+    }
+
+    public static String parseReturnTaskId(FlowElement flowElement) {
+        return parseExtensionElement(flowElement, USER_TASK_REJECT_RETURN_TASK_ID);
+    }
+
+    public static Integer parseAssignStartUserHandlerType(FlowElement userTask) {
+        return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE));
+    }
+
+    public static Integer parseAssignEmptyHandlerType(FlowElement userTask) {
+        return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE));
+    }
+
+    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;
+        }
+        ExtensionElement element = CollUtil.getFirst(flowElement.getExtensionElements().get(elementName));
+        return element != null ? element.getElementText() : null;
+    }
+
+    public static Map<String, String> parseFormFieldsPermission(BpmnModel bpmnModel, String flowElementId) {
+        if (bpmnModel == null || StrUtil.isEmpty(flowElementId)) {
+            return null;
+        }
+        FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
+        if (flowElement == null) {
+            return null;
+        }
+        List<ExtensionElement> extensionElements = flowElement.getExtensionElements().get(FORM_FIELD_PERMISSION_ELEMENT);
+        if (CollUtil.isEmpty(extensionElements)) {
+            return null;
+        }
+        Map<String, String> fieldsPermission = MapUtil.newHashMap();
+        extensionElements.forEach(element -> {
+            String field = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, FORM_FIELD_PERMISSION_ELEMENT_FIELD_ATTRIBUTE);
+            String permission = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, FORM_FIELD_PERMISSION_ELEMENT_PERMISSION_ATTRIBUTE);
+            if (StrUtil.isNotEmpty(field) && StrUtil.isNotEmpty(permission)) {
+                fieldsPermission.put(field, permission);
+            }
+        });
+        return fieldsPermission;
+    }
+
+    public static Map<Integer, BpmTaskRespVO.OperationButtonSetting> parseButtonsSetting(BpmnModel bpmnModel, String flowElementId) {
+        FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
+        if (flowElement == null) {
+            return null;
+        }
+        List<ExtensionElement> extensionElements = flowElement.getExtensionElements().get(BUTTON_SETTING_ELEMENT);
+        if (CollUtil.isEmpty(extensionElements)) {
+            return null;
+        }
+        Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonSettings = MapUtil.newHashMap(16);
+        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);
+            String enable = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE);
+            if (StrUtil.isNotEmpty(id)) {
+                BpmTaskRespVO.OperationButtonSetting setting = new BpmTaskRespVO.OperationButtonSetting();
+                buttonSettings.put(Integer.valueOf(id), setting.setDisplayName(displayName).setEnable(Boolean.parseBoolean(enable)));
+            }
+        });
+        return buttonSettings;
     }
     }
 
 
     /**
     /**
@@ -95,6 +191,12 @@ public class BpmnModelUtils {
         return (StartEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof StartEvent);
         return (StartEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof StartEvent);
     }
     }
 
 
+    public static EndEvent getEndEvent(BpmnModel model) {
+        Process process = model.getMainProcess();
+        // 从 flowElementList 找 endEvent. TODO 多个 EndEvent 会有问题
+        return (EndEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof EndEvent);
+    }
+
     public static BpmnModel getBpmnModel(byte[] bpmnBytes) {
     public static BpmnModel getBpmnModel(byte[] bpmnBytes) {
         if (ArrayUtil.isEmpty(bpmnBytes)) {
         if (ArrayUtil.isEmpty(bpmnBytes)) {
             return null;
             return null;
@@ -334,4 +436,11 @@ public class BpmnModelUtils {
         return userTaskList;
         return userTaskList;
     }
     }
 
 
+    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);
+    }
 }
 }

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

@@ -1,7 +1,9 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
 import org.flowable.common.engine.api.delegate.Expression;
 import org.flowable.common.engine.api.delegate.Expression;
 import org.flowable.common.engine.api.variable.VariableContainer;
 import org.flowable.common.engine.api.variable.VariableContainer;
 import org.flowable.common.engine.impl.el.ExpressionManager;
 import org.flowable.common.engine.impl.el.ExpressionManager;
@@ -16,6 +18,7 @@ import org.flowable.task.api.TaskInfo;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.Objects;
 
 
 /**
 /**
  * Flowable 相关的工具方法
  * Flowable 相关的工具方法
@@ -39,6 +42,16 @@ public class FlowableUtils {
         return tenantId != null ? String.valueOf(tenantId) : ProcessEngineConfiguration.NO_TENANT_ID;
         return tenantId != null ? String.valueOf(tenantId) : ProcessEngineConfiguration.NO_TENANT_ID;
     }
     }
 
 
+    public static void execute(String tenantIdStr, Runnable runnable) {
+        if (ObjectUtil.isEmpty(tenantIdStr)
+                || Objects.equals(tenantIdStr, ProcessEngineConfiguration.NO_TENANT_ID)) {
+            runnable.run();
+        } else {
+            Long tenantId = Long.valueOf(tenantIdStr);
+            TenantUtils.execute(tenantId, runnable);
+        }
+    }
+
     // ========== Execution 相关的工具方法 ==========
     // ========== Execution 相关的工具方法 ==========
 
 
     /**
     /**
@@ -78,7 +91,7 @@ public class FlowableUtils {
      * @return 状态
      * @return 状态
      */
      */
     private static Integer getProcessInstanceStatus(Map<String, Object> processVariables) {
     private static Integer getProcessInstanceStatus(Map<String, Object> processVariables) {
-        return (Integer) processVariables.get(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
+        return (Integer) processVariables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
     }
     }
 
 
     /**
     /**
@@ -102,7 +115,7 @@ public class FlowableUtils {
      * @return 过滤后的表单
      * @return 过滤后的表单
      */
      */
     public static Map<String, Object> filterProcessInstanceFormVariable(Map<String, Object> processVariables) {
     public static Map<String, Object> filterProcessInstanceFormVariable(Map<String, Object> processVariables) {
-        processVariables.remove(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
+        processVariables.remove(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
         return processVariables;
         return processVariables;
     }
     }
 
 
@@ -115,7 +128,7 @@ public class FlowableUtils {
     @SuppressWarnings("unchecked")
     @SuppressWarnings("unchecked")
     public static Map<String, List<Long>> getStartUserSelectAssignees(ProcessInstance processInstance) {
     public static Map<String, List<Long>> getStartUserSelectAssignees(ProcessInstance processInstance) {
         return (Map<String, List<Long>>) processInstance.getProcessVariables().get(
         return (Map<String, List<Long>>) processInstance.getProcessVariables().get(
-                BpmConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
+                BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
     }
     }
 
 
     // ========== Task 相关的工具方法 ==========
     // ========== Task 相关的工具方法 ==========
@@ -127,7 +140,7 @@ public class FlowableUtils {
      * @return 状态
      * @return 状态
      */
      */
     public static Integer getTaskStatus(TaskInfo task) {
     public static Integer getTaskStatus(TaskInfo task) {
-        return (Integer) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
+        return (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
     }
     }
 
 
     /**
     /**
@@ -137,7 +150,7 @@ public class FlowableUtils {
      * @return 审批原因
      * @return 审批原因
      */
      */
     public static String getTaskReason(TaskInfo task) {
     public static String getTaskReason(TaskInfo task) {
-        return (String) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_REASON);
+        return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON);
     }
     }
 
 
     /**
     /**
@@ -161,8 +174,8 @@ public class FlowableUtils {
      * @return 过滤后的表单
      * @return 过滤后的表单
      */
      */
     public static Map<String, Object> filterTaskFormVariable(Map<String, Object> taskLocalVariables) {
     public static Map<String, Object> filterTaskFormVariable(Map<String, Object> taskLocalVariables) {
-        taskLocalVariables.remove(BpmConstants.TASK_VARIABLE_STATUS);
-        taskLocalVariables.remove(BpmConstants.TASK_VARIABLE_REASON);
+        taskLocalVariables.remove(BpmnVariableConstants.TASK_VARIABLE_STATUS);
+        taskLocalVariables.remove(BpmnVariableConstants.TASK_VARIABLE_REASON);
         return taskLocalVariables;
         return taskLocalVariables;
     }
     }
 
 

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

@@ -0,0 +1,631 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+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.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.model.Process;
+import org.flowable.bpmn.model.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+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 static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
+import static org.flowable.bpmn.constants.BpmnXMLConstants.*;
+
+/**
+ * 仿钉钉快搭模型相关的工具方法
+ *
+ * @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}";
+
+    // TODO @yunai:注释需要完善下;
+
+    /**
+     * 仿钉钉流程设计模型数据结构(json) 转换成 Bpmn Model (待完善)
+     *
+     * @param processId       流程标识
+     * @param processName     流程名称
+     * @param simpleModelNode 仿钉钉流程设计模型数据结构
+     * @return Bpmn Model
+     */
+    public static BpmnModel buildBpmnModel(String processId, String processName, BpmSimpleModelNodeVO simpleModelNode) {
+        BpmnModel bpmnModel = new BpmnModel();
+        // 不加这个 解析 Message 会报 NPE 异常 .
+        bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason:待定:是不是搞个自定义的 namespace;
+        // TODO 芋艿:后续在 review
+
+        Process process = new Process();
+        process.setId(processId);
+        process.setName(processName);
+        process.setExecutable(Boolean.TRUE); // TODO @jason:这个是必须设置的么?
+        bpmnModel.addProcess(process);
+
+        // TODO 芋艿:这里可能纠结下,到底前端传递,还是后端创建出来。
+        // 目前前端的第一个节点是“发起人节点”。这里构建一个 StartNode,用于创建 Bpmn 的 StartEvent 节点
+        BpmSimpleModelNodeVO startNode = buildStartSimpleModelNode();
+        startNode.setChildNode(simpleModelNode);
+        // 从 前端模型数据结构 SimpleModel 构建 FlowNode 并添加到 Main Process
+        traverseNodeToBuildFlowNode(startNode, process);
+        // 找到 end event
+        EndEvent endEvent = (EndEvent) CollUtil.findOne(process.getFlowElements(), item -> item instanceof EndEvent);
+
+        // 构建并添加节点之间的连线 Sequence Flow
+        traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId());
+        // 自动布局
+        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;
+    }
+
+    // TODO @芋艿:在优化下这个注释
+    private static void traverseNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
+        // 1.1 无效节点返回
+        if (!isValidNode(node)) {
+            return;
+        }
+        // 1.2 END_NODE 直接返回
+        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
+        Assert.notNull(nodeType, "模型节点类型不支持");
+        if (nodeType == 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);
+            }
+        } 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);
+        }
+    }
+
+    /**
+     * 构建有附加节点的连线
+     *
+     * @param nodeId       当前节点 Id
+     * @param attachNodeId 附属节点 Id
+     * @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);
+    }
+
+    /**
+     * 构造条件表达式
+     *
+     * @param conditionNode 条件节点
+     */
+    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 "";
+                    }
+                }).toList();
+                conditionExpression = String.format("${%s}", CollUtil.join(strConditionGroups, conditionGroups.getAnd() ? " && " : " || "));
+            }
+        }
+        // TODO 待增加其它类型
+        return conditionExpression;
+    }
+
+    private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId, String seqFlowId, String seqName, String conditionExpression) {
+        Assert.notEmpty(sourceId, "sourceId 不能为空");
+        Assert.notEmpty(targetId, "targetId 不能为空");
+        // TODO @jason:如果 seqFlowId 不存在的时候,是不是要生成一个默认的 seqFlowId? @芋艿: 貌似不需要,Flowable 会默认生成
+        // TODO @jason:如果 name 不存在的时候,是不是要生成一个默认的 name? @芋艿: 不需要生成默认的吧? 这个会在流程图展示的, 一般用户填写的。不好生成默认的吧
+        SequenceFlow sequenceFlow = new SequenceFlow(sourceId, targetId);
+        if (StrUtil.isNotEmpty(conditionExpression)) {
+            sequenceFlow.setConditionExpression(conditionExpression);
+        }
+        if (StrUtil.isNotEmpty(seqFlowId)) {
+            sequenceFlow.setId(seqFlowId);
+        }
+        if (StrUtil.isNotEmpty(seqName)) {
+            sequenceFlow.setName(seqName);
+        }
+        return sequenceFlow;
+    }
+
+    // TODO @芋艿 改成了 traverseNodeToBuildFlowNode, 连线的叫 traverseNodeToBuildSequenceFlow
+    private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) {
+        // 判断是否有效节点
+        if (!isValidNode(node)) {
+            return;
+        }
+        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
+        Assert.notNull(nodeType, "模型节点类型不支持");
+
+        List<FlowElement> flowElements = buildFlowNode(node, nodeType);
+        flowElements.forEach(process::addFlowElement);
+
+        // 如果不是网关类型的接口, 并且chileNode为空退出
+        // 如果是“分支”节点,则递归处理条件
+        if (BpmSimpleModelNodeType.isBranchNode(node.getType())
+                && ArrayUtil.isNotEmpty(node.getConditionNodes())) {
+            node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process));
+        }
+
+        // 如果有“子”节点,则递归处理子节点
+        traverseNodeToBuildFlowNode(node.getChildNode(), process);
+    }
+
+    public static boolean isValidNode(BpmSimpleModelNodeVO node) {
+        return node != null && node.getId() != null;
+    }
+
+    public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) {
+        return APPROVE_NODE.getType().equals(node.getType()) && SEQUENTIAL.getMethod().equals(node.getApproveMethod());
+    }
+
+    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;
+            }
+            case COPY_NODE: { // 抄送节点
+                ServiceTask serviceTask = convertCopyNode(node);
+                list.add(serviceTask);
+                break;
+            }
+            case CONDITION_BRANCH_NODE: {
+                ExclusiveGateway exclusiveGateway = convertConditionBranchNode(node);
+                list.add(exclusiveGateway);
+                break;
+            }
+            case PARALLEL_BRANCH_NODE: {
+                List<ParallelGateway> parallelGateways = convertParallelBranchNode(node);
+                list.addAll(parallelGateways);
+                break;
+            }
+
+            case INCLUSIVE_BRANCH_NODE: {
+                // TODO jason 待实现
+                break;
+            }
+            default: {
+                // TODO 其它节点类型的实现
+            }
+        }
+        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);
+        }
+        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
+
+        // 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 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);
+    }
+
+    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) {
+            exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
+        }
+        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());
+            }
+        }
+        return inclusiveGateway;
+    }
+
+    private static UserTask buildBpmnUserTask(BpmSimpleModelNodeVO node) {
+        UserTask userTask = new UserTask();
+        userTask.setId(node.getId());
+        userTask.setName(node.getName());
+
+        // 如果不是审批人节点,则直接返回
+        addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, StrUtil.toStringOrNull(node.getApproveType()));
+        if (ObjectUtil.notEqual(node.getApproveType(), USER.getType())) {
+            return userTask;
+        }
+
+        // 添加候选人元素
+        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;
+        }
+        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;
+        }
+        addExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, assignStartUserHandlerType.toString());
+    }
+
+    private 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()));
+    }
+
+    private static void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) {
+        BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod);
+        // TODO @jason:这种枚举,最终不要去掉哈 BpmUserTaskApproveMethodEnum。因为容易不经意重叠
+        if (approveMethodEnum == null || approveMethodEnum == RANDOM) {
+            return;
+        }
+        // 添加审批方式的扩展属性
+        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);
+        }
+        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));
+        }
+    }
+
+    /**
+     * 给节点添加表单字段权限元素
+     */
+    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 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);
+    }
+
+    private 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);
+    }
+
+    // ========== 各种 build 节点的方法 ==========
+
+    private static StartEvent convertStartNode(BpmSimpleModelNodeVO node) {
+        StartEvent startEvent = new StartEvent();
+        startEvent.setId(node.getId());
+        startEvent.setName(node.getName());
+        return startEvent;
+    }
+
+    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;
+    }
+
+    private static EndEvent convertEndNode(BpmSimpleModelNodeVO node) {
+        EndEvent endEvent = new EndEvent();
+        endEvent.setId(node.getId());
+        endEvent.setName(node.getName());
+
+        // TODO @芋艿 + jason:要不要加一个终止定义?
+        return endEvent;
+    }
+
+}

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

@@ -1,7 +1,10 @@
 package cn.iocoder.yudao.module.bpm.service.definition;
 package cn.iocoder.yudao.module.bpm.service.definition;
 
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
+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 jakarta.validation.Valid;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.engine.repository.Model;
 import org.flowable.engine.repository.Model;
@@ -25,10 +28,9 @@ public interface BpmModelService {
      * 创建流程模型
      * 创建流程模型
      *
      *
      * @param modelVO 创建信息
      * @param modelVO 创建信息
-     * @param bpmnXml BPMN XML
      * @return 创建的流程模型的编号
      * @return 创建的流程模型的编号
      */
      */
-    String createModel(@Valid BpmModelCreateReqVO modelVO, String bpmnXml);
+    String createModel(@Valid BpmModelSaveReqVO modelVO);
 
 
     /**
     /**
      * 获得流程模块
      * 获得流程模块
@@ -46,34 +48,46 @@ public interface BpmModelService {
      */
      */
     byte[] getModelBpmnXML(String id);
     byte[] getModelBpmnXML(String id);
 
 
+    /**
+     * 修改流程模型的 BPMN XML
+     *
+     * @param id      编号
+     * @param bpmnXml BPMN XML
+     */
+    void updateModelBpmnXml(String id, String bpmnXml);
+
     /**
     /**
      * 修改流程模型
      * 修改流程模型
      *
      *
+     * @param userId 用户编号
      * @param updateReqVO 更新信息
      * @param updateReqVO 更新信息
      */
      */
-    void updateModel(@Valid BpmModelUpdateReqVO updateReqVO);
+    void updateModel(Long userId, @Valid BpmModelSaveReqVO updateReqVO);
 
 
     /**
     /**
      * 将流程模型,部署成一个流程定义
      * 将流程模型,部署成一个流程定义
      *
      *
+     * @param userId 用户编号
      * @param id 编号
      * @param id 编号
      */
      */
-    void deployModel(String id);
+    void deployModel(Long userId, String id);
 
 
     /**
     /**
      * 删除模型
      * 删除模型
      *
      *
+     * @param userId  用户编号
      * @param id 编号
      * @param id 编号
      */
      */
-    void deleteModel(String id);
+    void deleteModel(Long userId, String id);
 
 
     /**
     /**
      * 修改模型的状态,实际更新的部署的流程定义的状态
      * 修改模型的状态,实际更新的部署的流程定义的状态
      *
      *
+     * @param userId 用户编号
      * @param id    编号
      * @param id    编号
      * @param state 状态
      * @param state 状态
      */
      */
-    void updateModelState(String id, Integer state);
+    void updateModelState(Long userId, String id, Integer state);
 
 
     /**
     /**
      * 获得流程定义编号对应的 BPMN Model
      * 获得流程定义编号对应的 BPMN Model
@@ -83,4 +97,22 @@ public interface BpmModelService {
      */
      */
     BpmnModel getBpmnModelByDefinitionId(String processDefinitionId);
     BpmnModel getBpmnModelByDefinitionId(String processDefinitionId);
 
 
+    // ========== 仿钉钉/飞书的精简模型 =========
+
+    /**
+     * 获取仿钉钉流程设计模型结构
+     *
+     * @param modelId 流程模型编号
+     * @return 仿钉钉流程设计模型结构
+     */
+    BpmSimpleModelNodeVO getSimpleModel(String modelId);
+
+    /**
+     * 更新仿钉钉流程设计模型
+     *
+     * @param userId 用户编号
+     * @param reqVO 请求信息
+     */
+    void updateSimpleModel(Long userId, @Valid BpmSimpleModelUpdateReqVO reqVO);
+
 }
 }

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

@@ -1,20 +1,24 @@
 package cn.iocoder.yudao.module.bpm.service.definition;
 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.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 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.object.PageUtils;
 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelCreateReqVO;
+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.BpmModelPageReqVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelUpdateReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
 import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
 import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 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.framework.flowable.core.util.FlowableUtils;
-import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
 import jakarta.annotation.Resource;
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
 import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -28,7 +32,6 @@ import org.flowable.engine.repository.ModelQuery;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.util.ObjectUtils;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
 
 
 import java.util.List;
 import java.util.List;
@@ -86,63 +89,79 @@ public class BpmModelServiceImpl implements BpmModelService {
 
 
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
-    public String createModel(@Valid BpmModelCreateReqVO createReqVO, String bpmnXml) {
+    public String createModel(@Valid BpmModelSaveReqVO createReqVO) {
         if (!ValidationUtils.isXmlNCName(createReqVO.getKey())) {
         if (!ValidationUtils.isXmlNCName(createReqVO.getKey())) {
             throw exception(MODEL_KEY_VALID);
             throw exception(MODEL_KEY_VALID);
         }
         }
-        // 校验流程标识已经存在
+        // 1. 校验流程标识已经存在
         Model keyModel = getModelByKey(createReqVO.getKey());
         Model keyModel = getModelByKey(createReqVO.getKey());
         if (keyModel != null) {
         if (keyModel != null) {
             throw exception(MODEL_KEY_EXISTS, createReqVO.getKey());
             throw exception(MODEL_KEY_EXISTS, createReqVO.getKey());
         }
         }
 
 
-        // 创建流程定义
+        // 2.1 创建流程定义
         Model model = repositoryService.newModel();
         Model model = repositoryService.newModel();
-        BpmModelConvert.INSTANCE.copyToCreateModel(model, createReqVO);
+        BpmModelConvert.INSTANCE.copyToModel(model, createReqVO);
         model.setTenantId(FlowableUtils.getTenantId());
         model.setTenantId(FlowableUtils.getTenantId());
-        // 保存流程定义
+        // 2.2 保存流程定义
         repositoryService.saveModel(model);
         repositoryService.saveModel(model);
-        // 保存 BPMN XML
-        saveModelBpmnXml(model, bpmnXml);
         return model.getId();
         return model.getId();
     }
     }
 
 
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务
     @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务
-    public void updateModel(@Valid BpmModelUpdateReqVO updateReqVO) {
-        // 校验流程模型存在
-        Model model = getModel(updateReqVO.getId());
-        if (model == null) {
-            throw exception(MODEL_NOT_EXISTS);
-        }
+    public void updateModel(Long userId, @Valid BpmModelSaveReqVO updateReqVO) {
+        // 1. 校验流程模型存在
+        Model model = validateModelManager(updateReqVO.getId(), userId);
 
 
         // 修改流程定义
         // 修改流程定义
-        BpmModelConvert.INSTANCE.copyToUpdateModel(model, updateReqVO);
+        BpmModelConvert.INSTANCE.copyToModel(model, updateReqVO);
         // 更新模型
         // 更新模型
         repositoryService.saveModel(model);
         repositoryService.saveModel(model);
-        // 更新 BPMN XML
-        saveModelBpmnXml(model, updateReqVO.getBpmnXml());
+    }
+
+    private Model validateModelExists(String id) {
+        Model model = repositoryService.getModel(id);
+        if (model == null) {
+            throw exception(MODEL_NOT_EXISTS);
+        }
+        return model;
+    }
+
+    /**
+     * 校验是否有流程模型的管理权限
+     *
+     * @param id     流程模型编号
+     * @param userId 用户编号
+     * @return 流程模型
+     */
+    private Model validateModelManager(String id, Long userId) {
+        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);
+        }
+        return model;
     }
     }
 
 
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务
     @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务
-    public void deployModel(String id) {
+    public void deployModel(Long userId, String id) {
         // 1.1 校验流程模型存在
         // 1.1 校验流程模型存在
-        Model model = getModel(id);
-        if (ObjectUtils.isEmpty(model)) {
-            throw exception(MODEL_NOT_EXISTS);
-        }
+        Model model = validateModelManager(id, userId);
         // 1.2 校验流程图
         // 1.2 校验流程图
         byte[] bpmnBytes = getModelBpmnXML(model.getId());
         byte[] bpmnBytes = getModelBpmnXML(model.getId());
         validateBpmnXml(bpmnBytes);
         validateBpmnXml(bpmnBytes);
         // 1.3 校验表单已配
         // 1.3 校验表单已配
-        BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
+        BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
         BpmFormDO form = validateFormConfig(metaInfo);
         BpmFormDO form = validateFormConfig(metaInfo);
         // 1.4 校验任务分配规则已配置
         // 1.4 校验任务分配规则已配置
         taskCandidateInvoker.validateBpmnConfig(bpmnBytes);
         taskCandidateInvoker.validateBpmnConfig(bpmnBytes);
+        // 1.5 获取仿钉钉流程设计器模型数据
+        byte[] simpleBytes = getModelSimpleJson(model.getId());
 
 
         // 2.1 创建流程定义
         // 2.1 创建流程定义
-        String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, form);
+        String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, simpleBytes, form);
 
 
         // 2.2 将老的流程定义进行挂起。也就是说,只有最新部署的流程定义,才可以发起任务。
         // 2.2 将老的流程定义进行挂起。也就是说,只有最新部署的流程定义,才可以发起任务。
         updateProcessDefinitionSuspended(model.getDeploymentId());
         updateProcessDefinitionSuspended(model.getDeploymentId());
@@ -174,12 +193,10 @@ public class BpmModelServiceImpl implements BpmModelService {
 
 
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
-    public void deleteModel(String id) {
+    public void deleteModel(Long userId, String id) {
         // 校验流程模型存在
         // 校验流程模型存在
-        Model model = getModel(id);
-        if (model == null) {
-            throw exception(MODEL_NOT_EXISTS);
-        }
+        Model model = validateModelManager(id, userId);
+
         // 执行删除
         // 执行删除
         repositoryService.deleteModel(id);
         repositoryService.deleteModel(id);
         // 禁用流程定义
         // 禁用流程定义
@@ -187,12 +204,9 @@ public class BpmModelServiceImpl implements BpmModelService {
     }
     }
 
 
     @Override
     @Override
-    public void updateModelState(String id, Integer state) {
+    public void updateModelState(Long userId, String id, Integer state) {
         // 1.1 校验流程模型存在
         // 1.1 校验流程模型存在
-        Model model = getModel(id);
-        if (model == null) {
-            throw exception(MODEL_NOT_EXISTS);
-        }
+        Model model = validateModelManager(id, userId);
         // 1.2 校验流程定义存在
         // 1.2 校验流程定义存在
         ProcessDefinition definition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
         ProcessDefinition definition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
         if (definition == null) {
         if (definition == null) {
@@ -208,13 +222,34 @@ public class BpmModelServiceImpl implements BpmModelService {
         return repositoryService.getBpmnModel(processDefinitionId);
         return repositoryService.getBpmnModel(processDefinitionId);
     }
     }
 
 
+    @Override
+    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);
+    }
+
+    @Override
+    public void updateSimpleModel(Long userId, BpmSimpleModelUpdateReqVO reqVO) {
+        // 1. 校验流程模型存在
+        Model model = validateModelManager(reqVO.getId(), userId);
+
+        // 2.1 JSON 转换成 bpmnModel
+        BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel(model.getKey(), model.getName(), reqVO.getSimpleModel());
+        // 2.2 保存 Bpmn XML
+        updateModelBpmnXml(model.getId(), BpmnModelUtils.getBpmnXml(bpmnModel));
+        // 2.3 保存 JSON 数据
+        saveModelSimpleJson(model.getId(), JsonUtils.toJsonByte(reqVO.getSimpleModel()));
+    }
+
     /**
     /**
      * 校验流程表单已配置
      * 校验流程表单已配置
      *
      *
      * @param metaInfo 流程模型元数据
      * @param metaInfo 流程模型元数据
      * @return 表单配置
      * @return 表单配置
      */
      */
-    private BpmFormDO validateFormConfig(BpmModelMetaInfoRespDTO  metaInfo) {
+    private BpmFormDO validateFormConfig(BpmModelMetaInfoVO metaInfo) {
         if (metaInfo == null || metaInfo.getFormType() == null) {
         if (metaInfo == null || metaInfo.getFormType() == null) {
             throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
             throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
         }
         }
@@ -236,16 +271,28 @@ public class BpmModelServiceImpl implements BpmModelService {
         }
         }
     }
     }
 
 
-    private void saveModelBpmnXml(Model model, String bpmnXml) {
+    @Override
+    public void updateModelBpmnXml(String id, String bpmnXml) {
         if (StrUtil.isEmpty(bpmnXml)) {
         if (StrUtil.isEmpty(bpmnXml)) {
             return;
             return;
         }
         }
-        repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));
+        repositoryService.addModelEditorSource(id, StrUtil.utf8Bytes(bpmnXml));
+    }
+
+    private byte[] getModelSimpleJson(String id) {
+        return repositoryService.getModelEditorSourceExtra(id);
+    }
+
+    private void saveModelSimpleJson(String id, byte[] jsonBytes) {
+        if (ArrayUtil.isEmpty(jsonBytes)) {
+            return;
+        }
+        repositoryService.addModelEditorSourceExtra(id, jsonBytes);
     }
     }
 
 
     /**
     /**
      * 挂起 deploymentId 对应的流程定义
      * 挂起 deploymentId 对应的流程定义
-     *
+     * <p>
      * 注意:这里一个 deploymentId 只关联一个流程定义
      * 注意:这里一个 deploymentId 只关联一个流程定义
      *
      *
      * @param deploymentId 流程发布Id
      * @param deploymentId 流程发布Id

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

@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 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.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
-import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.Model;
 import org.flowable.engine.repository.Model;
@@ -48,10 +48,12 @@ public interface BpmProcessDefinitionService {
      * @param model 流程模型
      * @param model 流程模型
      * @param modelMetaInfo 流程模型元信息
      * @param modelMetaInfo 流程模型元信息
      * @param bpmnBytes BPMN XML 字节数组
      * @param bpmnBytes BPMN XML 字节数组
+     * @param simpleBytes SIMPLE Model JSON 字节数组
      * @param form 表单
      * @param form 表单
      * @return 流程编号
      * @return 流程编号
      */
      */
-    String createProcessDefinition(Model model, BpmModelMetaInfoRespDTO modelMetaInfo, byte[] bpmnBytes, BpmFormDO form);
+    String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo,
+                                   byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form);
 
 
     /**
     /**
      * 更新流程定义状态
      * 更新流程定义状态
@@ -133,6 +135,15 @@ public interface BpmProcessDefinitionService {
      */
      */
     ProcessDefinition getActiveProcessDefinition(String key);
     ProcessDefinition getActiveProcessDefinition(String key);
 
 
+    /**
+     * 判断用户是否可以使用该流程定义,进行流程的发起
+     *
+     * @param processDefinition 流程定义
+     * @param userId 用户编号
+     * @return 是否可以发起流程
+     */
+    boolean canUserStartProcessDefinition(BpmProcessDefinitionInfoDO processDefinition, Long userId);
+
     /**
     /**
      * 获得 ids 对应的 Deployment Map
      * 获得 ids 对应的 Deployment Map
      *
      *

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

@@ -5,13 +5,13 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 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.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
 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.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
-import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
 import jakarta.annotation.Resource;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.BpmnModel;
@@ -24,6 +24,7 @@ import org.flowable.engine.repository.ProcessDefinitionQuery;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
 
 
+import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.*;
 
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -84,6 +85,19 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
                 .processDefinitionKey(key).active().singleResult();
                 .processDefinitionKey(key).active().singleResult();
     }
     }
 
 
+    @Override
+    public boolean canUserStartProcessDefinition(BpmProcessDefinitionInfoDO processDefinition, Long userId) {
+        if (processDefinition == null) {
+            return false;
+        }
+        // 为空,则所有人都可以发起
+        if (CollUtil.isEmpty(processDefinition.getStartUserIds())) {
+            return true;
+        }
+        // 不为空,则需要存在里面
+        return processDefinition.getStartUserIds().contains(userId);
+    }
+
     @Override
     @Override
     public List<Deployment> getDeploymentList(Set<String> ids) {
     public List<Deployment> getDeploymentList(Set<String> ids) {
         if (CollUtil.isEmpty(ids)) {
         if (CollUtil.isEmpty(ids)) {
@@ -105,8 +119,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
     }
     }
 
 
     @Override
     @Override
-    public String createProcessDefinition(Model model, BpmModelMetaInfoRespDTO modelMetaInfo,
-                                          byte[] bpmnBytes, BpmFormDO form) {
+    public String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo,
+                                          byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form) {
         // 创建 Deployment 部署
         // 创建 Deployment 部署
         Deployment deploy = repositoryService.createDeployment()
         Deployment deploy = repositoryService.createDeployment()
                 .key(model.getKey()).name(model.getName()).category(model.getCategory())
                 .key(model.getKey()).name(model.getName()).category(model.getCategory())
@@ -131,7 +145,9 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
 
 
         // 插入拓展表
         // 插入拓展表
         BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class)
         BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class)
-                .setModelId(model.getId()).setProcessDefinitionId(definition.getId());
+                .setModelId(model.getId()).setProcessDefinitionId(definition.getId()).setModelType(modelMetaInfo.getType())
+                .setSimpleModel(StrUtil.str(simpleBytes, StandardCharsets.UTF_8));
+
         if (form != null) {
         if (form != null) {
             definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf());
             definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf());
         }
         }

+ 0 - 46
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmModelMetaInfoRespDTO.java

@@ -1,46 +0,0 @@
-package cn.iocoder.yudao.module.bpm.service.definition.dto;
-
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
-import lombok.Data;
-
-/**
- * BPM 流程 MetaInfo Response DTO
- * 主要用于 { Model#setMetaInfo(String)} 的存储
- *
- * 最终,它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的
- *
- * @author 芋道源码
- */
-@Data
-public class BpmModelMetaInfoRespDTO {
-
-    /**
-     * 流程图标
-     */
-    private String icon;
-    /**
-     * 流程描述
-     */
-    private String description;
-
-    /**
-     * 表单类型
-     */
-    private Integer formType;
-    /**
-     * 表单编号
-     * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时
-     */
-    private Long formId;
-    /**
-     * 自定义表单的提交路径,使用 Vue 的路由地址
-     * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时
-     */
-    private String formCustomCreatePath;
-    /**
-     * 自定义表单的查看路径,使用 Vue 的路由地址
-     * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时
-     */
-    private String formCustomViewPath;
-
-}

+ 8 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.bpm.service.message;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
 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.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
-
+import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
 import jakarta.validation.Valid;
 import jakarta.validation.Valid;
 
 
 /**
 /**
@@ -36,4 +36,11 @@ public interface BpmMessageService {
      */
      */
     void sendMessageWhenTaskAssigned(@Valid BpmMessageSendWhenTaskCreatedReqDTO reqDTO);
     void sendMessageWhenTaskAssigned(@Valid BpmMessageSendWhenTaskCreatedReqDTO reqDTO);
 
 
+    /**
+     * 发送任务审批超时的消息
+     *
+     * @param reqDTO 发送信息
+     */
+    void sendMessageWhenTaskTimeout(@Valid BpmMessageSendWhenTaskTimeoutReqDTO reqDTO);
+
 }
 }

+ 11 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
 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.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
+import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
 import cn.iocoder.yudao.module.system.api.sms.SmsSendApi;
 import cn.iocoder.yudao.module.system.api.sms.SmsSendApi;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
@@ -61,6 +62,16 @@ public class BpmMessageServiceImpl implements BpmMessageService {
                 BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams));
                 BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams));
     }
     }
 
 
+    @Override
+    public void sendMessageWhenTaskTimeout(BpmMessageSendWhenTaskTimeoutReqDTO reqDTO) {
+        Map<String, Object> templateParams = new HashMap<>();
+        templateParams.put("processInstanceName", reqDTO.getProcessInstanceName());
+        templateParams.put("taskName", reqDTO.getTaskName());
+        templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
+        smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(),
+                BpmMessageEnum.TASK_TIMEOUT.getSmsTemplateCode(), templateParams));
+    }
+
     private String getProcessInstanceDetailUrl(String taskId) {
     private String getProcessInstanceDetailUrl(String taskId) {
         return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId;
         return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId;
     }
     }

+ 41 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskTimeoutReqDTO.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.bpm.service.message.dto;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * BPM 发送任务审批超时 Request DTO
+ */
+@Data
+public class BpmMessageSendWhenTaskTimeoutReqDTO {
+
+    /**
+     * 流程实例的编号
+     */
+    @NotEmpty(message = "流程实例的编号不能为空")
+    private String processInstanceId;
+    /**
+     * 流程实例的名字
+     */
+    @NotEmpty(message = "流程实例的名字不能为空")
+    private String processInstanceName;
+
+    /**
+     * 流程任务的编号
+     */
+    @NotEmpty(message = "流程任务的编号不能为空")
+    private String taskId;
+    /**
+     * 流程任务的名字
+     */
+    @NotEmpty(message = "流程任务的名字不能为空")
+    private String taskName;
+
+    /**
+     * 审批人的用户编号
+     */
+    @NotNull(message = "审批人的用户编号不能为空")
+    private Long assigneeUserId;
+
+}

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.bpm.service.task;
 package cn.iocoder.yudao.module.bpm.service.task;
 
 
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
 import org.flowable.engine.history.HistoricActivityInstance;
 import org.flowable.engine.history.HistoricActivityInstance;
 
 
 import java.util.List;
 import java.util.List;
@@ -18,7 +17,7 @@ public interface BpmActivityService {
      * @param processInstanceId 流程实例的编号
      * @param processInstanceId 流程实例的编号
      * @return 活动实例列表
      * @return 活动实例列表
      */
      */
-    List<BpmActivityRespVO> getActivityListByProcessInstanceId(String processInstanceId);
+    List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId);
 
 
     /**
     /**
      * 获得执行编号对应的活动实例
      * 获得执行编号对应的活动实例

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

@@ -1,14 +1,12 @@
 package cn.iocoder.yudao.module.bpm.service.task;
 package cn.iocoder.yudao.module.bpm.service.task;
 
 
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
-import cn.iocoder.yudao.module.bpm.convert.task.BpmActivityConvert;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.engine.HistoryService;
 import org.flowable.engine.HistoryService;
 import org.flowable.engine.history.HistoricActivityInstance;
 import org.flowable.engine.history.HistoricActivityInstance;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
 
 
-import jakarta.annotation.Resource;
 import java.util.List;
 import java.util.List;
 
 
 
 
@@ -26,10 +24,9 @@ public class BpmActivityServiceImpl implements BpmActivityService {
     private HistoryService historyService;
     private HistoryService historyService;
 
 
     @Override
     @Override
-    public List<BpmActivityRespVO> getActivityListByProcessInstanceId(String processInstanceId) {
-        List<HistoricActivityInstance> activityList = historyService.createHistoricActivityInstanceQuery()
-                .processInstanceId(processInstanceId).list();
-        return BpmActivityConvert.INSTANCE.convertList(activityList);
+    public List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId) {
+        return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId)
+                .orderByHistoricActivityInstanceStartTime().asc().list();
     }
     }
 
 
     @Override
     @Override

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

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
 
 
 import java.util.Collection;
 import java.util.Collection;
+import java.util.Set;
 
 
 /**
 /**
  * 流程抄送 Service 接口
  * 流程抄送 Service 接口
@@ -21,6 +22,18 @@ public interface BpmProcessInstanceCopyService {
      */
      */
     void createProcessInstanceCopy(Collection<Long> userIds, String taskId);
     void createProcessInstanceCopy(Collection<Long> userIds, String taskId);
 
 
+    /**
+     * 流程实例的抄送
+     *
+     * @param userIds 抄送的用户编号
+     * @param processInstanceId 流程编号
+     * @param activityId 流程活动编号 id (对应 BPMN XML 节点 Id)
+     * // TODO 芋艿这个 taskId 是不是可以不要了
+     * @param taskId 任务编号
+     * @param taskName 任务名称
+     */
+    void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String activityId, String taskId, String taskName);
+
     /**
     /**
      * 获得抄送的流程的分页
      * 获得抄送的流程的分页
      *
      *
@@ -30,5 +43,14 @@ public interface BpmProcessInstanceCopyService {
      */
      */
     PageResult<BpmProcessInstanceCopyDO> getProcessInstanceCopyPage(Long userId,
     PageResult<BpmProcessInstanceCopyDO> getProcessInstanceCopyPage(Long userId,
                                                                     BpmProcessInstanceCopyPageReqVO pageReqVO);
                                                                     BpmProcessInstanceCopyPageReqVO pageReqVO);
+    // TODO @芋艿:重点在 review 下
+    /**
+     * 通过流程实例和流程活动编号获取抄送人的 Id
+     *
+     * @param processInstanceId 流程实例 Id
+     * @param activityId 流程活动编号 Id
+     * @return 抄送人 Ids
+     */
+    Set<Long> getCopyUserIds(String processInstanceId, String activityId);
 
 
 }
 }

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

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.service.task;
 
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper;
 import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper;
@@ -18,6 +19,7 @@ import org.springframework.validation.annotation.Validated;
 
 
 import java.util.Collection;
 import java.util.Collection;
 import java.util.List;
 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.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -48,18 +50,22 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
 
 
     @Override
     @Override
     public void createProcessInstanceCopy(Collection<Long> userIds, String taskId) {
     public void createProcessInstanceCopy(Collection<Long> userIds, String taskId) {
-        // 1.1 校验任务存在
         Task task = taskService.getTask(taskId);
         Task task = taskService.getTask(taskId);
         if (ObjectUtil.isNull(task)) {
         if (ObjectUtil.isNull(task)) {
             throw exception(ErrorCodeConstants.TASK_NOT_EXISTS);
             throw exception(ErrorCodeConstants.TASK_NOT_EXISTS);
         }
         }
-        // 1.2 校验流程实例存在
         String processInstanceId = task.getProcessInstanceId();
         String processInstanceId = task.getProcessInstanceId();
+        createProcessInstanceCopy(userIds, processInstanceId, task.getTaskDefinitionKey(), task.getId(), task.getName());
+    }
+
+    @Override
+    public void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String activityId, String taskId, String taskName) {
+        // 1.1 校验流程实例存在
         ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
         ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
         if (processInstance == null) {
         if (processInstance == null) {
             throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS);
             throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS);
         }
         }
-        // 1.3 校验流程定义存在
+        // 1.2 校验流程定义存在
         ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
         ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
                 processInstance.getProcessDefinitionId());
                 processInstance.getProcessDefinitionId());
         if (processDefinition == null) {
         if (processDefinition == null) {
@@ -70,7 +76,8 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
         List<BpmProcessInstanceCopyDO> copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO()
         List<BpmProcessInstanceCopyDO> copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO()
                 .setUserId(userId).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
                 .setUserId(userId).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
                 .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
                 .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
-                .setCategory(processDefinition.getCategory()).setTaskId(taskId).setTaskName(task.getName()));
+                .setCategory(processDefinition.getCategory()).setActivityId(activityId)
+                .setTaskId(taskId).setTaskName(taskName));
         processInstanceCopyMapper.insertBatch(copyList);
         processInstanceCopyMapper.insertBatch(copyList);
     }
     }
 
 
@@ -80,4 +87,10 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
         return processInstanceCopyMapper.selectPage(userId, pageReqVO);
         return processInstanceCopyMapper.selectPage(userId, pageReqVO);
     }
     }
 
 
+    @Override
+    public Set<Long> getCopyUserIds(String processInstanceId, String activityId) {
+        return CollectionUtils.convertSet(processInstanceCopyMapper.selectListByProcessInstanceIdAndActivityId(processInstanceId, activityId),
+                BpmProcessInstanceCopyDO::getUserId);
+    }
+
 }
 }

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

@@ -2,11 +2,8 @@ package cn.iocoder.yudao.module.bpm.service.task;
 
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
 import jakarta.validation.Valid;
 import jakarta.validation.Valid;
-import org.flowable.engine.delegate.event.FlowableCancelledEvent;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 
 
@@ -23,6 +20,8 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  */
  */
 public interface BpmProcessInstanceService {
 public interface BpmProcessInstanceService {
 
 
+    // ========== Query 查询相关方法 ==========
+
     /**
     /**
      * 获得流程实例
      * 获得流程实例
      *
      *
@@ -85,6 +84,28 @@ public interface BpmProcessInstanceService {
     PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
     PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
                                                                @Valid BpmProcessInstancePageReqVO pageReqVO);
                                                                @Valid BpmProcessInstancePageReqVO pageReqVO);
 
 
+    /**
+     * 获得表单字段权限
+     *
+     * @param reqVO 请求消息
+     * @return 表单字段权限
+     */
+    Map<String, String> getFormFieldsPermission(@Valid BpmFormFieldsPermissionReqVO reqVO);
+
+    // TODO @芋艿:重点在 review 下
+    /**
+     * 获取审批详情。
+     * <p>
+     * 可以是准备发起的流程、进行中的流程、已经结束的流程
+     *
+     * @param loginUserId  登录人的用户编号
+     * @param reqVO 请求信息
+     * @return 流程实例的进度
+     */
+    BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
+
+    // ========== Update 写入相关方法 ==========
+
     /**
     /**
      * 创建流程实例(提供给前端)
      * 创建流程实例(提供给前端)
      *
      *
@@ -114,31 +135,27 @@ public interface BpmProcessInstanceService {
     /**
     /**
      * 管理员取消流程实例
      * 管理员取消流程实例
      *
      *
-     * @param userId           用户编号
+     * @param userId      用户编号
      * @param cancelReqVO 取消信息
      * @param cancelReqVO 取消信息
      */
      */
     void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO);
     void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO);
 
 
     /**
     /**
-     * 更新 ProcessInstance 拓展记录为取消
+     * 更新 ProcessInstance 为不通过
      *
      *
-     * @param event 流程取消事件
+     * @param processInstance 流程实例
+     * @param reason          理由。例如说,审批不通过时,需要传递该值
      */
      */
-    void updateProcessInstanceWhenCancel(FlowableCancelledEvent event);
+    void updateProcessInstanceReject(ProcessInstance processInstance, String reason);
+
+    // ========== Event 事件相关方法 ==========
 
 
     /**
     /**
-     * 更新 ProcessInstance 拓展记录为完成
+     * 处理 ProcessInstance 完成事件,例如说:审批通过、不通过、取消
      *
      *
      * @param instance 流程任务
      * @param instance 流程任务
      */
      */
-    void updateProcessInstanceWhenApprove(ProcessInstance instance);
+    void processProcessInstanceCompleted(ProcessInstance instance);
 
 
-    /**
-     * 更新 ProcessInstance 拓展记录为不通过
-     *
-     * @param id     流程编号
-     * @param reason 理由。例如说,审批不通过时,需要传递该值
-     */
-    void updateProcessInstanceReject(String id, String reason);
 
 
 }
 }

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


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

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.service.task;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 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.controller.admin.task.vo.task.*;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
 import jakarta.validation.Valid;
 import jakarta.validation.Valid;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.task.api.Task;
 import org.flowable.task.api.Task;
@@ -20,6 +21,8 @@ import java.util.Map;
  */
  */
 public interface BpmTaskService {
 public interface BpmTaskService {
 
 
+    // ========== Query 查询相关方法 ==========
+
     /**
     /**
      * 获得待办的流程任务分页
      * 获得待办的流程任务分页
      *
      *
@@ -75,65 +78,96 @@ public interface BpmTaskService {
     List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId);
     List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId);
 
 
     /**
     /**
-     * 通过任务
+     * 获取任务
      *
      *
-     * @param userId 用户编号
-     * @param reqVO  通过请求
+     * @param id 任务编号
+     * @return 任务
      */
      */
-    void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO);
+    Task getTask(String id);
 
 
     /**
     /**
-     * 不通过任务
+     * 获取历史任务
      *
      *
-     * @param userId 用户编号
-     * @param reqVO  不通过请求
+     * @param id 任务编号
+     * @return 历史任务
      */
      */
-    void rejectTask(Long userId, @Valid BpmTaskRejectReqVO reqVO);
+    HistoricTaskInstance getHistoricTask(String id);
 
 
     /**
     /**
-     * 将流程任务分配给指定用户
+     * 获取历史任务列表
      *
      *
-     * @param userId 用户编号
-     * @param reqVO  分配请求
+     * @param taskIds 任务编号集合
+     * @return 历史任务列表
      */
      */
-    void transferTask(Long userId, BpmTaskTransferReqVO reqVO);
+    List<HistoricTaskInstance> getHistoricTasks(Collection<String> taskIds);
 
 
     /**
     /**
-     * 更新 Task 状态,在创建时
+     * 根据条件查询正在进行中的任务
      *
      *
-     * @param task 任务实体
+     * @param processInstanceId 流程实例编号,不允许为空
+     * @param assigned 是否分配了审批人,允许空
+     * @param taskDefineKey 任务定义 Key,允许空
      */
      */
-    void updateTaskStatusWhenCreated(Task task);
+    List<Task> getRunningTaskListByProcessInstanceId(String processInstanceId,
+                                                     Boolean assigned,
+                                                     String taskDefineKey);
 
 
     /**
     /**
-     * 更新 Task 状态,在取消时
+     * 获取当前任务的可回退的 UserTask 集合
      *
      *
-     * @param taskId 任务的编号
+     * @param id 当前的任务 ID
+     * @return 可以回退的节点列表
      */
      */
-    void updateTaskStatusWhenCanceled(String taskId);
+    List<UserTask> getUserTaskListByReturn(String id);
 
 
     /**
     /**
-     * 更新 Task 拓展记录,并发送通知
+     * 获取指定任务的子任务列表
      *
      *
-     * @param task 任务实体
+     * @param parentTaskId 父任务ID
+     * @return 子任务列表
      */
      */
-    void updateTaskExtAssign(Task task);
+    List<Task> getTaskListByParentTaskId(String parentTaskId);
 
 
     /**
     /**
-     * 获取任务
+     * 通过任务 ID,查询任务名 Map
      *
      *
-     * @param id 任务编号
-     * @return 任务
+     * @param taskIds 任务 ID
+     * @return 任务 ID 与名字的 Map
      */
      */
-    Task getTask(String id);
+    Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds);
+
+    // ========== Update 写入相关方法 ==========
 
 
     /**
     /**
-     * 获取当前任务的可回退的 UserTask 集合
+     * 通过任务
      *
      *
-     * @param id 当前的任务 ID
-     * @return 可以回退的节点列表
+     * @param userId 用户编号
+     * @param reqVO  通过请求
      */
      */
-    List<UserTask> getUserTaskListByReturn(String id);
+    void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO);
+
+    /**
+     * 不通过任务
+     *
+     * @param userId 用户编号
+     * @param reqVO  不通过请求
+     */
+    void rejectTask(Long userId, @Valid BpmTaskRejectReqVO reqVO);
+
+    /**
+     * 将流程任务分配给指定用户
+     *
+     * @param userId 用户编号
+     * @param reqVO  分配请求
+     */
+    void transferTask(Long userId, BpmTaskTransferReqVO reqVO);
+
+    /**
+     * 将指定流程实例的、进行中的流程任务,移动到结束节点
+     *
+     * @param processInstanceId 流程编号
+     */
+    void moveTaskToEnd(String processInstanceId);
 
 
     /**
     /**
      * 将任务回退到指定的 targetDefinitionKey 位置
      * 将任务回退到指定的 targetDefinitionKey 位置
@@ -167,20 +201,41 @@ public interface BpmTaskService {
      */
      */
     void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO);
     void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO);
 
 
+    // ========== Event 事件相关方法 ==========
+
     /**
     /**
-     * 获取指定任务的子任务列表
+     * 处理 Task 创建事件,目前是
      *
      *
-     * @param parentTaskId 父任务ID
-     * @return 子任务列表
+     * 1. 更新它的状态为审批中
+     * 2. 处理自动通过的情况,例如说:1)无审批人时,是否自动通过、不通过;2)非【人工审核】时,是否自动通过、不通过
+     *
+     * 注意:它的触发时机,晚于 {@link #processTaskAssigned(Task)} 之后
+     *
+     * @param task 任务实体
      */
      */
-    List<Task> getTaskListByParentTaskId(String parentTaskId);
+    void processTaskCreated(Task task);
 
 
     /**
     /**
-     * 通过任务 ID,查询任务名 Map
+     * 处理 Task 取消事件,目前是更新它的状态为已取消
      *
      *
-     * @param taskIds 任务 ID
-     * @return 任务 ID 与名字的 Map
+     * @param taskId 任务的编号
      */
      */
-    Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds);
+    void processTaskCanceled(String taskId);
+
+    /**
+     * 处理 Task 设置审批人事件,目前是发送审批消息
+     *
+     * @param task 任务实体
+     */
+    void processTaskAssigned(Task task);
+
+    /**
+     * 处理 Task 审批超时事件,可能会处理多个当前审批中的任务
+     *
+     * @param processInstanceId 流程示例编号
+     * @param taskDefineKey 任务 Key
+     * @param handlerType 处理类型,参见 {@link BpmUserTaskTimeoutHandlerTypeEnum}
+     */
+    void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType);
 
 
 }
 }

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

@@ -1,32 +1,37 @@
 package cn.iocoder.yudao.module.bpm.service.task;
 package cn.iocoder.yudao.module.bpm.service.task;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.IdUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.*;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
-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.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
 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.convert.task.BpmTaskConvert;
+import cn.iocoder.yudao.module.bpm.enums.definition.*;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
-import cn.iocoder.yudao.module.bpm.enums.task.BpmDeleteReasonEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
+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.BpmModelService;
 import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
 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;
+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.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import jakarta.annotation.Resource;
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
 import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.EndEvent;
 import org.flowable.bpmn.model.FlowElement;
 import org.flowable.bpmn.model.FlowElement;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.HistoryService;
 import org.flowable.engine.HistoryService;
@@ -45,7 +50,6 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
-import org.springframework.util.Assert;
 
 
 import java.util.*;
 import java.util.*;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
@@ -53,6 +57,7 @@ import java.util.stream.Stream;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
 
 
 /**
 /**
  * 流程任务实例 Service 实现类
  * 流程任务实例 Service 实现类
@@ -78,12 +83,16 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     @Resource
     @Resource
     private BpmProcessInstanceCopyService processInstanceCopyService;
     private BpmProcessInstanceCopyService processInstanceCopyService;
     @Resource
     @Resource
-    private BpmModelService bpmModelService;
+    private BpmModelService modelService;
     @Resource
     @Resource
     private BpmMessageService messageService;
     private BpmMessageService messageService;
 
 
     @Resource
     @Resource
     private AdminUserApi adminUserApi;
     private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+
+    // ========== Query 查询相关方法 ==========
 
 
     @Override
     @Override
     public PageResult<Task> getTaskTodoPage(Long userId, BpmTaskPageReqVO pageVO) {
     public PageResult<Task> getTaskTodoPage(Long userId, BpmTaskPageReqVO pageVO) {
@@ -173,6 +182,165 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         return tasks;
         return tasks;
     }
     }
 
 
+    /**
+     * 校验任务是否存在,并且是否是分配给自己的任务
+     *
+     * @param userId 用户 id
+     * @param taskId task id
+     */
+    private Task validateTask(Long userId, String taskId) {
+        Task task = validateTaskExist(taskId);
+        // 为什么判断 assignee 非空的情况下?
+        // 例如说:在审批人为空时,我们会有“自动审批通过”的策略,此时 userId 为 null,允许通过
+        if (StrUtil.isNotBlank(task.getAssignee())
+                && ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) {
+            throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
+        }
+        return task;
+    }
+
+    private Task validateTaskExist(String id) {
+        Task task = getTask(id);
+        if (task == null) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+        return task;
+    }
+
+    @Override
+    public Task getTask(String id) {
+        return taskService.createTaskQuery().taskId(id).includeTaskLocalVariables().singleResult();
+    }
+
+    @Override
+    public HistoricTaskInstance getHistoricTask(String id) {
+        return historyService.createHistoricTaskInstanceQuery().taskId(id).includeTaskLocalVariables().singleResult();
+    }
+
+    @Override
+    public List<HistoricTaskInstance> getHistoricTasks(Collection<String> taskIds) {
+        return historyService.createHistoricTaskInstanceQuery().taskIds(taskIds).includeTaskLocalVariables().list();
+    }
+
+    @Override
+    public List<Task> getRunningTaskListByProcessInstanceId(String processInstanceId, Boolean assigned, String defineKey) {
+        Assert.notNull(processInstanceId, "processInstanceId 不能为空");
+        TaskQuery taskQuery = taskService.createTaskQuery().processInstanceId(processInstanceId).active()
+                .includeTaskLocalVariables();
+        if (BooleanUtil.isTrue(assigned)) {
+            taskQuery.taskAssigned();
+        } else if (BooleanUtil.isFalse(assigned)) {
+            taskQuery.taskUnassigned();
+        }
+        if (StrUtil.isNotEmpty(defineKey)) {
+            taskQuery.taskDefinitionKey(defineKey);
+        }
+        return taskQuery.list();
+    }
+
+    @Override
+    public List<UserTask> getUserTaskListByReturn(String id) {
+        // 1.1 校验当前任务 task 存在
+        Task task = validateTaskExist(id);
+        // 1.2 根据流程定义获取流程模型信息
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
+        FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+        if (source == null) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+
+        // 2.1 查询该任务的前置任务节点的 key 集合
+        List<UserTask> previousUserList = BpmnModelUtils.getPreviousUserTaskList(source, null, null);
+        if (CollUtil.isEmpty(previousUserList)) {
+            return Collections.emptyList();
+        }
+        // 2.2 过滤:只有串行可到达的节点,才可以回退。类似非串行、子流程无法退回
+        previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null));
+        return previousUserList;
+    }
+
+    /**
+     * 获得所有子任务列表
+     *
+     * @param parentTask 父任务
+     * @return 所有子任务列表
+     */
+    private List<Task> getAllChildTaskList(Task parentTask) {
+        List<Task> result = new ArrayList<>();
+        // 1. 递归获取子级
+        Stack<Task> stack = new Stack<>();
+        stack.push(parentTask);
+        // 2. 递归遍历
+        for (int i = 0; i < Short.MAX_VALUE; i++) {
+            if (stack.isEmpty()) {
+                break;
+            }
+            // 2.1 获取子任务们
+            Task task = stack.pop();
+            List<Task> childTaskList = getTaskListByParentTaskId(task.getId());
+            // 2.2 如果非空,则添加到 stack 进一步递归
+            if (CollUtil.isNotEmpty(childTaskList)) {
+                stack.addAll(childTaskList);
+                result.addAll(childTaskList);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public List<Task> getTaskListByParentTaskId(String parentTaskId) {
+        String tableName = managementService.getTableName(TaskEntity.class);
+        // taskService.createTaskQuery() 没有 parentId 参数,所以写 sql 查询
+        String sql = "select ID_,NAME_,OWNER_,ASSIGNEE_ from " + tableName + " where PARENT_TASK_ID_=#{parentTaskId}";
+        return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).list();
+    }
+
+    /**
+     * 获取子任务个数
+     *
+     * @param parentTaskId 父任务 ID
+     * @return 剩余子任务个数
+     */
+    private Long getTaskCountByParentTaskId(String parentTaskId) {
+        String tableName = managementService.getTableName(TaskEntity.class);
+        String sql = "SELECT COUNT(1) from " + tableName + " WHERE PARENT_TASK_ID_=#{parentTaskId}";
+        return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).count();
+    }
+
+    /**
+     * 获得任务根任务的父任务编号
+     *
+     * @param task 任务
+     * @return 根任务的父任务编号
+     */
+    private String getTaskRootParentId(Task task) {
+        if (task == null || task.getParentTaskId() == null) {
+            return null;
+        }
+        for (int i = 0; i < Short.MAX_VALUE; i++) {
+            Task parentTask = getTask(task.getParentTaskId());
+            if (parentTask == null) {
+                return null;
+            }
+            if (parentTask.getParentTaskId() == null) {
+                return parentTask.getId();
+            }
+            task = parentTask;
+        }
+        throw new IllegalArgumentException(String.format("Task(%s) 层级过深,无法获取父节点编号", task.getId()));
+    }
+
+    @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);
+    }
+
+    // ========== Update 写入相关方法 ==========
+
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
     public void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO) {
     public void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO) {
@@ -211,6 +379,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
         // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
         if (CollUtil.isNotEmpty(reqVO.getVariables())) {
         if (CollUtil.isNotEmpty(reqVO.getVariables())) {
             Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
             Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
+            // 修改表单的值需要存储到 ProcessInstance 变量
+            runtimeService.setVariables(task.getProcessInstanceId(), variables);
             taskService.complete(task.getId(), variables, true);
             taskService.complete(task.getId(), variables, true);
         } else {
         } else {
             taskService.complete(task.getId());
             taskService.complete(task.getId());
@@ -244,7 +414,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
 
 
     /**
     /**
      * 如果父任务是有前后【加签】的任务,如果它【加签】出来的子任务都被处理,需要处理父任务:
      * 如果父任务是有前后【加签】的任务,如果它【加签】出来的子任务都被处理,需要处理父任务:
-     *
+     * <p>
      * 1. 如果是【向前】加签,则需要重新激活父任务,让它可以被审批
      * 1. 如果是【向前】加签,则需要重新激活父任务,让它可以被审批
      * 2. 如果是【向后】加签,则需要完成父任务,让它完成审批
      * 2. 如果是【向后】加签,则需要完成父任务,让它完成审批
      *
      *
@@ -277,11 +447,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             taskService.resolveTask(parentTaskId);
             taskService.resolveTask(parentTaskId);
             // 3.1.2 更新流程任务 status
             // 3.1.2 更新流程任务 status
             updateTaskStatus(parentTaskId, BpmTaskStatusEnum.RUNNING.getStatus());
             updateTaskStatus(parentTaskId, BpmTaskStatusEnum.RUNNING.getStatus());
-        // 3.2 情况二:处理向【向后】加签
+            // 3.2 情况二:处理向【向后】加签
         } else if (BpmTaskSignTypeEnum.AFTER.getType().equals(scopeType)) {
         } else if (BpmTaskSignTypeEnum.AFTER.getType().equals(scopeType)) {
             // 只有 parentTask 处于 APPROVING 的情况下,才可以继续 complete 完成
             // 只有 parentTask 处于 APPROVING 的情况下,才可以继续 complete 完成
             // 否则,一个未审批的 parentTask 任务,在加签出来的任务都被减签的情况下,就直接完成审批,这样会存在问题
             // 否则,一个未审批的 parentTask 任务,在加签出来的任务都被减签的情况下,就直接完成审批,这样会存在问题
-            Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
+            Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
             if (ObjectUtil.notEqual(status, BpmTaskStatusEnum.APPROVING.getStatus())) {
             if (ObjectUtil.notEqual(status, BpmTaskStatusEnum.APPROVING.getStatus())) {
                 return;
                 return;
             }
             }
@@ -326,135 +496,59 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             throw exception(PROCESS_INSTANCE_NOT_EXISTS);
             throw exception(PROCESS_INSTANCE_NOT_EXISTS);
         }
         }
 
 
-        // 2.1 更新流程实例为不通过
+        // 2.1 更新流程任务为不通过
         updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.REJECT.getStatus(), reqVO.getReason());
         updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.REJECT.getStatus(), reqVO.getReason());
-        // 2.2 添加评论
+        // 2.2 添加流程评论
         taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
         taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
                 BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
                 BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
+        // 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过
+        // 疑问:为什么要标记未通过呢?
+        // 回答:例如说 A 任务被向前加签除 B 任务时,B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示“已取消”的任务,导致展示不出审批不通过的细节。
+        if (task.getParentTaskId() != null) {
+            String rootParentId = getTaskRootParentId(task);
+            updateTaskStatusAndReason(rootParentId, BpmTaskStatusEnum.REJECT.getStatus(),
+                    BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过"));
+            taskService.addComment(rootParentId, task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
+                    BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过"));
+        }
 
 
-        // 3. 更新流程实例,审批不通过!
-        processInstanceService.updateProcessInstanceReject(instance.getProcessInstanceId(), reqVO.getReason());
+        // 3. 根据不同的 RejectHandler 处理策略
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
+        FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+        // 3.1 情况一:驳回到指定的任务节点
+        BpmUserTaskRejectHandlerType userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement);
+        if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.RETURN_USER_TASK) {
+            String returnTaskId = BpmnModelUtils.parseReturnTaskId(userTaskElement);
+            Assert.notNull(returnTaskId, "回退的节点不能为空");
+            returnTask(userId, new BpmTaskReturnReqVO().setId(task.getId())
+                    .setTargetTaskDefinitionKey(returnTaskId).setReason(reqVO.getReason()));
+            return;
+        }
+        // 3.2 情况二:直接结束,审批不通过
+        processInstanceService.updateProcessInstanceReject(instance, reqVO.getReason()); // 标记不通过
+        moveTaskToEnd(task.getProcessInstanceId()); // 结束流程
     }
     }
 
 
     /**
     /**
      * 更新流程任务的 status 状态
      * 更新流程任务的 status 状态
      *
      *
-     * @param id    任务编号
+     * @param id     任务编号
      * @param status 状态
      * @param status 状态
      */
      */
     private void updateTaskStatus(String id, Integer status) {
     private void updateTaskStatus(String id, Integer status) {
-        taskService.setVariableLocal(id, BpmConstants.TASK_VARIABLE_STATUS, status);
+        taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_STATUS, status);
     }
     }
 
 
     /**
     /**
      * 更新流程任务的 status 状态、reason 理由
      * 更新流程任务的 status 状态、reason 理由
      *
      *
-     * @param id 任务编号
+     * @param id     任务编号
      * @param status 状态
      * @param status 状态
      * @param reason 理由(审批通过、审批不通过的理由)
      * @param reason 理由(审批通过、审批不通过的理由)
      */
      */
     private void updateTaskStatusAndReason(String id, Integer status, String reason) {
     private void updateTaskStatusAndReason(String id, Integer status, String reason) {
         updateTaskStatus(id, status);
         updateTaskStatus(id, status);
-        taskService.setVariableLocal(id, BpmConstants.TASK_VARIABLE_REASON, reason);
-    }
-
-    /**
-     * 校验任务是否存在,并且是否是分配给自己的任务
-     *
-     * @param userId 用户 id
-     * @param taskId task id
-     */
-    private Task validateTask(Long userId, String taskId) {
-        Task task = validateTaskExist(taskId);
-        if (!Objects.equals(userId, NumberUtils.parseLong(task.getAssignee()))) {
-            throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
-        }
-        return task;
-    }
-
-    @Override
-    public void updateTaskStatusWhenCreated(Task task) {
-        Integer status = (Integer) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
-        if (status != null) {
-            log.error("[updateTaskStatusWhenCreated][taskId({}) 已经有状态({})]", task.getId(), status);
-            return;
-        }
-        updateTaskStatus(task.getId(), BpmTaskStatusEnum.RUNNING.getStatus());
-    }
-
-    @Override
-    public void updateTaskStatusWhenCanceled(String taskId) {
-        Task task = getTask(taskId);
-        // 1. 可能只是活动,不是任务,所以查询不到
-        if (task == null) {
-            log.error("[updateTaskStatusWhenCanceled][taskId({}) 任务不存在]", taskId);
-            return;
-        }
-
-        // 2. 更新 task 状态 + 原因
-        Integer status = (Integer) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
-        if (BpmTaskStatusEnum.isEndStatus(status)) {
-            log.error("[updateTaskStatusWhenCanceled][taskId({}) 处于结果({}),无需进行更新]", taskId, status);
-            return;
-        }
-        updateTaskStatusAndReason(taskId, BpmTaskStatusEnum.CANCEL.getStatus(), BpmDeleteReasonEnum.CANCEL_BY_SYSTEM.getReason());
-        // 补充说明:由于 Task 被删除成 HistoricTask 后,无法通过 taskService.addComment 添加理由,所以无法存储具体的取消理由
-    }
-
-    @Override
-    public void updateTaskExtAssign(Task task) {
-        // 发送通知。在事务提交时,批量执行操作,所以直接查询会无法查询到 ProcessInstance,所以这里是通过监听事务的提交来实现。
-        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
-
-            @Override
-            public void afterCommit() {
-                if (StrUtil.isEmpty(task.getAssignee())) {
-                    return;
-                }
-                ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
-                AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
-                messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
-            }
-
-        });
-    }
-
-    private Task validateTaskExist(String id) {
-        Task task = getTask(id);
-        if (task == null) {
-            throw exception(TASK_NOT_EXISTS);
-        }
-        return task;
-    }
-
-    @Override
-    public Task getTask(String id) {
-        return taskService.createTaskQuery().taskId(id).includeTaskLocalVariables().singleResult();
-    }
-
-    private HistoricTaskInstance getHistoricTask(String id) {
-        return historyService.createHistoricTaskInstanceQuery().taskId(id).includeTaskLocalVariables().singleResult();
-    }
-
-    @Override
-    public List<UserTask> getUserTaskListByReturn(String id) {
-        // 1.1 校验当前任务 task 存在
-        Task task = validateTaskExist(id);
-        // 1.2 根据流程定义获取流程模型信息
-        BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
-        FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
-        if (source == null) {
-            throw exception(TASK_NOT_EXISTS);
-        }
-
-        // 2.1 查询该任务的前置任务节点的 key 集合
-        List<UserTask> previousUserList = BpmnModelUtils.getPreviousUserTaskList(source, null, null);
-        if (CollUtil.isEmpty(previousUserList)) {
-            return Collections.emptyList();
-        }
-        // 2.2 过滤:只有串行可到达的节点,才可以回退。类似非串行、子流程无法退回
-        previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null));
-        return previousUserList;
+        taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_REASON, reason);
     }
     }
 
 
     @Override
     @Override
@@ -483,7 +577,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
      */
      */
     private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {
     private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {
         // 1.1 获取流程模型信息
         // 1.1 获取流程模型信息
-        BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(processDefinitionId);
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
         // 1.3 获取当前任务节点元素
         // 1.3 获取当前任务节点元素
         FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
         FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
         // 1.3 获取跳转的节点元素
         // 1.3 获取跳转的节点元素
@@ -529,7 +623,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.RETURN.getStatus(), reqVO.getReason());
             updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.RETURN.getStatus(), reqVO.getReason());
         });
         });
 
 
-        // 3. 执行驳回
+        // 3. 设置流程变量节点驳回标记:用于驳回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过
+        runtimeService.setVariable(currentTask.getProcessInstanceId(),
+                String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE);
+
+        // 4. 执行驳回
         runtimeService.createChangeActivityStateBuilder()
         runtimeService.createChangeActivityStateBuilder()
                 .processInstanceId(currentTask.getProcessInstanceId())
                 .processInstanceId(currentTask.getProcessInstanceId())
                 .moveActivityIdsToSingleActivityId(returnTaskKeyList, // 当前要跳转的节点列表( 1 或多)
                 .moveActivityIdsToSingleActivityId(returnTaskKeyList, // 当前要跳转的节点列表( 1 或多)
@@ -592,6 +690,35 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString());
         taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString());
     }
     }
 
 
+    @Override
+    public void moveTaskToEnd(String processInstanceId) {
+        List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
+        if (CollUtil.isEmpty(taskList)) {
+            return;
+        }
+
+        // 1. 其它未结束的任务,直接取消
+        // 疑问:为什么不通过 updateTaskStatusWhenCanceled 监听取消,而是直接提前调用呢?
+        // 回答:详细见 updateTaskStatusWhenCanceled 的方法,加签的场景
+        taskList.forEach(task -> {
+            Integer otherTaskStatus = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
+            if (BpmTaskStatusEnum.isEndStatus(otherTaskStatus)) {
+                return;
+            }
+            processTaskCanceled(task.getId());
+        });
+
+        // 2. 终止流程
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskList.get(0).getProcessDefinitionId());
+        List<String> activityIds = CollUtil.newArrayList(convertSet(taskList, Task::getTaskDefinitionKey));
+        EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel);
+        Assert.notNull(endEvent, "结束节点不能未空");
+        runtimeService.createChangeActivityStateBuilder()
+                .processInstanceId(processInstanceId)
+                .moveActivityIdsToSingleActivityId(activityIds, endEvent.getId())
+                .changeState();
+    }
+
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
     public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) {
     public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) {
@@ -656,7 +783,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         List<Long> currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况,因为向后加签时,它暂时没 assignee 而是 owner
         List<Long> currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况,因为向后加签时,它暂时没 assignee 而是 owner
                 Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner())));
                 Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner())));
         if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) {
         if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) {
-            List<AdminUserRespDTO> userList = adminUserApi.getUserList( CollUtil.intersection(currentAssigneeList, reqVO.getUserIds()));
+            List<AdminUserRespDTO> userList = adminUserApi.getUserList(CollUtil.intersection(currentAssigneeList, reqVO.getUserIds()));
             throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname)));
             throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname)));
         }
         }
         return taskEntity;
         return taskEntity;
@@ -665,8 +792,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     /**
     /**
      * 创建加签子任务
      * 创建加签子任务
      *
      *
-     * @param userIds 被加签的用户 ID
-     * @param taskEntity        被加签的任务
+     * @param userIds    被加签的用户 ID
+     * @param taskEntity 被加签的任务
      */
      */
     private void createSignTaskList(List<String> userIds, TaskEntityImpl taskEntity) {
     private void createSignTaskList(List<String> userIds, TaskEntityImpl taskEntity) {
         if (CollUtil.isEmpty(userIds)) {
         if (CollUtil.isEmpty(userIds)) {
@@ -695,7 +822,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         // 2.1 向前加签,设置审批人
         // 2.1 向前加签,设置审批人
         if (BpmTaskSignTypeEnum.BEFORE.getType().equals(parentTask.getScopeType())) {
         if (BpmTaskSignTypeEnum.BEFORE.getType().equals(parentTask.getScopeType())) {
             task.setAssignee(assignee);
             task.setAssignee(assignee);
-        // 2.2 向后加签,设置 owner 不设置 assignee 是因为不能同时审批,需要等父任务完成
+            // 2.2 向后加签,设置 owner 不设置 assignee 是因为不能同时审批,需要等父任务完成
         } else {
         } else {
             task.setOwner(assignee);
             task.setOwner(assignee);
         }
         }
@@ -762,61 +889,203 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         return task;
         return task;
     }
     }
 
 
+    // ========== Event 事件相关方法 ==========
+
+    @Override
+    public void processTaskCreated(Task task) {
+        // 1. 设置为待办中
+        Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
+        if (status != null) {
+            log.error("[updateTaskStatusWhenCreated][taskId({}) 已经有状态({})]", task.getId(), status);
+            return;
+        }
+        updateTaskStatus(task.getId(), BpmTaskStatusEnum.RUNNING.getStatus());
+
+        // 2. 处理自动通过的情况,例如说:1)无审批人时,是否自动通过、不通过;2)非【人工审核】时,是否自动通过、不通过
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
+        if (processInstance == null) {
+            log.error("[processTaskCreated][taskId({}) 没有找到流程实例]", task.getId());
+            return;
+        }
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
+        FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+        Integer approveType = BpmnModelUtils.parseApproveType(userTaskElement);
+        Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(userTaskElement);
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+
+            @Override
+            public void afterCompletion(int transactionStatus) {
+                // 特殊情况:部分情况下,TransactionSynchronizationManager 注册 afterCommit 监听时,不会被调用,但是 afterCompletion 可以
+                // 例如说:第一个 task 就是配置【自动通过】或者【自动拒绝】时
+                if (ObjectUtil.notEqual(transactionStatus, TransactionSynchronization.STATUS_COMMITTED)) {
+                    return;
+                }
+                // 特殊情况一:【人工审核】审批人为空,根据配置是否要自动通过、自动拒绝
+                if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.USER.getType())) {
+                    if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.APPROVE.getType())) {
+                        SpringUtil.getBean(BpmTaskService.class).approveTask(null, new BpmTaskApproveReqVO()
+                                .setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_APPROVE.getReason()));
+                    } else if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.REJECT.getType())) {
+                        SpringUtil.getBean(BpmTaskService.class).rejectTask(null, new BpmTaskRejectReqVO()
+                                .setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_REJECT.getReason()));
+                    }
+                    // 特殊情况二:【自动审核】审批类型为自动通过、不通过
+                } else {
+                    if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())) {
+                        SpringUtil.getBean(BpmTaskService.class).approveTask(null, new BpmTaskApproveReqVO()
+                                .setId(task.getId()).setReason(BpmReasonEnum.APPROVE_TYPE_AUTO_APPROVE.getReason()));
+                    } else if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
+                        SpringUtil.getBean(BpmTaskService.class).rejectTask(null, new BpmTaskRejectReqVO()
+                                .setId(task.getId()).setReason(BpmReasonEnum.APPROVE_TYPE_AUTO_REJECT.getReason()));
+                    }
+                }
+            }
+
+        });
+    }
+
     /**
     /**
-     * 获得所有子任务列表
-     *
-     * @param parentTask 父任务
-     * @return 所有子任务列表
+     * 重要补充说明:该方法目前主要有两个情况会调用到:
+     * <p>
+     * 1. 或签场景 + 审批通过:一个或签有多个审批时,如果 A 审批通过,其它或签 B、C 等任务会被 Flowable 自动删除,此时需要通过该方法更新状态为已取消
+     * 2. 审批不通过:在 {@link #rejectTask(Long, BpmTaskRejectReqVO)} 不通过时,对于加签的任务,不会被 Flowable 删除,此时需要通过该方法更新状态为已取消
      */
      */
-    private List<Task> getAllChildTaskList(Task parentTask) {
-        List<Task> result = new ArrayList<>();
-        // 1. 递归获取子级
-        Stack<Task> stack = new Stack<>();
-        stack.push(parentTask);
-        // 2. 递归遍历
-        for (int i = 0; i < Short.MAX_VALUE; i++) {
-            if (stack.isEmpty()) {
-                break;
-            }
-            // 2.1 获取子任务们
-            Task task = stack.pop();
-            List<Task> childTaskList = getTaskListByParentTaskId(task.getId());
-            // 2.2 如果非空,则添加到 stack 进一步递归
-            if (CollUtil.isNotEmpty(childTaskList)) {
-                stack.addAll(childTaskList);
-                result.addAll(childTaskList);
-            }
+    @Override
+    public void processTaskCanceled(String taskId) {
+        Task task = getTask(taskId);
+        // 1. 可能只是活动,不是任务,所以查询不到
+        if (task == null) {
+            log.error("[updateTaskStatusWhenCanceled][taskId({}) 任务不存在]", taskId);
+            return;
         }
         }
-        return result;
+
+        // 2. 更新 task 状态 + 原因
+        Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
+        if (BpmTaskStatusEnum.isEndStatus(status)) {
+            log.error("[updateTaskStatusWhenCanceled][taskId({}) 处于结果({}),无需进行更新]", taskId, status);
+            return;
+        }
+        updateTaskStatusAndReason(taskId, BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+        // 补充说明:由于 Task 被删除成 HistoricTask 后,无法通过 taskService.addComment 添加理由,所以无法存储具体的取消理由
     }
     }
 
 
     @Override
     @Override
-    public List<Task> getTaskListByParentTaskId(String parentTaskId) {
-        String tableName = managementService.getTableName(TaskEntity.class);
-        // taskService.createTaskQuery() 没有 parentId 参数,所以写 sql 查询
-        String sql = "select ID_,NAME_,OWNER_,ASSIGNEE_ from " + tableName + " where PARENT_TASK_ID_=#{parentTaskId}";
-        return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).list();
-    }
+    public void processTaskAssigned(Task task) {
+        // 发送通知。在事务提交时,批量执行操作,所以直接查询会无法查询到 ProcessInstance,所以这里是通过监听事务的提交来实现。
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
 
 
-    /**
-     * 获取子任务个数
-     *
-     * @param parentTaskId 父任务 ID
-     * @return 剩余子任务个数
-     */
-    private Long getTaskCountByParentTaskId(String parentTaskId) {
-        String tableName = managementService.getTableName(TaskEntity.class);
-        String sql = "SELECT COUNT(1) from " + tableName + " WHERE PARENT_TASK_ID_=#{parentTaskId}";
-        return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).count();
+            @Override
+            public void afterCommit() {
+                if (StrUtil.isEmpty(task.getAssignee())) {
+                    log.error("[processTaskAssigned][taskId({}) 没有分配到负责人]", task.getId());
+                    return;
+                }
+                ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
+                if (processInstance == null) {
+                    log.error("[processTaskAssigned][taskId({}) 没有找到流程实例]", task.getId());
+                    return;
+                }
+                // 审批人与提交人为同一人时,根据 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);
+                    if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
+                        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
+                        if (bpmnModel == null) {
+                            log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
+                            return;
+                        }
+                        FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+                        Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
+
+                        // 情况一:自动跳过
+                        if (ObjectUtils.equalsAny(assignStartUserHandlerType,
+                                BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
+                            getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
+                                    .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason()));
+                            return;
+                        }
+                        // 情况二:转交给部门负责人审批
+                        if (ObjectUtils.equalsAny(assignStartUserHandlerType,
+                                BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) {
+                            AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
+                            Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId());
+                            DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()) : null;
+                            Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
+                            // 找不到部门负责人的情况下,自动审批通过
+                            // noinspection DataFlowIssue
+                            if (dept.getLeaderUserId() == null) {
+                                getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
+                                        .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason()));
+                                return;
+                            }
+                            // 找得到部门负责人的情况下,修改负责人
+                            if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
+                                getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
+                                        .setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
+                                        .setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
+                                return;
+                            }
+                            // 如果部门负责人是自己,还是自己审批吧~
+                        }
+                    }
+                }
+
+                AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
+                messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
+            }
+
+        });
     }
     }
 
 
     @Override
     @Override
-    public Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds) {
-        if (CollUtil.isEmpty(taskIds)) {
-            return Collections.emptyMap();
+    @Transactional(rollbackFor = Exception.class)
+    public void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType) {
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
+        if (processInstance == null) {
+            log.error("[processTaskTimeout][processInstanceId({}) 没有找到流程实例]", processInstanceId);
+            return;
         }
         }
-        List<Task> tasks = taskService.createTaskQuery().taskIds(taskIds).list();
-        return convertMap(tasks, Task::getId, Task::getName);
+        List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey);
+        // TODO 优化:未来需要考虑加签的情况
+        if (CollUtil.isEmpty(taskList)) {
+            log.error("[processTaskTimeout][processInstanceId({}) 定义Key({}) 没有找到任务]", processInstanceId, taskDefineKey);
+            return;
+        }
+
+        taskList.forEach(task -> FlowableUtils.execute(task.getTenantId(), () -> {
+            // 情况一:自动提醒
+            if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType())) {
+                messageService.sendMessageWhenTaskTimeout(new BpmMessageSendWhenTaskTimeoutReqDTO()
+                        .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
+                        .setTaskId(task.getId()).setTaskName(task.getName()).setAssigneeUserId(Long.parseLong(task.getAssignee())));
+                return;
+            }
+
+            // 情况二:自动同意
+            if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.APPROVE.getType())) {
+                approveTask(Long.parseLong(task.getAssignee()),
+                        new BpmTaskApproveReqVO().setId(task.getId()).setReason(BpmReasonEnum.TIMEOUT_APPROVE.getReason()));
+                return;
+            }
+
+            // 情况三:自动拒绝
+            if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.REJECT.getType())) {
+                rejectTask(Long.parseLong(task.getAssignee()),
+                        new BpmTaskRejectReqVO().setId(task.getId()).setReason(BpmReasonEnum.REJECT_TASK.getReason()));
+            }
+        }));
+    }
+
+    /**
+     * 获得自身的代理对象,解决 AOP 生效问题
+     *
+     * @return 自己
+     */
+    private BpmTaskServiceImpl getSelf() {
+        return SpringUtil.getBean(getClass());
     }
     }
 
 
 }
 }

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

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

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

@@ -10,8 +10,8 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.delegate.DelegateExecution;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mock;
 import org.mockito.Spy;
 import org.mockito.Spy;
 
 
@@ -34,15 +34,23 @@ import static org.mockito.Mockito.when;
  */
  */
 public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
 public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
 
 
-    @InjectMocks
     private BpmTaskCandidateInvoker taskCandidateInvoker;
     private BpmTaskCandidateInvoker taskCandidateInvoker;
 
 
     @Mock
     @Mock
     private AdminUserApi adminUserApi;
     private AdminUserApi adminUserApi;
+
     @Spy
     @Spy
-    private BpmTaskCandidateStrategy strategy = new BpmTaskCandidateUserStrategy();
+    private BpmTaskCandidateStrategy strategy ;
+
     @Spy
     @Spy
-    private List<BpmTaskCandidateStrategy> strategyList = Collections.singletonList(strategy);
+    private List<BpmTaskCandidateStrategy> strategyList ;
+
+    @BeforeEach
+    public void setUp() {
+        strategy = new BpmTaskCandidateUserStrategy(adminUserApi); // 创建strategy实例
+        strategyList = Collections.singletonList(strategy); // 创建strategyList
+        taskCandidateInvoker = new BpmTaskCandidateInvoker(strategyList, adminUserApi);
+    }
 
 
     @Test
     @Test
     public void testCalculateUsers() {
     public void testCalculateUsers() {

+ 5 - 5
yudao-server/pom.xml

@@ -46,11 +46,11 @@
 <!--            <version>${revision}</version>-->
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 <!--        </dependency>-->
         <!-- 工作流。默认注释,保证编译速度 -->
         <!-- 工作流。默认注释,保证编译速度 -->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-bpm-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-bpm-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
         <!-- 支付服务。默认注释,保证编译速度 -->
         <!-- 支付服务。默认注释,保证编译速度 -->
 <!--        <dependency>-->
 <!--        <dependency>-->
 <!--            <groupId>cn.iocoder.boot</groupId>-->
 <!--            <groupId>cn.iocoder.boot</groupId>-->

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