Przeglądaj źródła

!1286 2.4.2:工作流的更新
Merge pull request !1286 from 芋道源码/feature/bpm

芋道源码 5 miesięcy temu
rodzic
commit
367e3b9880
58 zmienionych plików z 2175 dodań i 602 usunięć
  1. 33 10
      README.md
  2. 2 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java
  3. 21 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java
  4. 3 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  5. 2 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java
  6. 37 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java
  7. 36 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java
  8. 35 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java
  9. 3 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java
  10. 6 3
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java
  11. 1 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java
  12. 3 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java
  13. 25 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java
  14. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
  15. 13 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
  16. 39 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
  17. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java
  18. 139 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java
  19. 9 21
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java
  20. 27 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  21. 3 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java
  22. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java
  23. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java
  24. 3 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java
  25. 3 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java
  26. 9 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
  27. 21 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
  28. 42 19
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
  29. 43 20
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
  30. 6 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java
  31. 78 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java
  32. 5 29
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java
  33. 2 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java
  34. 14 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java
  35. 15 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
  36. 6 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java
  37. 5 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java
  38. 159 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java
  39. 209 35
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  40. 32 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
  41. 213 76
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
  42. 18 9
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  43. 13 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
  44. 29 6
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
  45. 199 58
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  46. 26 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
  47. 186 56
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
  48. 96 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java
  49. 21 49
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java
  50. 0 124
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java
  51. 0 44
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java
  52. 73 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java
  53. 66 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java
  54. 14 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java
  55. 59 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java
  56. 54 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java
  57. 3 3
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java
  58. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java

+ 33 - 10
README.md

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

+ 2 - 1
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.bpm.api.task;
 
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
-
 import jakarta.validation.Valid;
 
 /**
@@ -20,4 +19,6 @@ public interface BpmProcessInstanceApi {
      */
     String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO);
 
+
+
 }

+ 21 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.bpm.api.task;
+
+import jakarta.validation.constraints.NotEmpty;
+
+/**
+ * 流程任务 Api 接口
+ *
+ * @author jason
+ */
+public interface BpmProcessTaskApi {
+
+    /**
+     * 触发流程任务的执行
+     *
+     * @param processInstanceId 流程实例编号
+     * @param taskDefineKey 任务 Key
+     */
+    void triggerTask(@NotEmpty(message = "流程实例的编号不能为空") String processInstanceId,
+                     @NotEmpty(message = "任务 Key 不能为空") String taskDefineKey);
+
+}

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

@@ -24,6 +24,7 @@ public interface ErrorCodeConstants {
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败,原因:BPMN 流程图中,没有开始事件");
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在");
     ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员");
+    ErrorCode MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】");
 
     // ========== 流程定义 1-009-003-000 ==========
     ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
@@ -39,6 +40,8 @@ public interface ErrorCodeConstants {
     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, "发起流程失败,你没有权限发起该流程");
     ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消");
+    ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败");
+    ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置");
 
     // ========== 流程任务 1-009-005-000 ==========
     ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");

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

@@ -14,7 +14,8 @@ import lombok.Getter;
 public enum BpmBoundaryEventTypeEnum {
 
     USER_TASK_TIMEOUT(1, "用户任务超时"),
-    DELAY_TIMER_TIMEOUT(2, "延迟器超时");
+    DELAY_TIMER_TIMEOUT(2, "延迟器超时"),
+    CHILD_PROCESS_TIMEOUT(3, "子流程超时");
 
     private final Integer type;
     private final String name;

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

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 子流程多实例来源类型枚举
+ *
+ * @author Lesan
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmChildProcessMultiInstanceSourceTypeEnum implements ArrayValuable<Integer> {
+
+    FIXED_QUANTITY(1, "固定数量"),
+    NUMBER_FORM(2, "数字表单"),
+    MULTIPLE_FORM(3, "多选表单");
+
+    private final Integer type;
+    private final String name;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessMultiInstanceSourceTypeEnum::getType).toArray(Integer[]::new);
+
+    public static BpmChildProcessMultiInstanceSourceTypeEnum typeOf(Integer type) {
+        return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
+    }
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+}

+ 36 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.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.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 当子流程发起人为空时类型枚举
+ *
+ * @author Lesan
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmChildProcessStartUserEmptyTypeEnum implements ArrayValuable<Integer> {
+
+    MAIN_PROCESS_START_USER(1, "同主流程发起人"),
+    CHILD_PROCESS_ADMIN(2, "子流程管理员"),
+    MAIN_PROCESS_ADMIN(3, "主流程管理员");
+
+    private final Integer type;
+    private final String name;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserEmptyTypeEnum::getType).toArray(Integer[]::new);
+
+    public static BpmChildProcessStartUserEmptyTypeEnum typeOf(Integer type) {
+        return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
+    }
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+}

+ 35 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.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.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 子流程发起人类型枚举
+ *
+ * @author Lesan
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmChildProcessStartUserTypeEnum implements ArrayValuable<Integer> {
+
+    MAIN_PROCESS_START_USER(1, "同主流程发起人"),
+    FROM_FORM(2, "表单");
+
+    private final Integer type;
+    private final String name;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserTypeEnum::getType).toArray(Integer[]::new);
+
+    public static BpmChildProcessStartUserTypeEnum typeOf(Integer type) {
+        return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
+    }
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+}

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

@@ -25,10 +25,13 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable<Integer> {
     START_USER_NODE(10, "发起人", "userTask"), // 发起人节点。前端的开始节点,Id 固定
     APPROVE_NODE(11, "审批人", "userTask"),
     COPY_NODE(12, "抄送人", "serviceTask"),
+    TRANSACTOR_NODE(13, "办理人", "userTask"),
 
     DELAY_TIMER_NODE(14, "延迟器", "receiveTask"),
     TRIGGER_NODE(15, "触发器", "serviceTask"),
 
+    CHILD_PROCESS(20, "子流程", "callActivity"),
+
     // 50 ~ 条件分支
     CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式
     CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"),

+ 6 - 3
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java

@@ -16,8 +16,12 @@ import java.util.Arrays;
 @AllArgsConstructor
 public enum BpmTriggerTypeEnum implements ArrayValuable<Integer> {
 
-    HTTP_REQUEST(1, "发起 HTTP 请求"),
-    UPDATE_NORMAL_FORM(2, "更新流程表单"); // TODO @jason:FORM_UPDATE
+    HTTP_REQUEST(1, "发起 HTTP 请求"), // BPM => 业务,流程继续执行,无需等待业务
+    HTTP_CALLBACK(2, "接收 HTTP 回调"), // BPM => 业务 => BPM,流程卡主,等待业务回调
+
+    FORM_UPDATE(10, "更新流程表单数据"),
+    FORM_DELETE(11, "删除流程表单数据"),
+    ;
 
     /**
      * 触发器执行动作类型
@@ -39,5 +43,4 @@ public enum BpmTriggerTypeEnum implements ArrayValuable<Integer> {
     public static BpmTriggerTypeEnum typeOf(Integer type) {
         return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
     }
-
 }

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

@@ -26,6 +26,7 @@ public enum BpmReasonEnum {
     TIMEOUT_REJECT("审批超时,系统自动不通过"),
     ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"),
     ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"),
+    ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE("发起人节点首次自动通过"), // 目前仅“子流程”使用
     ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"),
     ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"),
     ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"),

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java

@@ -2,11 +2,10 @@ package cn.iocoder.yudao.module.bpm.api.task;
 
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
-import org.springframework.stereotype.Service;
-import org.springframework.validation.annotation.Validated;
-
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
 
 /**
  * Flowable 流程实例 Api 实现类
@@ -25,4 +24,5 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi {
     public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO) {
         return processInstanceService.createProcessInstance(userId, reqDTO);
     }
+
 }

+ 25 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.bpm.api.task;
+
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * 流程任务 Api 实现类
+ *
+ * @author jason
+ */
+@Service
+@Validated
+public class BpmProcessTaskApiImpl implements BpmProcessTaskApi {
+
+    @Resource
+    private BpmTaskService bpmTaskService;
+
+    @Override
+    public void triggerTask(String processInstanceId, String taskDefineKey) {
+        bpmTaskService.triggerTask(processInstanceId, taskDefineKey);
+    }
+
+}

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

@@ -57,7 +57,7 @@ public class BpmModelController {
     @GetMapping("/list")
     @Operation(summary = "获得模型分页")
     @Parameter(name = "name", description = "模型名称", example = "芋艿")
-    public CommonResult<List<BpmModelRespVO>> getModelPage(@RequestParam(value = "name", required = false) String name) {
+    public CommonResult<List<BpmModelRespVO>> getModelList(@RequestParam(value = "name", required = false) String name) {
         List<Model> list = modelService.getModelList(name);
         if (CollUtil.isEmpty(list)) {
             return success(Collections.emptyList());

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

@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.common.engine.impl.db.SuspensionState;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -31,6 +32,7 @@ import java.util.List;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -99,6 +101,17 @@ public class BpmProcessDefinitionController {
                 list, null, processDefinitionMap, null, null));
     }
 
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得流程定义精简列表", description = "只包含未挂起的流程,主要用于前端的下拉选项")
+    public CommonResult<List<BpmProcessDefinitionRespVO>> getSimpleProcessDefinitionList() {
+        // 只查询未挂起的流程
+        List<ProcessDefinition> list = processDefinitionService.getProcessDefinitionListBySuspensionState(
+                SuspensionState.ACTIVE.getStateCode());
+        // 拼接 VO 返回,只返回 id、name、key
+        return success(convertList(list, definition -> new BpmProcessDefinitionRespVO()
+                .setId(definition.getId()).setName(definition.getName()).setKey(definition.getKey())));
+    }
+
     @GetMapping ("/get")
     @Operation(summary = "获得流程定义")
     @Parameter(name = "id", description = "流程编号", required = true, example = "1024")

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

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
 
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
@@ -27,8 +28,7 @@ import java.util.List;
 @Data
 public class BpmModelMetaInfoVO {
 
-    @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
-    @NotEmpty(message = "流程图标不能为空")
+    @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
     @URL(message = "流程图标格式不正确")
     private String icon;
 
@@ -46,6 +46,7 @@ public class BpmModelMetaInfoVO {
     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")
@@ -81,6 +82,14 @@ public class BpmModelMetaInfoVO {
     @Schema(description = "摘要设置", example = "{}")
     private SummarySetting summarySetting;
 
+    // TODO @lesan:processBeforeTriggerSetting;要不叫这个?主要考虑,notify 留给后续的站内信、短信、邮件这种 notify 通知哈。
+    @Schema(description = "流程前置通知设置", example = "{}")
+    private HttpRequestSetting PreProcessNotifySetting;
+
+    // TODO @lesan:processAfterTriggerSetting
+    @Schema(description = "流程后置通知设置", example = "{}")
+    private HttpRequestSetting PostProcessNotifySetting;
+
     @Schema(description = "流程 ID 规则")
     @Data
     @Valid
@@ -133,4 +142,32 @@ public class BpmModelMetaInfoVO {
 
     }
 
+    @Schema(description = "http 请求通知设置", example = "{}")
+    @Data
+    public static class HttpRequestSetting {
+
+        @Schema(description = "请求路径", example = "http://127.0.0.1")
+        @NotEmpty(message = "请求 URL 不能为空")
+        @URL(message = "请求 URL 格式不正确")
+        private String url;
+
+        @Schema(description = "请求头参数设置", example = "[]")
+        @Valid
+        private List<BpmSimpleModelNodeVO.HttpRequestParam> header;
+
+        @Schema(description = "请求头参数设置", example = "[]")
+        @Valid
+        private List<BpmSimpleModelNodeVO.HttpRequestParam> body;
+
+        /**
+         * 请求返回处理设置,用于修改流程表单值
+         * <p>
+         * key:表示要修改的流程表单字段名(name)
+         * value:接口返回的字段名
+         */
+        @Schema(description = "请求返回处理设置", example = "[]")
+        private List<KeyValue<String, String>> response;
+
+    }
+
 }

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

@@ -25,7 +25,7 @@ public class BpmModelRespVO extends BpmModelMetaInfoVO {
     @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
     private String icon;
 
-    @Schema(description = "流程分类编", example = "1")
+    @Schema(description = "流程分类编", example = "1")
     private String category;
     @Schema(description = "流程分类名字", example = "请假")
     private String categoryName;

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

@@ -4,16 +4,19 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
 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 org.flowable.bpmn.model.IOParameter;
 import org.hibernate.validator.constraints.URL;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 @Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO")
 @Data
@@ -114,7 +117,8 @@ public class BpmSimpleModelNodeVO {
     @Schema(description = "路由分支组", example = "[]")
     private List<RouterSetting> routerGroups;
 
-    @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成,所以 hidden = true
+    @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成(不从前端传递),所以 hidden = true
+    @JsonIgnore
     private String routerDefaultFlowId; // 仅用于路由分支节点 BpmSimpleModelNodeType.ROUTER_BRANCH_NODE
 
     /**
@@ -122,6 +126,15 @@ public class BpmSimpleModelNodeVO {
      */
     private TriggerSetting triggerSetting;
 
+    @Schema(description = "附加节点 Id", example = "UserTask_xxx", hidden = true) // 由后端生成(不从前端传递),所以 hidden = true
+    @JsonIgnore
+    private String attachNodeId; // 目前用于触发器节点(HTTP 回调)。需要 UserTask 和 ReceiveTask(附加节点) 来完成
+
+    /**
+     * 子流程设置
+     */
+    private ChildProcessSetting childProcessSetting;
+
     @Schema(description = "任务监听器")
     @Valid
     @Data
@@ -345,12 +358,10 @@ public class BpmSimpleModelNodeVO {
         @Valid
         private HttpRequestTriggerSetting httpRequestSetting;
 
-        // TODO @jason:这个要不直接叫 formSetting,更好理解一点哈
-        // TODO @jason:如果搞成 List<NormalFormTriggerSetting>,是不是可以做条件组了?微信讨论哈
         /**
          * 流程表单触发器设置
          */
-        private NormalFormTriggerSetting normalFormSetting;
+        private List<FormTriggerSetting> formSettings;
 
         @Schema(description = "http 请求触发器设置", example = "{}")
         @Data
@@ -369,7 +380,6 @@ public class BpmSimpleModelNodeVO {
             @Valid
             private List<HttpRequestParam> body;
 
-            // TODO @json:可能未来看情况,搞个 HttpResponseParam;得看看有没别的业务需要,抽象统一
             /**
              * 请求返回处理设置,用于修改流程表单值
              * <p>
@@ -379,15 +389,137 @@ public class BpmSimpleModelNodeVO {
             @Schema(description = "请求返回处理设置", example = "[]")
             private List<KeyValue<String, String>> response;
 
+            /**
+             * Http 回调请求,需要指定回调任务 Key,用于回调执行
+             */
+            @Schema(description = "回调任务 Key", example = "xxx", hidden = true)
+            private String callbackTaskDefineKey;
+
         }
 
         @Schema(description = "流程表单触发器设置", example = "{}")
         @Data
-        public static class NormalFormTriggerSetting {
+        public static class FormTriggerSetting {
 
-            @Schema(description = "修改的表单字段", example = "userName")
+            @Schema(description = "条件类型", example = "1")
+            @InEnum(BpmSimpleModeConditionTypeEnum.class)
+            private Integer conditionType;
+
+            @Schema(description = "条件表达式", example = "${day>3}")
+            private String conditionExpression;
+
+            @Schema(description = "条件组", example = "{}")
+            private ConditionGroups conditionGroups;
+
+            @Schema(description = "修改的表单字段", example = "{}")
             private Map<String, Object> updateFormFields;
 
+            @Schema(description = "删除表单字段", example = "[]")
+            private Set<String> deleteFields;
+        }
+    }
+
+    @Schema(description = "子流程节点配置")
+    @Data
+    @Valid
+    public static class ChildProcessSetting {
+
+        @Schema(description = "被调用流程", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx")
+        @NotEmpty(message = "被调用流程不能为空")
+        private String calledProcessDefinitionKey;
+
+        @Schema(description = "被调用流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx")
+        @NotEmpty(message = "被调用流程名称不能为空")
+        private String calledProcessDefinitionName;
+
+        @Schema(description = "是否异步", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+        @NotNull(message = "是否异步不能为空")
+        private Boolean async;
+
+        @Schema(description = "输入参数(主->子)", example = "[]")
+        private List<IOParameter> inVariables;
+
+        @Schema(description = "输出参数(子->主)", example = "[]")
+        private List<IOParameter> outVariables;
+
+        @Schema(description = "是否自动跳过子流程发起节点", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+        @NotNull(message = "是否自动跳过子流程发起节点不能为空")
+        private Boolean skipStartUserNode;
+
+        @Schema(description = "子流程发起人配置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
+        @NotNull(message = "子流程发起人配置不能为空")
+        private StartUserSetting startUserSetting;
+
+        @Schema(description = "超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
+        private TimeoutSetting timeoutSetting;
+
+        @Schema(description = "多实例设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
+        private MultiInstanceSetting multiInstanceSetting;
+
+        @Schema(description = "子流程发起人配置")
+        @Data
+        @Valid
+        public static class StartUserSetting {
+
+            @Schema(description = "子流程发起人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+            @NotNull(message = "子流程发起人类型")
+            @InEnum(BpmChildProcessStartUserTypeEnum.class)
+            private Integer type;
+
+            @Schema(description = "表单", example = "xxx")
+            private String formField;
+
+            @Schema(description = "当子流程发起人为空时类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+            @NotNull(message = "当子流程发起人为空时类型不能为空")
+            @InEnum(BpmChildProcessStartUserEmptyTypeEnum.class)
+            private Integer emptyType;
+
+        }
+
+        @Schema(description = "超时设置")
+        @Data
+        @Valid
+        public static class TimeoutSetting {
+
+            @Schema(description = "是否开启超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+            @NotNull(message = "是否开启超时设置不能为空")
+            private Boolean enable;
+
+            @Schema(description = "时间类型", example = "1")
+            @InEnum(BpmDelayTimerTypeEnum.class)
+            private Integer type;
+
+            @Schema(description = "时间表达式", example = "PT1H,2025-01-01T00:00:00")
+            private String timeExpression;
+
+        }
+
+        @Schema(description = "多实例设置")
+        @Data
+        @Valid
+        public static class MultiInstanceSetting {
+
+            @Schema(description = "是否开启多实例", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+            @NotNull(message = "是否开启多实例不能为空")
+            private Boolean enable;
+
+            @Schema(description = "是否串行", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+            @NotNull(message = "是否串行不能为空")
+            private Boolean sequential;
+
+            @Schema(description = "完成比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+            @NotNull(message = "完成比例不能为空")
+            private Integer approveRatio;
+
+            @Schema(description = "多实例来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+            @NotNull(message = "多实例来源类型不能为空")
+            @InEnum(BpmChildProcessMultiInstanceSourceTypeEnum.class)
+            private Integer sourceType;
+
+            @Schema(description = "多实例来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+            @NotNull(message = "多实例来源不能为空")
+            private String source;
+
         }
 
     }

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process;
 
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -8,7 +9,7 @@ import java.util.List;
 
 @Schema(description = "管理后台 - 流程定义 Response VO")
 @Data
-public class BpmProcessDefinitionRespVO {
+public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private String id;
@@ -19,15 +20,9 @@ public class BpmProcessDefinitionRespVO {
     @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
     private String name;
 
-    @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
+    @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "youdao")
     private String key;
 
-    @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
-    private String icon;
-
-    @Schema(description = "流程描述", example = "我是描述")
-    private String description;
-
     @Schema(description = "流程分类", example = "1")
     private String category;
     @Schema(description = "流程分类名字", example = "请假")
@@ -36,22 +31,15 @@ public class BpmProcessDefinitionRespVO {
     @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
     private Integer modelType; // 参见 BpmModelTypeEnum 枚举类
 
-    @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
-    private Integer formType;
-    @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024")
-    private Long formId;
-    @Schema(description = "表单名字", example = "请假表单")
-    private String formName;
+    @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
+    private String modelId;
+
     @Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED)
     private String formConf;
     @Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED)
     private List<String> formFields;
-    @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;
+    @Schema(description = "表单名字", example = "请假表单")
+    private String formName;
 
     @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Integer suspensionState; // 参见 SuspensionState 枚举
@@ -67,7 +55,7 @@ public class BpmProcessDefinitionRespVO {
 
     @Schema(description = "流程定义排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long sort;
-    
+
     @Schema(description = "BPMN UserTask 用户任务")
     @Data
     public static class UserTask {

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

@@ -1,8 +1,10 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
@@ -30,10 +32,10 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-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 = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请”
@@ -76,8 +78,14 @@ public class BpmProcessInstanceController {
                 convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory));
         Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
                 convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId));
+        Set<Long> userIds = convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId()));
+        userIds.addAll(convertSetByFlatMap(taskMap.values(),
+                tasks -> tasks.stream().map(Task::getAssignee).filter(StrUtil::isNotBlank).map(Long::parseLong)));
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+                convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
         return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult,
-                processDefinitionMap, categoryMap, taskMap, null, null, processDefinitionInfoMap));
+                processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap));
     }
 
     @GetMapping("/manager-page")
@@ -140,6 +148,7 @@ public class BpmProcessInstanceController {
                 processDefinition, processDefinitionInfo, startUser, dept));
     }
 
+    // TODO @lesan:【子流程】子流程如果取消,主流程应该是通过、还是不通过哈?还是禁用掉子流程的取消?
     @DeleteMapping("/cancel-by-start-user")
     @Operation(summary = "用户取消流程实例", description = "取消发起的流程")
     @PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')")
@@ -162,10 +171,25 @@ public class BpmProcessInstanceController {
     @Operation(summary = "获得审批详情")
     @Parameter(name = "id", description = "流程实例的编号", required = true)
     @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
+    @SuppressWarnings("unchecked")
     public CommonResult<BpmApprovalDetailRespVO> getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) {
+        if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) {
+            reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class));
+        }
         return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
     }
 
+    @GetMapping("/get-next-approval-nodes")
+    @Operation(summary = "获取下一个执行的流程节点")
+    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
+    @SuppressWarnings("unchecked")
+    public CommonResult<List<BpmApprovalDetailRespVO.ActivityNode>> getNextApprovalNodes(@Valid BpmApprovalDetailReqVO reqVO) {
+        if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) {
+            reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class));
+        }
+        return success(processInstanceService.getNextApprovalNodes(getLoginUserId(), reqVO));
+    }
+
     @GetMapping("/get-bpmn-model-view")
     @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
     @Parameter(name = "id", description = "流程实例的编号", required = true)

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

@@ -18,6 +18,9 @@ public class BpmApprovalDetailReqVO {
     @Schema(description = "流程变量")
     private Map<String, Object> processVariables; // 使用场景:同 processDefinitionId,用于流程预测
 
+    @Schema(description = "流程变量")
+    private String processVariablesStr; // 解决 GET 无法传递对象的问题,最终转换成 processVariables 变量
+
     @Schema(description = "流程实例的编号", example = "1024")
     private String processInstanceId;  // 使用场景:流程已发起时候传流程实例 ID
 

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

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -73,6 +74,13 @@ public class BpmProcessInstanceRespVO {
         @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
         private String name;
 
+        @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048")
+        @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser
+        private Long assignee;
+
+        @Schema(description = "任务分配人", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048")
+        private UserSimpleBaseVO assigneeUser;
+
     }
 
 }

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

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotEmpty;
 import lombok.Data;
 
+import java.util.List;
 import java.util.Map;
 
 @Schema(description = "管理后台 - 通过流程任务的 Request VO")
@@ -23,4 +24,7 @@ public class BpmTaskApproveReqVO {
     @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
     private Map<String, Object> variables;
 
+    @Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}")
+    private Map<String, List<Long>> nextAssignees; // 为什么是 Map,而不是 List 呢?因为下一个节点可能是多个,例如说并行网关的情况
+
 }

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

@@ -18,6 +18,9 @@ public class BpmTaskPageReqVO extends PageParam {
     @Schema(description = "流程分类", example = "1")
     private String category;
 
+    @Schema(description = "流程定义的标识", example = "2048")
+    private String processDefinitionKey; // 精准匹配
+
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;

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

@@ -85,6 +85,9 @@ public class BpmTaskRespVO {
     @Schema(description = "是否填写审批意见", example = "false")
     private Boolean reasonRequire;
 
+    @Schema(description = "节点类型", example = "10")
+    private Integer nodeType; // 参见 BpmSimpleModelNodeTypeEnum 枚举。
+
     @Data
     @Schema(description = "流程实例")
     public static class ProcessInstance {

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

@@ -76,6 +76,15 @@ public interface BpmProcessInstanceConvert {
                     respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
                     MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName()));
                 }
+                if (CollUtil.isNotEmpty(respVO.getTasks())) {
+                    respVO.getTasks().forEach(task -> {
+                        AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee());
+                        if (assigneeUser!= null) {
+                            task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class));
+                            MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName()));
+                        }
+                    });
+                }
             }
             // 摘要
             respVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(respVO.getProcessDefinitionId()),

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

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
-import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
@@ -60,6 +59,14 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
      */
     private Integer modelType;
 
+    /**
+     * 流程分类的编码
+     *
+     * 关联 {@link BpmCategoryDO#getCode()}
+     *
+     * 为什么要存储?原因是,{@link ProcessDefinition#getCategory()} 无法设置
+     */
+    private String category;
     /**
      * 图标
      */
@@ -149,7 +156,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
      *
      * 关联 {@link AdminUserRespDTO#getId()} 字段的数组
      */
-    @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
+    @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
     private List<Long> managerUserIds;
 
     /**
@@ -175,11 +182,22 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
      */
     @TableField(typeHandler = JacksonTypeHandler.class)
     private BpmModelMetaInfoVO.TitleSetting titleSetting;
-
     /**
      * 摘要设置
      */
     @TableField(typeHandler = JacksonTypeHandler.class)
     private BpmModelMetaInfoVO.SummarySetting summarySetting;
 
+    // TODO @lesan:processBeforeTriggerSetting;要不叫这个?主要考虑,notify 留给后续的站内信、短信、邮件这种 notify 通知哈。
+    /**
+     * 流程前置通知设置
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class, exist = false) // TODO @芋艿:临时注释 exist,因为要合并 master-jdk17
+    private BpmModelMetaInfoVO.HttpRequestSetting PreProcessNotifySetting;
+    /**
+     * 流程后置通知设置
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class, exist = false) // TODO @芋艿:临时注释 exist,因为要合并 master-jdk17
+    private BpmModelMetaInfoVO.HttpRequestSetting PostProcessNotifySetting;
+
 }

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

@@ -1,15 +1,21 @@
 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.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
 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.FlowableUtils;
-import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import lombok.Setter;
 import org.flowable.bpmn.model.Activity;
+import org.flowable.bpmn.model.CallActivity;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
 import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
 
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -42,27 +48,44 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
      */
     @Override
     protected int resolveNrOfInstances(DelegateExecution execution) {
-        // 第一步,设置 collectionVariable 和 CollectionVariable
-        // 从  execution.getVariable() 读取所有任务处理人的 key
-        super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
-        super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
-        // 从 execution.getVariable() 读取当前所有任务处理的人的 key
-        super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
+        // 情况一:UserTask 节点
+        if (execution.getCurrentFlowElement() instanceof UserTask) {
+            // 第一步,设置 collectionVariable 和 CollectionVariable
+            // 从  execution.getVariable() 读取所有任务处理人的 key
+            super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
+            super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
+            // 从 execution.getVariable() 读取当前所有任务处理的人的 key
+            super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
 
-        // 第二步,获取任务的所有处理人
-        @SuppressWarnings("unchecked")
-        Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
-        if (assigneeUserIds == null) {
-            assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
-            if (CollUtil.isEmpty(assigneeUserIds)) {
-                // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
-                // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
-                // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
-                assigneeUserIds = SetUtils.asSet((Long) null);
+            // 第二步,获取任务的所有处理人
+            @SuppressWarnings("unchecked")
+            Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
+            if (assigneeUserIds == null) {
+                assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
+                if (CollUtil.isEmpty(assigneeUserIds)) {
+                    // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
+                    // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
+                    // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
+                    assigneeUserIds = SetUtils.asSet((Long) null);
+                }
+                execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
             }
-            execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
+            return assigneeUserIds.size();
         }
-        return assigneeUserIds.size();
+
+        // 情况二:CallActivity 节点
+        if (execution.getCurrentFlowElement() instanceof CallActivity) {
+            FlowElement flowElement = execution.getCurrentFlowElement();
+            Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
+            if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
+                return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
+            }
+            if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
+                return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
+            }
+        }
+
+        return super.resolveNrOfInstances(execution);
     }
 
 }

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

@@ -2,14 +2,20 @@ 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.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
 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.FlowableUtils;
 import lombok.Setter;
 import org.flowable.bpmn.model.Activity;
+import org.flowable.bpmn.model.CallActivity;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
 import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
 
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -35,28 +41,45 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
      */
     @Override
     protected int resolveNrOfInstances(DelegateExecution execution) {
-        // 第一步,设置 collectionVariable 和 CollectionVariable
-        // 从  execution.getVariable() 读取所有任务处理人的 key
-        super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
-        super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
-        // 从 execution.getVariable() 读取当前所有任务处理的人的 key
-        super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
-
-        // 第二步,获取任务的所有处理人
-        // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人
-        @SuppressWarnings("unchecked")
-        Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
-        if (assigneeUserIds == null) {
-            assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
-            if (CollUtil.isEmpty(assigneeUserIds)) {
-                // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
-                // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
-                // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
-                assigneeUserIds = SetUtils.asSet((Long) null);
+        // 情况一:UserTask 节点
+        if (execution.getCurrentFlowElement() instanceof UserTask) {
+            // 第一步,设置 collectionVariable 和 CollectionVariable
+            // 从  execution.getVariable() 读取所有任务处理人的 key
+            super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
+            super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
+            // 从 execution.getVariable() 读取当前所有任务处理的人的 key
+            super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
+
+            // 第二步,获取任务的所有处理人
+            // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人
+            @SuppressWarnings("unchecked")
+            Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
+            if (assigneeUserIds == null) {
+                assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
+                if (CollUtil.isEmpty(assigneeUserIds)) {
+                    // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
+                    // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
+                    // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
+                    assigneeUserIds = SetUtils.asSet((Long) null);
+                }
+                execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
+            }
+            return assigneeUserIds.size();
+        }
+
+        // 情况二:CallActivity 节点
+        if (execution.getCurrentFlowElement() instanceof CallActivity) {
+            FlowElement flowElement = execution.getCurrentFlowElement();
+            Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
+            if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
+                return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
+            }
+            if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
+                return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
             }
-            execution.setVariableLocal(super.collectionVariable, assigneeUserIds);
         }
-        return assigneeUserIds.size();
+
+        return super.resolveNrOfInstances(execution);
     }
 
 }

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

@@ -19,6 +19,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.CallActivity;
 import org.flowable.bpmn.model.FlowElement;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
@@ -129,8 +130,12 @@ public class BpmTaskCandidateInvoker {
 
     public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
                                               Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
-        // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过
+        // 如果是 CallActivity 子流程,不进行计算候选人
         FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId);
+        if (flowElement instanceof CallActivity) {
+            return new HashSet<>();
+        }
+        // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过
         Integer approveType = BpmnModelUtils.parseApproveType(flowElement);
         if (ObjectUtils.equalsAny(approveType,
                 BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),

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

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import com.google.common.collect.Sets;
+import jakarta.annotation.Resource;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 审批人自选 {@link BpmTaskCandidateUserStrategy} 实现类
+ * 审批人在审批时选择下一个节点的审批人
+ *
+ * @author smallNorthLee
+ */
+@Component
+public class BpmTaskCandidateApproveUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
+
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public BpmTaskCandidateStrategyEnum getStrategy() {
+        return BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT;
+    }
+
+    @Override
+    public void validateParam(String param) {}
+
+    @Override
+    public boolean isParamRequired() {
+        return false;
+    }
+
+    @Override
+    public LinkedHashSet<Long> calculateUsersByTask(DelegateExecution execution, String param) {
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
+        Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId());
+        Map<String, List<Long>> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processInstance);
+        Assert.notNull(approveUserSelectAssignees, "流程实例({}) 的下一个执行节点审批人不能为空",
+                execution.getProcessInstanceId());
+        if (approveUserSelectAssignees == null) {
+            return Sets.newLinkedHashSet();
+        }
+        // 获得审批人
+        List<Long> assignees = approveUserSelectAssignees.get(execution.getCurrentActivityId());
+        return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
+    }
+
+    @Override
+    public LinkedHashSet<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
+                                                        Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+        if (processVariables == null) {
+            return Sets.newLinkedHashSet();
+        }
+        // 流程预测时会使用,允许审批人为空,如果为空前端会弹出提示选择下一个节点审批人,避免流程无法进行,审批时会真正校验节点是否配置审批人
+        Map<String, List<Long>> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processVariables);
+        if (approveUserSelectAssignees == null) {
+            return Sets.newLinkedHashSet();
+        }
+        // 获得审批人
+        List<Long> assignees = approveUserSelectAssignees.get(activityId);
+        return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
+    }
+
+}

+ 5 - 29
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java

@@ -2,24 +2,21 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.d
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import com.google.common.collect.Sets;
 import jakarta.annotation.Resource;
 import org.flowable.bpmn.model.BpmnModel;
-import org.flowable.bpmn.model.ServiceTask;
-import org.flowable.bpmn.model.Task;
-import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.delegate.DelegateExecution;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import java.util.*;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 发起人自选 {@link BpmTaskCandidateUserStrategy} 实现类
@@ -55,7 +52,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand
                 execution.getProcessInstanceId());
         // 获得审批人
         List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
-        return new LinkedHashSet<>(assignees);
+        return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
     }
 
     @Override
@@ -70,28 +67,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand
         }
         // 获得审批人
         List<Long> assignees = startUserSelectAssignees.get(activityId);
-        return new LinkedHashSet<>(assignees);
-    }
-
-    /**
-     * 获得发起人自选审批人或抄送人的 Task 列表
-     *
-     * @param bpmnModel BPMN 模型
-     * @return Task 列表
-     */
-    public static List<Task> getStartUserSelectTaskList(BpmnModel bpmnModel) {
-        if (bpmnModel == null) {
-            return Collections.emptyList();
-        }
-        List<Task> tasks = new ArrayList<>();
-        tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class));
-        tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, ServiceTask.class));
-        if (CollUtil.isEmpty(tasks)) {
-            return Collections.emptyList();
-        }
-        tasks.removeIf(task -> ObjectUtil.notEqual(BpmnModelUtils.parseCandidateStrategy(task),
-                BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()));
-        return tasks;
+        return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
     }
 
 }

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

@@ -24,7 +24,8 @@ public enum BpmTaskCandidateStrategyEnum implements ArrayValuable<Integer> {
     MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"),
     POST(22, "岗位"),
     USER(30, "用户"),
-    START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时选择此节点的审批人
+    APPROVE_USER_SELECT(34, "审批人自身"), // 当前审批人,可在审批时,选择下一个节点的审批人
+    START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时,选择此节点的审批人
     START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点,常用于发起人信息审核场景
     START_USER_DEPT_LEADER(37, "发起人部门负责人"),
     START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"),

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

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
 
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+
 /**
  * BPMN XML 常量信息
  *
@@ -66,6 +68,11 @@ public interface BpmnModelConstants {
      */
     String USER_TASK_APPROVE_METHOD = "approveMethod";
 
+    /**
+     * BPMN Child Process 的扩展属性,用于标记多实例来源类型
+     */
+    String CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = "childProcessMultiInstanceSourceType";
+
     /**
      * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
      */
@@ -129,4 +136,11 @@ public interface BpmnModelConstants {
      */
     String REASON_REQUIRE = "reasonRequire";
 
+    /**
+     * 节点类型
+     *
+     * 目前只有 {@link BpmModelTypeEnum#SIMPLE} 的 UserTask 节点会设置该属性,用于区分是审批节点、还是办理节点
+     */
+    String NODE_TYPE = "nodeType";
+
 }

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

@@ -27,8 +27,16 @@ public class BpmnVariableConstants {
      * 流程实例的变量 - 发起用户选择的审批人 Map
      *
      * @see ProcessInstance#getProcessVariables()
+     * @see BpmTaskCandidateStrategyEnum#START_USER_SELECT
      */
     public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
+    /**
+     * 流程实例的变量 - 审批人选择的审批人 Map
+     *
+     * @see ProcessInstance#getProcessVariables()
+     * @see BpmTaskCandidateStrategyEnum#APPROVE_USER_SELECT
+     */
+    public static final String PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES = "PROCESS_APPROVE_USER_SELECT_ASSIGNEES";
     /**
      * 流程实例的变量 - 发起用户 ID
      *
@@ -51,6 +59,13 @@ public class BpmnVariableConstants {
      */
     public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED";
 
+    /**
+     * 流程实例的变量 - 用于判断流程是否需要跳过发起人节点
+     *
+     * @see ProcessInstance#getProcessVariables()
+     */
+    public static final String PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE = "PROCESS_SKIP_START_USER_NODE";
+
     /**
      * 流程实例的变量 - 流程开始时间
      *

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

@@ -22,6 +22,7 @@ import java.util.Set;
 public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener {
 
     public static final Set<FlowableEngineEventType> PROCESS_INSTANCE_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
+            .add(FlowableEngineEventType.PROCESS_CREATED)
             .add(FlowableEngineEventType.PROCESS_COMPLETED)
             .add(FlowableEngineEventType.PROCESS_CANCELLED)
             .build();
@@ -34,6 +35,11 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
         super(PROCESS_INSTANCE_EVENTS);
     }
 
+    @Override
+    protected void processCreated(FlowableEngineEntityEvent event) {
+        processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity());
+    }
+
     @Override
     protected void processCompleted(FlowableEngineEntityEvent event) {
         processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity());

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

@@ -109,7 +109,11 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
             // 2.2 延迟器超时处理
         } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) {
             String taskKey = boundaryEvent.getAttachedToRefId();
-            taskService.processDelayTimerTimeout(event.getProcessInstanceId(), taskKey);
+            taskService.triggerTask(event.getProcessInstanceId(), taskKey);
+            // 2.3 子流程超时处理
+        } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT)) {
+            String taskKey = boundaryEvent.getAttachedToRefId();
+            taskService.processChildProcessTimeout(event.getProcessInstanceId(), taskKey);
         }
     }
 

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

@@ -0,0 +1,159 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR;
+
+/**
+ * 工作流发起 HTTP 请求工具类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class BpmHttpRequestUtils {
+
+    public static void executeBpmHttpRequest(ProcessInstance processInstance,
+                                             String url,
+                                             List<BpmSimpleModelNodeVO.HttpRequestParam> headerParams,
+                                             List<BpmSimpleModelNodeVO.HttpRequestParam> bodyParams,
+                                             Boolean handleResponse,
+                                             List<KeyValue<String, String>> response,
+                                             // TODO @lesan:RestTemplate 直接通过 springUtil 获取好咧;
+                                             RestTemplate restTemplate,
+                                             // TODO @lesan:processInstanceService 直接通过 springUtil 获取好咧;
+                                             BpmProcessInstanceService processInstanceService) {
+
+        // 1.1 设置请求头
+        MultiValueMap<String, String> headers = buildHttpHeaders(processInstance, headerParams);
+        // 1.2 设置请求体
+        MultiValueMap<String, String> body = buildHttpBody(processInstance, bodyParams);
+
+        // 2. 发起请求
+        ResponseEntity<String> responseEntity = sendHttpRequest(url, headers, body, restTemplate);
+
+        // 3. 处理返回
+        // TODO @lesan:可以用 if return,让括号小点
+        if (Boolean.TRUE.equals(handleResponse)) {
+            // 3.1 判断是否需要解析返回值
+            if (responseEntity == null
+                    || StrUtil.isEmpty(responseEntity.getBody())
+                    || !responseEntity.getStatusCode().is2xxSuccessful()
+                    || CollUtil.isEmpty(response)) {
+                return;
+            }
+            // 3.2 解析返回值, 返回值必须符合 CommonResult 规范。
+            CommonResult<Map<String, Object>> respResult = JsonUtils.parseObjectQuietly(responseEntity.getBody(),
+                    new TypeReference<>() {});
+            if (respResult == null || !respResult.isSuccess()) {
+                return;
+            }
+            // 3.3 获取需要更新的流程变量
+            Map<String, Object> updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), response);
+            // 3.4 更新流程变量
+            if (CollUtil.isNotEmpty(updateVariables)) {
+                processInstanceService.updateProcessInstanceVariables(processInstance.getId(), updateVariables);
+            }
+        }
+    }
+
+    public static ResponseEntity<String> sendHttpRequest(String url,
+                                                         MultiValueMap<String, String> headers,
+                                                         MultiValueMap<String, String> body,
+                                                         RestTemplate restTemplate) {
+        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
+        ResponseEntity<String> responseEntity;
+        try {
+            responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
+            log.info("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity);
+        } catch (RestClientException e) {
+            log.error("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage());
+            throw exception(PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR);
+        }
+        return responseEntity;
+    }
+
+    public static MultiValueMap<String, String> buildHttpHeaders(ProcessInstance processInstance,
+                                                                 List<BpmSimpleModelNodeVO.HttpRequestParam> headerSettings) {
+        Map<String, Object> processVariables = processInstance.getProcessVariables();
+        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
+        headers.add(HEADER_TENANT_ID, processInstance.getTenantId());
+        addHttpRequestParam(headers, headerSettings, processVariables);
+        return headers;
+    }
+
+    public static MultiValueMap<String, String> buildHttpBody(ProcessInstance processInstance,
+                                                              List<BpmSimpleModelNodeVO.HttpRequestParam> bodySettings) {
+        Map<String, Object> processVariables = processInstance.getProcessVariables();
+        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
+        addHttpRequestParam(body, bodySettings, processVariables);
+        body.add("processInstanceId", processInstance.getId());
+        return body;
+    }
+
+    /**
+     * 从请求返回值获取需要更新的流程变量
+     *
+     * @param result           请求返回结果
+     * @param responseSettings 返回设置
+     * @return 需要更新的流程变量
+     */
+    public static Map<String, Object> getNeedUpdatedVariablesFromResponse(Map<String, Object> result,
+                                                                          List<KeyValue<String, String>> responseSettings) {
+        Map<String, Object> updateVariables = new HashMap<>();
+        if (CollUtil.isEmpty(result)) {
+            return updateVariables;
+        }
+        responseSettings.forEach(responseSetting -> {
+            if (StrUtil.isNotEmpty(responseSetting.getKey()) && result.containsKey(responseSetting.getValue())) {
+                updateVariables.put(responseSetting.getKey(), result.get(responseSetting.getValue()));
+            }
+        });
+        return updateVariables;
+    }
+
+    /**
+     * 添加 HTTP 请求参数。请求头或者请求体
+     *
+     * @param params           HTTP 请求参数
+     * @param paramSettings    HTTP 请求参数设置
+     * @param processVariables 流程变量
+     */
+    public static void addHttpRequestParam(MultiValueMap<String, String> params,
+                                           List<BpmSimpleModelNodeVO.HttpRequestParam> paramSettings,
+                                           Map<String, Object> processVariables) {
+        if (CollUtil.isEmpty(paramSettings)) {
+            return;
+        }
+        paramSettings.forEach(item -> {
+            if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) {
+                params.add(item.getKey(), item.getValue());
+            } else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) {
+                params.add(item.getKey(), processVariables.get(item.getValue()).toString());
+            }
+        });
+    }
+
+}

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

@@ -158,6 +158,17 @@ public class BpmnModelUtils {
         return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE));
     }
 
+    /**
+     * 解析子流程多实例来源类型
+     *
+     * @see BpmChildProcessMultiInstanceSourceTypeEnum
+     * @param element 任务节点
+     * @return 多实例来源类型
+     */
+    public static Integer parseMultiInstanceSourceType(FlowElement element) {
+        return NumberUtils.parseInt(parseExtensionElement(element, BpmnModelConstants.CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE));
+    }
+
     /**
      * 添加任务拒绝处理元素
      *
@@ -410,6 +421,26 @@ public class BpmnModelUtils {
         return parseExtensionElement(flowElement, TRIGGER_PARAM);
     }
 
+    /**
+     * 给节点添加节点类型
+     *
+     * @param nodeType 节点类型
+     * @param flowElement 节点
+     */
+    public static void addNodeType(Integer nodeType, FlowElement flowElement) {
+        addExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE, nodeType);
+    }
+
+    /**
+     * 解析节点类型
+     *
+     * @param flowElement 节点
+     * @return 节点类型
+     */
+    public static Integer parseNodeType(FlowElement flowElement) {
+        return NumberUtils.parseInt(parseExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE));
+    }
+
     // ========== BPM 简单查找相关的方法 ==========
 
     /**
@@ -777,71 +808,214 @@ public class BpmnModelUtils {
         // 情况:ExclusiveGateway 排它,只有一个满足条件的。如果没有,就走默认的
         if (currentElement instanceof ExclusiveGateway) {
             // 查找满足条件的 SequenceFlow 路径
-            Gateway gateway = (Gateway) currentElement;
-            SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
-                    flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
-                            && evalConditionExpress(variables, flow.getConditionExpression()));
-            if (matchSequenceFlow == null) {
-                matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
-                        flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
-                // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
-                if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) {
-                    matchSequenceFlow = gateway.getOutgoingFlows().get(0);
-                }
-            }
+            SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway((Gateway) currentElement, variables);
             // 遍历满足条件的 SequenceFlow 路径
             if (matchSequenceFlow != null) {
                 simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements);
             }
-            return;
         }
-
         // 情况:InclusiveGateway 包容,多个满足条件的。如果没有,就走默认的
-        if (currentElement instanceof InclusiveGateway) {
+        else if (currentElement instanceof InclusiveGateway) {
             // 查找满足条件的 SequenceFlow 路径
-            Gateway gateway = (Gateway) currentElement;
-            Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
-                    flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
-                            && evalConditionExpress(variables, flow.getConditionExpression()));
-            if (CollUtil.isEmpty(matchSequenceFlows)) {
-                matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
-                        flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
-                // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
-                if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) {
-                    matchSequenceFlows = gateway.getOutgoingFlows();
-                }
-            }
+            Collection<SequenceFlow> matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway((Gateway) currentElement, variables);
             // 遍历满足条件的 SequenceFlow 路径
             matchSequenceFlows.forEach(
                     flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements));
         }
-
         // 情况:ParallelGateway 并行,都满足,都走
-        if (currentElement instanceof ParallelGateway) {
+        else if (currentElement instanceof ParallelGateway) {
             Gateway gateway = (Gateway) currentElement;
             // 遍历子节点
             gateway.getOutgoingFlows().forEach(
                     nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements));
-            return;
         }
     }
 
+    /**
+     * 根据当前节点,获取下一个节点
+     *
+     * @param currentElement 当前节点
+     * @param bpmnModel  BPMN模型
+     * @param variables 流程变量
+     */
+    @SuppressWarnings("PatternVariableCanBeUsed")
+    public static List<FlowNode> getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel,
+                                                  Map<String, Object> variables){
+        List<FlowNode> nextFlowNodes = new ArrayList<>(); // 下一个执行的流程节点集合
+        FlowNode currentNode = (FlowNode) currentElement;  // 当前执行节点的基本属性
+        List<SequenceFlow> outgoingFlows = currentNode.getOutgoingFlows();  // 当前节点的关联节点
+        if (CollUtil.isEmpty(outgoingFlows)) {
+            log.warn("[getNextFlowNodes][当前节点({}) 的 outgoingFlows 为空]", currentNode.getId());
+            return nextFlowNodes;
+        }
+
+        // 遍历每个出口流
+        for (SequenceFlow outgoingFlow : outgoingFlows) {
+            // 获取目标节点的基本属性
+            FlowElement targetElement = bpmnModel.getFlowElement(outgoingFlow.getTargetRef());
+            if (targetElement == null) {
+                continue;
+            }
+            // 如果是结束节点,直接返回
+            if (targetElement instanceof EndEvent) {
+                break;
+            }
+            // 情况一:处理不同类型的网关
+            if (targetElement instanceof Gateway) {
+                Gateway gateway = (Gateway) targetElement;
+                if (gateway instanceof ExclusiveGateway) {
+                    handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
+                } else if (gateway instanceof InclusiveGateway) {
+                    handleInclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
+                } else if (gateway instanceof ParallelGateway) {
+                    handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes);
+                }
+            } else {
+                // 情况二:如果不是网关,直接添加到下一个节点列表
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        }
+        return nextFlowNodes;
+    }
+
+    /**
+     * 处理排它网关
+     *
+     * @param gateway 排他网关
+     * @param bpmnModel BPMN模型
+     * @param variables 流程变量
+     * @param nextFlowNodes 下一个执行的流程节点集合
+     */
+    private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
+                                               Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
+        // 查找满足条件的 SequenceFlow 路径
+        SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway(gateway, variables);
+        // 遍历满足条件的 SequenceFlow 路径
+        if (matchSequenceFlow != null) {
+            FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        }
+    }
+
+    /**
+     * 处理排它网关(Exclusive Gateway),选择符合条件的路径
+     *
+     * @param gateway 排他网关
+     * @param variables 流程变量
+     * @return 符合条件的路径
+     */
+    private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) {
+        // TODO 表单无可编辑字段时variables为空,流程走向会出现问题,比如流程审批过程中无需要修改的字段值,
+        // TODO @小北:是不是还是保证,编辑的时候,如果计算下一个节点,还是 variables 是完整体?而不是空的!!!(可以微信讨论下)
+        SequenceFlow matchSequenceFlow;
+        if (CollUtil.isNotEmpty(variables)) {
+             matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
+                    flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
+                            && (evalConditionExpress(variables, flow.getConditionExpression())));
+        } else {
+            matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
+                    flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()));
+        }
+        if (matchSequenceFlow == null) {
+            matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
+                    flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
+            // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
+            if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) {
+                matchSequenceFlow = gateway.getOutgoingFlows().get(0);
+            }
+        }
+        return matchSequenceFlow;
+    }
+
+    /**
+     * 处理包容网关
+     *
+     * @param gateway 排他网关
+     * @param bpmnModel BPMN模型
+     * @param variables 流程变量
+     * @param nextFlowNodes 下一个执行的流程节点集合
+     */
+    private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
+                                               Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
+        // 查找满足条件的 SequenceFlow 路径集合
+        Collection<SequenceFlow> matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway(gateway, variables);
+        // 遍历满足条件的 SequenceFlow 路径,获取目标节点
+        matchSequenceFlows.forEach(flow -> {
+            FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        });
+    }
+
+    /**
+     * 处理排它网关(Inclusive Gateway),选择符合条件的路径
+     *
+     * @param gateway 排他网关
+     * @param variables 流程变量
+     * @return 符合条件的路径
+     */
+    private static Collection<SequenceFlow> findMatchSequenceFlowsByInclusiveGateway(Gateway gateway, Map<String, Object> variables) {
+        // 查找满足条件的 SequenceFlow 路径
+        Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
+                flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
+                        && evalConditionExpress(variables, flow.getConditionExpression()));
+        if (CollUtil.isEmpty(matchSequenceFlows)) {
+            matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
+                    flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
+            // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的
+            if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) {
+                matchSequenceFlows = gateway.getOutgoingFlows();
+            }
+        }
+        return matchSequenceFlows;
+    }
+
+
+    /**
+     * 处理并行网关
+     *
+     * @param gateway 排他网关
+     * @param bpmnModel BPMN模型
+     * @param variables 流程变量
+     * @param nextFlowNodes 下一个执行的流程节点集合
+     */
+    private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel,
+                                              Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
+        // 并行网关,遍历所有出口路径,获取目标节点
+        gateway.getOutgoingFlows().forEach(flow -> {
+            FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
+            if (targetElement instanceof FlowNode) {
+                nextFlowNodes.add((FlowNode) targetElement);
+            }
+        });
+    }
+
     /**
      * 计算条件表达式是否为 true 满足条件
      *
      * @param variables 流程实例
-     * @param express 条件表达式
+     * @param expression 条件表达式
      * @return 是否满足条件
      */
-    public static boolean evalConditionExpress(Map<String, Object> variables, String express) {
-        if (express == null) {
+    public static boolean evalConditionExpress(Map<String, Object> variables, String expression) {
+        if (expression == null) {
             return Boolean.FALSE;
         }
+        // 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables
+        if (variables == null) {
+            variables = new HashMap<>();
+        }
+
+        // 执行计算
         try {
-            Object result = FlowableUtils.getExpressionValue(variables, express);
+            Object result = FlowableUtils.getExpressionValue(variables, expression);
             return Boolean.TRUE.equals(result);
         } catch (FlowableException ex) {
-            log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", express, variables, ex);
+            // 为什么使用 info 日志?原因是,expression 如果从 variables 取不到值,会报错。实际这种情况下,可以忽略
+            log.info("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", expression, variables, ex);
             return Boolean.FALSE;
         }
     }

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
@@ -24,7 +25,10 @@ import org.flowable.engine.impl.util.CommandContextUtil;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.TaskInfo;
 
-import java.util.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Callable;
 import java.util.stream.Collectors;
 
@@ -190,12 +194,37 @@ public class FlowableUtils {
     @SuppressWarnings("unchecked")
     public static Map<String, List<Long>> getStartUserSelectAssignees(Map<String, Object> processVariables) {
         if (processVariables == null) {
-            return null;
+            return new HashMap<>();
         }
         return (Map<String, List<Long>>) processVariables.get(
                 BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
     }
 
+    /**
+     * 获得流程实例的审批用户选择的下一个节点的审批人 Map
+     *
+     * @param processInstance 流程实例
+     * @return 审批用户选择的下一个节点的审批人Map
+     */
+    public static Map<String, List<Long>> getApproveUserSelectAssignees(ProcessInstance processInstance) {
+        return processInstance != null ? getApproveUserSelectAssignees(processInstance.getProcessVariables()) : null;
+    }
+
+    /**
+     * 获得流程实例的审批用户选择的下一个节点的审批人 Map
+     *
+     * @param processVariables 流程变量
+     * @return 审批用户选择的下一个节点的审批人Map Map
+     */
+    @SuppressWarnings("unchecked")
+    public static Map<String, List<Long>> getApproveUserSelectAssignees(Map<String, Object> processVariables) {
+        if (processVariables == null) {
+            return new HashMap<>();
+        }
+        return (Map<String, List<Long>>) processVariables.get(
+                BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES);
+    }
+
     /**
      * 获得流程实例的摘要
      *
@@ -240,7 +269,7 @@ public class FlowableUtils {
         return formFieldsMap.entrySet().stream()
                 .limit(3)
                 .map(entry -> new KeyValue<>(entry.getValue().getTitle(),
-                        processVariables.getOrDefault(entry.getValue().getField(), "").toString()))
+                        MapUtil.getStr(processVariables, entry.getValue().getField(), "")))
                 .collect(Collectors.toList());
     }
 

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

@@ -5,25 +5,27 @@ 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.framework.common.util.json.JsonUtils;
 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.enums.definition.*;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate;
+import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener;
+import cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener;
 import org.flowable.bpmn.BpmnAutoLayout;
 import org.flowable.bpmn.constants.BpmnXMLConstants;
 import org.flowable.bpmn.model.Process;
 import org.flowable.bpmn.model.*;
+import org.flowable.engine.delegate.ExecutionListener;
 import org.flowable.engine.delegate.TaskListener;
-import org.springframework.util.MultiValueMap;
 
 import java.util.*;
 
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
-import static cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener.DELEGATE_EXPRESSION;
 import static java.util.Arrays.asList;
 
 /**
@@ -40,9 +42,10 @@ public class SimpleModelUtils {
 
     static {
         List<NodeConvert> converts = asList(new StartNodeConvert(), new EndNodeConvert(),
-                new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(),
+                new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(), new TransactorNodeConvert(),
                 new DelayTimerNodeConvert(), new TriggerNodeConvert(),
-                new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert());
+                new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert(),
+                new ChildProcessConvert());
         converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert));
     }
 
@@ -78,7 +81,7 @@ public class SimpleModelUtils {
         traverseNodeToBuildFlowNode(startNode, process);
 
         // 3. 构建并添加节点之间的连线 Sequence Flow
-        EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel);
+        EndEvent endEvent = getEndEvent(bpmnModel);
         traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId());
 
         // 4. 自动布局
@@ -164,8 +167,16 @@ public class SimpleModelUtils {
         // 情况一:有“子”节点,则建立连线
         // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点
         String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId;
-        SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId);
-        process.addFlowElement(sequenceFlow);
+
+        // 如果没有附加节点:则直接建立连线
+        if (StrUtil.isEmpty(node.getAttachNodeId())) {
+            SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId);
+            process.addFlowElement(sequenceFlow);
+        } else {
+            // 如果有附加节点:需要先建立和附加节点的连线,再建立附加节点和目标节点的连线。例如说,触发器节点(HTTP 回调)
+            List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), finalTargetNodeId);
+            sequenceFlows.forEach(process::addFlowElement);
+        }
 
         // 因为有子节点,递归调用后续子节点
         if (isChildNodeValid) {
@@ -173,6 +184,19 @@ public class SimpleModelUtils {
         }
     }
 
+    /**
+     * 构建有附加节点的连线
+     *
+     * @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);
+    }
+
     /**
      * 遍历条件节点,构建 SequenceFlow 元素
      *
@@ -337,7 +361,7 @@ public class SimpleModelUtils {
             userTask.setName(node.getName());
 
             // 人工审批
-            addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType());
+            addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType());
             // 候选人策略为发起人自己
             addCandidateElements(BpmTaskCandidateStrategyEnum.START_USER.getStrategy(), null, userTask);
             // 添加表单字段权限属性元素
@@ -388,24 +412,17 @@ public class SimpleModelUtils {
          */
         private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask,
                                                                 BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) {
-            // 1.1 定时器边界事件
-            BoundaryEvent boundaryEvent = new BoundaryEvent();
-            boundaryEvent.setId("Event-" + IdUtil.fastUUID());
-            boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断
-            boundaryEvent.setAttachedToRef(userTask);
-            // 1.2 定义超时时间、最大提醒次数
-            TimerEventDefinition eventDefinition = new TimerEventDefinition();
-            eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration());
+            // 1. 创建 Timeout Boundary Event
+            String timeCycle = null;
             if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) &&
                     timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) {
-                eventDefinition.setTimeCycle(String.format("R%d/%s",
-                        timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration()));
+                timeCycle = String.format("R%d/%s",
+                        timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration());
             }
-            boundaryEvent.addEventDefinition(eventDefinition);
+            BoundaryEvent boundaryEvent = buildTimeoutBoundaryEvent(userTask, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType(),
+                    timeoutHandler.getTimeDuration(), timeCycle, null);
 
-            // 2.1 添加定时器边界事件类型
-            addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType());
-            // 2.2 添加超时执行动作元素
+            // 2 添加超时执行动作元素
             addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType());
             return boundaryEvent;
         }
@@ -445,6 +462,8 @@ public class SimpleModelUtils {
             addSignEnable(node.getSignEnable(), userTask);
             // 审批意见
             addReasonRequire(node.getReasonRequire(), userTask);
+            // 节点类型
+            addNodeType(node.getType(), userTask);
             return userTask;
         }
 
@@ -455,7 +474,7 @@ public class SimpleModelUtils {
                 FlowableListener flowableListener = new FlowableListener();
                 flowableListener.setEvent(TaskListener.EVENTNAME_CREATE);
                 flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
-                flowableListener.setImplementation(DELEGATE_EXPRESSION);
+                flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION);
                 addListenerConfig(flowableListener, node.getTaskCreateListener());
                 flowableListeners.add(flowableListener);
             }
@@ -464,7 +483,7 @@ public class SimpleModelUtils {
                 FlowableListener flowableListener = new FlowableListener();
                 flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT);
                 flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
-                flowableListener.setImplementation(DELEGATE_EXPRESSION);
+                flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION);
                 addListenerConfig(flowableListener, node.getTaskAssignListener());
                 flowableListeners.add(flowableListener);
             }
@@ -473,7 +492,7 @@ public class SimpleModelUtils {
                 FlowableListener flowableListener = new FlowableListener();
                 flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE);
                 flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
-                flowableListener.setImplementation(DELEGATE_EXPRESSION);
+                flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION);
                 addListenerConfig(flowableListener, node.getTaskCompleteListener());
                 flowableListeners.add(flowableListener);
             }
@@ -486,7 +505,7 @@ public class SimpleModelUtils {
             BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod);
             Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum);
             // 添加审批方式的扩展属性
-            addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD, approveMethod);
+            addExtensionElement(userTask, USER_TASK_APPROVE_METHOD, approveMethod);
             if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) {
                 // 随机审批,不需要设置多实例属性
                 return;
@@ -514,6 +533,15 @@ public class SimpleModelUtils {
 
     }
 
+    private static class TransactorNodeConvert extends ApproveNodeConvert {
+
+        @Override
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE;
+        }
+
+    }
+
     private static class CopyNodeConvert implements NodeConvert {
 
         @Override
@@ -684,20 +712,16 @@ public class SimpleModelUtils {
 
             // 2. 添加接收任务的 Timer Boundary Event
             if (node.getDelaySetting() != null) {
-                // 2.1 定时器边界事件
-                BoundaryEvent boundaryEvent = new BoundaryEvent();
-                boundaryEvent.setId("Event-" + IdUtil.fastUUID());
-                boundaryEvent.setCancelActivity(false);
-                boundaryEvent.setAttachedToRef(receiveTask);
-                // 2.2 定义超时时间
-                TimerEventDefinition eventDefinition = new TimerEventDefinition();
+                BoundaryEvent boundaryEvent = null;
                 if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
-                    eventDefinition.setTimeDuration(node.getDelaySetting().getDelayTime());
+                    boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
+                            node.getDelaySetting().getDelayTime(), null, null);
                 } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
-                    eventDefinition.setTimeDate(node.getDelaySetting().getDelayTime());
+                    boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
+                            null, null, node.getDelaySetting().getDelayTime());
+                } else {
+                    throw new UnsupportedOperationException("不支持的延迟类型:" + node.getDelaySetting());
                 }
-                boundaryEvent.addEventDefinition(eventDefinition);
-                addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType());
                 flowElements.add(boundaryEvent);
             }
             return flowElements;
@@ -712,23 +736,36 @@ public class SimpleModelUtils {
     public static class TriggerNodeConvert implements NodeConvert {
 
         @Override
-        public ServiceTask convert(BpmSimpleModelNodeVO node) {
+        public List<? extends FlowElement> convertList(BpmSimpleModelNodeVO node) {
+            Assert.notNull(node.getTriggerSetting(), "触发器节点设置不能为空");
+            List<FlowElement> flowElements = new ArrayList<>(2);
+            // HTTP 回调请求。需要附加一个 ReceiveTask、发起请求后、等待回调执行
+            if (BpmTriggerTypeEnum.HTTP_CALLBACK.getType().equals(node.getTriggerSetting().getType())) {
+                Assert.notNull(node.getTriggerSetting().getHttpRequestSetting(), "触发器 HTTP 回调请求设置不能为空");
+                ReceiveTask receiveTask = new ReceiveTask();
+                receiveTask.setId("Activity_" + IdUtil.fastUUID());
+                receiveTask.setName("HTTP 回调");
+                node.setAttachNodeId(receiveTask.getId());
+                flowElements.add(receiveTask);
+                // 重要:设置 callbackTaskDefineKey,用于 HTTP 回调
+                node.getTriggerSetting().getHttpRequestSetting().setCallbackTaskDefineKey(receiveTask.getId());
+            }
+
             // 触发器使用 ServiceTask 来实现
             ServiceTask serviceTask = new ServiceTask();
             serviceTask.setId(node.getId());
             serviceTask.setName(node.getName());
             serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
             serviceTask.setImplementation("${" + BpmTriggerTaskDelegate.BEAN_NAME + "}");
-            if (node.getTriggerSetting() != null) {
-                addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType());
-                if (node.getTriggerSetting().getHttpRequestSetting() != null) {
-                    addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting());
-                }
-                if (node.getTriggerSetting().getNormalFormSetting() != null) {
-                    addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getNormalFormSetting());
-                }
+            addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType());
+            if (node.getTriggerSetting().getHttpRequestSetting() != null) {
+                addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting());
             }
-            return serviceTask;
+            if (node.getTriggerSetting().getFormSettings() != null) {
+                addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getFormSettings());
+            }
+            flowElements.add(serviceTask);
+            return flowElements;
         }
 
         @Override
@@ -762,10 +799,131 @@ public class SimpleModelUtils {
 
     }
 
+    private static class ChildProcessConvert implements NodeConvert {
+
+        @Override
+        public List<FlowElement> convertList(BpmSimpleModelNodeVO node) {
+            List<FlowElement> flowElements = new ArrayList<>(2);
+            BpmSimpleModelNodeVO.ChildProcessSetting childProcessSetting = node.getChildProcessSetting();
+            List<IOParameter> inVariables = childProcessSetting.getInVariables() == null ?
+                    new ArrayList<>() : new ArrayList<>(childProcessSetting.getInVariables());
+            CallActivity callActivity = new CallActivity();
+            callActivity.setId(node.getId());
+            callActivity.setName(node.getName());
+            callActivity.setCalledElementType("key");
+            // 1. 是否异步
+            if (node.getChildProcessSetting().getAsync()) {
+                // TODO @lesan: 这里目前测试没有跳过执行call activity 后面的节点
+                callActivity.setAsynchronous(true);
+            }
+
+            // 2. 调用的子流程
+            callActivity.setCalledElement(childProcessSetting.getCalledProcessDefinitionKey());
+            callActivity.setProcessInstanceName(childProcessSetting.getCalledProcessDefinitionName());
+
+            // 3. 是否自动跳过子流程发起节点
+            IOParameter ioParameter = new IOParameter();
+            ioParameter.setSourceExpression(childProcessSetting.getSkipStartUserNode().toString());
+            ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE);
+            inVariables.add(ioParameter);
+
+            // 4. 【默认需要传递的一些变量】流程状态
+            ioParameter = new IOParameter();
+            ioParameter.setSource(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
+            ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
+            inVariables.add(ioParameter);
+
+            // 5. 主→子变量传递、子->主变量传递
+            callActivity.setInParameters(inVariables);
+            if (ArrayUtil.isNotEmpty(childProcessSetting.getOutVariables()) && ObjUtil.notEqual(childProcessSetting.getAsync(), Boolean.TRUE)) {
+                callActivity.setOutParameters(childProcessSetting.getOutVariables());
+            }
+
+            // 6. 子流程发起人配置
+            List<FlowableListener> executionListeners = new ArrayList<>();
+            FlowableListener flowableListener = new FlowableListener();
+            flowableListener.setEvent(ExecutionListener.EVENTNAME_START);
+            flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
+            flowableListener.setImplementation(BpmCallActivityListener.DELEGATE_EXPRESSION);
+            FieldExtension fieldExtension = new FieldExtension();
+            fieldExtension.setFieldName("listenerConfig");
+            fieldExtension.setStringValue(JsonUtils.toJsonString(childProcessSetting.getStartUserSetting()));
+            flowableListener.getFieldExtensions().add(fieldExtension);
+            executionListeners.add(flowableListener);
+            callActivity.setExecutionListeners(executionListeners);
+
+            // 7. 超时设置
+            if (childProcessSetting.getTimeoutSetting() != null && Boolean.TRUE.equals(childProcessSetting.getTimeoutSetting().getEnable())) {
+                BoundaryEvent boundaryEvent = null;
+                if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
+                    boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
+                            childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null);
+                } else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
+                    boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(),
+                            null, null, childProcessSetting.getTimeoutSetting().getTimeExpression());
+                }
+                flowElements.add(boundaryEvent);
+            }
+
+            // 8. 多实例
+            if (childProcessSetting.getMultiInstanceSetting() != null && Boolean.TRUE.equals(childProcessSetting.getMultiInstanceSetting().getEnable())) {
+                MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
+                multiInstanceCharacteristics.setSequential(childProcessSetting.getMultiInstanceSetting().getSequential());
+                if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())) {
+                    multiInstanceCharacteristics.setLoopCardinality(childProcessSetting.getMultiInstanceSetting().getSource());
+                }
+                if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType()) ||
+                        childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
+                    multiInstanceCharacteristics.setInputDataItem(childProcessSetting.getMultiInstanceSetting().getSource());
+                }
+                multiInstanceCharacteristics.setCompletionCondition(String.format(BpmUserTaskApproveMethodEnum.RATIO.getCompletionCondition(),
+                        String.format("%.2f", childProcessSetting.getMultiInstanceSetting().getApproveRatio() / 100D)));
+                callActivity.setLoopCharacteristics(multiInstanceCharacteristics);
+                addExtensionElement(callActivity, CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE, childProcessSetting.getMultiInstanceSetting().getSourceType());
+            }
+
+            // 添加节点类型
+            addNodeType(node.getType(), callActivity);
+            flowElements.add(callActivity);
+            return flowElements;
+        }
+
+        @Override
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.CHILD_PROCESS;
+        }
+
+    }
+
     private static String buildGatewayJoinId(String id) {
         return id + "_join";
     }
 
+    private static BoundaryEvent buildTimeoutBoundaryEvent(Activity attachedToRef, Integer type,
+                                                           String timeDuration, String timeCycle, String timeDate) {
+        // 1.1 定时器边界事件
+        BoundaryEvent boundaryEvent = new BoundaryEvent();
+        boundaryEvent.setId("Event-" + IdUtil.fastUUID());
+        boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断
+        boundaryEvent.setAttachedToRef(attachedToRef);
+        // 1.2 定义超时时间表达式
+        TimerEventDefinition eventDefinition = new TimerEventDefinition();
+        if (ObjUtil.isNotNull(timeDuration)) {
+            eventDefinition.setTimeDuration(timeDuration);
+        }
+        if (ObjUtil.isNotNull(timeDuration)) {
+            eventDefinition.setTimeCycle(timeCycle);
+        }
+        if (ObjUtil.isNotNull(timeDate)) {
+            eventDefinition.setTimeDate(timeDate);
+        }
+        boundaryEvent.addEventDefinition(eventDefinition);
+
+        // 2. 添加定时器边界事件类型
+        addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, type);
+        return boundaryEvent;
+    }
+
     // ========== SIMPLE 流程预测相关的方法 ==========
 
     public static List<BpmSimpleModelNodeVO> simulateProcess(BpmSimpleModelNodeVO rootNode, Map<String, Object> variables) {
@@ -785,11 +943,13 @@ public class SimpleModelUtils {
         BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType());
         Assert.notNull(nodeType, "模型节点类型不支持");
 
-        // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE
+        // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE/TRANSACTOR_NODE
         if (nodeType == BpmSimpleModelNodeTypeEnum.START_NODE
                 || nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE
                 || nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE
                 || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS
                 || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) {
             // 添加元素
             resultNodes.add(currentNode);
@@ -799,8 +959,8 @@ public class SimpleModelUtils {
         if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE) {
             // 查找满足条件的 BpmSimpleModelNodeVO 节点
             BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
-                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
-                            && evalConditionExpress(variables, conditionNode.getConditionSetting()));
+                        conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
+                                && evalConditionExpress(variables, conditionNode.getConditionSetting()));
             if (matchConditionNode == null) {
                 matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
                         conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()));
@@ -814,8 +974,8 @@ public class SimpleModelUtils {
         if (nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) {
             // 查找满足条件的 BpmSimpleModelNodeVO 节点
             Collection<BpmSimpleModelNodeVO> matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
-                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
-                            && evalConditionExpress(variables, conditionNode.getConditionSetting()));
+                        conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
+                                && evalConditionExpress(variables, conditionNode.getConditionSetting()));
             if (CollUtil.isEmpty(matchConditionNodes)) {
                 matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
                         conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()));
@@ -841,27 +1001,4 @@ public class SimpleModelUtils {
         return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting));
     }
 
-    // TODO @芋艿:【高】要不要优化下,抽个 HttpUtils
-
-    /**
-     * 添加 HTTP 请求参数。请求头或者请求体
-     *
-     * @param params           HTTP 请求参数
-     * @param paramSettings    HTTP 请求参数设置
-     * @param processVariables 流程变量
-     */
-    public static void addHttpRequestParam(MultiValueMap<String, String> params,
-                                           List<BpmSimpleModelNodeVO.HttpRequestParam> paramSettings,
-                                           Map<String, Object> processVariables) {
-        if (CollUtil.isEmpty(paramSettings)) {
-            return;
-        }
-        paramSettings.forEach(item -> {
-            if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) {
-                params.add(item.getKey(), item.getValue());
-            } else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) {
-                params.add(item.getKey(), processVariables.get(item.getValue()).toString());
-            }
-        });
-    }
 }

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

@@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
@@ -23,9 +24,7 @@ import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
-import org.flowable.bpmn.model.BpmnModel;
-import org.flowable.bpmn.model.StartEvent;
-import org.flowable.bpmn.model.UserTask;
+import org.flowable.bpmn.model.*;
 import org.flowable.common.engine.impl.db.SuspensionState;
 import org.flowable.engine.HistoryService;
 import org.flowable.engine.RepositoryService;
@@ -41,13 +40,12 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseCandidateStrategy;
 
 /**
  * 流程模型实现:主要进行 Flowable {@link Model} 的维护
@@ -209,11 +207,11 @@ public class BpmModelServiceImpl implements BpmModelService {
     public void deployModel(Long userId, String id) {
         // 1.1 校验流程模型存在
         Model model = validateModelManager(id, userId);
+        BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
         // 1.2 校验流程图
         byte[] bpmnBytes = getModelBpmnXML(model.getId());
-        validateBpmnXml(bpmnBytes);
+        validateBpmnXml(bpmnBytes, metaInfo.getType());
         // 1.3 校验表单已配
-        BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
         BpmFormDO form = validateFormConfig(metaInfo);
         // 1.4 校验任务分配规则已配置
         taskCandidateInvoker.validateBpmnConfig(bpmnBytes);
@@ -233,7 +231,7 @@ public class BpmModelServiceImpl implements BpmModelService {
         repositoryService.saveModel(model);
     }
 
-    private void validateBpmnXml(byte[] bpmnBytes) {
+    private void validateBpmnXml(byte[] bpmnBytes, Integer type) {
         BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes);
         if (bpmnModel == null) {
             throw exception(MODEL_NOT_EXISTS);
@@ -250,6 +248,17 @@ public class BpmModelServiceImpl implements BpmModelService {
                 throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId());
             }
         });
+        // TODO @小北:是不是可以 UserTask firUserTask = CollUtil.get(userTasks, BpmModelTypeEnum.BPMN.getType().equals(type) ? 0 : 1);然后,最好判空。。。极端情况下,没 usertask ,哈哈哈哈。
+        // 3. 校验第一个用户任务节点的规则类型是否为“审批人自选”
+        Map<Integer, UserTask> userTaskMap = new HashMap<>();
+        // BPMN 设计器,校验第一个用户任务节点
+        userTaskMap.put(BpmModelTypeEnum.BPMN.getType(), userTasks.get(0));
+        // SIMPLE 设计器,第一个节点固定为发起人所以校验第二个用户任务节点
+        userTaskMap.put(BpmModelTypeEnum.SIMPLE.getType(), userTasks.get(1));
+        Integer candidateStrategy = parseCandidateStrategy(userTaskMap.get(type));
+        if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) {
+            throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, userTaskMap.get(type).getName());
+        }
     }
 
     @Override

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

@@ -28,8 +28,7 @@ import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.addIfNotNull;
-import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_KEY_NOT_MATCH;
-import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NAME_NOT_MATCH;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
 import static java.util.Collections.emptyList;
 
 /**
@@ -144,9 +143,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
 
         // 插入拓展表
         BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class)
-                .setModelId(model.getId()).setProcessDefinitionId(definition.getId())
+                .setModelId(model.getId()).setCategory(model.getCategory()).setProcessDefinitionId(definition.getId())
                 .setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson);
-
         if (form != null) {
             definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf());
         }
@@ -156,16 +154,25 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
 
     @Override
     public void updateProcessDefinitionState(String id, Integer state) {
+        ProcessDefinition processDefinition = repositoryService.getProcessDefinition(id);
+        if (processDefinition == null) {
+            throw exception(PROCESS_DEFINITION_NOT_EXISTS);
+        }
+
         // 激活
         if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), state)) {
-            repositoryService.activateProcessDefinitionById(id, false, null);
+            if (processDefinition.isSuspended()) {
+                repositoryService.activateProcessDefinitionById(id, false, null);
+            }
             return;
         }
         // 挂起
         if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) {
             // suspendProcessInstances = false,进行中的任务,不进行挂起。
             // 原因:只要新的流程不允许发起即可,老流程继续可以执行。
-            repositoryService.suspendProcessDefinitionById(id, false, null);
+            if (!processDefinition.isSuspended()) {
+                repositoryService.suspendProcessDefinitionById(id, false, null);
+            }
             return;
         }
         log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state);

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

@@ -7,6 +7,7 @@ import jakarta.validation.Valid;
 import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.runtime.ProcessInstance;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -84,7 +85,6 @@ public interface BpmProcessInstanceService {
     PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
                                                                @Valid BpmProcessInstancePageReqVO pageReqVO);
 
-    // TODO @芋艿:重点在 review 下
     /**
      * 获取审批详情。
      * <p>
@@ -96,6 +96,15 @@ public interface BpmProcessInstanceService {
      */
     BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
 
+    /**
+     * 获取下一个执行节点信息
+     *
+     * @param loginUserId 登录人的用户编号
+     * @param reqVO 请求信息
+     * @return 下一个执行节点信息
+     */
+    List<BpmApprovalDetailRespVO.ActivityNode> getNextApprovalNodes(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
+
     /**
      * 获取流程实例的 BPMN 模型视图
      *
@@ -148,6 +157,22 @@ public interface BpmProcessInstanceService {
      */
     void updateProcessInstanceReject(ProcessInstance processInstance, String reason);
 
+    /**
+     * 更新 ProcessInstance 的变量
+     *
+     * @param id 流程编号
+     * @param variables 流程变量
+     */
+    void updateProcessInstanceVariables(String id, Map<String, Object> variables);
+
+    /**
+     * 删除 ProcessInstance 的变量
+     *
+     * @param id  流程编号
+     * @param variableNames 流程变量名
+     */
+    void removeProcessInstanceVariables(String id, Collection<String> variableNames);
+
     // ========== Event 事件相关方法 ==========
 
     /**
@@ -158,11 +183,9 @@ public interface BpmProcessInstanceService {
     void processProcessInstanceCompleted(ProcessInstance instance);
 
     /**
-     * 更新 ProcessInstance 的变量
+     * 处理 ProcessInstance 开始事件,例如说:流程前置通知
      *
-     * @param id 流程编号
-     * @param variables 流程变量
+     * @param instance 流程任务
      */
-    void updateProcessInstanceVariables(String id, Map<String, Object> variables);
-
+    void processProcessInstanceCreated(ProcessInstance instance);
 }

+ 199 - 58
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -13,6 +14,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+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.simple.BpmSimpleModelNodeVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
@@ -28,10 +30,11 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept.BpmTaskCandidateStartUserSelectStrategy;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
 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.SimpleModelUtils;
@@ -54,11 +57,13 @@ import org.flowable.engine.history.HistoricProcessInstanceQuery;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.engine.runtime.ProcessInstanceBuilder;
+import org.flowable.task.api.Task;
 import org.flowable.task.api.history.HistoricTaskInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
+import org.springframework.web.client.RestTemplate;
 
 import java.util.*;
 
@@ -67,6 +72,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
 import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
 import static org.flowable.bpmn.constants.BpmnXMLConstants.*;
@@ -116,6 +122,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     @Resource
     private BpmProcessIdRedisDAO processIdRedisDAO;
 
+    @Resource
+    private RestTemplate restTemplate;
+
     // ========== Query 查询相关方法 ==========
 
     @Override
@@ -144,7 +153,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     }
 
     private Map<String, String> getFormFieldsPermission(BpmnModel bpmnModel,
-            String activityId, String taskId) {
+                                                        String activityId, String taskId) {
         // 1. 获取流程活动编号。流程活动 Id 为空事,从流程任务中获取流程活动 Id
         if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(taskId)) {
             activityId = Optional.ofNullable(taskService.getHistoricTask(taskId))
@@ -164,7 +173,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         Long startUserId = loginUserId; // 流程发起人
         HistoricProcessInstance historicProcessInstance = null; // 流程实例
         Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态
-        Map<String, Object> processVariables = reqVO.getProcessVariables(); // 流程变量
+        Map<String, Object> processVariables = new HashMap<>(); // 流程变量
         // 1.2 如果是流程已发起的场景,则使用流程实例的数据
         if (reqVO.getProcessInstanceId() != null) {
             historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId());
@@ -173,7 +182,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             }
             startUserId = Long.valueOf(historicProcessInstance.getStartUserId());
             processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance);
-            processVariables = historicProcessInstance.getProcessVariables();
+            // 合并 DB 和前端传递的流量变量,以前端的为主
+            if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) {
+                processVariables.putAll(historicProcessInstance.getProcessVariables());
+            }
+        }
+        if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
+            processVariables.putAll(reqVO.getProcessVariables());
         }
         // 1.3 读取其它相关数据
         ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
@@ -205,20 +220,78 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         }
 
         // 3.1 计算当前登录用户的待办任务
-        // TODO @jason:有一个极端情况,如果一个用户有 2 个 task A 和 B,A 已经通过,B 需要审核。这个时,通过 A 进来,todo 拿到
-        // B,会不会表单权限不一致哈。
-        BpmTaskRespVO todoTask = taskService.getFirstTodoTask(loginUserId, reqVO.getProcessInstanceId());
-
+        BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId());
         // 3.2 预测未运行节点的审批信息
         List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel,
                 processDefinitionInfo,
                 processVariables, activities);
+        // 3.3 如果是发起动作,activityId 为开始节点,不校验审批人自选节点
+        if (ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) {
+            simulateActivityNodes.removeIf(node ->
+                    BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy()));
+        }
 
         // 4. 拼接最终数据
         return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
                 processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask);
     }
 
+    @Override
+    public List<ActivityNode> getNextApprovalNodes(Long loginUserId, BpmApprovalDetailReqVO reqVO) {
+        // 1.1 校验任务存在,且是当前用户的
+        Task task = taskService.validateTask(loginUserId, reqVO.getTaskId());
+        // 1.2 校验流程实例存在
+        ProcessInstance instance = getProcessInstance(task.getProcessInstanceId());
+        if (instance == null) {
+            throw exception(PROCESS_INSTANCE_NOT_EXISTS);
+        }
+        HistoricProcessInstance historicProcessInstance = getHistoricProcessInstance(task.getProcessInstanceId());
+        if (historicProcessInstance == null) {
+            throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS);
+        }
+        // 1.3 校验BpmnModel
+        BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(task.getProcessDefinitionId());
+        if (bpmnModel == null) {
+            return null;
+        }
+
+        // 2. 设置流程变量
+        Map<String, Object> processVariables = new HashMap<>();
+        // 2.1 获取历史中流程变量
+        if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) {
+            processVariables.putAll(historicProcessInstance.getProcessVariables());
+        }
+        // 2.2 合并前端传递的流程变量,以前端为准
+        if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
+            processVariables.putAll(reqVO.getProcessVariables());
+        }
+
+        // 3 获取当前任务节点的信息
+        // 3.1 获取下一个将要执行的节点集合
+        FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
+        List<FlowNode> nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables);
+        return convertList(nextFlowNodes, node -> {
+            List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
+                    loginUserId, historicProcessInstance.getProcessDefinitionId(), processVariables);
+            // 3.2 获取节点的审批人信息
+            Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(candidateUserIds);
+            // 3.3 获取节点的审批人部门信息
+            Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+            // 3.4 存在一个节点多人审批的情况,组装审批人信息
+            List<UserSimpleBaseVO> candidateUsers = new ArrayList<>();
+            userMap.forEach((key, value) -> candidateUsers.add(BpmProcessInstanceConvert.INSTANCE.buildUser(key, userMap, deptMap)));
+            return new ActivityNode().setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
+                    .setId(node.getId())
+                    .setName(node.getName())
+                    .setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
+                    .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node))
+                    // TODO @小北:先把 candidateUserIds 设置完,然后最后拼接 candidateUsers 信息。这样,如果有多个节点,就不用重复查询啦;类似 buildApprovalDetail 思路;
+                    // TODO 先拼接处 List ActivityNode
+                    // TODO 接着,再起一段,处理 adminUserApi.getUserMap(candidateUserIds)、deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId))
+                    .setCandidateUsers(candidateUsers);
+        });
+    }
+
     @Override
     @SuppressWarnings("unchecked")
     public PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
@@ -283,15 +356,15 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
      * 主要是,拼接审批人的用户信息、部门信息
      */
     private BpmApprovalDetailRespVO buildApprovalDetail(BpmApprovalDetailReqVO reqVO,
-            BpmnModel bpmnModel,
-            ProcessDefinition processDefinition,
-            BpmProcessDefinitionInfoDO processDefinitionInfo,
-            HistoricProcessInstance processInstance,
-            Integer processInstanceStatus,
-            List<ActivityNode> endApprovalNodeInfos,
-            List<ActivityNode> runningApprovalNodeInfos,
-            List<ActivityNode> simulateApprovalNodeInfos,
-            BpmTaskRespVO todoTask) {
+                                                        BpmnModel bpmnModel,
+                                                        ProcessDefinition processDefinition,
+                                                        BpmProcessDefinitionInfoDO processDefinitionInfo,
+                                                        HistoricProcessInstance processInstance,
+                                                        Integer processInstanceStatus,
+                                                        List<ActivityNode> endApprovalNodeInfos,
+                                                        List<ActivityNode> runningApprovalNodeInfos,
+                                                        List<ActivityNode> simulateApprovalNodeInfos,
+                                                        BpmTaskRespVO todoTask) {
         // 1. 获取所有需要读取用户信息的 userIds
         List<ActivityNode> approveNodes = newArrayList(
                 asList(endApprovalNodeInfos, runningApprovalNodeInfos, simulateApprovalNodeInfos));
@@ -313,19 +386,21 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
      * 获得【已结束】的活动节点们
      */
     private List<ActivityNode> getEndActivityNodeList(Long startUserId, BpmnModel bpmnModel,
-            BpmProcessDefinitionInfoDO processDefinitionInfo,
-            HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus,
-            List<HistoricActivityInstance> activities, List<HistoricTaskInstance> tasks) {
+                                                      BpmProcessDefinitionInfoDO processDefinitionInfo,
+                                                      HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus,
+                                                      List<HistoricActivityInstance> activities, List<HistoricTaskInstance> tasks) {
         // 遍历 tasks 列表,只处理已结束的 UserTask
-        // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities
-        // 的话,它无法成为一个节点
+        // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点
+        // TODO @芋艿:子流程只有activity,这里获取不到已结束的子流程;
+        // TODO @lesan:【子流程】基于 activities 查询出 usertask、callactivity,然后拼接?如果是子流程,就是可以点击过去?
         List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null);
         List<ActivityNode> approvalNodes = convertList(endTasks, task -> {
             FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
             ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName())
                     .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey())
                             ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
-                            : BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
+                            : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别
+                            BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()))
                     .setStatus(FlowableUtils.getTaskStatus(task))
                     .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
                     .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
@@ -381,18 +456,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
      * 获得【进行中】的活动节点们
      */
     private List<ActivityNode> getRunApproveNodeList(Long startUserId,
-            BpmnModel bpmnModel,
-            ProcessDefinition processDefinition,
-            Map<String, Object> processVariables,
-            List<HistoricActivityInstance> activities,
-            List<HistoricTaskInstance> tasks) {
-        // 构建运行中的任务,基于 activityId 分组
+                                                     BpmnModel bpmnModel,
+                                                     ProcessDefinition processDefinition,
+                                                     Map<String, Object> processVariables,
+                                                     List<HistoricActivityInstance> activities,
+                                                     List<HistoricTaskInstance> tasks) {
+        // 构建运行中的任务、子流程,基于 activityId 分组
         List<HistoricActivityInstance> runActivities = filterList(activities, activity -> activity.getEndTime() == null
-                && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER)));
+                && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER, ELEMENT_CALL_ACTIVITY)));
         Map<String, List<HistoricActivityInstance>> runningTaskMap = convertMultiMap(runActivities,
                 HistoricActivityInstance::getActivityId);
 
         // 按照 activityId 分组,构建 ApprovalNodeInfo 节点
+        // TODO @lesan:【子流程】在子流程进行审批的时候,HistoricActivityInstance 里面可以拿到 runActivities.get(0).getCalledProcessInstanceId()。要不要支持跳转???
         Map<String, HistoricTaskInstance> taskMap = convertMap(tasks, HistoricTaskInstance::getId);
         return convertList(runningTaskMap.entrySet(), entry -> {
             String activityId = entry.getKey();
@@ -402,7 +478,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务,会签/或签的任务,开始时间相同
             ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId())
                     .setName(firstActivity.getActivityName())
-                    .setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
+                    .setNodeType(ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”和"子流程"的识别
+                            BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()))
                     .setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
                     .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
                     .setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime()))
@@ -410,6 +487,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             // 处理每个任务的 tasks 属性
             for (HistoricActivityInstance activity : taskActivities) {
                 HistoricTaskInstance task = taskMap.get(activity.getTaskId());
+                // 特殊情况:子流程节点 ChildProcess 仅存在于 activity 中,并且没有自身的 task,需要跳过执行
+                // TODO @芋艿:后续看看怎么优化!
+                if (task == null) {
+                    continue;
+                }
                 activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task));
                 // 加签子任务,需要过滤掉已经完成的加签子任务
                 List<HistoricTaskInstance> childrenTasks = filterList(
@@ -440,9 +522,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
      * 获得【预测(未来)】的活动节点们
      */
     private List<ActivityNode> getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel,
-            BpmProcessDefinitionInfoDO processDefinitionInfo,
-            Map<String, Object> processVariables,
-            List<HistoricActivityInstance> activities) {
+                                                          BpmProcessDefinitionInfoDO processDefinitionInfo,
+                                                          Map<String, Object> processVariables,
+                                                          List<HistoricActivityInstance> activities) {
         // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance
         // 包括了历史的操作,不是只有 startEvent 到当前节点的记录
         Set<String> runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId);
@@ -464,8 +546,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     }
 
     private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel,
-            BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
-            BpmSimpleModelNodeVO node, Set<String> runActivityIds) {
+                                                         BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
+                                                         BpmSimpleModelNodeVO node, Set<String> runActivityIds) {
         // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance
         // 包括了历史的操作,不是只有 startEvent 到当前节点的记录
         if (runActivityIds.contains(node.getId())) {
@@ -479,7 +561,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         // 1. 开始节点/审批节点
         if (ObjectUtils.equalsAny(node.getType(),
                 BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(),
-                BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) {
+                BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType(),
+                BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE.getType())) {
             List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
                     startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables);
             activityNode.setCandidateUserIds(candidateUserIds);
@@ -494,14 +577,22 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         // 3. 抄送节点
         if (CollUtil.isEmpty(runActivityIds) && // 流程发起时:需要展示抄送节点,用于选择抄送人
                 BpmSimpleModelNodeTypeEnum.COPY_NODE.getType().equals(node.getType())) {
+            List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
+                    startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables);
+            activityNode.setCandidateUserIds(candidateUserIds);
+            return activityNode;
+        }
+
+        // 4. 子流程节点
+        if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(node.getType())) {
             return activityNode;
         }
         return null;
     }
 
     private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel,
-            BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
-            FlowElement node, Set<String> runActivityIds) {
+                                                       BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
+                                                       FlowElement node, Set<String> runActivityIds) {
         if (runActivityIds.contains(node.getId())) {
             return null;
         }
@@ -532,7 +623,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     }
 
     private List<Long> getTaskCandidateUserList(BpmnModel bpmnModel, String activityId,
-            Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
+                                                Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
         Set<Long> userIds = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId,
                 startUserId, processDefinitionId, processVariables);
         return new ArrayList<>(userIds);
@@ -568,11 +659,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         Set<String> finishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId,
                 activityInstance -> activityInstance.getEndTime() != null
                         && ObjectUtil.notEqual(activityInstance.getActivityType(),
-                                BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
+                        BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
         Set<String> finishedSequenceFlowActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId,
                 activityInstance -> activityInstance.getEndTime() != null
                         && ObjectUtil.equals(activityInstance.getActivityType(),
-                                BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
+                        BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW));
         // 特殊:会签情况下,会有部分已完成(审批)、部分未完成(待审批),此时需要 finishedTaskActivityIds 移除掉
         finishedTaskActivityIds.removeAll(unfinishedTaskActivityIds);
         // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。
@@ -624,8 +715,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     }
 
     private String createProcessInstance0(Long userId, ProcessDefinition definition,
-            Map<String, Object> variables, String businessKey,
-            Map<String, List<Long>> startUserSelectAssignees) {
+                                          Map<String, Object> variables, String businessKey,
+                                          Map<String, List<Long>> startUserSelectAssignees) {
         // 1.1 校验流程定义
         if (definition == null) {
             throw exception(PROCESS_DEFINITION_NOT_EXISTS);
@@ -643,7 +734,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             throw exception(PROCESS_INSTANCE_START_USER_CAN_START);
         }
         // 1.3 校验发起人自选审批人
-        validateStartUserSelectAssignees(definition, startUserSelectAssignees);
+        validateStartUserSelectAssignees(userId, definition, startUserSelectAssignees, variables);
 
         // 2. 创建流程实例
         if (variables == null) {
@@ -653,10 +744,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量,发起人 ID
         variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中
                 BpmProcessInstanceStatusEnum.RUNNING.getStatus());
-        variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为
-                                                                                             // true,不影响没配置
-                                                                                             // skipExpression 的节点
+        variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true,不影响没配置 skipExpression 的节点
         if (CollUtil.isNotEmpty(startUserSelectAssignees)) {
+            // 设置流程变量,发起人自选审批人
             variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES,
                     startUserSelectAssignees);
         }
@@ -688,17 +778,23 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         return instance.getId();
     }
 
-    private void validateStartUserSelectAssignees(ProcessDefinition definition,
-            Map<String, List<Long>> startUserSelectAssignees) {
-        // 1. 获得发起人自选审批人的 UserTask/ServiceTask 列表
-        BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId());
-        List<Task> tasks = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectTaskList(bpmnModel);
-        if (CollUtil.isEmpty(tasks)) {
+    private void validateStartUserSelectAssignees(Long userId, ProcessDefinition definition,
+                                                  Map<String, List<Long>> startUserSelectAssignees,
+                                                  Map<String, Object> variables) {
+        // 1. 获取预测的节点信息
+        BpmApprovalDetailRespVO detailRespVO = getApprovalDetail(userId, new BpmApprovalDetailReqVO()
+                .setProcessDefinitionId(definition.getId())
+                .setProcessVariables(variables));
+        List<ActivityNode> activityNodes = detailRespVO.getActivityNodes();
+        if (CollUtil.isEmpty(activityNodes)) {
             return;
         }
 
-        // 2. 校验发起人自选审批人的审批人和抄送人是否都配置了
-        tasks.forEach(task -> {
+        // 2.1 移除掉不是发起人自选审批人节点
+        activityNodes.removeIf(task ->
+                ObjectUtil.notEqual(BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy(), task.getCandidateStrategy()));
+        // 2.2 流程发起时要先获取当前流程的预测走向节点,发起时只校验预测的节点发起人自选审批人的审批人和抄送人是否都配置了
+        activityNodes.forEach(task -> {
             List<Long> assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(task.getId()) : null;
             if (CollUtil.isEmpty(assignees)) {
                 throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, task.getName());
@@ -771,6 +867,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
                 BpmReasonEnum.REJECT_TASK.format(reason));
     }
 
+    @Override
+    public void updateProcessInstanceVariables(String id, Map<String, Object> variables) {
+        runtimeService.setVariables(id, variables);
+    }
+
+    @Override
+    public void removeProcessInstanceVariables(String id, Collection<String> variableNames) {
+        runtimeService.removeVariables(id, variableNames);
+    }
+
     // ========== Event 事件相关方法 ==========
 
     @Override
@@ -802,12 +908,47 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             // 3. 发送流程实例的状态事件
             processInstanceEventPublisher.sendProcessInstanceResultEvent(
                     BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status));
+
+            // 4. 流程后置通知
+            if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) {
+                BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.
+                        getProcessDefinitionInfo(instance.getProcessDefinitionId());
+                if (ObjUtil.isNotNull(processDefinitionInfo) &&
+                        ObjUtil.isNotNull(processDefinitionInfo.getPostProcessNotifySetting())) {
+                    BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getPostProcessNotifySetting();
+
+                    BpmHttpRequestUtils.executeBpmHttpRequest(instance,
+                            setting.getUrl(),
+                            setting.getHeader(),
+                            setting.getBody(),
+                            true, setting.getResponse(),
+                            restTemplate,
+                            this);
+                }
+            }
         });
     }
 
     @Override
-    public void updateProcessInstanceVariables(String id, Map<String, Object> variables) {
-        runtimeService.setVariables(id, variables);
+    public void processProcessInstanceCreated(ProcessInstance instance) {
+        // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号
+        FlowableUtils.execute(instance.getTenantId(), () -> {
+            // 流程前置通知
+            BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.
+                    getProcessDefinitionInfo(instance.getProcessDefinitionId());
+            // TODO @lesan:if return 哈。减少括号。
+            if (ObjUtil.isNotNull(processDefinitionInfo) &&
+                    ObjUtil.isNotNull(processDefinitionInfo.getPreProcessNotifySetting())) {
+                BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getPreProcessNotifySetting();
+                BpmHttpRequestUtils.executeBpmHttpRequest(instance,
+                        setting.getUrl(),
+                        setting.getHeader(),
+                        setting.getBody(),
+                        true, setting.getResponse(),
+                        restTemplate,
+                        this);
+            }
+        });
     }
 
 }

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

@@ -35,13 +35,16 @@ public interface BpmTaskService {
     PageResult<Task> getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO);
 
     /**
-     * 获得用户在指定流程下,首个需要处理(待办)的任务
+     * 获得用户(待办)的任务:
+     * 1. 根据 taskId 查询待办任务
+     * 2. 如果任务不存在(或者已审核),获取指定流程下,首个需要处理任务
      *
      * @param userId 用户编号
+     * @param taskId 任务编号
      * @param processInstanceId 流程实例编号
      * @return 待办任务
      */
-    BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId);
+    BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId);
 
     /**
      * 获得已办的流程任务分页
@@ -89,6 +92,14 @@ public interface BpmTaskService {
      */
     List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId, Boolean asc);
 
+    /**
+     * 校验任务是否存在,并且是否是分配给自己的任务
+     *
+     * @param userId 用户 id
+     * @param taskId task id
+     */
+    Task validateTask(Long userId, String taskId);
+
     /**
      * 获取任务
      *
@@ -277,11 +288,22 @@ public interface BpmTaskService {
     void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType);
 
     /**
-     * 处理 延迟器 超时事件
+     * 处理 ChildProcess 子流程的审批超时事件
+     *
+     * @param processInstanceId 流程示例编号
+     * @param taskDefineKey     任务 Key
+     */
+    void processChildProcessTimeout(String processInstanceId, String taskDefineKey);
+
+    /**
+     * 触发流程任务 (ReceiveTask) 的执行
+     * <p>
+     * 1. Simple 模型 HTTP 回调请求触发器节点的回调,触发流程继续执行
+     * 2. Simple 模型延迟器节点,到时触发流程继续执行
      *
      * @param processInstanceId 流程示例编号
      * @param taskDefineKey     任务 Key
      */
-    void processDelayTimerTimeout(String processInstanceId, String taskDefineKey);
+    void triggerTask(String processInstanceId, String taskDefineKey);
 
 }

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

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.bpm.service.task;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.*;
 import cn.hutool.extra.spring.SpringUtil;
@@ -19,6 +20,7 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
 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.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 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;
@@ -40,6 +42,7 @@ import org.flowable.engine.ManagementService;
 import org.flowable.engine.RuntimeService;
 import org.flowable.engine.TaskService;
 import org.flowable.engine.history.HistoricActivityInstance;
+import org.flowable.engine.runtime.ActivityInstance;
 import org.flowable.engine.runtime.Execution;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.DelegationState;
@@ -61,7 +64,9 @@ import java.util.stream.Stream;
 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.module.bpm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
 
 /**
@@ -116,6 +121,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         if (StrUtil.isNotEmpty(pageVO.getCategory())) {
             taskQuery.taskCategory(pageVO.getCategory());
         }
+        if (StrUtil.isNotEmpty(pageVO.getProcessDefinitionKey())) {
+            taskQuery.processDefinitionKey(pageVO.getProcessDefinitionKey());
+        }
         if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
             taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
             taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
@@ -129,32 +137,19 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     }
 
     @Override
-    public BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId) {
-        if (processInstanceId == null) {
-            return null;
-        }
-        // 1. 查询所有任务
-        List<Task> tasks = taskService.createTaskQuery()
-                .active()
-                .processInstanceId(processInstanceId)
-                .includeTaskLocalVariables()
-                .includeProcessVariables()
-                .orderByTaskCreateTime().asc() // 按创建时间升序
-                .list();
-        if (CollUtil.isEmpty(tasks)) {
-            return null;
+    public BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId) {
+        // 1.1 获取指定的用户待办任务
+        Task todoTask = getMyTodoTask(userId, taskId);
+        // 1.2 获取不到,则获取该流程实例下,第一个用户的待办任务
+        if (todoTask == null) {
+            todoTask = getMyFirstTodoTask(userId, processInstanceId);
         }
-
-        // 2.1 查询我的首个任务
-        Task todoTask = CollUtil.findOne(tasks, task -> {
-            return isAssignUserTask(userId, task) // 当前用户为审批人
-                    || isAddSignUserTask(userId, task); // 当前用户为加签人(为了减签)
-        });
         if (todoTask == null) {
             return null;
         }
-        // 2.2 查询该任务的子任务
-        List<Task> childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), tasks);
+
+        // 2. 查询该任务的子任务
+        List<Task> childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), CollUtil.newArrayList(todoTask));
 
         // 3. 转换返回
         BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId());
@@ -162,6 +157,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                 bpmnModel, todoTask.getTaskDefinitionKey());
         Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey());
         Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey());
+        Integer nodeType = parseNodeType(BpmnModelUtils.getFlowElementById(bpmnModel, todoTask.getTaskDefinitionKey()));
 
         // 4. 任务表单
         BpmFormDO taskForm = null;
@@ -170,8 +166,55 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
 
         return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm)
-                .setSignEnable(signEnable)
-                .setReasonRequire(reasonRequire);
+                .setNodeType(nodeType).setSignEnable(signEnable).setReasonRequire(reasonRequire);
+    }
+
+    /**
+     * 获得用户指定 taskId 任务编号的“待办”(未审批、且可审核)的任务
+     *
+     * @param userId 用户编号
+     * @param taskId 任务编号
+     * @return 任务
+     */
+    private Task getMyTodoTask(Long userId, String taskId) {
+        if (StrUtil.isEmpty(taskId)) {
+            return null;
+        }
+        Task task = getTask(taskId);
+        if (task == null) {
+            return null;
+        }
+        if (!isAssignUserTask(userId, task) && !isAddSignUserTask(userId, task)) {
+            return null;
+        }
+        return task;
+    }
+
+    /**
+     * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务
+     *
+     * @param userId 用户编号
+     * @param processInstanceId 流程编号
+     * @return 任务
+     */
+    private Task getMyFirstTodoTask(Long userId, String processInstanceId) {
+        if (processInstanceId == null) {
+            return null;
+        }
+        // 1. 查询所有任务
+        List<Task> tasks = taskService.createTaskQuery()
+                .active()
+                .processInstanceId(processInstanceId)
+                .includeTaskLocalVariables()
+                .includeProcessVariables()
+                .orderByTaskCreateTime().asc() // 按创建时间升序
+                .list();
+
+        // 2. 查询我的首个任务
+        return CollUtil.findOne(tasks, task -> {
+            return isAssignUserTask(userId, task) // 当前用户为审批人
+                    || isAddSignUserTask(userId, task); // 当前用户为加签人(为了减签)
+        });
     }
 
     @Override
@@ -194,6 +237,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             return PageResult.empty();
         }
         List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
+
+        // 特殊:强制移除自动完成的“发起人”节点
+        // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤
+        tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID));
         return new PageResult<>(tasks, count);
     }
 
@@ -243,13 +290,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         return query.list();
     }
 
-    /**
-     * 校验任务是否存在,并且是否是分配给自己的任务
-     *
-     * @param userId 用户 id
-     * @param taskId task id
-     */
-    private Task validateTask(Long userId, String taskId) {
+    @Override
+    public Task validateTask(Long userId, String taskId) {
         Task task = validateTaskExist(taskId);
         // 为什么判断 assignee 非空的情况下?
         // 例如说:在审批人为空时,我们会有“自动审批通过”的策略,此时 userId 为 null,允许通过
@@ -515,21 +557,87 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         // 2.2 添加评论
         taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
                 BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
-        // 2.3 调用 BPM complete 去完成任务
-        // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
-        if (CollUtil.isNotEmpty(reqVO.getVariables())) {
-            Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
-            // 修改表单的值需要存储到 ProcessInstance 变量
-            runtimeService.setVariables(task.getProcessInstanceId(), variables);
-            taskService.complete(task.getId(), variables, true);
-        } else {
-            taskService.complete(task.getId());
-        }
+        // 2.3 校验并处理 APPROVE_USER_SELECT 当前审批人,选择下一节点审批人的逻辑
+        Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(),
+                bpmnModel, reqVO.getNextAssignees(), instance);
+        runtimeService.setVariables(task.getProcessInstanceId(), variables);
+        // 2.4 调用 BPM complete 去完成任务
+        taskService.complete(task.getId(), variables, true);
 
         // 【加签专属】处理加签任务
         handleParentTaskIfSign(task.getParentTaskId());
     }
 
+
+    /**
+     * 校验选择的下一个节点的审批人,是否合法
+     *
+     * 1. 是否有漏选:没有选择审批人
+     * 2. 是否有多选:非下一个节点
+     *
+     * @param taskDefinitionKey 当前任务节点标识
+     * @param variables 流程变量
+     * @param bpmnModel 流程模型
+     * @param nextAssignees 下一个节点审批人集合(参数)
+     * @param processInstance 流程实例
+     */
+    private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
+                                                            Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) {
+        // 1. 获取下一个将要执行的节点集合
+        FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
+        List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
+
+        // 2. 校验选择的下一个节点的审批人,是否合法
+        Map<String, List<Long>> processVariables;
+        for (FlowNode nextFlowNode : nextFlowNodes) {
+            Integer candidateStrategy = parseCandidateStrategy(nextFlowNode);
+            // 2.1 情况一:如果节点中的审批人策略为 发起人自选
+            if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) {
+                // 如果节点存在,但未配置审批人
+                List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null;
+                if (CollUtil.isEmpty(assignees)) {
+                    throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
+                }
+                processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
+                // 特殊:如果当前节点已经存在审批人,则不允许覆盖
+                // TODO @小北:【不用改】通过 if return,让逻辑更简洁一点;虽然会多判断一次 processVariables,但是 if else 层级更少。
+                if (processVariables != null
+                        && CollUtil.isNotEmpty(processVariables.get(nextFlowNode.getId()))) {
+                    continue;
+                }
+                // 设置 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES
+                if (processVariables == null) {
+                    processVariables = new HashMap<>();
+                }
+                processVariables.put(nextFlowNode.getId(), assignees);
+                variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, processVariables);
+            }
+            // 2.2 情况二:如果节点中的审批人策略为 审批人,在审批时选择下一个节点的审批人,并且该节点的审批人为空
+            if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) {
+                // 如果节点存在,但未配置审批人
+                List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null;
+                if (CollUtil.isEmpty(assignees)) {
+                    throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
+                }
+                processVariables = FlowableUtils.getApproveUserSelectAssignees(processInstance.getProcessVariables());
+                if (processVariables == null) {
+                    processVariables = new HashMap<>();
+                } else  {
+                    List<Long> approveUserSelectAssignee = processVariables.get(nextFlowNode.getId());
+                    // 特殊:如果当前节点已经存在审批人,则不允许覆盖
+                    // TODO @小北:这种,应该可以覆盖呢。
+                    if (CollUtil.isNotEmpty(approveUserSelectAssignee)) {
+                        continue;
+                    }
+                }
+                // 设置 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES
+                processVariables.put(nextFlowNode.getId(), assignees);
+                variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, processVariables);
+            }
+        }
+        return variables;
+    }
+
     /**
      * 审批通过存在“后加签”的任务。
      * <p>
@@ -991,6 +1099,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    @SuppressWarnings("DataFlowIssue")
     public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) {
         // 1.1 校验 task 可以被减签
         Task task = validateTaskCanSignDelete(reqVO.getId());
@@ -1207,19 +1316,31 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                     }
                 }
 
-                // 审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
-                if (StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
-                    // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
-                    // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识
-                    Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
-                            String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
+                // 获取发起人节点
+                BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
+                if (bpmnModel == null) {
+                    log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
+                    return;
+                }
+                FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+                // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
+                // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识
+                Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
+                        String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
+                Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
+                        PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
+                if (userTaskElement.getId().equals(START_USER_NODE_ID)
+                        && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
+                        || Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
+                        && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
+                    getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
+                            .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));
+                    return;
+                }
+                // 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
+                if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID)
+                        && StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
                     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);
 
                         // 情况一:自动跳过
@@ -1304,14 +1425,23 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     }
 
     @Override
-    public void processDelayTimerTimeout(String processInstanceId, String taskDefineKey) {
+    @Transactional(rollbackFor = Exception.class)
+    public void processChildProcessTimeout(String processInstanceId, String taskDefineKey) {
+        List<ActivityInstance> activityInstances = runtimeService.createActivityInstanceQuery()
+                .processInstanceId(processInstanceId)
+                .activityId(taskDefineKey).list();
+        activityInstances.forEach(activityInstance -> FlowableUtils.execute(activityInstance.getTenantId(),
+                () -> moveTaskToEnd(activityInstance.getCalledProcessInstanceId(), BpmReasonEnum.TIMEOUT_APPROVE.getReason())));
+    }
+
+    @Override
+    public void triggerTask(String processInstanceId, String taskDefineKey) {
         Execution execution = runtimeService.createExecutionQuery()
                 .processInstanceId(processInstanceId)
                 .activityId(taskDefineKey)
                 .singleResult();
         if (execution == null) {
-            log.error("[processDelayTimerTimeout][processInstanceId({}) activityId({}) 没有找到执行活动]",
-                    processInstanceId, taskDefineKey);
+            log.error("[triggerTask][processInstanceId({}) activityId({}) 没有找到执行活动]", processInstanceId, taskDefineKey);
             return;
         }
 

+ 96 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java

@@ -0,0 +1,96 @@
+package cn.iocoder.yudao.module.bpm.service.task.listener;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import jakarta.annotation.Resource;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.delegate.ExecutionListener;
+import org.flowable.engine.impl.el.FixedValue;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * BPM 子流程监听器:设置流程的发起人
+ *
+ * @author Lesan
+ */
+@Component
+@Slf4j
+public class BpmCallActivityListener implements ExecutionListener {
+
+    public static final String DELEGATE_EXPRESSION = "${bpmCallActivityListener}";
+
+    @Setter
+    private FixedValue listenerConfig;
+
+    @Resource
+    private BpmProcessDefinitionService processDefinitionService;
+
+    @Resource
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public void notify(DelegateExecution execution) {
+        String expressionText = listenerConfig.getExpressionText();
+        Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText);
+        BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting startUserSetting = JsonUtils.parseObject(
+                expressionText, BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting.class);
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getRootProcessInstanceId());
+
+        // 1. 当发起人来源为主流程发起人时,并兜底 startUserSetting 为空时
+        if (startUserSetting == null
+                || startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())) {
+            FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
+            return;
+        }
+
+        // 2. 当发起人来源为表单时
+        if (startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.FROM_FORM.getType())) {
+            String formFieldValue = MapUtil.getStr(processInstance.getProcessVariables(), startUserSetting.getFormField());
+            // 2.1 当表单值为空时
+            if (StrUtil.isEmpty(formFieldValue)) {
+                // 2.1.1 来自主流程发起人
+                if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType())) {
+                    FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
+                    return;
+                }
+                // 2.1.2 来自子流程管理员
+                if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN.getType())) {
+                    BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId());
+                    List<Long> managerUserIds = processDefinition.getManagerUserIds();
+                    FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0));
+                    return;
+                }
+                // 2.1.3 来自主流程管理员
+                if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN.getType())) {
+                    BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processInstance.getProcessDefinitionId());
+                    List<Long> managerUserIds = processDefinition.getManagerUserIds();
+                    FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0));
+                    return;
+                }
+            }
+            // 2.2 使用表单值,并兜底字符串转 Long 失败时使用主流程发起人
+            try {
+                FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue));
+            } catch (Exception e) {
+                log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]",
+                        DELEGATE_EXPRESSION, formFieldValue);
+                FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
+            }
+        }
+    }
+
+}

+ 21 - 49
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java

@@ -1,29 +1,20 @@
 package cn.iocoder.yudao.module.bpm.service.task.listener;
 
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import jakarta.annotation.Resource;
 import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.engine.delegate.TaskListener;
-import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.impl.el.FixedValue;
+import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.service.delegate.DelegateTask;
 import org.springframework.context.annotation.Scope;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
-import java.util.Map;
-
-import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig;
 
 // TODO @芋艿:可能会想换个包地址
@@ -51,46 +42,27 @@ public class BpmUserTaskListener implements TaskListener {
     @Override
     public void notify(DelegateTask delegateTask) {
         // 1. 获取所需基础信息
-        HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(delegateTask.getProcessInstanceId());
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(delegateTask.getProcessInstanceId());
         BpmSimpleModelNodeVO.ListenerHandler listenerHandler = parseListenerConfig(listenerConfig);
 
-        // 2. 获取请求头和请求体
-        Map<String, Object> processVariables = processInstance.getProcessVariables();
-        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
-        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
-        SimpleModelUtils.addHttpRequestParam(headers, listenerHandler.getHeader(), processVariables);
-        SimpleModelUtils.addHttpRequestParam(body, listenerHandler.getBody(), processVariables);
-        // 2.1 请求头默认参数
-        if (StrUtil.isNotEmpty(delegateTask.getTenantId())) {
-            headers.add(HEADER_TENANT_ID, delegateTask.getTenantId());
-        }
-        // 2.2 请求体默认参数
+        // 2. 发起请求
         // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去;
-        body.add("processInstanceId", delegateTask.getProcessInstanceId());
-        body.add("assignee", delegateTask.getAssignee());
-        body.add("taskDefinitionKey", delegateTask.getTaskDefinitionKey());
-        body.add("taskId", delegateTask.getId());
+        listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId")
+                .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getProcessInstanceId()));
+        listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee")
+                .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getAssignee()));
+        listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey")
+                .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getTaskDefinitionKey()));
+        listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId")
+                .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getId()));
+        BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
+                listenerHandler.getPath(),
+                listenerHandler.getHeader(),
+                listenerHandler.getBody(),
+                false, null,
+                restTemplate,
+                processInstanceService);
 
-        // 3. 异步发起请求
-        // TODO @芋艿:确认要同步,还是异步
-        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
-        try {
-            ResponseEntity<String> responseEntity = restTemplate.exchange(listenerHandler.getPath(), HttpMethod.POST,
-                    requestEntity, String.class);
-            log.info("[notify][监听器:{},事件类型:{},请求头:{},请求体:{},响应结果:{}]",
-                    DELEGATE_EXPRESSION,
-                    delegateTask.getEventName(),
-                    headers,
-                    body,
-                    responseEntity);
-        } catch (RestClientException e) {
-            log.error("[error][监听器:{},事件类型:{},请求头:{},请求体:{},请求出错:{}]",
-                    DELEGATE_EXPRESSION,
-                    delegateTask.getEventName(),
-                    headers,
-                    body,
-                    e.getMessage());
-        }
-        // 4. 是否需要后续操作?TODO 芋艿:待定!
+        // 3. 是否需要后续操作?TODO 芋艿:待定!
     }
 }

+ 0 - 124
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java

@@ -1,124 +0,0 @@
-package cn.iocoder.yudao.module.bpm.service.task.trigger;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
-import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
-import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
-import com.fasterxml.jackson.core.type.TypeReference;
-import jakarta.annotation.Resource;
-import lombok.extern.slf4j.Slf4j;
-import org.flowable.engine.runtime.ProcessInstance;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Component;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
-
-/**
- * BPM 发送 HTTP 请求触发器
- *
- * @author jason
- */
-@Component
-@Slf4j
-public class BpmHttpRequestTrigger implements BpmTrigger {
-
-    @Resource
-    private BpmProcessInstanceService processInstanceService;
-
-    @Resource
-    private RestTemplate restTemplate;
-
-    @Override
-    public BpmTriggerTypeEnum getType() {
-        return BpmTriggerTypeEnum.HTTP_REQUEST;
-    }
-
-    @Override
-    public void execute(String processInstanceId, String param) {
-        // 1. 解析 http 请求配置
-        HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class);
-        if (setting == null) {
-            log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId);
-            return;
-        }
-        // 2.1 设置请求头
-        ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
-        Map<String, Object> processVariables = processInstance.getProcessVariables();
-        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
-        headers.add(HEADER_TENANT_ID, processInstance.getTenantId());
-        SimpleModelUtils.addHttpRequestParam(headers, setting.getHeader(), processVariables);
-        // 2.2 设置请求体
-        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
-        SimpleModelUtils.addHttpRequestParam(body, setting.getBody(), processVariables);
-        body.add("processInstanceId", processInstanceId);
-
-        // TODO @芋艿:要不要抽象一个 Http 请求的工具类,方便复用呢?
-        // 3. 发起请求
-        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
-        ResponseEntity<String> responseEntity;
-        try {
-            responseEntity = restTemplate.exchange(setting.getUrl(), HttpMethod.POST,
-                    requestEntity, String.class);
-            log.info("[execute][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity);
-        } catch (RestClientException e) {
-            log.error("[execute][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage());
-            return;
-        }
-
-        // 4.1 判断是否需要解析返回值
-        if (StrUtil.isEmpty(responseEntity.getBody())
-                || !responseEntity.getStatusCode().is2xxSuccessful()
-                || CollUtil.isEmpty(setting.getResponse())) {
-            return;
-        }
-        // 4.2 解析返回值, 返回值必须符合 CommonResult 规范。
-        CommonResult<Map<String, Object>> respResult = JsonUtils.parseObjectQuietly(
-                responseEntity.getBody(), new TypeReference<>() {});
-        if (respResult == null || !respResult.isSuccess()){
-            return;
-        }
-        // 4.3 获取需要更新的流程变量
-        Map<String, Object> updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), setting.getResponse());
-        // 4.4 更新流程变量
-        if (CollUtil.isNotEmpty(updateVariables)) {
-            processInstanceService.updateProcessInstanceVariables(processInstanceId, updateVariables);
-        }
-    }
-
-    /**
-     * 从请求返回值获取需要更新的流程变量
-     *
-     * @param result 请求返回结果
-     * @param responseSettings 返回设置
-     * @return 需要更新的流程变量
-     */
-    private Map<String, Object> getNeedUpdatedVariablesFromResponse(Map<String,Object> result,
-                                                                    List<KeyValue<String, String>> responseSettings) {
-        Map<String, Object> updateVariables = new HashMap<>();
-        if (CollUtil.isEmpty(result)) {
-            return updateVariables;
-        }
-        responseSettings.forEach(responseSetting -> {
-            if (StrUtil.isNotEmpty(responseSetting.getKey()) && result.containsKey(responseSetting.getValue())) {
-                updateVariables.put(responseSetting.getKey(), result.get(responseSetting.getValue()));
-            }
-        });
-        return updateVariables;
-    }
-
-}

+ 0 - 44
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java

@@ -1,44 +0,0 @@
-package cn.iocoder.yudao.module.bpm.service.task.trigger;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.NormalFormTriggerSetting;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
-import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
-import jakarta.annotation.Resource;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-// TODO @jason:改成 BpmFormUpdateTrigger
-/**
- * BPM 更新流程表单触发器
- *
- * @author jason
- */
-@Component
-@Slf4j
-public class BpmUpdateNormalFormTrigger implements BpmTrigger {
-
-    @Resource
-    private BpmProcessInstanceService processInstanceService;
-
-    @Override
-    public BpmTriggerTypeEnum getType() {
-        return BpmTriggerTypeEnum.UPDATE_NORMAL_FORM;
-    }
-
-    @Override
-    public void execute(String processInstanceId, String param) {
-        // 1. 解析更新流程表单配置
-        NormalFormTriggerSetting setting = JsonUtils.parseObject(param, NormalFormTriggerSetting.class);
-        if (setting == null) {
-            log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId);
-            return;
-        }
-        // 2.更新流程变量
-        if (CollUtil.isNotEmpty(setting.getUpdateFormFields())) {
-            processInstanceService.updateProcessInstanceVariables(processInstanceId, setting.getUpdateFormFields());
-        }
-    }
-
-}

+ 73 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java

@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.bpm.service.task.trigger.form;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger;
+import com.fasterxml.jackson.core.type.TypeReference;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * BPM 删除流程表单数据触发器
+ *
+ * @author jason
+ */
+@Component
+@Slf4j
+public class BpmFormDeleteTrigger implements BpmTrigger {
+
+    @Resource
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public BpmTriggerTypeEnum getType() {
+        return BpmTriggerTypeEnum.FORM_DELETE;
+    }
+
+    @Override
+    public void execute(String processInstanceId, String param) {
+        // 1. 解析删除流程表单数据配置
+        List<BpmSimpleModelNodeVO.TriggerSetting.FormTriggerSetting> settings = JsonUtils.parseObject(param, new TypeReference<>() {});
+        if (CollUtil.isEmpty(settings)) {
+            log.error("[execute][流程({}) 删除流程表单数据触发器配置为空]", processInstanceId);
+            return;
+        }
+
+        // 2. 获取流程变量
+        Map<String, Object> processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables();
+
+        // 3.1 获取需要删除的表单字段
+        Set<String> deleteFields = new HashSet<>();
+        settings.forEach(setting -> {
+            if (CollUtil.isEmpty(setting.getDeleteFields())) {
+                return;
+            }
+            // 配置了条件,判断条件是否满足
+            boolean isFieldDeletedNeeded = true;
+            if (setting.getConditionType() != null) {
+                String conditionExpression = SimpleModelUtils.buildConditionExpression(
+                        setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups());
+                isFieldDeletedNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression);
+            }
+            if (isFieldDeletedNeeded) {
+                deleteFields.addAll(setting.getDeleteFields());
+            }
+        });
+
+        // 3.2 删除流程变量
+        if (CollUtil.isNotEmpty(deleteFields)) {
+            processInstanceService.removeProcessInstanceVariables(processInstanceId, deleteFields);
+        }
+    }
+}

+ 66 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.bpm.service.task.trigger.form;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.FormTriggerSetting;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger;
+import com.fasterxml.jackson.core.type.TypeReference;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * BPM 更新流程表单触发器
+ *
+ * @author jason
+ */
+@Component
+@Slf4j
+public class BpmFormUpdateTrigger implements BpmTrigger {
+
+    @Resource
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public BpmTriggerTypeEnum getType() {
+        return BpmTriggerTypeEnum.FORM_UPDATE;
+    }
+
+    @Override
+    public void execute(String processInstanceId, String param) {
+        // 1. 解析更新流程表单配置
+        List<FormTriggerSetting> settings = JsonUtils.parseObject(param, new TypeReference<>() {});
+        if (CollUtil.isEmpty(settings)) {
+            log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId);
+            return;
+        }
+
+        // 2. 获取流程变量
+        Map<String, Object> processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables();
+
+        // 3. 更新流程变量
+        for (FormTriggerSetting setting : settings) {
+            if (CollUtil.isEmpty(setting.getUpdateFormFields())) {
+                continue;
+            }
+            // 配置了条件,判断条件是否满足
+            boolean isFormUpdateNeeded = true;
+            if (setting.getConditionType() != null) {
+                String conditionExpression = SimpleModelUtils.buildConditionExpression(
+                        setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups());
+                isFormUpdateNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression);
+            }
+            // 更新流程表单
+            if (isFormUpdateNeeded) {
+                processInstanceService.updateProcessInstanceVariables(processInstanceId, setting.getUpdateFormFields());
+            }
+        }
+    }
+}

+ 14 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.bpm.service.task.trigger.http;
+
+import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * BPM 发送 HTTP 请求触发器抽象类
+ *
+ * @author jason
+ */
+@Slf4j
+public abstract class BpmAbstractHttpRequestTrigger implements BpmTrigger {
+
+}

+ 59 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.bpm.service.task.trigger.http;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * BPM HTTP 回调触发器
+ *
+ * @author jason
+ */
+@Component
+@Slf4j
+public class BpmHttpCallbackTrigger extends BpmAbstractHttpRequestTrigger {
+
+    @Resource
+    private RestTemplate restTemplate;
+
+    @Resource
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public BpmTriggerTypeEnum getType() {
+        return BpmTriggerTypeEnum.HTTP_CALLBACK;
+    }
+
+    @Override
+    public void execute(String processInstanceId, String param) {
+        // 1. 解析 http 请求配置
+        BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting setting = JsonUtils.parseObject(param,
+                BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting.class);
+        if (setting == null) {
+            log.error("[execute][流程({}) HTTP 回调触发器配置为空]", processInstanceId);
+            return;
+        }
+
+        // 2. 发起请求
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
+        setting.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam()
+                .setKey("taskDefineKey") // 重要:回调请求 taskDefineKey 需要传给被调用方,用于回调执行
+                .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(setting.getCallbackTaskDefineKey()));
+        BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
+                setting.getUrl(),
+                setting.getHeader(),
+                setting.getBody(),
+                false, null,
+                restTemplate,
+                processInstanceService);
+    }
+
+}

+ 54 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.bpm.service.task.trigger.http;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * BPM 发送同步 HTTP 请求触发器
+ *
+ * @author jason
+ */
+@Component
+@Slf4j
+public class BpmSyncHttpRequestTrigger extends BpmAbstractHttpRequestTrigger {
+
+    @Resource
+    private RestTemplate restTemplate;
+
+    @Resource
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public BpmTriggerTypeEnum getType() {
+        return BpmTriggerTypeEnum.HTTP_REQUEST;
+    }
+
+    @Override
+    public void execute(String processInstanceId, String param) {
+        // 1. 解析 http 请求配置
+        HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class);
+        if (setting == null) {
+            log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId);
+            return;
+        }
+
+        // 2. 发起请求
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
+        BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
+                setting.getUrl(),
+                setting.getHeader(),
+                setting.getBody(),
+                true, setting.getResponse(),
+                restTemplate,
+                processInstanceService);
+    }
+
+}

+ 3 - 3
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.infra.service.logger;
 
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
@@ -35,8 +35,8 @@ public class ApiAccessLogServiceImpl implements ApiAccessLogService {
     @Override
     public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) {
         ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class);
-        apiAccessLog.setRequestParams(StrUtil.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
-        apiAccessLog.setResultMsg(StrUtil.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH));
+        apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
+        apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH));
         if (TenantContextHolder.getTenantId() != null) {
             apiAccessLogMapper.insert(apiAccessLog);
         } else {

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.infra.service.logger;
 
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
@@ -39,7 +39,7 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService {
     public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) {
         ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class)
                 .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus());
-        apiErrorLog.setRequestParams(StrUtil.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
+        apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
         if (TenantContextHolder.getTenantId() != null) {
             apiErrorLogMapper.insert(apiErrorLog);
         } else {