Browse Source

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

# Conflicts:
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
YunaiV 6 tháng trước cách đây
mục cha
commit
599c432abb
51 tập tin đã thay đổi với 1618 bổ sung238 xóa
  1. 1 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  2. 3 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  3. 32 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java
  4. 4 3
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java
  5. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java
  6. 31 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmHttpRequestParamTypeEnum.java
  7. 1 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java
  8. 1 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java
  9. 3 3
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java
  10. 9 4
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java
  11. 42 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java
  12. 3 3
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java
  13. 9 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
  14. 24 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java
  15. 73 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
  16. 175 22
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java
  17. 6 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  18. 13 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
  19. 3 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java
  20. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java
  21. 4 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java
  22. 0 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java
  23. 11 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java
  24. 7 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
  25. 12 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java
  26. 32 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
  27. 2 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.java
  28. 2 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java
  29. 55 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java
  30. 15 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java
  31. 32 27
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java
  32. 19 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java
  33. 22 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
  34. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java
  35. 13 9
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java
  36. 55 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java
  37. 64 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  38. 90 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
  39. 266 83
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
  40. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
  41. 48 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  42. 6 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java
  43. 7 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java
  44. 5 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java
  45. 59 25
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  46. 10 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
  47. 96 14
      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/BpmUserTaskListener.java
  49. 75 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java
  50. 30 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java
  51. 1 1
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java

@@ -128,7 +128,7 @@ public class YudaoWebSecurityConfigurerAdapter {
                 // ①:全局共享规则
                 .authorizeHttpRequests(c -> c
                     // 1.1 静态资源,可匿名访问
-                    .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
+                    .requestMatchers(HttpMethod.GET, "/*.html", "/*.css", "/*.js").permitAll()
                     // 1.2 设置 @PermitAll 无需认证
                     .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
                     .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()

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

@@ -38,6 +38,7 @@ public interface ErrorCodeConstants {
     ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "任务({})的候选人未配置");
     ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在");
     ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
+    ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消");
 
     // ========== 流程任务 1-009-005-000 ==========
     ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");
@@ -54,6 +55,8 @@ public interface ErrorCodeConstants {
     ErrorCode TASK_TRANSFER_FAIL_USER_REPEAT = new ErrorCode(1_009_005_013, "任务转办失败,转办人和当前审批人为同一人");
     ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在");
     ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!");
+    ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!");
+    ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!");
 
     // ========== 动态表单模块 1-009-010-000 ==========
     ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在");

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

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+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 BpmAutoApproveTypeEnum implements ArrayValuable<Integer> {
+
+    NONE(0, "不自动通过"),
+    APPROVE_ALL(1, "仅审批一次,后续重复的审批节点均自动通过"),
+    APPROVE_SEQUENT(2, "仅针对连续审批的节点自动通过");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmAutoApproveTypeEnum::getType).toArray(Integer[]::new);
+
+    private final Integer type;
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+}

+ 4 - 3
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventType.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java

@@ -11,14 +11,15 @@ import lombok.Getter;
  */
 @Getter
 @AllArgsConstructor
-public enum BpmBoundaryEventType {
+public enum BpmBoundaryEventTypeEnum {
 
-    USER_TASK_TIMEOUT(1,"用户任务超时");
+    USER_TASK_TIMEOUT(1, "用户任务超时"),
+    DELAY_TIMER_TIMEOUT(2, "延迟器超时");
 
     private final Integer type;
     private final String name;
 
-    public static BpmBoundaryEventType typeOf(Integer type) {
+    public static BpmBoundaryEventTypeEnum typeOf(Integer type) {
         return ArrayUtil.firstMatch(eventType -> eventType.getType().equals(type), values());
     }
 

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

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+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 BpmDelayTimerTypeEnum implements ArrayValuable<Integer> {
+
+    FIXED_TIME_DURATION(1, "固定时长"),
+    FIXED_DATE_TIME(2, "固定日期");
+
+    private final Integer type;
+    private final String name;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmDelayTimerTypeEnum::getType).toArray(Integer[]::new);
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+}

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

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM HTTP 请求参数设置类型。用于 Simple 设计器任务监听器和触发器配置。
+ *
+ * @author Lesan
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmHttpRequestParamTypeEnum implements ArrayValuable<Integer> {
+
+    FIXED_VALUE(1, "固定值"),
+    FROM_FORM(2, "表单");
+
+    private final Integer type;
+    private final String name;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmHttpRequestParamTypeEnum::getType).toArray(Integer[]::new);
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+}

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerType.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java

@@ -10,7 +10,7 @@ import lombok.Getter;
  */
 @Getter
 @AllArgsConstructor
-public enum BpmProcessListenerType {
+public enum BpmProcessListenerTypeEnum {
 
     EXECUTION("execution", "执行监听器"),
     TASK("task", "任务执行器");

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueType.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java

@@ -10,7 +10,7 @@ import lombok.Getter;
  */
 @Getter
 @AllArgsConstructor
-public enum BpmProcessListenerValueType {
+public enum BpmProcessListenerValueTypeEnum {
 
     CLASS("class", "Java 类"),
     DELEGATE_EXPRESSION("delegateExpression", "代理表达式"),

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionType.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java

@@ -14,18 +14,18 @@ import java.util.Arrays;
  */
 @Getter
 @AllArgsConstructor
-public enum BpmSimpleModeConditionType implements ArrayValuable<Integer> {
+public enum BpmSimpleModeConditionTypeEnum implements ArrayValuable<Integer> {
 
     EXPRESSION(1, "条件表达式"),
     RULE(2, "条件规则");
 
-    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModeConditionType::getType).toArray(Integer[]::new);
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModeConditionTypeEnum::getType).toArray(Integer[]::new);
 
     private final Integer type;
 
     private final String name;
 
-    public static BpmSimpleModeConditionType valueOf(Integer type) {
+    public static BpmSimpleModeConditionTypeEnum valueOf(Integer type) {
         return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
     }
 

+ 9 - 4
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java

@@ -15,7 +15,7 @@ import java.util.Objects;
  */
 @Getter
 @AllArgsConstructor
-public enum BpmSimpleModelNodeType implements ArrayValuable<Integer> {
+public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable<Integer> {
 
     // 0 ~ 1 开始和结束
     START_NODE(0, "开始", "startEvent"),
@@ -26,14 +26,18 @@ public enum BpmSimpleModelNodeType implements ArrayValuable<Integer> {
     APPROVE_NODE(11, "审批人", "userTask"),
     COPY_NODE(12, "抄送人", "serviceTask"),
 
+    DELAY_TIMER_NODE(14, "延迟器", "receiveTask"),
+    TRIGGER_NODE(15, "触发器", "serviceTask"),
+
     // 50 ~ 条件分支
     CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式
     CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"),
     PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"),
     INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"),
+    ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway")
     ;
 
-    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModelNodeType::getType).toArray(Integer[]::new);
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModelNodeTypeEnum::getType).toArray(Integer[]::new);
 
     private final Integer type;
     private final String name;
@@ -47,10 +51,11 @@ public enum BpmSimpleModelNodeType implements ArrayValuable<Integer> {
     public static boolean isBranchNode(Integer type) {
         return Objects.equals(CONDITION_BRANCH_NODE.getType(), type)
                 || Objects.equals(PARALLEL_BRANCH_NODE.getType(), type)
-                || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type);
+                || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type)
+                || Objects.equals(ROUTER_BRANCH_NODE.getType(), type);
     }
 
-    public static BpmSimpleModelNodeType valueOf(Integer type) {
+    public static BpmSimpleModelNodeTypeEnum valueOf(Integer type) {
         return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
     }
 

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

@@ -0,0 +1,42 @@
+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 Simple 触发器类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmTriggerTypeEnum implements ArrayValuable<Integer> {
+
+    HTTP_REQUEST(1, "发起 HTTP 请求");
+
+    /**
+     * 触发器执行动作类型
+     */
+    private final Integer type;
+
+    /**
+     * 触发器执行动作描述
+     */
+    private final String desc;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmTriggerTypeEnum::getType).toArray(Integer[]::new);
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+    public static BpmTriggerTypeEnum typeOf(Integer type) {
+        return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
+    }
+
+}

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java

@@ -14,7 +14,7 @@ import java.util.Arrays;
  */
 @Getter
 @AllArgsConstructor
-public enum BpmUserTaskRejectHandlerType implements ArrayValuable<Integer> {
+public enum BpmUserTaskRejectHandlerTypeEnum implements ArrayValuable<Integer> {
 
     FINISH_PROCESS_INSTANCE(1, "终止流程"),
     RETURN_USER_TASK(2, "驳回到指定任务节点");
@@ -22,9 +22,9 @@ public enum BpmUserTaskRejectHandlerType implements ArrayValuable<Integer> {
     private final Integer type;
     private final String name;
 
-    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskRejectHandlerType::getType).toArray(Integer[]::new);
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskRejectHandlerTypeEnum::getType).toArray(Integer[]::new);
 
-    public static BpmUserTaskRejectHandlerType typeOf(Integer type) {
+    public static BpmUserTaskRejectHandlerTypeEnum typeOf(Integer type) {
         return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
     }
 

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

@@ -161,6 +161,15 @@ public class BpmModelController {
         return success(true);
     }
 
+    @DeleteMapping("/clean")
+    @Operation(summary = "清理模型")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('bpm:model:clean')")
+    public CommonResult<Boolean> cleanModel(@RequestParam("id") String id) {
+        modelService.cleanModel(getLoginUserId(), id);
+        return success(true);
+    }
+
     // ========== 仿钉钉/飞书的精简模型 =========
 
     @GetMapping("/simple/get")

+ 24 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form;
+
+import lombok.Data;
+
+/**
+ * 流程表单字段 VO
+ */
+@Data
+public class BpmFormFieldVO {
+
+    /**
+     * 字段类型
+     */
+    private String type;
+    /**
+     * 字段标识
+     */
+    private String field;
+    /**
+     * 字段标题
+     */
+    private String title;
+
+}

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

@@ -1,14 +1,17 @@
 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.enums.definition.BpmAutoApproveTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import org.hibernate.validator.constraints.URL;
 
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
 import java.util.List;
 
 /**
@@ -62,4 +65,72 @@ public class BpmModelMetaInfoVO {
     @Schema(description = "排序", example = "1")
     private Long sort; // 创建时,后端自动生成
 
+    @Schema(description = "允许撤销审批中的申请", example = "true")
+    private Boolean allowCancelRunningProcess;
+
+    @Schema(description = "流程 ID 规则", example = "{}")
+    private ProcessIdRule processIdRule;
+
+    @Schema(description = "自动去重类型", example = "1")
+    @InEnum(BpmAutoApproveTypeEnum.class)
+    private Integer autoApprovalType;
+
+    @Schema(description = "标题设置", example = "{}")
+    private TitleSetting titleSetting;
+
+    @Schema(description = "摘要设置", example = "{}")
+    private SummarySetting summarySetting;
+
+    @Schema(description = "流程 ID 规则")
+    @Data
+    @Valid
+    public static class ProcessIdRule {
+
+        @Schema(description = "是否启用", example = "false")
+        @NotNull(message = "是否启用不能为空")
+        private Boolean enable;
+
+        @Schema(description = "前缀", example = "XX")
+        private String prefix;
+
+        @Schema(description = "中缀", example = "20250120")
+        private String infix; // 精确到日、精确到时、精确到分、精确到秒
+
+        @Schema(description = "后缀", example = "YY")
+        private String postfix;
+
+        @Schema(description = "序列长度", example = "5")
+        @NotNull(message = "序列长度不能为空")
+        private Integer length;
+
+    }
+
+    @Schema(description = "标题设置")
+    @Data
+    @Valid
+    public static class TitleSetting {
+
+        @Schema(description = "是否自定义", example = "false")
+        @NotNull(message = "是否自定义不能为空")
+        private Boolean enable;
+
+        @Schema(description = "标题", example = "流程标题")
+        private String title;
+
+    }
+
+    @Schema(description = "摘要设置")
+    @Data
+    @Valid
+    public static class SummarySetting {
+
+        @Schema(description = "是否自定义", example = "false")
+        @NotNull(message = "是否自定义不能为空")
+        private Boolean enable;
+
+        @Schema(description = "摘要字段数组", example = "[]")
+        private List<String> summary;
+
+    }
+
 }

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

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
+import org.hibernate.validator.constraints.URL;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
@@ -24,7 +25,7 @@ public class BpmSimpleModelNodeVO {
 
     @Schema(description = "模型节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     @NotNull(message = "模型节点类型不能为空")
-    @InEnum(BpmSimpleModelNodeType.class)
+    @InEnum(BpmSimpleModelNodeTypeEnum.class)
     private Integer type;
 
     @Schema(description = "模型节点名称", example = "领导审批")
@@ -36,23 +37,6 @@ public class BpmSimpleModelNodeVO {
     @Schema(description = "子节点")
     private BpmSimpleModelNodeVO childNode; // 补充说明:在该模型下,子节点有且仅有一个,不会有多个
 
-    @Schema(description = "条件节点")
-    private List<BpmSimpleModelNodeVO> conditionNodes; // 补充说明:有且仅有条件、并行、包容等分支会使用
-
-    @Schema(description = "条件类型", example = "1")
-    @InEnum(BpmSimpleModeConditionType.class)
-    private Integer conditionType; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
-
-    @Schema(description = "条件表达式", example = "${day>3}")
-    private String conditionExpression; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
-
-    @Schema(description = "是否默认条件", example = "true")
-    private Boolean defaultFlow; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
-    /**
-     * 条件组
-     */
-    private ConditionGroups conditionGroups; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
-
     @Schema(description = "候选人策略", example = "30")
     @InEnum(BpmTaskCandidateStrategyEnum.class)
     private Integer candidateStrategy; // 用于审批,抄送节点
@@ -77,6 +61,12 @@ public class BpmSimpleModelNodeVO {
     @Schema(description = "操作按钮设置", example = "[]")
     private List<OperationButtonSetting> buttonsSetting;  // 用于审批节点
 
+    @Schema(description = "是否需要签名", example = "false")
+    private Boolean signEnable;
+
+    @Schema(description = "是否填写审批意见", example = "false")
+    private Boolean reasonRequire;
+
     /**
      * 审批节点拒绝处理
      */
@@ -96,12 +86,85 @@ public class BpmSimpleModelNodeVO {
      */
     private AssignEmptyHandler assignEmptyHandler;
 
+    /**
+     * 创建任务监听器
+     */
+    private ListenerHandler taskCreateListener;
+    /**
+     * 指派任务监听器
+     */
+    private ListenerHandler taskAssignListener;
+    /**
+     * 完成任务监听器
+     */
+    private ListenerHandler taskCompleteListener;
+
+    @Schema(description = "延迟器设置", example = "{}")
+    private DelaySetting delaySetting;
+
+    @Schema(description = "条件节点")
+    private List<BpmSimpleModelNodeVO> conditionNodes; // 补充说明:有且仅有条件、并行、包容分支会使用
+
+    /**
+     * 条件节点设置
+     */
+    private ConditionSetting conditionSetting; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
+
+    @Schema(description = "路由分支组", example = "[]")
+    private List<RouterSetting> routerGroups;
+
+    @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成,所以 hidden = true
+    private String routerDefaultFlowId; // 仅用于路由分支节点 BpmSimpleModelNodeType.ROUTER_BRANCH_NODE
+
+    /**
+     * 触发器节点设置
+     */
+    private TriggerSetting triggerSetting;
+
+    @Schema(description = "任务监听器")
+    @Valid
+    @Data
+    public static class ListenerHandler {
+
+        @Schema(description = "是否开启任务监听器", example = "false")
+        @NotNull(message = "是否开启任务监听器不能为空")
+        private Boolean enable;
+
+        @Schema(description = "请求路径", example = "http://xxxxx")
+        private String path;
+
+        @Schema(description = "请求头", example = "[]")
+        private List<HttpRequestParam> header;
+
+        @Schema(description = "请求体", example = "[]")
+        private List<HttpRequestParam> body;
+
+    }
+
+    @Schema(description = "HTTP 请求参数设置")
+    @Data
+    public static class HttpRequestParam {
+
+        @Schema(description = "值类型", example = "1")
+        @InEnum(BpmHttpRequestParamTypeEnum.class)
+        @NotNull(message = "值类型不能为空")
+        private Integer type;
+
+        @Schema(description = "键", example = "xxx")
+        @NotEmpty(message = "键不能为空")
+        private String key;
+
+        @Schema(description = "值", example = "xxx")
+        @NotEmpty(message = "值不能为空")
+        private String value;
+    }
+
     @Schema(description = "审批节点拒绝处理策略")
     @Data
     public static class RejectHandler {
 
         @Schema(description = "拒绝处理类型", example = "1")
-        @InEnum(BpmUserTaskRejectHandlerType.class)
+        @InEnum(BpmUserTaskRejectHandlerTypeEnum.class)
         private Integer type;
 
         @Schema(description = "任务拒绝后驳回的节点 Id", example = "Activity_1")
@@ -128,7 +191,6 @@ public class BpmSimpleModelNodeVO {
 
         @Schema(description = "最大提醒次数", example = "1")
         private Integer maxRemindCount;
-
     }
 
     @Schema(description = "空处理策略")
@@ -143,7 +205,6 @@ public class BpmSimpleModelNodeVO {
 
         @Schema(description = "指定人员审批的用户编号数组", example = "1")
         private List<Long> userIds;
-
     }
 
     @Schema(description = "操作按钮设置")
@@ -162,6 +223,28 @@ public class BpmSimpleModelNodeVO {
         private Boolean enable;
     }
 
+    @Schema(description = "条件设置")
+    @Data
+    @Valid
+    // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
+    public static class ConditionSetting {
+
+        @Schema(description = "条件类型", example = "1")
+        @InEnum(BpmSimpleModeConditionTypeEnum.class)
+        private Integer conditionType;
+
+        @Schema(description = "条件表达式", example = "${day>3}")
+        private String conditionExpression;
+
+        @Schema(description = "是否默认条件", example = "true")
+        private Boolean defaultFlow;
+
+        /**
+         * 条件组
+         */
+        private ConditionGroups conditionGroups;
+    }
+
     @Schema(description = "条件组")
     @Data
     @Valid
@@ -208,5 +291,75 @@ public class BpmSimpleModelNodeVO {
         private String rightSide;
     }
 
-    // TODO @芋艿:条件;建议可以固化的一些选项;然后有个表达式兜底;要支持
+    @Schema(description = "延迟器")
+    @Data
+    @Valid
+    public static class DelaySetting {
+
+        @Schema(description = "延迟时间类型", example = "1")
+        @NotNull(message = "延迟时间类型不能为空")
+        @InEnum(BpmDelayTimerTypeEnum.class)
+        private Integer delayType;
+
+        @Schema(description = "延迟时间表达式", example = "PT1H,2025-01-01T00:00:00")
+        @NotEmpty(message = "延迟时间表达式不能为空")
+        private String delayTime;
+    }
+
+    @Schema(description = "路由分支")
+    @Data
+    @Valid
+    public static class RouterSetting {
+
+        @Schema(description = "节点 Id", example = "Activity_xxx") // 跳转到该节点
+        @NotEmpty(message = "节点 Id 不能为空")
+        private String nodeId;
+
+        @Schema(description = "条件类型", example = "1")
+        @InEnum(BpmSimpleModeConditionTypeEnum.class)
+        @NotNull(message = "条件类型不能为空")
+        private Integer conditionType;
+
+        @Schema(description = "条件表达式", example = "${day>3}")
+        private String conditionExpression;
+
+        @Schema(description = "条件组", example = "{}")
+        private ConditionGroups conditionGroups;
+    }
+
+    @Schema(description = "触发器节点配置")
+    @Data
+    @Valid
+    public static class TriggerSetting {
+
+        @Schema(description = "触发器类型", example = "1")
+        @InEnum(BpmTriggerTypeEnum.class)
+        @NotNull(message = "触发器类型不能为空")
+        private Integer type;
+
+        /**
+         * http 请求触发器设置
+         */
+        @Valid
+        private HttpRequestTriggerSetting httpRequestSetting;
+
+        @Schema(description = "http 请求触发器设置", example = "{}")
+        @Data
+        public static class HttpRequestTriggerSetting {
+
+            @Schema(description = "请求路径", example = "http://127.0.0.1")
+            @NotEmpty(message = "请求 URL 不能为空")
+            @URL(message = "请求 URL 格式不正确")
+            private String url;
+
+            @Schema(description = "请求头参数设置", example = "[]")
+            @Valid
+            private List<HttpRequestParam> header;
+
+            @Schema(description = "请求头参数设置", example = "[]")
+            @Valid
+            private List<HttpRequestParam> body;
+        }
+
+    }
 }

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

@@ -74,8 +74,10 @@ public class BpmProcessInstanceController {
                 convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId));
         Map<String, BpmCategoryDO> categoryMap = categoryService.getCategoryMap(
                 convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory));
+        Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
+                convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId));
         return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult,
-                processDefinitionMap, categoryMap, taskMap, null, null));
+                processDefinitionMap, categoryMap, taskMap, null, null, processDefinitionInfoMap));
     }
 
     @GetMapping("/manager-page")
@@ -101,8 +103,10 @@ public class BpmProcessInstanceController {
                 convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId())));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
                 convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
+                convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId));
         return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult,
-                processDefinitionMap, categoryMap, taskMap, userMap, deptMap));
+                processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap));
     }
 
     @PostMapping("/create")

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

@@ -7,7 +7,9 @@ import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -50,6 +52,8 @@ public class BpmTaskController {
     private BpmProcessInstanceService processInstanceService;
     @Resource
     private BpmFormService formService;
+    @Resource
+    private BpmProcessDefinitionService processDefinitionService;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -70,7 +74,9 @@ public class BpmTaskController {
                 convertSet(pageResult.getList(), Task::getProcessInstanceId));
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                 convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId())));
-        return success(BpmTaskConvert.INSTANCE.buildTodoTaskPage(pageResult, processInstanceMap, userMap));
+        Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
+                convertSet(pageResult.getList(), Task::getProcessDefinitionId));
+        return success(BpmTaskConvert.INSTANCE.buildTodoTaskPage(pageResult, processInstanceMap, userMap, processDefinitionInfoMap));
     }
 
     @GetMapping("done-page")
@@ -87,7 +93,9 @@ public class BpmTaskController {
                 convertSet(pageResult.getList(), HistoricTaskInstance::getProcessInstanceId));
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                 convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId())));
-        return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, null));
+        Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
+                convertSet(pageResult.getList(), HistoricTaskInstance::getProcessDefinitionId));
+        return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, null, processDefinitionInfoMap));
     }
 
     @GetMapping("manager-page")
@@ -108,7 +116,9 @@ public class BpmTaskController {
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
                 convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
-        return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, deptMap));
+        Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
+                convertSet(pageResult.getList(), HistoricTaskInstance::getProcessDefinitionId));
+        return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, deptMap, processDefinitionInfoMap));
     }
 
     @GetMapping("/list-by-process-instance-id")

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

@@ -101,6 +101,9 @@ public class BpmApprovalDetailRespVO {
         @Schema(description = "审批意见", example = "同意")
         private String reason;
 
+        @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
+        private String signPicUrl;
+
     }
 
 }

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
 
+import cn.iocoder.yudao.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 io.swagger.v3.oas.annotations.media.Schema;
@@ -19,6 +20,9 @@ public class BpmProcessInstanceRespVO {
     @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
     private String name;
 
+    @Schema(description = "流程摘要")
+    private List<KeyValue<String, String>> summary; // 只有流程表单,才有摘要!
+
     @Schema(description = "流程分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private String category;
     @Schema(description = "流程分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "请假")

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

@@ -14,10 +14,12 @@ public class BpmTaskApproveReqVO {
     @NotEmpty(message = "任务编号不能为空")
     private String id;
 
-    @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!")
-    @NotEmpty(message = "审批意见不能为空")
+    @Schema(description = "审批意见", example = "不错不错!")
     private String reason;
 
+    @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
+    private String signPicUrl;
+
     @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
     private Map<String, Object> variables;
 

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

@@ -14,7 +14,6 @@ public class BpmTaskRejectReqVO {
     private String id;
 
     @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!")
-    @NotEmpty(message = "审批意见不能为空")
     private String reason;
 
 }

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
 
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
@@ -78,6 +79,16 @@ public class BpmTaskRespVO {
     @Schema(description = "操作按钮设置值")
     private Map<Integer, OperationButtonSetting> buttonsSetting;
 
+    @Schema(description = "是否需要签名", example = "false")
+    private Boolean signEnable;
+
+    @Schema(description = "是否填写审批意见", example = "false")
+    private Boolean reasonRequire;
+
+    // TODO @lesan:要不放到 processInstance 里面?因为摘要是流程实例的,不是流程任务的
+    @Schema(description = "流程摘要", example = "[]")
+    private List<KeyValue<String, String>> summary; // 只有流程表单,才有摘要!
+
     @Data
     @Schema(description = "流程实例")
     public static class ProcessInstance {

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

@@ -58,7 +58,8 @@ public interface BpmProcessInstanceConvert {
                                                                           Map<String, BpmCategoryDO> categoryMap,
                                                                           Map<String, List<Task>> taskMap,
                                                                           Map<Long, AdminUserRespDTO> userMap,
-                                                                          Map<Long, DeptRespDTO> deptMap) {
+                                                                          Map<Long, DeptRespDTO> deptMap,
+                                                                          Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap) {
         PageResult<BpmProcessInstanceRespVO> vpPageResult = BeanUtils.toBean(pageResult, BpmProcessInstanceRespVO.class);
         for (int i = 0; i < pageResult.getList().size(); i++) {
             BpmProcessInstanceRespVO respVO = vpPageResult.getList().get(i);
@@ -76,6 +77,9 @@ public interface BpmProcessInstanceConvert {
                     MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName()));
                 }
             }
+            // 摘要
+            respVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(respVO.getProcessDefinitionId()),
+                    pageResult.getList().get(i).getProcessVariables()));
         }
         return vpPageResult;
     }
@@ -186,7 +190,8 @@ public interface BpmProcessInstanceConvert {
             return null;
         }
         return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class)
-                .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task));
+                .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
+                .setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task));
     }
 
     default Set<Long> parseUserIds(HistoricProcessInstance processInstance,

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

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.convert.task;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
@@ -9,6 +10,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
@@ -41,7 +43,8 @@ public interface BpmTaskConvert {
 
     default PageResult<BpmTaskRespVO> buildTodoTaskPage(PageResult<Task> pageResult,
                                                         Map<String, ProcessInstance> processInstanceMap,
-                                                        Map<Long, AdminUserRespDTO> userMap) {
+                                                        Map<Long, AdminUserRespDTO> userMap,
+                                                        Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap) {
         return BeanUtils.toBean(pageResult, BpmTaskRespVO.class, taskVO -> {
             ProcessInstance processInstance = processInstanceMap.get(taskVO.getProcessInstanceId());
             if (processInstance == null) {
@@ -50,13 +53,17 @@ public interface BpmTaskConvert {
             taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
             AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
             taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
+            // 摘要
+            taskVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(processInstance.getProcessDefinitionId()),
+                    processInstance.getProcessVariables()));
         });
     }
 
     default PageResult<BpmTaskRespVO> buildTaskPage(PageResult<HistoricTaskInstance> pageResult,
                                                     Map<String, HistoricProcessInstance> processInstanceMap,
                                                     Map<Long, AdminUserRespDTO> userMap,
-                                                    Map<Long, DeptRespDTO> deptMap) {
+                                                    Map<Long, DeptRespDTO> deptMap,
+                                                    Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap) {
         List<BpmTaskRespVO> taskVOList = CollectionUtils.convertList(pageResult.getList(), task -> {
             BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class);
             taskVO.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task));
@@ -72,6 +79,9 @@ public interface BpmTaskConvert {
                 AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
                 taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
                 taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
+                // 摘要
+                taskVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(processInstance.getProcessDefinitionId()),
+                        processInstance.getProcessVariables()));
             }
             return taskVO;
         });

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

@@ -3,6 +3,8 @@ 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;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -150,4 +152,34 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
     @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
     private List<Long> managerUserIds;
 
+    /**
+     * 是否允许撤销审批中的申请
+     */
+    private Boolean allowCancelRunningProcess;
+
+    /**
+     * 流程 ID 规则
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private BpmModelMetaInfoVO.ProcessIdRule processIdRule;
+
+    /**
+     * 自动去重类型
+     *
+     * 枚举 {@link BpmAutoApproveTypeEnum}
+     */
+    private Integer autoApprovalType;
+
+    /**
+     * 标题设置
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private BpmModelMetaInfoVO.TitleSetting titleSetting;
+
+    /**
+     * 摘要设置
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private BpmModelMetaInfoVO.SummarySetting summarySetting;
+
 }

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

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -42,7 +43,7 @@ public class BpmProcessListenerDO extends BaseDO {
     /**
      * 监听类型
      *
-     * 枚举 {@link cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerType}
+     * 枚举 {@link BpmProcessListenerTypeEnum}
      *
      * 1. execution:ExecutionListener <a href="https://tkjohn.github.io/flowable-userguide/#executionListeners">执行监听器</a>
      * 2. task:TaskListener <a href="https://tkjohn.github.io/flowable-userguide/#taskListeners">任务监听器</a>

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

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

+ 55 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.bpm.dal.redis;
+
+import cn.hutool.core.date.DateUtil;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * BPM 流程 Id 编码的 Redis DAO
+ *
+ * @author Lesan
+ */
+@Repository
+public class BpmProcessIdRedisDAO {
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 生成序号,使用定义的 processIdRule 规则生成
+     *
+     * @param processIdRule 规则
+     * @return 序号
+     */
+    public String generate(BpmModelMetaInfoVO.ProcessIdRule processIdRule) {
+        // 生成日期前缀
+        String infix = "";
+        switch (processIdRule.getInfix()) {
+            case "DAY":
+                infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDD");
+                break;
+            case "HOUR":
+                infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHH");
+                break;
+            case "MINUTE":
+                infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHHmm");
+                break;
+            case "SECOND":
+                infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHHmmss");
+                break;
+        }
+
+        // 生成序号
+        String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix();
+        String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix;
+        Long no = stringRedisTemplate.opsForValue().increment(key);
+        stringRedisTemplate.expire(key, Duration.ofDays(1L));
+        return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no);
+    }
+
+}

+ 15 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.bpm.dal.redis;
+
+/**
+ * BPM Redis Key 枚举类
+ *
+ * @author 芋道源码
+ */
+public interface RedisKeyConstants {
+
+    /**
+     * 流程 ID 的缓存
+     */
+    String BPM_PROCESS_ID = "bpm:process_id:";
+
+}

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

@@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -91,35 +92,39 @@ public class BpmTaskCandidateInvoker {
      */
     @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人
     public Set<Long> calculateUsersByTask(DelegateExecution execution) {
-        // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过
-        FlowElement flowElement = execution.getCurrentFlowElement();
-        Integer approveType = BpmnModelUtils.parseApproveType(flowElement);
-        if (ObjectUtils.equalsAny(approveType,
-                BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
-                BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
-            return new HashSet<>();
-        }
-
-        // 1.1 计算任务的候选人
-        Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement);
-        String param = BpmnModelUtils.parseCandidateParam(flowElement);
-        Set<Long> userIds = getCandidateStrategy(strategy).calculateUsersByTask(execution, param);
-        // 1.2 移除被禁用的用户
-        removeDisableUsers(userIds);
+        // 注意:解决极端情况下,Flowable 异步调用,导致租户 id 丢失的情况
+        // 例如说,SIMPLE 延迟器在 trigger 的时候!!!
+        return FlowableUtils.execute(execution.getTenantId(), () -> {
+            // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过
+            FlowElement flowElement = execution.getCurrentFlowElement();
+            Integer approveType = BpmnModelUtils.parseApproveType(flowElement);
+            if (ObjectUtils.equalsAny(approveType,
+                    BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
+                    BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
+                return new HashSet<>();
+            }
 
-        // 2. 候选人为空时,根据“审批人为空”的配置补充
-        if (CollUtil.isEmpty(userIds)) {
-            userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
-                    .calculateUsersByTask(execution, param);
-            // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!!
-        }
+            // 1.1 计算任务的候选人
+            Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement);
+            String param = BpmnModelUtils.parseCandidateParam(flowElement);
+            Set<Long> userIds = getCandidateStrategy(strategy).calculateUsersByTask(execution, param);
+            // 1.2 移除被禁用的用户
+            removeDisableUsers(userIds);
+
+            // 2. 候选人为空时,根据“审批人为空”的配置补充
+            if (CollUtil.isEmpty(userIds)) {
+                userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
+                        .calculateUsersByTask(execution, param);
+                // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!!
+            }
 
-        // 3. 移除发起人的用户
-        ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class)
-                .getProcessInstance(execution.getProcessInstanceId());
-        Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId());
-        removeStartUserIfSkip(userIds, flowElement, Long.valueOf(processInstance.getStartUserId()));
-        return userIds;
+            // 3. 移除发起人的用户
+            ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class)
+                    .getProcessInstance(execution.getProcessInstanceId());
+            Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId());
+            removeStartUserIfSkip(userIds, flowElement, Long.valueOf(processInstance.getStartUserId()));
+            return userIds;
+        });
     }
 
     public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,

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

@@ -100,6 +100,15 @@ public interface BpmnModelConstants {
      */
     String BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE = "enable";
 
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记触发器的类型
+     */
+    String TRIGGER_TYPE = "triggerType";
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记触发器参数
+     */
+    String TRIGGER_PARAM = "triggerParam";
+
     /**
      * BPMN Start Event Node Id
      */
@@ -110,4 +119,14 @@ public interface BpmnModelConstants {
      */
     String START_USER_NODE_ID = "StartUserNode";
 
+    /**
+     * 是否需要签名
+     */
+    String SIGN_ENABLE = "signEnable";
+
+    /**
+     * 审批意见是否必填
+     */
+    String REASON_REQUIRE = "reasonRequire";
+
 }

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

@@ -43,6 +43,24 @@ public class BpmnVariableConstants {
      * @see ProcessInstance#getProcessVariables()
      */
     public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
+    /**
+     * 流程实例的变量 - 是否跳过表达式
+     *
+     * @see ProcessInstance#getProcessVariables()
+     * @see <a href="https://blog.csdn.net/weixin_42065235/article/details/126039993">Flowable/Activiti之SkipExpression 完成自动审批</a>
+     */
+    public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED";
+
+    /**
+     * 流程实例的变量 - 流程开始时间
+     *
+     * 【非存储变量】用于部分需要 format 的场景,例如说:流程实例的自定义标题
+     */
+    public static final String PROCESS_START_TIME = "PROCESS_START_TIME";
+    /**
+     * 流程实例的变量 - 流程定义名称
+     */
+    public static final String PROCESS_DEFINITION_NAME = "PROCESS_DEFINITION_NAME";
 
     /**
      * 任务的变量 - 状态
@@ -58,5 +76,9 @@ public class BpmnVariableConstants {
      * @see org.flowable.task.api.Task#getTaskLocalVariables()
      */
     public static final String TASK_VARIABLE_REASON = "TASK_REASON";
+    /**
+     * 任务变量 - 签名图片 URL
+     */
+    public static final String TASK_SIGN_PIC_URL = "TASK_SIGN_PIC_URL";
 
 }

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

@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableSet;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
 import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
 import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
+import org.flowable.engine.delegate.event.FlowableCancelledEvent;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
@@ -22,6 +23,7 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
 
     public static final Set<FlowableEngineEventType> PROCESS_INSTANCE_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
             .add(FlowableEngineEventType.PROCESS_COMPLETED)
+            .add(FlowableEngineEventType.PROCESS_CANCELLED)
             .build();
 
     @Resource
@@ -37,4 +39,10 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
         processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity());
     }
 
+    @Override // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法
+    protected void processCancelled(FlowableCancelledEvent event) {
+        ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId());
+        processInstanceService.processProcessInstanceCompleted(processInstance);
+    }
+
 }

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

@@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
@@ -97,16 +97,20 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
         BoundaryEvent boundaryEvent = (BoundaryEvent) element;
         String boundaryEventType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent,
                 BpmnModelConstants.BOUNDARY_EVENT_TYPE);
-        BpmBoundaryEventType bpmTimerBoundaryEventType = BpmBoundaryEventType.typeOf(NumberUtils.parseInt(boundaryEventType));
-        if (ObjectUtil.notEqual(bpmTimerBoundaryEventType, BpmBoundaryEventType.USER_TASK_TIMEOUT)) {
-            return;
-        }
+        BpmBoundaryEventTypeEnum bpmTimerBoundaryEventType = BpmBoundaryEventTypeEnum.typeOf(NumberUtils.parseInt(boundaryEventType));
 
         // 2. 处理超时
-        String timeoutHandlerType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent,
-                BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE);
-        String taskKey = boundaryEvent.getAttachedToRefId();
-        taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutHandlerType));
+        if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT)) {
+            // 2.1 用户任务超时处理
+            String timeoutHandlerType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent,
+                    BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE);
+            String taskKey = boundaryEvent.getAttachedToRefId();
+            taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutHandlerType));
+            // 2.2 延迟器超时处理
+        } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) {
+            String taskKey = boundaryEvent.getAttachedToRefId();
+            taskService.processDelayTimerTimeout(event.getProcessInstanceId(), taskKey);
+        }
     }
 
 }

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

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
+
+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.service.task.trigger.BpmTrigger;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.delegate.JavaDelegate;
+import org.springframework.stereotype.Component;
+
+import java.util.EnumMap;
+import java.util.List;
+
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate.BEAN_NAME;
+
+
+/**
+ * 处理触发器任务 {@link JavaDelegate} 的实现类
+ * <p>
+ * 目前只有 Simple 设计器【触发器节点】使用
+ *
+ * @author jason
+ */
+@Component(BEAN_NAME)
+@Slf4j
+public class BpmTriggerTaskDelegate implements JavaDelegate {
+
+    public static final String BEAN_NAME = "bpmTriggerTaskDelegate";
+
+    @Resource
+    private List<BpmTrigger> triggers;
+
+    private final EnumMap<BpmTriggerTypeEnum, BpmTrigger> triggerMap = new EnumMap<>(BpmTriggerTypeEnum.class);
+
+    @PostConstruct
+    private void init() {
+        triggers.forEach(trigger -> triggerMap.put(trigger.getType(), trigger));
+    }
+
+    @Override
+    public void execute(DelegateExecution execution) {
+        FlowElement flowElement = execution.getCurrentFlowElement();
+        BpmTriggerTypeEnum bpmTriggerType = BpmnModelUtils.parserTriggerType(flowElement);
+        BpmTrigger bpmTrigger = triggerMap.get(bpmTriggerType);
+        if (bpmTrigger == null) {
+            log.error("[execute][FlowElement({}), {} 找不到匹配的触发器]", execution.getCurrentActivityId(), flowElement);
+            return;
+        }
+        bpmTrigger.execute(execution.getProcessInstanceId(), BpmnModelUtils.parserTriggerParam(flowElement));
+    }
+
+}

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

@@ -1,19 +1,19 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType;
+import cn.iocoder.yudao.module.bpm.enums.definition.*;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import com.google.common.collect.Maps;
 import lombok.extern.slf4j.Slf4j;
@@ -22,6 +22,7 @@ import org.flowable.bpmn.model.Process;
 import org.flowable.bpmn.model.*;
 import org.flowable.common.engine.api.FlowableException;
 import org.flowable.common.engine.impl.util.io.BytesStreamSource;
+import org.flowable.engine.impl.el.FixedValue;
 
 import java.util.*;
 
@@ -170,9 +171,9 @@ public class BpmnModelUtils {
      * @param userTask 任务节点
      * @return 任务拒绝处理类型
      */
-    public static BpmUserTaskRejectHandlerType parseRejectHandlerType(FlowElement userTask) {
+    public static BpmUserTaskRejectHandlerTypeEnum parseRejectHandlerType(FlowElement userTask) {
         Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE));
-        return BpmUserTaskRejectHandlerType.typeOf(rejectHandlerType);
+        return BpmUserTaskRejectHandlerTypeEnum.typeOf(rejectHandlerType);
     }
 
     /**
@@ -346,6 +347,62 @@ public class BpmnModelUtils {
         return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null);
     }
 
+    public static void addSignEnable(Boolean signEnable, FlowElement userTask) {
+        addExtensionElement(userTask, SIGN_ENABLE,
+                ObjUtil.isNotNull(signEnable) ? signEnable.toString() : Boolean.FALSE.toString());
+    }
+
+    public static Boolean parseSignEnable(BpmnModel bpmnModel, String flowElementId) {
+        FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
+        if (flowElement == null) {
+            return false;
+        }
+        List<ExtensionElement> extensionElements = flowElement.getExtensionElements().get(SIGN_ENABLE);
+        if (CollUtil.isEmpty(extensionElements)) {
+            return false;
+        }
+        return Convert.toBool(extensionElements.get(0).getElementText(), false);
+    }
+
+    public static void addReasonRequire(Boolean reasonRequire, FlowElement userTask) {
+        addExtensionElement(userTask, REASON_REQUIRE,
+                ObjUtil.isNotNull(reasonRequire) ? reasonRequire.toString() : Boolean.FALSE.toString());
+    }
+
+    public static Boolean parseReasonRequire(BpmnModel bpmnModel, String flowElementId) {
+        FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
+        if (flowElement == null) {
+            return false;
+        }
+        List<ExtensionElement> extensionElements = flowElement.getExtensionElements().get(REASON_REQUIRE);
+        if (CollUtil.isEmpty(extensionElements)) {
+            return false;
+        }
+        return Convert.toBool(extensionElements.get(0).getElementText(), false);
+    }
+
+    public static void addListenerConfig(FlowableListener flowableListener, BpmSimpleModelNodeVO.ListenerHandler handler) {
+        FieldExtension fieldExtension = new FieldExtension();
+        fieldExtension.setFieldName("listenerConfig");
+        fieldExtension.setStringValue(JsonUtils.toJsonString(handler));
+        flowableListener.getFieldExtensions().add(fieldExtension);
+    }
+
+    public static BpmSimpleModelNodeVO.ListenerHandler parseListenerConfig(FixedValue fixedValue) {
+        String expressionText = fixedValue.getExpressionText();
+        Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText);
+        return JsonUtils.parseObject(expressionText, BpmSimpleModelNodeVO.ListenerHandler.class);
+    }
+
+    public static BpmTriggerTypeEnum parserTriggerType(FlowElement flowElement) {
+        Integer triggerType = NumberUtils.parseInt(parseExtensionElement(flowElement, TRIGGER_TYPE));
+        return BpmTriggerTypeEnum.typeOf(triggerType);
+    }
+
+    public static String parserTriggerParam(FlowElement flowElement) {
+        return parseExtensionElement(flowElement, TRIGGER_PARAM);
+    }
+
     // ========== BPM 简单查找相关的方法 ==========
 
     /**
@@ -777,7 +834,7 @@ public class BpmnModelUtils {
             Object result = FlowableUtils.getExpressionValue(variables, express);
             return Boolean.TRUE.equals(result);
         } catch (FlowableException ex) {
-            log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错", express, variables, ex);
+            log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", express, variables, ex);
             return Boolean.FALSE;
         }
     }

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

@@ -2,9 +2,15 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormFieldVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
+import lombok.SneakyThrows;
 import org.flowable.common.engine.api.delegate.Expression;
 import org.flowable.common.engine.api.variable.VariableContainer;
 import org.flowable.common.engine.impl.el.ExpressionManager;
@@ -18,10 +24,7 @@ import org.flowable.engine.impl.util.CommandContextUtil;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.TaskInfo;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 import java.util.concurrent.Callable;
 
 /**
@@ -67,6 +70,17 @@ public class FlowableUtils {
         }
     }
 
+    @SneakyThrows
+    public static <V> V execute(String tenantIdStr, Callable<V> callable) {
+        if (ObjectUtil.isEmpty(tenantIdStr)
+                || Objects.equals(tenantIdStr, ProcessEngineConfiguration.NO_TENANT_ID)) {
+            return callable.call();
+        } else {
+            Long tenantId = Long.valueOf(tenantIdStr);
+            return TenantUtils.execute(tenantId, callable);
+        }
+    }
+
     // ========== Execution 相关的工具方法 ==========
 
     /**
@@ -179,6 +193,68 @@ public class FlowableUtils {
                 BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
     }
 
+    // TODO @lesan:如果值是 null 的情况,可能要调研下飞书、钉钉,是不是不返回哈!
+    /**
+     * 获得流程实例的摘要
+     *
+     * 仅有 {@link BpmModelFormTypeEnum#getType()} 表单,才有摘要。
+     * 原因是,只有它才有表单项的配置,从而可以根据配置,展示摘要。
+     *
+     * @param processDefinitionInfo 流程定义
+     * @param processVariables      流程实例的 variables
+     * @return 摘要
+     */
+    public static List<KeyValue<String, String>> getSummary(BpmProcessDefinitionInfoDO processDefinitionInfo,
+                                                            Map<String, Object> processVariables) {
+        // TODO @lesan:建议 if return,减少 { 层级
+        if (ObjectUtil.isNotNull(processDefinitionInfo)
+                && BpmModelFormTypeEnum.NORMAL.getType().equals(processDefinitionInfo.getFormType())) {
+            List<KeyValue<String, String>> summaryList = new ArrayList<>();
+            // TODO @lesan:可以使用 CollUtils.convertMap 简化工作量哈。
+            Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
+            processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
+                BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
+                if (formField != null) {
+                    formFieldsMap.put(formField.getField(), formField);
+                }
+            });
+
+            // TODO @lesan:这里也可以 if return,还是为了减少括号哈。这样,就可以写注释,情况一:;情况二:
+            if (ObjectUtil.isNotNull(processDefinitionInfo.getSummarySetting())
+                    && Boolean.TRUE.equals(processDefinitionInfo.getSummarySetting().getEnable())) {
+                // TODO @lesan:这里,也可以通过 CollUtils.convertList 简化哈。
+                for (String item : processDefinitionInfo.getSummarySetting().getSummary()) {
+                    BpmFormFieldVO formField = formFieldsMap.get(item);
+                    if (formField != null) {
+                        summaryList.add(new KeyValue<>(formField.getTitle(),
+                                processVariables.getOrDefault(item, "").toString()));
+                    }
+                }
+            } else {
+                // 默认展示前三个
+                /* TODO @lesan:stream 简化
+                 * summaryList.addAll(formFieldsMap.entrySet().stream()
+                 *         .limit(3)
+                 *         .map(entry -> new KeyValue<>(entry.getValue().getTitle(),
+                 *                 processVariables.getOrDefault(entry.getValue().getField(), "").toString()))
+                 *         .collect(Collectors.toList()));
+                 */
+                int j = 0;
+                for (Map.Entry<String, BpmFormFieldVO> entry : formFieldsMap.entrySet()) {
+                    BpmFormFieldVO formField = entry.getValue();
+                    if (j > 2) {
+                        break;
+                    }
+                    summaryList.add(new KeyValue<>(formField.getTitle(),
+                            processVariables.getOrDefault(formField.getField(), "").toString()));
+                    j++;
+                }
+            }
+            return summaryList;
+        }
+        return null;
+    }
+
     // ========== Task 相关的工具方法 ==========
 
     /**
@@ -201,6 +277,16 @@ public class FlowableUtils {
         return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON);
     }
 
+    /**
+     * 获得任务的签名图片 URL
+     *
+     * @param task 任务
+     * @return 签名图片 URL
+     */
+    public static String getTaskSignPicUrl(TaskInfo task) {
+        return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_SIGN_PIC_URL);
+    }
+
     /**
      * 获得任务的表单
      *

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

@@ -5,21 +5,26 @@ 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.listener.BpmCopyTaskDelegate;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate;
 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.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;
 
 /**
@@ -32,12 +37,13 @@ import static java.util.Arrays.asList;
  */
 public class SimpleModelUtils {
 
-    private static final Map<BpmSimpleModelNodeType, NodeConvert> NODE_CONVERTS = MapUtil.newHashMap();
+    private static final Map<BpmSimpleModelNodeTypeEnum, NodeConvert> NODE_CONVERTS = MapUtil.newHashMap();
 
     static {
         List<NodeConvert> converts = asList(new StartNodeConvert(), new EndNodeConvert(),
                 new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(),
-                new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert());
+                new DelayTimerNodeConvert(), new TriggerNodeConvert(),
+                new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert());
         converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert));
     }
 
@@ -83,14 +89,14 @@ public class SimpleModelUtils {
 
     private static BpmSimpleModelNodeVO buildStartNode() {
         return new BpmSimpleModelNodeVO().setId(START_EVENT_NODE_ID)
-                .setName(BpmSimpleModelNodeType.START_NODE.getName())
-                .setType(BpmSimpleModelNodeType.START_NODE.getType());
+                .setName(BpmSimpleModelNodeTypeEnum.START_NODE.getName())
+                .setType(BpmSimpleModelNodeTypeEnum.START_NODE.getType());
     }
 
     /**
      * 遍历节点,构建 FlowNode 元素
      *
-     * @param node SIMPLE 节点
+     * @param node    SIMPLE 节点
      * @param process BPMN 流程
      */
     private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) {
@@ -98,7 +104,7 @@ public class SimpleModelUtils {
         if (!isValidNode(node)) {
             return;
         }
-        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
+        BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType());
         Assert.notNull(nodeType, "模型节点类型({})不支持", node.getType());
 
         // 2. 处理当前节点
@@ -108,7 +114,7 @@ public class SimpleModelUtils {
         flowElements.forEach(process::addFlowElement);
 
         // 3.1 情况一:如果当前是分支节点,并且存在条件节点,则处理每个条件的子节点
-        if (BpmSimpleModelNodeType.isBranchNode(node.getType())
+        if (BpmSimpleModelNodeTypeEnum.isBranchNode(node.getType())
                 && CollUtil.isNotEmpty(node.getConditionNodes())) {
             // 注意:这里的 item.getChildNode() 处理的是每个条件的子节点,不是处理条件
             node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process));
@@ -121,8 +127,8 @@ public class SimpleModelUtils {
     /**
      * 遍历节点,构建 SequenceFlow 元素
      *
-     * @param process Bpmn 流程
-     * @param node 当前节点
+     * @param process      Bpmn 流程
+     * @param node         当前节点
      * @param targetNodeId 目标节点 ID
      */
     private static void traverseNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
@@ -131,14 +137,14 @@ public class SimpleModelUtils {
             return;
         }
         // 1.2 END_NODE 直接返回
-        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
+        BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType());
         Assert.notNull(nodeType, "模型节点类型不支持");
-        if (nodeType == BpmSimpleModelNodeType.END_NODE) {
+        if (nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) {
             return;
         }
 
         // 2.1 情况一:普通节点
-        if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) {
+        if (!BpmSimpleModelNodeTypeEnum.isBranchNode(node.getType())) {
             traverseNormalNodeToBuildSequenceFlow(process, node, targetNodeId);
         } else {
             // 2.2 情况二:分支节点
@@ -149,8 +155,8 @@ public class SimpleModelUtils {
     /**
      * 遍历普通(非条件)节点,构建 SequenceFlow 元素
      *
-     * @param process Bpmn 流程
-     * @param node 当前节点
+     * @param process      Bpmn 流程
+     * @param node         当前节点
      * @param targetNodeId 目标节点 ID
      */
     private static void traverseNormalNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
@@ -158,7 +164,7 @@ public class SimpleModelUtils {
         boolean isChildNodeValid = isValidNode(childNode);
         // 情况一:有“子”节点,则建立连线
         // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点
-        String finalTargetNodeId = isChildNodeValid? childNode.getId() : targetNodeId;
+        String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId;
         SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId);
         process.addFlowElement(sequenceFlow);
 
@@ -171,53 +177,68 @@ public class SimpleModelUtils {
     /**
      * 遍历条件节点,构建 SequenceFlow 元素
      *
-     * @param process Bpmn 流程
-     * @param node 当前节点
+     * @param process      Bpmn 流程
+     * @param node         当前节点
      * @param targetNodeId 目标节点 ID
      */
     private static void traverseBranchNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
-        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
+        BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType());
         BpmSimpleModelNodeVO childNode = node.getChildNode();
         List<BpmSimpleModelNodeVO> conditionNodes = node.getConditionNodes();
-        Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空");
+        // TODO @芋艿 路由分支没有conditionNodes 这里注释会影响吗?@jason:一起帮忙瞅瞅!
+//        Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空");
         // 分支终点节点 ID
         String branchEndNodeId = null;
-        if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) { // 条件分支
+        if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) { // 条件分支或路由分支
             // 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点 ID
             branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
-        } else if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE
-                || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) {  // 并行分支或包容分支
+        } else if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) {  // 并行分支或包容分支
             // 分支节点:分支终点节点 Id 为程序创建的网关集合节点。目前不会从前端传入。
             branchEndNodeId = buildGatewayJoinId(node.getId());
         }
         Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空");
 
         // 3. 遍历分支节点
-        // 下面的注释,以如下情况举例子。分支 1:A->B->C->D->E,分支 2:A->D->E。其中,A 为分支节点, D 为 A 孩子节点
-        for (BpmSimpleModelNodeVO item : conditionNodes) {
-            Assert.isTrue(Objects.equals(item.getType(), BpmSimpleModelNodeType.CONDITION_NODE.getType()),
-                    "条件节点类型({})不符合", item.getType());
-            BpmSimpleModelNodeVO conditionChildNode = item.getChildNode();
-            // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况
-            if (isValidNode(conditionChildNode)) {
-                // 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线
-                SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), item);
-                process.addFlowElement(sequenceFlow);
-                // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线
-                traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId);
-            } else {
-                // 3.2 分支没有后续节点。例如说,建立 A->D 的连线
-                SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item);
+        if (nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) {
+            // 路由分支遍历
+            for (BpmSimpleModelNodeVO.RouterSetting router : node.getRouterGroups()) {
+                SequenceFlow sequenceFlow = RouteBranchNodeConvert.buildSequenceFlow(node.getId(), router);
                 process.addFlowElement(sequenceFlow);
             }
+        } else {
+            // 下面的注释,以如下情况举例子。分支 1:A->B->C->D->E,分支 2:A->D->E。其中,A 为分支节点, D 为 A 孩子节点
+            for (BpmSimpleModelNodeVO item : conditionNodes) {
+                Assert.isTrue(Objects.equals(item.getType(), BpmSimpleModelNodeTypeEnum.CONDITION_NODE.getType()),
+                        "条件节点类型({})不符合", item.getType());
+                BpmSimpleModelNodeVO conditionChildNode = item.getChildNode();
+                // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况
+                if (isValidNode(conditionChildNode)) {
+                    // 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线
+                    SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), item);
+                    process.addFlowElement(sequenceFlow);
+                    // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线
+                    traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId);
+                } else {
+                    // 3.2 分支没有后续节点。例如说,建立 A->D 的连线
+                    SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item);
+                    process.addFlowElement(sequenceFlow);
+                }
+            }
         }
 
-        // 4. 如果是并行分支、包容分支,由于是程序创建的聚合网关,需要手工创建聚合网关和下一个节点的连线
-        if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE
-                || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE ) {
+        // 4.1 如果是并行分支、包容分支,由于是程序创建的聚合网关,需要手工创建聚合网关和下一个节点的连线
+        if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) {
             String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
             SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId);
             process.addFlowElement(sequenceFlow);
+            // 4.2 如果是路由分支,需要连接后续节点为默认路由
+        } else if (nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) {
+            SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), branchEndNodeId, node.getRouterDefaultFlowId(),
+                    null, null);
+            process.addFlowElement(sequenceFlow);
         }
 
         // 5. 递归调用后续节点 继续递归。例如说,建立 D->E 的连线
@@ -253,7 +274,7 @@ public class SimpleModelUtils {
     }
 
     public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) {
-        return BpmSimpleModelNodeType.APPROVE_NODE.getType().equals(node.getType())
+        return BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType().equals(node.getType())
                 && BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod().equals(node.getApproveMethod());
     }
 
@@ -269,7 +290,7 @@ public class SimpleModelUtils {
             throw new UnsupportedOperationException("请实现该方法");
         }
 
-        BpmSimpleModelNodeType getType();
+        BpmSimpleModelNodeTypeEnum getType();
 
     }
 
@@ -284,8 +305,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.START_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.START_NODE;
         }
 
     }
@@ -302,8 +323,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.END_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.END_NODE;
         }
 
     }
@@ -331,8 +352,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.START_USER_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.START_USER_NODE;
         }
 
     }
@@ -355,8 +376,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.APPROVE_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.APPROVE_NODE;
         }
 
         /**
@@ -384,7 +405,7 @@ public class SimpleModelUtils {
             boundaryEvent.addEventDefinition(eventDefinition);
 
             // 2.1 添加定时器边界事件类型
-            addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventType.USER_TASK_TIMEOUT.getType());
+            addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType());
             // 2.2 添加超时执行动作元素
             addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType());
             return boundaryEvent;
@@ -419,9 +440,49 @@ public class SimpleModelUtils {
             if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) {
                 userTask.setDueDate(node.getTimeoutHandler().getTimeDuration());
             }
+            // 设置监听器
+            addUserTaskListener(node, userTask);
+            // 添加是否需要签名
+            addSignEnable(node.getSignEnable(), userTask);
+            // 审批意见
+            addReasonRequire(node.getReasonRequire(), userTask);
             return userTask;
         }
 
+        private void addUserTaskListener(BpmSimpleModelNodeVO node, UserTask userTask) {
+            List<FlowableListener> flowableListeners = new ArrayList<>(3);
+            if (node.getTaskCreateListener() != null
+                    && Boolean.TRUE.equals(node.getTaskCreateListener().getEnable())) {
+                FlowableListener flowableListener = new FlowableListener();
+                flowableListener.setEvent(TaskListener.EVENTNAME_CREATE);
+                flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
+                flowableListener.setImplementation(DELEGATE_EXPRESSION);
+                addListenerConfig(flowableListener, node.getTaskCreateListener());
+                flowableListeners.add(flowableListener);
+            }
+            if (node.getTaskAssignListener() != null
+                    && Boolean.TRUE.equals(node.getTaskAssignListener().getEnable())) {
+                FlowableListener flowableListener = new FlowableListener();
+                flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT);
+                flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
+                flowableListener.setImplementation(DELEGATE_EXPRESSION);
+                addListenerConfig(flowableListener, node.getTaskAssignListener());
+                flowableListeners.add(flowableListener);
+            }
+            if (node.getTaskCompleteListener() != null
+                    && Boolean.TRUE.equals(node.getTaskCompleteListener().getEnable())) {
+                FlowableListener flowableListener = new FlowableListener();
+                flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE);
+                flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
+                flowableListener.setImplementation(DELEGATE_EXPRESSION);
+                addListenerConfig(flowableListener, node.getTaskCompleteListener());
+                flowableListeners.add(flowableListener);
+            }
+            if (CollUtil.isNotEmpty(flowableListeners)) {
+                userTask.setTaskListeners(flowableListeners);
+            }
+        }
+
         private void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) {
             BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod);
             Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum);
@@ -472,8 +533,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.COPY_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.COPY_NODE;
         }
 
     }
@@ -488,15 +549,15 @@ public class SimpleModelUtils {
 
             // 设置默认的序列流(条件)
             BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(),
-                    item -> BooleanUtil.isTrue(item.getDefaultFlow()));
+                    item -> BooleanUtil.isTrue(item.getConditionSetting().getDefaultFlow()));
             Assert.notNull(defaultSeqFlow, "条件分支节点({})的默认序列流不能为空", node.getId());
             exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
             return exclusiveGateway;
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.CONDITION_BRANCH_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE;
         }
 
     }
@@ -517,8 +578,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE;
         }
 
     }
@@ -531,7 +592,7 @@ public class SimpleModelUtils {
             inclusiveGateway.setId(node.getId());
             // 设置默认的序列流(条件)
             BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(),
-                    item -> BooleanUtil.isTrue(item.getDefaultFlow()));
+                    item -> BooleanUtil.isTrue(item.getConditionSetting().getDefaultFlow()));
             Assert.notNull(defaultSeqFlow, "包容分支节点({})的默认序列流不能为空", node.getId());
             inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
             // TODO @jason:setName
@@ -544,8 +605,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE;
         }
 
     }
@@ -559,8 +620,8 @@ public class SimpleModelUtils {
         }
 
         @Override
-        public BpmSimpleModelNodeType getType() {
-            return BpmSimpleModelNodeType.CONDITION_NODE;
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.CONDITION_NODE;
         }
 
         public static SequenceFlow buildSequenceFlow(String sourceId, String targetId,
@@ -575,12 +636,22 @@ public class SimpleModelUtils {
          * @param node 条件节点
          */
         public static String buildConditionExpression(BpmSimpleModelNodeVO node) {
-            BpmSimpleModeConditionType conditionTypeEnum = BpmSimpleModeConditionType.valueOf(node.getConditionType());
-            if (conditionTypeEnum == BpmSimpleModeConditionType.EXPRESSION) {
-                return node.getConditionExpression();
+            return buildConditionExpression(node.getConditionSetting().getConditionType(), node.getConditionSetting().getConditionExpression(),
+                    node.getConditionSetting().getConditionGroups());
+        }
+
+        public static String buildConditionExpression(BpmSimpleModelNodeVO.RouterSetting router) {
+            return buildConditionExpression(router.getConditionType(), router.getConditionExpression(),
+                    router.getConditionGroups());
+        }
+
+        public static String buildConditionExpression(Integer conditionType, String conditionExpression,
+                                                      ConditionGroups conditionGroups) {
+            BpmSimpleModeConditionTypeEnum conditionTypeEnum = BpmSimpleModeConditionTypeEnum.valueOf(conditionType);
+            if (conditionTypeEnum == BpmSimpleModeConditionTypeEnum.EXPRESSION) {
+                return conditionExpression;
             }
-            if (conditionTypeEnum == BpmSimpleModeConditionType.RULE) {
-                ConditionGroups conditionGroups = node.getConditionGroups();
+            if (conditionTypeEnum == BpmSimpleModeConditionTypeEnum.RULE) {
                 if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) {
                     return null;
                 }
@@ -605,6 +676,96 @@ public class SimpleModelUtils {
 
     }
 
+    public static class DelayTimerNodeConvert implements NodeConvert {
+
+        @Override
+        public List<FlowElement> convertList(BpmSimpleModelNodeVO node) {
+            List<FlowElement> flowElements = new ArrayList<>(2);
+            // 1. 构建接收任务,通过接收任务可卡住节点
+            ReceiveTask receiveTask = new ReceiveTask();
+            receiveTask.setId(node.getId());
+            receiveTask.setName(node.getName());
+            flowElements.add(receiveTask);
+
+            // 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();
+                if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
+                    eventDefinition.setTimeDuration(node.getDelaySetting().getDelayTime());
+                } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
+                    eventDefinition.setTimeDate(node.getDelaySetting().getDelayTime());
+                }
+                boundaryEvent.addEventDefinition(eventDefinition);
+                addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType());
+                flowElements.add(boundaryEvent);
+            }
+            return flowElements;
+        }
+
+        @Override
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.DELAY_TIMER_NODE;
+        }
+    }
+
+    public static class TriggerNodeConvert implements NodeConvert {
+
+        @Override
+        public ServiceTask convert(BpmSimpleModelNodeVO node) {
+            // 触发器使用 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) {
+                    // TODO @jason:加个 addExtensionElementJson 方法,方便设置 JSON 类型的属性
+                    addExtensionElement(serviceTask, TRIGGER_PARAM,
+                            JsonUtils.toJsonString(node.getTriggerSetting().getHttpRequestSetting()));
+                }
+            }
+            return serviceTask;
+        }
+
+        @Override
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.TRIGGER_NODE;
+        }
+    }
+
+    public static class RouteBranchNodeConvert implements NodeConvert {
+
+        @Override
+        public ExclusiveGateway convert(BpmSimpleModelNodeVO node) {
+            ExclusiveGateway exclusiveGateway = new ExclusiveGateway();
+            exclusiveGateway.setId(node.getId());
+
+            // 设置默认的序列流(条件)
+            node.setRouterDefaultFlowId("Flow_" + IdUtil.fastUUID());
+            exclusiveGateway.setDefaultFlow(node.getRouterDefaultFlowId());
+            return exclusiveGateway;
+        }
+
+        @Override
+        public BpmSimpleModelNodeTypeEnum getType() {
+            return BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE;
+        }
+
+        public static SequenceFlow buildSequenceFlow(String nodeId, BpmSimpleModelNodeVO.RouterSetting router) {
+            String conditionExpression = ConditionNodeConvert.buildConditionExpression(router);
+            return buildBpmnSequenceFlow(nodeId, router.getNodeId(), null, null, conditionExpression);
+        }
+
+    }
+
     private static String buildGatewayJoinId(String id) {
         return id + "_join";
     }
@@ -620,33 +781,33 @@ public class SimpleModelUtils {
     }
 
     private static void simulateNextNode(BpmSimpleModelNodeVO currentNode, Map<String, Object> variables,
-                                  List<BpmSimpleModelNodeVO> resultNodes) {
+                                         List<BpmSimpleModelNodeVO> resultNodes) {
         // 如果不合法(包括为空),则直接结束
         if (!isValidNode(currentNode)) {
             return;
         }
-        BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(currentNode.getType());
+        BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType());
         Assert.notNull(nodeType, "模型节点类型不支持");
 
         // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE
-        if (nodeType == BpmSimpleModelNodeType.START_NODE
-            || nodeType == BpmSimpleModelNodeType.START_USER_NODE
-            || nodeType == BpmSimpleModelNodeType.APPROVE_NODE
-            || nodeType == BpmSimpleModelNodeType.COPY_NODE
-            || nodeType == BpmSimpleModelNodeType.END_NODE) {
+        if (nodeType == BpmSimpleModelNodeTypeEnum.START_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE
+                || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) {
             // 添加元素
             resultNodes.add(currentNode);
         }
 
         // 情况:CONDITION_BRANCH_NODE 排它,只有一个满足条件的。如果没有,就走默认的
-        if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) {
+        if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE) {
             // 查找满足条件的 BpmSimpleModelNodeVO 节点
             BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
-                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow())
-                        && evalConditionExpress(variables, conditionNode));
+                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
+                            && evalConditionExpress(variables, conditionNode));
             if (matchConditionNode == null) {
                 matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
-                        conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow()));
+                        conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()));
             }
             Assert.notNull(matchConditionNode, "找不到条件节点({})", currentNode);
             // 遍历满足条件的 BpmSimpleModelNodeVO 节点
@@ -654,14 +815,14 @@ public class SimpleModelUtils {
         }
 
         // 情况:INCLUSIVE_BRANCH_NODE 包容,多个满足条件的。如果没有,就走默认的
-        if (nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) {
+        if (nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) {
             // 查找满足条件的 BpmSimpleModelNodeVO 节点
             Collection<BpmSimpleModelNodeVO> matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
-                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow())
+                    conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
                             && evalConditionExpress(variables, conditionNode));
             if (CollUtil.isEmpty(matchConditionNodes)) {
                 matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
-                        conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow()));
+                        conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()));
             }
             Assert.isTrue(!matchConditionNodes.isEmpty(), "找不到条件节点({})", currentNode);
             // 遍历满足条件的 BpmSimpleModelNodeVO 节点
@@ -670,7 +831,7 @@ public class SimpleModelUtils {
         }
 
         // 情况:PARALLEL_BRANCH_NODE 并行,都满足,都走
-        if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE) {
+        if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE) {
             // 遍历所有 BpmSimpleModelNodeVO 节点
             currentNode.getConditionNodes().forEach(matchConditionNode ->
                     simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes));
@@ -684,4 +845,26 @@ public class SimpleModelUtils {
         return BpmnModelUtils.evalConditionExpress(variables, ConditionNodeConvert.buildConditionExpression(conditionNode));
     }
 
+    // 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());
+            }
+        });
+    }
 }

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

@@ -88,6 +88,14 @@ public interface BpmModelService {
      */
     void deleteModel(Long userId, String id);
 
+    /**
+     * 清理模型,包括流程实例
+     *
+     * @param userId  用户编号
+     * @param id 编号
+     */
+    void cleanModel(Long userId, String id);
+
     /**
      * 修改模型的状态,实际更新的部署的流程定义的状态
      *

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

@@ -14,25 +14,33 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.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.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
+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.common.engine.impl.db.SuspensionState;
+import org.flowable.engine.HistoryService;
 import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.history.HistoricProcessInstance;
 import org.flowable.engine.repository.Model;
 import org.flowable.engine.repository.ModelQuery;
 import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -63,6 +71,15 @@ public class BpmModelServiceImpl implements BpmModelService {
     @Resource
     private BpmTaskCandidateInvoker taskCandidateInvoker;
 
+    @Resource
+    private HistoryService historyService;
+    @Resource
+    private RuntimeService runtimeService;
+    @Resource
+    private TaskService taskService;
+    @Resource
+    private BpmProcessInstanceCopyService processInstanceCopyService;
+
     @Override
     public List<Model> getModelList(String name) {
         ModelQuery modelQuery = repositoryService.createModelQuery();
@@ -246,6 +263,35 @@ public class BpmModelServiceImpl implements BpmModelService {
         updateProcessDefinitionSuspended(model.getDeploymentId());
     }
 
+    @Override
+    public void cleanModel(Long userId, String id) {
+        // 1. 校验流程模型存在
+        Model model = validateModelManager(id, userId);
+
+        // 2. 清理所有流程数据
+        // 2.1 先取消所有正在运行的流程
+        List<ProcessInstance> processInstances = runtimeService.createProcessInstanceQuery()
+                .processDefinitionKey(model.getKey()).list();
+        processInstances.forEach(processInstance -> {
+            runtimeService.deleteProcessInstance(processInstance.getId(),
+                    BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+            historyService.deleteHistoricProcessInstance(processInstance.getId());
+            processInstanceCopyService.deleteProcessInstanceCopy(processInstance.getId());
+        });
+        // 2.2 再从历史中删除所有相关的流程数据
+        List<HistoricProcessInstance> historicProcessInstances = historyService.createHistoricProcessInstanceQuery()
+                .processDefinitionKey(model.getKey()).list();
+        historicProcessInstances.forEach(historicProcessInstance -> {
+            historyService.deleteHistoricProcessInstance(historicProcessInstance.getId());
+            processInstanceCopyService.deleteProcessInstanceCopy(historicProcessInstance.getId());
+        });
+        // 2.3 清理所有 Task
+        List<Task> tasks = taskService.createTaskQuery()
+                .processDefinitionKey(model.getKey()).list();
+        // TODO @lesan:貌似传递一个 reason 会好点!
+        tasks.forEach(task -> taskService.deleteTask(task.getId()));
+    }
+
     @Override
     public void updateModelState(Long userId, String id, Integer state) {
         // 1.1 校验流程模型存在

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

@@ -7,15 +7,14 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmPr
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerSaveReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessListenerMapper;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerType;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerValueType;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerValueTypeEnum;
+import jakarta.annotation.Resource;
 import org.flowable.engine.delegate.JavaDelegate;
 import org.flowable.engine.delegate.TaskListener;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
-
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
 
@@ -53,14 +52,14 @@ public class BpmProcessListenerServiceImpl implements BpmProcessListenerService
 
     private void validateCreateProcessListenerValue(BpmProcessListenerSaveReqVO createReqVO) {
         // class 类型
-        if (createReqVO.getValueType().equals(BpmProcessListenerValueType.CLASS.getType())) {
+        if (createReqVO.getValueType().equals(BpmProcessListenerValueTypeEnum.CLASS.getType())) {
             try {
                 Class<?> clazz = Class.forName(createReqVO.getValue());
-                if (createReqVO.getType().equals(BpmProcessListenerType.EXECUTION.getType())
+                if (createReqVO.getType().equals(BpmProcessListenerTypeEnum.EXECUTION.getType())
                     && !JavaDelegate.class.isAssignableFrom(clazz)) {
                     throw exception(PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, createReqVO.getValue(),
                             JavaDelegate.class.getName());
-                } else if (createReqVO.getType().equals(BpmProcessListenerType.TASK.getType())
+                } else if (createReqVO.getType().equals(BpmProcessListenerTypeEnum.TASK.getType())
                     && !TaskListener.class.isAssignableFrom(clazz)) {
                     throw exception(PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, createReqVO.getValue(),
                             TaskListener.class.getName());

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

@@ -50,4 +50,11 @@ public interface BpmProcessInstanceCopyService {
     PageResult<BpmProcessInstanceCopyDO> getProcessInstanceCopyPage(Long userId,
                                                                     BpmProcessInstanceCopyPageReqVO pageReqVO);
 
+    /**
+     * 删除抄送流程
+     *
+     * @param processInstanceId 流程实例 ID
+     */
+    void deleteProcessInstanceCopy(String processInstanceId);
+
 }

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

@@ -87,4 +87,9 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
         return processInstanceCopyMapper.selectPage(userId, pageReqVO);
     }
 
+    @Override
+    public void deleteProcessInstanceCopy(String processInstanceId) {
+        processInstanceCopyMapper.deleteByProcessInstanceId(processInstanceId);
+    }
+
 }

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

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.service.task;
 
 import cn.hutool.core.collection.CollUtil;
 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.ObjectUtil;
@@ -12,15 +13,17 @@ 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.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.*;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNodeTask;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
 import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
-import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
 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;
@@ -38,6 +41,8 @@ import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.bpmn.constants.BpmnXMLConstants;
 import org.flowable.bpmn.model.*;
@@ -48,14 +53,13 @@ import org.flowable.engine.history.HistoricProcessInstance;
 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.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 javax.validation.Valid;
-import javax.annotation.Resource;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -109,6 +113,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     @Resource
     private BpmTaskCandidateInvoker taskCandidateInvoker;
 
+    @Resource
+    private BpmProcessIdRedisDAO processIdRedisDAO;
+
     // ========== Query 查询相关方法 ==========
 
     @Override
@@ -121,7 +128,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
 
     @Override
     public List<ProcessInstance> getProcessInstances(Set<String> ids) {
-        return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
+        return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).includeProcessVariables().list();
     }
 
     @Override
@@ -131,7 +138,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
 
     @Override
     public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) {
-        return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
+        return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).includeProcessVariables().list();
     }
 
     @Override
@@ -288,7 +295,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             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()) ?
-                            BpmSimpleModelNodeType.START_USER_NODE.getType() : BpmSimpleModelNodeType.APPROVE_NODE.getType())
+                            BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() : BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
                     .setStatus(FlowableUtils.getTaskStatus(task))
                     .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
                     .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
@@ -310,8 +317,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
                 ActivityNodeTask startTask = new ActivityNodeTask().setId(BpmnModelConstants.START_USER_NODE_ID)
                         .setAssignee(startUserId).setStatus(BpmTaskStatusEnum.APPROVE.getStatus());
                 ActivityNode startNode = new ActivityNode().setId(startTask.getId())
-                        .setName(BpmSimpleModelNodeType.START_USER_NODE.getName())
-                        .setNodeType(BpmSimpleModelNodeType.START_USER_NODE.getType())
+                        .setName(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getName())
+                        .setNodeType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType())
                         .setStatus(startTask.getStatus()).setTasks(ListUtil.of(startTask))
                         .setStartTime(DateUtils.of(activity.getStartTime())).setEndTime(DateUtils.of(activity.getEndTime()));
                 approvalNodes.add(0, startNode);
@@ -324,8 +331,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
                     return;
                 }
                 ActivityNode endNode = new ActivityNode().setId(activity.getId())
-                        .setName(BpmSimpleModelNodeType.END_NODE.getName())
-                        .setNodeType(BpmSimpleModelNodeType.END_NODE.getType()).setStatus(processInstanceStatus)
+                        .setName(BpmSimpleModelNodeTypeEnum.END_NODE.getName())
+                        .setNodeType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()).setStatus(processInstanceStatus)
                         .setStartTime(DateUtils.of(activity.getStartTime())).setEndTime(DateUtils.of(activity.getEndTime()));
                 String reason = FlowableUtils.getProcessInstanceReason(historicProcessInstance);
                 if (StrUtil.isNotEmpty(reason)) {
@@ -361,7 +368,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, activityId);
             HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务,会签/或签的任务,开始时间相同
             ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId()).setName(firstActivity.getActivityName())
-                    .setNodeType(BpmSimpleModelNodeType.APPROVE_NODE.getType()).setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
+                    .setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()).setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
                     .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
                     .setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime()))
                     .setTasks(new ArrayList<>());
@@ -431,8 +438,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
 
         // 1. 开始节点/审批节点
         if (ObjectUtils.equalsAny(node.getType(),
-                BpmSimpleModelNodeType.START_USER_NODE.getType(),
-                BpmSimpleModelNodeType.APPROVE_NODE.getType())) {
+                BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(),
+                BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) {
             List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
                     startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables);
             activityNode.setCandidateUserIds(candidateUserIds);
@@ -440,13 +447,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         }
 
         // 2. 结束节点
-        if (BpmSimpleModelNodeType.END_NODE.getType().equals(node.getType())) {
+        if (BpmSimpleModelNodeTypeEnum.END_NODE.getType().equals(node.getType())) {
             return activityNode;
         }
 
         // 3. 抄送节点
         if (CollUtil.isEmpty(runActivityIds) && // 流程发起时:需要展示抄送节点,用于选择抄送人
-                BpmSimpleModelNodeType.COPY_NODE.getType().equals(node.getType())) {
+                BpmSimpleModelNodeTypeEnum.COPY_NODE.getType().equals(node.getType())) {
             return activityNode;
         }
         return null;
@@ -462,23 +469,23 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
 
         // 1. 开始节点
         if (node instanceof StartEvent) {
-            return activityNode.setName(BpmSimpleModelNodeType.START_USER_NODE.getName())
-                    .setNodeType(BpmSimpleModelNodeType.START_USER_NODE.getType());
+            return activityNode.setName(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getName())
+                    .setNodeType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType());
         }
 
         // 2. 审批节点
         if (node instanceof UserTask) {
             List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
                     startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables);
-            return activityNode.setName(node.getName()).setNodeType(BpmSimpleModelNodeType.APPROVE_NODE.getType())
+            return activityNode.setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
                     .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node))
                     .setCandidateUserIds(candidateUserIds);
         }
 
         // 3. 结束节点
         if (node instanceof EndEvent) {
-            return activityNode.setName(BpmSimpleModelNodeType.END_NODE.getName())
-                    .setNodeType(BpmSimpleModelNodeType.END_NODE.getType());
+            return activityNode.setName(BpmSimpleModelNodeTypeEnum.END_NODE.getName())
+                    .setNodeType(BpmSimpleModelNodeTypeEnum.END_NODE.getType());
         }
         return null;
     }
@@ -595,15 +602,35 @@ 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 的节点
         if (CollUtil.isNotEmpty(startUserSelectAssignees)) {
             variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees);
         }
-        ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
+
+        // 3. 创建流程
+        ProcessInstanceBuilder processInstanceBuilder = runtimeService.createProcessInstanceBuilder()
                 .processDefinitionId(definition.getId())
                 .businessKey(businessKey)
-                .name(definition.getName().trim())
-                .variables(variables)
-                .start();
+                .variables(variables);
+        // 3.1 创建流程 ID
+        BpmModelMetaInfoVO.ProcessIdRule processIdRule = processDefinitionInfo.getProcessIdRule();
+        if (processIdRule != null && Boolean.TRUE.equals(processIdRule.getEnable())) {
+            processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule));
+        }
+        // 3.2 流程名称
+        BpmModelMetaInfoVO.TitleSetting titleSetting = processDefinitionInfo.getTitleSetting();
+        if (titleSetting != null && Boolean.TRUE.equals(titleSetting.getEnable())) {
+            AdminUserRespDTO user = adminUserApi.getUser(userId);
+            Map<String, Object> cloneVariables = new HashMap<>(variables);
+            cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname());
+            cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now());
+            cloneVariables.put(BpmnVariableConstants.PROCESS_DEFINITION_NAME, definition.getName().trim());
+            processInstanceBuilder.name(StrUtil.format(titleSetting.getTitle(), cloneVariables));
+        } else {
+            processInstanceBuilder.name(definition.getName().trim());
+        }
+        // 3.3 发起流程实例
+        ProcessInstance instance = processInstanceBuilder.start();
         return instance.getId();
     }
 
@@ -641,6 +668,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
             throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
         }
+        // 1.3 校验允许撤销审批中的申请
+        BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(instance.getProcessDefinitionId());
+        Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo);
+        if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消
+                && Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) {
+            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW);
+        }
 
         // 2. 取消流程
         updateProcessInstanceCancel(cancelReqVO.getId(),
@@ -668,7 +702,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason);
 
         // 2. 结束流程
-        taskService.moveTaskToEnd(id);
+        taskService.moveTaskToEnd(id, reason);
     }
 
     @Override

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

@@ -195,8 +195,9 @@ public interface BpmTaskService {
      * 将指定流程实例的、进行中的流程任务,移动到结束节点
      *
      * @param processInstanceId 流程编号
+     * @param reason 原因
      */
-    void moveTaskToEnd(String processInstanceId);
+    void moveTaskToEnd(String processInstanceId, String reason);
 
     /**
      * 将任务退回到指定的 targetDefinitionKey 位置
@@ -275,4 +276,12 @@ public interface BpmTaskService {
      */
     void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType);
 
+    /**
+     * 处理 延迟器 超时事件
+     *
+     * @param processInstanceId 流程示例编号
+     * @param taskDefineKey     任务 Key
+     */
+    void processDelayTimerTimeout(String processInstanceId, String taskDefineKey);
+
 }

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

@@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.enums.definition.*;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
@@ -31,15 +32,13 @@ import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import lombok.extern.slf4j.Slf4j;
-import org.flowable.bpmn.model.BpmnModel;
-import org.flowable.bpmn.model.EndEvent;
-import org.flowable.bpmn.model.FlowElement;
-import org.flowable.bpmn.model.UserTask;
+import org.flowable.bpmn.model.*;
 import org.flowable.engine.HistoryService;
 import org.flowable.engine.ManagementService;
 import org.flowable.engine.RuntimeService;
 import org.flowable.engine.TaskService;
 import org.flowable.engine.history.HistoricActivityInstance;
+import org.flowable.engine.runtime.Execution;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.DelegationState;
 import org.flowable.task.api.Task;
@@ -63,6 +62,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
 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.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
 
 /**
  * 流程任务实例 Service 实现类
@@ -160,13 +160,18 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId());
         Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonsSetting = BpmnModelUtils.parseButtonsSetting(
                 bpmnModel, todoTask.getTaskDefinitionKey());
+        Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey());
+        Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey());
 
         // 4. 任务表单
         BpmFormDO taskForm = null;
-        if (StrUtil.isNotBlank(todoTask.getFormKey())){
+        if (StrUtil.isNotBlank(todoTask.getFormKey())) {
             taskForm = formService.getForm(NumberUtils.parseLong(todoTask.getFormKey()));
         }
-        return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm);
+
+        return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm)
+                .setSignEnable(signEnable)
+                .setReasonRequire(reasonRequire);
     }
 
     @Override
@@ -477,6 +482,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         if (instance == null) {
             throw exception(PROCESS_INSTANCE_NOT_EXISTS);
         }
+        // 1.3 校验签名
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
+        Boolean signEnable = parseSignEnable(bpmnModel, task.getTaskDefinitionKey());
+        if (signEnable && StrUtil.isEmpty(reqVO.getSignPicUrl())) {
+            throw exception(TASK_SIGNATURE_NOT_EXISTS);
+        }
+        // 1.4 校验审批意见
+        Boolean reasonRequire = parseReasonRequire(bpmnModel, task.getTaskDefinitionKey());
+        if (reasonRequire && StrUtil.isEmpty(reqVO.getReason())) {
+            throw exception(TASK_REASON_REQUIRE);
+        }
 
         // 情况一:被委派的任务,不调用 complete 去完成任务
         if (DelegationState.PENDING.equals(task.getDelegationState())) {
@@ -491,8 +507,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
 
         // 情况三:审批普通的任务。大多数情况下,都是这样
-        // 2.1 更新 task 状态、原因
+        // 2.1 更新 task 状态、原因、签字
         updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), reqVO.getReason());
+        if (signEnable) {
+            taskService.setVariableLocal(task.getId(), BpmnVariableConstants.TASK_SIGN_PIC_URL, reqVO.getSignPicUrl());
+        }
         // 2.2 添加评论
         taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
                 BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
@@ -637,8 +656,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
         FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
         // 3.1 情况一:驳回到指定的任务节点
-        BpmUserTaskRejectHandlerType userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement);
-        if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.RETURN_USER_TASK) {
+        BpmUserTaskRejectHandlerTypeEnum userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement);
+        if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerTypeEnum.RETURN_USER_TASK) {
             String returnTaskId = BpmnModelUtils.parseReturnTaskId(userTaskElement);
             Assert.notNull(returnTaskId, "退回的节点不能为空");
             returnTask(userId, new BpmTaskReturnReqVO().setId(task.getId())
@@ -647,7 +666,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
         // 3.2 情况二:直接结束,审批不通过
         processInstanceService.updateProcessInstanceReject(instance, reqVO.getReason()); // 标记不通过
-        moveTaskToEnd(task.getProcessInstanceId()); // 结束流程
+        moveTaskToEnd(task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); // 结束流程
     }
 
     /**
@@ -818,7 +837,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     }
 
     @Override
-    public void moveTaskToEnd(String processInstanceId) {
+    public void moveTaskToEnd(String processInstanceId, String reason) {
         List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
         if (CollUtil.isEmpty(taskList)) {
             return;
@@ -844,6 +863,14 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                 .processInstanceId(processInstanceId)
                 .moveActivityIdsToSingleActivityId(activityIds, endEvent.getId())
                 .changeState();
+
+        // 3. 特殊:如果跳转到 EndEvent 流程还未结束, 执行 deleteProcessInstance 方法
+        // TODO 芋艿:目前发现并行分支情况下,会存在这个情况,后续看看有没更好的方案;
+        List<Execution> executions = runtimeService.createExecutionQuery().processInstanceId(processInstanceId).list();
+        if (CollUtil.isNotEmpty(executions)) {
+            log.warn("[moveTaskToEnd][执行跳转到 EndEvent 后, 流程实例未结束,强制执行 deleteProcessInstance 方法]");
+            runtimeService.deleteProcessInstance(processInstanceId, reason);
+        }
     }
 
     @Override
@@ -1144,6 +1171,42 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                     log.error("[processTaskAssigned][taskId({}) 没有找到流程实例]", task.getId());
                     return;
                 }
+
+                // 自动去重,通过自动审批的方式 TODO @芋艿 驳回的情况得考虑一下;@lesan:驳回后,又自动审批么?
+                BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
+                if (processDefinitionInfo == null) {
+                    log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());
+                    return;
+                }
+                if (processDefinitionInfo.getAutoApprovalType() != null) {
+                    HistoricTaskInstanceQuery sameAssigneeQuery = historyService.createHistoricTaskInstanceQuery()
+                            .processInstanceId(task.getProcessInstanceId())
+                            .taskAssignee(task.getAssignee()) // 相同审批人
+                            .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus())
+                            .finished();
+                    if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType())
+                        && sameAssigneeQuery.count() > 0) {
+                        getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
+                                .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName()));
+                        return;
+                    }
+                    if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) {
+                        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
+                        if (bpmnModel == null) {
+                            log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId());
+                            return;
+                        }
+                        List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点
+                                BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())),
+                                SequenceFlow::getSourceRef);
+                        if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) {
+                            getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
+                                    .setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName()));
+                            return;
+                        }
+                    }
+                }
+
                 // 审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
                 if (StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
                     // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
@@ -1191,9 +1254,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                         }
                     }
                 }
-
-                AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
-                messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
+                // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号
+                FlowableUtils.execute(processInstance.getTenantId(), () -> {
+                    AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
+                    messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
+                });
             }
 
         });
@@ -1238,6 +1303,23 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }));
     }
 
+    @Override
+    public void processDelayTimerTimeout(String processInstanceId, String taskDefineKey) {
+        Execution execution = runtimeService.createExecutionQuery()
+                .processInstanceId(processInstanceId)
+                .activityId(taskDefineKey)
+                .singleResult();
+        if (execution == null) {
+            log.error("[processDelayTimerTimeout][processInstanceId({}) activityId({}) 没有找到执行活动]",
+                    processInstanceId, taskDefineKey);
+            return;
+        }
+
+        // 若存在直接触发接收任务,执行后续节点
+        FlowableUtils.execute(execution.getTenantId(),
+                () -> runtimeService.trigger(execution.getId()));
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *

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

@@ -0,0 +1,96 @@
+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.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.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 @芋艿:可能会想换个包地址
+/**
+ * BPM 用户任务通用监听器
+ *
+ * @author Lesan
+ */
+@Component
+@Slf4j
+@Scope("prototype")
+public class BpmUserTaskListener implements TaskListener {
+
+    public static final String DELEGATE_EXPRESSION = "${bpmUserTaskListener}";
+
+    @Resource
+    private BpmProcessInstanceService processInstanceService;
+
+    @Resource
+    private RestTemplate restTemplate;
+
+    @Setter
+    private FixedValue listenerConfig;
+
+    @Override
+    public void notify(DelegateTask delegateTask) {
+        // 1. 获取所需基础信息
+        HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(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 请求体默认参数
+        // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去;
+        body.add("processInstanceId", delegateTask.getProcessInstanceId());
+        body.add("assignee", delegateTask.getAssignee());
+        body.add("taskDefinitionKey", delegateTask.getTaskDefinitionKey());
+        body.add("taskId", delegateTask.getId());
+
+        // 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 芋艿:待定!
+    }
+}

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

@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.bpm.service.task.trigger;
+
+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 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.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);
+        try {
+            ResponseEntity<String> 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());
+        }
+    }
+
+}

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

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.bpm.service.task.trigger;
+
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
+
+// TODO @芋艿:可能会想换个包地址
+/**
+ * BPM 触发器接口
+ * <p>
+ * 处理不同的动作
+ *
+ * @author jason
+ */
+public interface BpmTrigger {
+
+    /**
+     * 对应触发器类型
+     *
+     * @return 触发器类型
+     */
+    BpmTriggerTypeEnum getType();
+
+    /**
+     * 触发器执行
+     *
+     * @param processInstanceId 流程实例编号
+     * @param param 触发器参数
+     */
+    void execute(String processInstanceId, String param);
+
+}

+ 1 - 1
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

@@ -48,7 +48,7 @@ import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClient
 /**
  * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
  *
- * @author 遇到源码
+ * @author 芋道源码
  */
 @Slf4j
 public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientConfig> {