瀏覽代碼

仿钉钉流程设计- 基于服务任务实现会签下的拒绝需要全员

jason 1 年之前
父節點
當前提交
12108e7365

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

@@ -15,7 +15,7 @@ public enum BpmUserTaskRejectHandlerType {
 
     FINISH_PROCESS(1, "终止流程"),
     RETURN_PRE_USER_TASK(2, "驳回到指定任务节点"),
-    FINISH_PROCESS_BY_REJECT_RATIO(3, "按拒绝人数比例终止流程"), // 用于会签
+    FINISH_PROCESS_BY_REJECT_NUMBER(3, "按拒绝人数终止流程"), // 用于会签
     FINISH_TASK(4, "结束任务"); // 待实现,可能会用于意见分支
 
     private final Integer type;

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

@@ -41,6 +41,12 @@ public class BpmSimpleModelNodeVO {
 
     @Schema(description = "节点的属性")
     private Map<String, Object> attributes; // TODO @jason:建议是字段分拆下;类似说:
+
+    /**
+     * 附加节点 Id, 该节点不从前端传入。 由程序生成. 由于当个节点无法完成功能。 需要附加节点来完成。
+     * 例如: 会签时需要按拒绝人数来终止流程。 需要 userTask + ServiceTask 两个节点配合完成。 serviceTask 由后端生成。
+     */
+    private String attachNodeId;
     // Map<String, Integer> formPermissions; 表单权限;仅发起、审批、抄送节点会使用
     // Integer approveMethod; 审批方式;仅审批节点会使用
     // TODO @jason 后面和前端一起调整一下

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

@@ -50,6 +50,16 @@ public interface BpmnModelConstants {
      */
     String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId";
 
+    /**
+     * BPMN UserTask 的扩展属性,用于标记用户任务的审批方式
+     */
+    String USER_TASK_APPROVE_METHOD = "approveMethod";
+
+    /**
+     * BPMN ExtensionElement 的扩展属性,用于标记 服务任务附属的用户任务 Id
+     */
+    String SERVICE_TASK_ATTACH_USER_TASK_ID = "attachUserTaskId";
+
     /**
      * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
      */
@@ -75,6 +85,4 @@ public interface BpmnModelConstants {
      * 支持转仿钉钉设计模型的 Bpmn 节点
      */
     Set<Class<? extends FlowNode>> SUPPORT_CONVERT_SIMPLE_FlOW_NODES = ImmutableSet.of(UserTask.class, EndEvent.class);
-
-    String REJECT_POST_PROCESS_MESSAGE_NAME = "message_reject_post_process";
 }

+ 66 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/CompleteByRejectCountExpression.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.expression;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.bpm.enums.definition.BpmApproveMethodEnum.ANY_APPROVE_ALL_REJECT;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.USER_TASK_APPROVE_METHOD;
+
+/**
+ * 按拒绝人数计算会签的完成条件的流程表达式实现
+ *
+ * @author jason
+ */
+@Component
+@Slf4j
+public class CompleteByRejectCountExpression {
+
+    /**
+     *  会签的完成条件
+     */
+    public boolean completionCondition(DelegateExecution execution) {
+        FlowElement flowElement = execution.getCurrentFlowElement();
+        // 实例总数
+        Integer nrOfInstances = (Integer) execution.getVariable("nrOfInstances");
+        // 完成的实例数
+        Integer nrOfCompletedInstances = (Integer) execution.getVariable("nrOfCompletedInstances");
+        // 审批方式
+        Integer approveMethod = NumberUtils.parseInt(BpmnModelUtils.parseExtensionElement(flowElement, USER_TASK_APPROVE_METHOD));
+        Assert.notNull(approveMethod, "审批方式不能空");
+        // 计算拒绝的人数
+        Integer rejectCount = CollectionUtils.getSumValue(execution.getExecutions(),
+                item -> Objects.equals(BpmTaskStatusEnum.REJECT.getStatus(), item.getVariableLocal(BpmConstants.TASK_VARIABLE_STATUS, Integer.class)) ? 1 : 0,
+                Integer::sum, 0);
+        // 同意的人数为 完成人数 - 拒绝人数
+        int agreeCount = nrOfCompletedInstances - rejectCount;
+        // 1. 多人会签(通过只需一人,拒绝需要全员)
+        if (Objects.equals(ANY_APPROVE_ALL_REJECT.getMethod(), approveMethod)) {
+            // 1.1 一人同意. 会签任务完成
+            if (agreeCount > 0) {
+                return true;
+            } else {
+                // 1.2 所有人都拒绝了。设置任务拒绝变量, 会签任务完成。 后续终止流程在 ServiceTask【MultiInstanceServiceTaskExpression】处理
+                if (Objects.equals(nrOfInstances, rejectCount)) {
+                    execution.setVariable(String.format("%s_reject",flowElement.getId()), Boolean.TRUE);
+                    return true;
+                }
+                return false;
+            }
+        }
+        // TODO 多人会签(按比例投票)
+        log.error("[completionCondition] 按拒绝人数计算会签的完成条件的审批方式[{}],配置有误", approveMethod);
+        throw exception(GlobalErrorCodeConstants.ERROR_CONFIGURATION);
+    }
+}

+ 38 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/MultiInstanceServiceTaskExpression.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.expression;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.BooleanUtil;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import jakarta.annotation.Resource;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.delegate.JavaDelegate;
+import org.springframework.stereotype.Component;
+
+/**
+ * 处理会签 Service Task 代理表达式
+ *
+ * @author jason
+ */
+@Component
+public class MultiInstanceServiceTaskExpression implements JavaDelegate {
+
+    @Resource
+    private BpmProcessInstanceService processInstanceService;
+
+    @Override
+    public void execute(DelegateExecution execution) {
+        String attachUserTaskId = BpmnModelUtils.parseExtensionElement(execution.getCurrentFlowElement(),
+                BpmnModelConstants.SERVICE_TASK_ATTACH_USER_TASK_ID);
+        Assert.notNull(attachUserTaskId, "附属的用户任务 Id 不能为空");
+        // 获取会签任务是否被拒绝
+        Boolean userTaskRejected = execution.getVariable(String.format("%s_reject", attachUserTaskId), Boolean.class);
+        // 如果会签任务被拒绝, 终止流程
+        if (BooleanUtil.isTrue(userTaskRejected)) {
+            processInstanceService.updateProcessInstanceReject(execution.getProcessInstanceId(),
+                    BpmCommentTypeEnum.REJECT.formatComment("会签任务拒绝人数满足条件"));
+        }
+    }
+}

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

@@ -26,6 +26,7 @@ import java.util.Objects;
 
 import static cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType.USER_TASK_TIMEOUT;
 import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.*;
+import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_NUMBER;
 import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutActionEnum.AUTO_REMINDER;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.SimpleModelConstants.*;
@@ -55,6 +56,11 @@ public class SimpleModelUtils {
      */
     public static final String ANY_OF_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances > 0 }";
 
+    /**
+     * 按拒绝人数计算多实例完成条件的表达式
+     */
+    public static final String COMPLETE_BY_REJECT_COUNT_EXPRESSION = "${completeByRejectCountExpression.completionCondition(execution)}";
+
     // TODO-DONE @jason:建议方法名,改成 buildBpmnModel
     // TODO @yunai:注释需要完善下;
 
@@ -71,10 +77,6 @@ public class SimpleModelUtils {
         // 不加这个 解析 Message 会报 NPE 异常 .
         bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason:待定:是不是搞个自定义的 namespace;
         // TODO 芋艿:后续在 review
-        // @芋艿 这个 Message 可以去掉 暂时用不上
-        Message rejectPostProcessMsg = new Message();
-        rejectPostProcessMsg.setName(REJECT_POST_PROCESS_MESSAGE_NAME);
-        bpmnModel.addMessage(rejectPostProcessMsg);
 
         Process process = new Process();
         process.setId(processId);
@@ -107,19 +109,30 @@ public class SimpleModelUtils {
         if (nodeType == END_NODE) {
             return;
         }
-
         // 2.1 情况一:普通节点
         BpmSimpleModelNodeVO childNode = node.getChildNode();
         if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) {
             if (!isValidNode(childNode)) {
                 // 2.1.1 普通节点且无孩子节点。分两种情况
                 // a.结束节点  b. 条件分支的最后一个节点.与分支节点的孩子节点或聚合节点建立连线。
-                SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null);
-                process.addFlowElement(sequenceFlow);
+                if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
+                    // 2.1.1.1 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线
+                    List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), targetNodeId);
+                    sequenceFlows.forEach(process::addFlowElement);
+                } else {
+                    SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null);
+                    process.addFlowElement(sequenceFlow);
+                }
             } else {
                 // 2.1.2 普通节点且有孩子节点。建立连线
-                SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null);
-                process.addFlowElement(sequenceFlow);
+                if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
+                    // 2.1.1.2 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线
+                    List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), childNode.getId());
+                    sequenceFlows.forEach(process::addFlowElement);
+                } else {
+                    SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null);
+                    process.addFlowElement(sequenceFlow);
+                }
                 // 递归调用后续节点
                 traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
             }
@@ -173,6 +186,18 @@ public class SimpleModelUtils {
         }
     }
 
+    /**
+     *  构建有附加节点的连线
+     * @param nodeId 当前节点 Id
+     * @param attachNodeId 附属节点 Id
+     * @param targetNodeId 目标节点 Id
+     */
+    private static List<SequenceFlow> buildAttachNodeSequenceFlow(String nodeId, String attachNodeId, String targetNodeId) {
+        SequenceFlow sequenceFlow = buildBpmnSequenceFlow(nodeId, attachNodeId, null, null, null);
+        SequenceFlow attachSequenceFlow = buildBpmnSequenceFlow(attachNodeId, targetNodeId, null, null, null);
+        return CollUtil.newArrayList(sequenceFlow, attachSequenceFlow);
+    }
+
     /**
      * 构造条件表达式
      *
@@ -331,9 +356,28 @@ public class SimpleModelUtils {
             BoundaryEvent boundaryEvent = buildUserTaskTimerBoundaryEvent(userTask, userTaskConfig.getTimeoutHandler());
             flowElements.add(boundaryEvent);
         }
+        // 如果按拒绝人数终止流程。需要添加附加的 ServiceTask 处理
+        if (userTaskConfig.getRejectHandler() != null &&
+                Objects.equals(FINISH_PROCESS_BY_REJECT_NUMBER.getType(), userTaskConfig.getRejectHandler().getType())) {
+            ServiceTask serviceTask = buildMultiInstanceServiceTask(node);
+            flowElements.add(serviceTask);
+        }
         return flowElements;
     }
 
+    private static ServiceTask buildMultiInstanceServiceTask(BpmSimpleModelNodeVO node) {
+        ServiceTask serviceTask = new ServiceTask();
+        String id = String.format("Activity-%s", IdUtil.fastSimpleUUID());
+        serviceTask.setId(id);
+        serviceTask.setName("会签服务任务");
+        serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
+        serviceTask.setImplementation("${multiInstanceServiceTaskExpression}");
+        serviceTask.setAsynchronous(false);
+        addExtensionElement(serviceTask, SERVICE_TASK_ATTACH_USER_TASK_ID, node.getId());
+        node.setAttachNodeId(id);
+        return serviceTask;
+    }
+
     private static BoundaryEvent buildUserTaskTimerBoundaryEvent(UserTask userTask, SimpleModelUserTaskConfig.TimeoutHandler timeoutHandler) {
         // 定时器边界事件
         BoundaryEvent boundaryEvent = new BoundaryEvent();
@@ -468,6 +512,9 @@ public class SimpleModelUtils {
         if (bpmApproveMethodEnum == null || bpmApproveMethodEnum == BpmApproveMethodEnum.SINGLE_PERSON_APPROVE) {
             return;
         }
+        // 添加审批方式的扩展属性
+        addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD,
+                approveMethod == null ? null : approveMethod.toString());
         MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
         //  设置 collectionVariable。本系统用不到。会在 仅仅为了校验。
         multiInstanceCharacteristics.setInputDataItem("${coll_userList}");
@@ -484,8 +531,7 @@ public class SimpleModelUtils {
             multiInstanceCharacteristics.setLoopCardinality("1");
             userTask.setLoopCharacteristics(multiInstanceCharacteristics);
         } else if (bpmApproveMethodEnum == BpmApproveMethodEnum.ANY_APPROVE_ALL_REJECT) {
-            // 这种情况。拒绝任务时候,不会终止或者完成任务 参见 BpmTaskService#rejectTask 方法
-            multiInstanceCharacteristics.setCompletionCondition(ANY_OF_APPROVE_COMPLETE_EXPRESSION);
+            multiInstanceCharacteristics.setCompletionCondition(COMPLETE_BY_REJECT_COUNT_EXPRESSION);
             multiInstanceCharacteristics.setSequential(false);
         }
         // TODO 会签(按比例投票 )

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

@@ -35,7 +35,6 @@ import org.flowable.engine.HistoryService;
 import org.flowable.engine.ManagementService;
 import org.flowable.engine.RuntimeService;
 import org.flowable.engine.TaskService;
-import org.flowable.engine.runtime.Execution;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.task.api.DelegationState;
 import org.flowable.task.api.Task;
@@ -352,30 +351,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                     .setReason(reqVO.getReason());
             returnTask(userId, returnReq);
             return;
-        } else if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_RATIO) {
-            // 3.3 按拒绝人数比例终止流程
+        } else if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_NUMBER) {
+            // 3.3 按拒绝人数终止流程
             if (!flowElement.hasMultiInstanceLoopCharacteristics()) {
                 log.error("[rejectTask] 用户任务拒绝处理类型配置错误, 按拒绝人数终止流程只能用于会签任务");
                 throw exception(GlobalErrorCodeConstants.ERROR_CONFIGURATION);
             }
-            // 获取并行任务总数
-            Execution execution = runtimeService.createExecutionQuery().processInstanceId(task.getProcessInstanceId())
-                    .executionId(task.getExecutionId()).singleResult();
-            Integer nrOfInstances = runtimeService.getVariable(execution.getParentId(), "nrOfInstances", Integer.class);
-            // 获取未完成任务列表
-            List<Task> taskList = getTaskListByProcessInstanceIdAndAssigned(task.getProcessInstanceId(), null,
-                    task.getTaskDefinitionKey());
-            // 获取已经拒绝的任务数
-            Integer rejectNumber = getSumValue(taskList,
-                    item -> Objects.equals(BpmTaskStatusEnum.REJECT.getStatus(), FlowableUtils.getTaskStatus(item)) ? 1 : 0,
-                    Integer::sum, 0);
-//            // TODO @jason:如果这样的话,后续会不会在【已完成】里面查询不到哈?【重要!!!!】
-//            // 拒绝任务后,任务分配人清空。但不能完成任务
-//            taskService.setAssignee(task.getId(), "");
-            // 不是所有人拒绝返回。 TODO 后续需要做按拒绝人数比例来判断
-            if (!Objects.equals(nrOfInstances, rejectNumber)) {
-                return;
-            }
+            // 设置变量值为拒绝
+            runtimeService.setVariableLocal(task.getExecutionId(), BpmConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.REJECT.getStatus());
+            // 完成任务
+            taskService.complete(task.getId());
+            return;
         }
         // 3.4 其他情况 终止流程。 TODO 后续可能会增加处理类型
         processInstanceService.updateProcessInstanceReject(instance.getProcessInstanceId(), reqVO.getReason());