diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java index 53bb2abc7..0f298a255 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java +++ b/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; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java index e13253e99..aab93ed1b 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java +++ b/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 attributes; // TODO @jason:建议是字段分拆下;类似说: + + /** + * 附加节点 Id, 该节点不从前端传入。 由程序生成. 由于当个节点无法完成功能。 需要附加节点来完成。 + * 例如: 会签时需要按拒绝人数来终止流程。 需要 userTask + ServiceTask 两个节点配合完成。 serviceTask 由后端生成。 + */ + private String attachNodeId; // Map formPermissions; 表单权限;仅发起、审批、抄送节点会使用 // Integer approveMethod; 审批方式;仅审批节点会使用 // TODO @jason 后面和前端一起调整一下 diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java index a3b0bc0c8..ada89443d 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java +++ b/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> SUPPORT_CONVERT_SIMPLE_FlOW_NODES = ImmutableSet.of(UserTask.class, EndEvent.class); - - String REJECT_POST_PROCESS_MESSAGE_NAME = "message_reject_post_process"; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/CompleteByRejectCountExpression.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/CompleteByRejectCountExpression.java new file mode 100644 index 000000000..99d121b95 --- /dev/null +++ b/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); + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/MultiInstanceServiceTaskExpression.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/expression/MultiInstanceServiceTaskExpression.java new file mode 100644 index 000000000..5d2fc5522 --- /dev/null +++ b/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("会签任务拒绝人数满足条件")); + } + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index 7b2f2ef81..4ad525ca1 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/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 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 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 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 会签(按比例投票 ) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 281a42b08..13e696f6a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/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 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());