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

This commit is contained in:
jason 2024-06-07 22:07:47 +08:00
parent 0db7796c62
commit 12108e7365
7 changed files with 185 additions and 35 deletions

View File

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

View File

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

View File

@ -50,6 +50,16 @@ public interface BpmnModelConstants {
*/ */
String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId"; 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 流程表单字段权限元素, 用于标记字段权限 * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
*/ */
@ -75,6 +85,4 @@ public interface BpmnModelConstants {
* 支持转仿钉钉设计模型的 Bpmn 节点 * 支持转仿钉钉设计模型的 Bpmn 节点
*/ */
Set<Class<? extends FlowNode>> SUPPORT_CONVERT_SIMPLE_FlOW_NODES = ImmutableSet.of(UserTask.class, EndEvent.class); 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";
} }

View File

@ -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 所有人都拒绝了设置任务拒绝变量, 会签任务完成 后续终止流程在 ServiceTaskMultiInstanceServiceTaskExpression处理
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);
}
}

View File

@ -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("会签任务拒绝人数满足条件"));
}
}
}

View File

@ -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.BpmBoundaryEventType.USER_TASK_TIMEOUT;
import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.*; 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.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.BpmnModelConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.SimpleModelConstants.*; 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 ANY_OF_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances > 0 }";
/**
* 按拒绝人数计算多实例完成条件的表达式
*/
public static final String COMPLETE_BY_REJECT_COUNT_EXPRESSION = "${completeByRejectCountExpression.completionCondition(execution)}";
// TODO-DONE @jason建议方法名改成 buildBpmnModel // TODO-DONE @jason建议方法名改成 buildBpmnModel
// TODO @yunai注释需要完善下 // TODO @yunai注释需要完善下
@ -71,10 +77,6 @@ public class SimpleModelUtils {
// 不加这个 解析 Message 会报 NPE 异常 . // 不加这个 解析 Message 会报 NPE 异常 .
bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason待定是不是搞个自定义的 namespace bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason待定是不是搞个自定义的 namespace
// TODO 芋艿后续在 review // TODO 芋艿后续在 review
// @芋艿 这个 Message 可以去掉 暂时用不上
Message rejectPostProcessMsg = new Message();
rejectPostProcessMsg.setName(REJECT_POST_PROCESS_MESSAGE_NAME);
bpmnModel.addMessage(rejectPostProcessMsg);
Process process = new Process(); Process process = new Process();
process.setId(processId); process.setId(processId);
@ -107,19 +109,30 @@ public class SimpleModelUtils {
if (nodeType == END_NODE) { if (nodeType == END_NODE) {
return; return;
} }
// 2.1 情况一普通节点 // 2.1 情况一普通节点
BpmSimpleModelNodeVO childNode = node.getChildNode(); BpmSimpleModelNodeVO childNode = node.getChildNode();
if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) { if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) {
if (!isValidNode(childNode)) { if (!isValidNode(childNode)) {
// 2.1.1 普通节点且无孩子节点分两种情况 // 2.1.1 普通节点且无孩子节点分两种情况
// a.结束节点 b. 条件分支的最后一个节点.与分支节点的孩子节点或聚合节点建立连线 // a.结束节点 b. 条件分支的最后一个节点.与分支节点的孩子节点或聚合节点建立连线
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null); if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
process.addFlowElement(sequenceFlow); // 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 { } else {
// 2.1.2 普通节点且有孩子节点建立连线 // 2.1.2 普通节点且有孩子节点建立连线
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null); if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
process.addFlowElement(sequenceFlow); // 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); 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()); BoundaryEvent boundaryEvent = buildUserTaskTimerBoundaryEvent(userTask, userTaskConfig.getTimeoutHandler());
flowElements.add(boundaryEvent); 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; 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) { private static BoundaryEvent buildUserTaskTimerBoundaryEvent(UserTask userTask, SimpleModelUserTaskConfig.TimeoutHandler timeoutHandler) {
// 定时器边界事件 // 定时器边界事件
BoundaryEvent boundaryEvent = new BoundaryEvent(); BoundaryEvent boundaryEvent = new BoundaryEvent();
@ -468,6 +512,9 @@ public class SimpleModelUtils {
if (bpmApproveMethodEnum == null || bpmApproveMethodEnum == BpmApproveMethodEnum.SINGLE_PERSON_APPROVE) { if (bpmApproveMethodEnum == null || bpmApproveMethodEnum == BpmApproveMethodEnum.SINGLE_PERSON_APPROVE) {
return; return;
} }
// 添加审批方式的扩展属性
addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD,
approveMethod == null ? null : approveMethod.toString());
MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics(); MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
// 设置 collectionVariable本系统用不到会在 仅仅为了校验 // 设置 collectionVariable本系统用不到会在 仅仅为了校验
multiInstanceCharacteristics.setInputDataItem("${coll_userList}"); multiInstanceCharacteristics.setInputDataItem("${coll_userList}");
@ -484,8 +531,7 @@ public class SimpleModelUtils {
multiInstanceCharacteristics.setLoopCardinality("1"); multiInstanceCharacteristics.setLoopCardinality("1");
userTask.setLoopCharacteristics(multiInstanceCharacteristics); userTask.setLoopCharacteristics(multiInstanceCharacteristics);
} else if (bpmApproveMethodEnum == BpmApproveMethodEnum.ANY_APPROVE_ALL_REJECT) { } else if (bpmApproveMethodEnum == BpmApproveMethodEnum.ANY_APPROVE_ALL_REJECT) {
// 这种情况拒绝任务时候不会终止或者完成任务 参见 BpmTaskService#rejectTask 方法 multiInstanceCharacteristics.setCompletionCondition(COMPLETE_BY_REJECT_COUNT_EXPRESSION);
multiInstanceCharacteristics.setCompletionCondition(ANY_OF_APPROVE_COMPLETE_EXPRESSION);
multiInstanceCharacteristics.setSequential(false); multiInstanceCharacteristics.setSequential(false);
} }
// TODO 会签(按比例投票 ) // TODO 会签(按比例投票 )

View File

@ -35,7 +35,6 @@ import org.flowable.engine.HistoryService;
import org.flowable.engine.ManagementService; import org.flowable.engine.ManagementService;
import org.flowable.engine.RuntimeService; import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService; import org.flowable.engine.TaskService;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.DelegationState; import org.flowable.task.api.DelegationState;
import org.flowable.task.api.Task; import org.flowable.task.api.Task;
@ -352,30 +351,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
.setReason(reqVO.getReason()); .setReason(reqVO.getReason());
returnTask(userId, returnReq); returnTask(userId, returnReq);
return; return;
} else if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_RATIO) { } else if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.FINISH_PROCESS_BY_REJECT_NUMBER) {
// 3.3 按拒绝人数比例终止流程 // 3.3 按拒绝人数终止流程
if (!flowElement.hasMultiInstanceLoopCharacteristics()) { if (!flowElement.hasMultiInstanceLoopCharacteristics()) {
log.error("[rejectTask] 用户任务拒绝处理类型配置错误, 按拒绝人数终止流程只能用于会签任务"); log.error("[rejectTask] 用户任务拒绝处理类型配置错误, 按拒绝人数终止流程只能用于会签任务");
throw exception(GlobalErrorCodeConstants.ERROR_CONFIGURATION); throw exception(GlobalErrorCodeConstants.ERROR_CONFIGURATION);
} }
// 获取并行任务总数 // 设置变量值为拒绝
Execution execution = runtimeService.createExecutionQuery().processInstanceId(task.getProcessInstanceId()) runtimeService.setVariableLocal(task.getExecutionId(), BpmConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.REJECT.getStatus());
.executionId(task.getExecutionId()).singleResult(); // 完成任务
Integer nrOfInstances = runtimeService.getVariable(execution.getParentId(), "nrOfInstances", Integer.class); taskService.complete(task.getId());
// 获取未完成任务列表 return;
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;
}
} }
// 3.4 其他情况 终止流程 TODO 后续可能会增加处理类型 // 3.4 其他情况 终止流程 TODO 后续可能会增加处理类型
processInstanceService.updateProcessInstanceReject(instance.getProcessInstanceId(), reqVO.getReason()); processInstanceService.updateProcessInstanceReject(instance.getProcessInstanceId(), reqVO.getReason());