Commit 12108e7365a0655d53b486ffce9907210e0121e0

Authored by jason
1 parent 0db7796c

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

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 {
15 15  
16 16 FINISH_PROCESS(1, "终止流程"),
17 17 RETURN_PRE_USER_TASK(2, "驳回到指定任务节点"),
18   - FINISH_PROCESS_BY_REJECT_RATIO(3, "按拒绝人数比例终止流程"), // 用于会签
  18 + FINISH_PROCESS_BY_REJECT_NUMBER(3, "按拒绝人数终止流程"), // 用于会签
19 19 FINISH_TASK(4, "结束任务"); // 待实现,可能会用于意见分支
20 20  
21 21 private final Integer type;
... ...
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 {
41 41  
42 42 @Schema(description = "节点的属性")
43 43 private Map<String, Object> attributes; // TODO @jason:建议是字段分拆下;类似说:
  44 +
  45 + /**
  46 + * 附加节点 Id, 该节点不从前端传入。 由程序生成. 由于当个节点无法完成功能。 需要附加节点来完成。
  47 + * 例如: 会签时需要按拒绝人数来终止流程。 需要 userTask + ServiceTask 两个节点配合完成。 serviceTask 由后端生成。
  48 + */
  49 + private String attachNodeId;
44 50 // Map<String, Integer> formPermissions; 表单权限;仅发起、审批、抄送节点会使用
45 51 // Integer approveMethod; 审批方式;仅审批节点会使用
46 52 // TODO @jason 后面和前端一起调整一下
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java
... ... @@ -51,6 +51,16 @@ public interface BpmnModelConstants {
51 51 String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId";
52 52  
53 53 /**
  54 + * BPMN UserTask 的扩展属性,用于标记用户任务的审批方式
  55 + */
  56 + String USER_TASK_APPROVE_METHOD = "approveMethod";
  57 +
  58 + /**
  59 + * BPMN ExtensionElement 的扩展属性,用于标记 服务任务附属的用户任务 Id
  60 + */
  61 + String SERVICE_TASK_ATTACH_USER_TASK_ID = "attachUserTaskId";
  62 +
  63 + /**
54 64 * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
55 65 */
56 66 String FORM_FIELD_PERMISSION_ELEMENT = "fieldsPermission";
... ... @@ -75,6 +85,4 @@ public interface BpmnModelConstants {
75 85 * 支持转仿钉钉设计模型的 Bpmn 节点
76 86 */
77 87 Set<Class<? extends FlowNode>> SUPPORT_CONVERT_SIMPLE_FlOW_NODES = ImmutableSet.of(UserTask.class, EndEvent.class);
78   -
79   - String REJECT_POST_PROCESS_MESSAGE_NAME = "message_reject_post_process";
80 88 }
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/CompleteByRejectCountExpression.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable.core.expression;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  6 +import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
  7 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  8 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants;
  9 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.flowable.bpmn.model.FlowElement;
  12 +import org.flowable.engine.delegate.DelegateExecution;
  13 +import org.springframework.stereotype.Component;
  14 +
  15 +import java.util.Objects;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  18 +import static cn.iocoder.yudao.module.bpm.enums.definition.BpmApproveMethodEnum.ANY_APPROVE_ALL_REJECT;
  19 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.USER_TASK_APPROVE_METHOD;
  20 +
  21 +/**
  22 + * 按拒绝人数计算会签的完成条件的流程表达式实现
  23 + *
  24 + * @author jason
  25 + */
  26 +@Component
  27 +@Slf4j
  28 +public class CompleteByRejectCountExpression {
  29 +
  30 + /**
  31 + * 会签的完成条件
  32 + */
  33 + public boolean completionCondition(DelegateExecution execution) {
  34 + FlowElement flowElement = execution.getCurrentFlowElement();
  35 + // 实例总数
  36 + Integer nrOfInstances = (Integer) execution.getVariable("nrOfInstances");
  37 + // 完成的实例数
  38 + Integer nrOfCompletedInstances = (Integer) execution.getVariable("nrOfCompletedInstances");
  39 + // 审批方式
  40 + Integer approveMethod = NumberUtils.parseInt(BpmnModelUtils.parseExtensionElement(flowElement, USER_TASK_APPROVE_METHOD));
  41 + Assert.notNull(approveMethod, "审批方式不能空");
  42 + // 计算拒绝的人数
  43 + Integer rejectCount = CollectionUtils.getSumValue(execution.getExecutions(),
  44 + item -> Objects.equals(BpmTaskStatusEnum.REJECT.getStatus(), item.getVariableLocal(BpmConstants.TASK_VARIABLE_STATUS, Integer.class)) ? 1 : 0,
  45 + Integer::sum, 0);
  46 + // 同意的人数为 完成人数 - 拒绝人数
  47 + int agreeCount = nrOfCompletedInstances - rejectCount;
  48 + // 1. 多人会签(通过只需一人,拒绝需要全员)
  49 + if (Objects.equals(ANY_APPROVE_ALL_REJECT.getMethod(), approveMethod)) {
  50 + // 1.1 一人同意. 会签任务完成
  51 + if (agreeCount > 0) {
  52 + return true;
  53 + } else {
  54 + // 1.2 所有人都拒绝了。设置任务拒绝变量, 会签任务完成。 后续终止流程在 ServiceTask【MultiInstanceServiceTaskExpression】处理
  55 + if (Objects.equals(nrOfInstances, rejectCount)) {
  56 + execution.setVariable(String.format("%s_reject",flowElement.getId()), Boolean.TRUE);
  57 + return true;
  58 + }
  59 + return false;
  60 + }
  61 + }
  62 + // TODO 多人会签(按比例投票)
  63 + log.error("[completionCondition] 按拒绝人数计算会签的完成条件的审批方式[{}],配置有误", approveMethod);
  64 + throw exception(GlobalErrorCodeConstants.ERROR_CONFIGURATION);
  65 + }
  66 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/MultiInstanceServiceTaskExpression.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable.core.expression;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.BooleanUtil;
  5 +import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
  6 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
  7 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  8 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
  9 +import jakarta.annotation.Resource;
  10 +import org.flowable.engine.delegate.DelegateExecution;
  11 +import org.flowable.engine.delegate.JavaDelegate;
  12 +import org.springframework.stereotype.Component;
  13 +
  14 +/**
  15 + * 处理会签 Service Task 代理表达式
  16 + *
  17 + * @author jason
  18 + */
  19 +@Component
  20 +public class MultiInstanceServiceTaskExpression implements JavaDelegate {
  21 +
  22 + @Resource
  23 + private BpmProcessInstanceService processInstanceService;
  24 +
  25 + @Override
  26 + public void execute(DelegateExecution execution) {
  27 + String attachUserTaskId = BpmnModelUtils.parseExtensionElement(execution.getCurrentFlowElement(),
  28 + BpmnModelConstants.SERVICE_TASK_ATTACH_USER_TASK_ID);
  29 + Assert.notNull(attachUserTaskId, "附属的用户任务 Id 不能为空");
  30 + // 获取会签任务是否被拒绝
  31 + Boolean userTaskRejected = execution.getVariable(String.format("%s_reject", attachUserTaskId), Boolean.class);
  32 + // 如果会签任务被拒绝, 终止流程
  33 + if (BooleanUtil.isTrue(userTaskRejected)) {
  34 + processInstanceService.updateProcessInstanceReject(execution.getProcessInstanceId(),
  35 + BpmCommentTypeEnum.REJECT.formatComment("会签任务拒绝人数满足条件"));
  36 + }
  37 + }
  38 +}
... ...
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;
26 26  
27 27 import static cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType.USER_TASK_TIMEOUT;
28 28 import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.*;
  29 +import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_NUMBER;
29 30 import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutActionEnum.AUTO_REMINDER;
30 31 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
31 32 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.SimpleModelConstants.*;
... ... @@ -55,6 +56,11 @@ public class SimpleModelUtils {
55 56 */
56 57 public static final String ANY_OF_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances > 0 }";
57 58  
  59 + /**
  60 + * 按拒绝人数计算多实例完成条件的表达式
  61 + */
  62 + public static final String COMPLETE_BY_REJECT_COUNT_EXPRESSION = "${completeByRejectCountExpression.completionCondition(execution)}";
  63 +
58 64 // TODO-DONE @jason:建议方法名,改成 buildBpmnModel
59 65 // TODO @yunai:注释需要完善下;
60 66  
... ... @@ -71,10 +77,6 @@ public class SimpleModelUtils {
71 77 // 不加这个 解析 Message 会报 NPE 异常 .
72 78 bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason:待定:是不是搞个自定义的 namespace;
73 79 // TODO 芋艿:后续在 review
74   - // @芋艿 这个 Message 可以去掉 暂时用不上
75   - Message rejectPostProcessMsg = new Message();
76   - rejectPostProcessMsg.setName(REJECT_POST_PROCESS_MESSAGE_NAME);
77   - bpmnModel.addMessage(rejectPostProcessMsg);
78 80  
79 81 Process process = new Process();
80 82 process.setId(processId);
... ... @@ -107,19 +109,30 @@ public class SimpleModelUtils {
107 109 if (nodeType == END_NODE) {
108 110 return;
109 111 }
110   -
111 112 // 2.1 情况一:普通节点
112 113 BpmSimpleModelNodeVO childNode = node.getChildNode();
113 114 if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) {
114 115 if (!isValidNode(childNode)) {
115 116 // 2.1.1 普通节点且无孩子节点。分两种情况
116 117 // a.结束节点 b. 条件分支的最后一个节点.与分支节点的孩子节点或聚合节点建立连线。
117   - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null);
118   - process.addFlowElement(sequenceFlow);
  118 + if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
  119 + // 2.1.1.1 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线
  120 + List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), targetNodeId);
  121 + sequenceFlows.forEach(process::addFlowElement);
  122 + } else {
  123 + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null);
  124 + process.addFlowElement(sequenceFlow);
  125 + }
119 126 } else {
120 127 // 2.1.2 普通节点且有孩子节点。建立连线
121   - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null);
122   - process.addFlowElement(sequenceFlow);
  128 + if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
  129 + // 2.1.1.2 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线
  130 + List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), childNode.getId());
  131 + sequenceFlows.forEach(process::addFlowElement);
  132 + } else {
  133 + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null);
  134 + process.addFlowElement(sequenceFlow);
  135 + }
123 136 // 递归调用后续节点
124 137 traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
125 138 }
... ... @@ -174,6 +187,18 @@ public class SimpleModelUtils {
174 187 }
175 188  
176 189 /**
  190 + * 构建有附加节点的连线
  191 + * @param nodeId 当前节点 Id
  192 + * @param attachNodeId 附属节点 Id
  193 + * @param targetNodeId 目标节点 Id
  194 + */
  195 + private static List<SequenceFlow> buildAttachNodeSequenceFlow(String nodeId, String attachNodeId, String targetNodeId) {
  196 + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(nodeId, attachNodeId, null, null, null);
  197 + SequenceFlow attachSequenceFlow = buildBpmnSequenceFlow(attachNodeId, targetNodeId, null, null, null);
  198 + return CollUtil.newArrayList(sequenceFlow, attachSequenceFlow);
  199 + }
  200 +
  201 + /**
177 202 * 构造条件表达式
178 203 *
179 204 * @param conditionNode 条件节点
... ... @@ -331,9 +356,28 @@ public class SimpleModelUtils {
331 356 BoundaryEvent boundaryEvent = buildUserTaskTimerBoundaryEvent(userTask, userTaskConfig.getTimeoutHandler());
332 357 flowElements.add(boundaryEvent);
333 358 }
  359 + // 如果按拒绝人数终止流程。需要添加附加的 ServiceTask 处理
  360 + if (userTaskConfig.getRejectHandler() != null &&
  361 + Objects.equals(FINISH_PROCESS_BY_REJECT_NUMBER.getType(), userTaskConfig.getRejectHandler().getType())) {
  362 + ServiceTask serviceTask = buildMultiInstanceServiceTask(node);
  363 + flowElements.add(serviceTask);
  364 + }
334 365 return flowElements;
335 366 }
336 367  
  368 + private static ServiceTask buildMultiInstanceServiceTask(BpmSimpleModelNodeVO node) {
  369 + ServiceTask serviceTask = new ServiceTask();
  370 + String id = String.format("Activity-%s", IdUtil.fastSimpleUUID());
  371 + serviceTask.setId(id);
  372 + serviceTask.setName("会签服务任务");
  373 + serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
  374 + serviceTask.setImplementation("${multiInstanceServiceTaskExpression}");
  375 + serviceTask.setAsynchronous(false);
  376 + addExtensionElement(serviceTask, SERVICE_TASK_ATTACH_USER_TASK_ID, node.getId());
  377 + node.setAttachNodeId(id);
  378 + return serviceTask;
  379 + }
  380 +
337 381 private static BoundaryEvent buildUserTaskTimerBoundaryEvent(UserTask userTask, SimpleModelUserTaskConfig.TimeoutHandler timeoutHandler) {
338 382 // 定时器边界事件
339 383 BoundaryEvent boundaryEvent = new BoundaryEvent();
... ... @@ -468,6 +512,9 @@ public class SimpleModelUtils {
468 512 if (bpmApproveMethodEnum == null || bpmApproveMethodEnum == BpmApproveMethodEnum.SINGLE_PERSON_APPROVE) {
469 513 return;
470 514 }
  515 + // 添加审批方式的扩展属性
  516 + addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD,
  517 + approveMethod == null ? null : approveMethod.toString());
471 518 MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
472 519 // 设置 collectionVariable。本系统用不到。会在 仅仅为了校验。
473 520 multiInstanceCharacteristics.setInputDataItem("${coll_userList}");
... ... @@ -484,8 +531,7 @@ public class SimpleModelUtils {
484 531 multiInstanceCharacteristics.setLoopCardinality("1");
485 532 userTask.setLoopCharacteristics(multiInstanceCharacteristics);
486 533 } else if (bpmApproveMethodEnum == BpmApproveMethodEnum.ANY_APPROVE_ALL_REJECT) {
487   - // 这种情况。拒绝任务时候,不会终止或者完成任务 参见 BpmTaskService#rejectTask 方法
488   - multiInstanceCharacteristics.setCompletionCondition(ANY_OF_APPROVE_COMPLETE_EXPRESSION);
  534 + multiInstanceCharacteristics.setCompletionCondition(COMPLETE_BY_REJECT_COUNT_EXPRESSION);
489 535 multiInstanceCharacteristics.setSequential(false);
490 536 }
491 537 // TODO 会签(按比例投票 )
... ...
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;
35 35 import org.flowable.engine.ManagementService;
36 36 import org.flowable.engine.RuntimeService;
37 37 import org.flowable.engine.TaskService;
38   -import org.flowable.engine.runtime.Execution;
39 38 import org.flowable.engine.runtime.ProcessInstance;
40 39 import org.flowable.task.api.DelegationState;
41 40 import org.flowable.task.api.Task;
... ... @@ -352,30 +351,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
352 351 .setReason(reqVO.getReason());
353 352 returnTask(userId, returnReq);
354 353 return;
355   - } else if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_RATIO) {
356   - // 3.3 按拒绝人数比例终止流程
  354 + } else if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_NUMBER) {
  355 + // 3.3 按拒绝人数终止流程
357 356 if (!flowElement.hasMultiInstanceLoopCharacteristics()) {
358 357 log.error("[rejectTask] 用户任务拒绝处理类型配置错误, 按拒绝人数终止流程只能用于会签任务");
359 358 throw exception(GlobalErrorCodeConstants.ERROR_CONFIGURATION);
360 359 }
361   - // 获取并行任务总数
362   - Execution execution = runtimeService.createExecutionQuery().processInstanceId(task.getProcessInstanceId())
363   - .executionId(task.getExecutionId()).singleResult();
364   - Integer nrOfInstances = runtimeService.getVariable(execution.getParentId(), "nrOfInstances", Integer.class);
365   - // 获取未完成任务列表
366   - List<Task> taskList = getTaskListByProcessInstanceIdAndAssigned(task.getProcessInstanceId(), null,
367   - task.getTaskDefinitionKey());
368   - // 获取已经拒绝的任务数
369   - Integer rejectNumber = getSumValue(taskList,
370   - item -> Objects.equals(BpmTaskStatusEnum.REJECT.getStatus(), FlowableUtils.getTaskStatus(item)) ? 1 : 0,
371   - Integer::sum, 0);
372   -// // TODO @jason:如果这样的话,后续会不会在【已完成】里面查询不到哈?【重要!!!!】
373   -// // 拒绝任务后,任务分配人清空。但不能完成任务
374   -// taskService.setAssignee(task.getId(), "");
375   - // 不是所有人拒绝返回。 TODO 后续需要做按拒绝人数比例来判断
376   - if (!Objects.equals(nrOfInstances, rejectNumber)) {
377   - return;
378   - }
  360 + // 设置变量值为拒绝
  361 + runtimeService.setVariableLocal(task.getExecutionId(), BpmConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.REJECT.getStatus());
  362 + // 完成任务
  363 + taskService.complete(task.getId());
  364 + return;
379 365 }
380 366 // 3.4 其他情况 终止流程。 TODO 后续可能会增加处理类型
381 367 processInstanceService.updateProcessInstanceReject(instance.getProcessInstanceId(), reqVO.getReason());
... ...