flowable 中级 - 3. 节点回退、驳回

4,218 阅读6分钟

1、需求

国内做工作流需求都会遇到驳回的需求,有只回到上个节点,也有驳回是提供可供驳回的节点列表,然后用户选择回到哪个节点。

2、解决方案

我能想到解决方案有两个,一个通过加 排他网关跟条件来实现,另一个是引擎提供的节点跳转

2.1 排他网关

在用Activity时我当时初步了解到不支持驳回,这都是国内特有需求。然后实在要做就通过排他网关,通过条件、连线方式来实现

gateway-back.png

优点:这种解决方案就是流程到底该怎么走,完完全全按照引擎的规则走就行,都不需要额外进行代码开发

缺点:

1. 必须要在引擎层面有网关+线(有些产品设计不接受在配置时需要拖网关、线)
1. 不支持动态驳回到任意节点。这种动态驳回到任意节点会让真个设计的图形显得非常复杂,凌乱

其实对于第一种,可以给用户展示的流程节点没有网关+线(也就是换一套展示UI),但是真正xml里面必须的有网关、线这些。这么这时要么时前端需要来额外处理,后者后端需要额外来处理。也是工作量

对于第二点,越复杂的流程设计,这种通过网关+线的方式越难做,很可能会导致你线画错了,完全不合符业务逻辑

2.2 节点跳转

flowable在6.3.0在了ChangeActivityStateBuilder类增加了跳转的实现

顾名思义,引擎已经支持给定两个节点:从起始节点跳到目标节点。

这样就极大层面方便我们做回退功能了。调一下ChangeActivityStateBuilder类下面的api就行

  • moveActivityIdTo(String currentActivityId, String newActivityId)
  • moveActivityIdsToSingleActivityId(List currentActivityIds, String newActivityId)
  • moveSingleActivityIdToActivityIds(String currentActivityId, List newActivityIds)
  • ...

上面只是简单列举了通过activityId来进行跳转的方式,还支持executionId

调用形式的话,就很简单了

runtimeService.createChangeActivityStateBuilder()
  .processInstanceId(dto.getProcInstId())
  .moveActivityIdsToSingleActivityId(currentActivityIds, backTargetNode)
  .changeState();

看到网上很多都在说是6.4.0里面增加的,我去翻了 6.4.0的release说明 里面完全没有提及跳转相关的。不过倒是提到迁移了。这是回退的另一个种实现方案,但是感觉成本更大,而且可能不符合产品预期。这个有机会回头再说

引用部分

A first version of process instance migration has been added to the Flowable BPMN Engine. In the RuntimeService you can now use the createProcessInstanceMigrationBuilder to define and execute the process instance migration

3 高级篇

3.1 业务场景

上面说了最基本的。但是回退牵涉到业务,就需要考虑更多的问题,比如 如下场景

back-gateway-overview.png

前提1:如果此时流程走到 B12、B21、C1、C21、C22 这5个节点时。

此时可能需求场景:

  1. 需要从B12 回退到 B11(不垮网关)
    1. 此时回退很简单,不影响其他平行线的节点
  2. 需要从B12回退到B节点(跨一层网关)
    1. 此时需要额外处理B21节点。都已经回退到B节点,B21还能继续处理不符合业务
  3. 需要从B12回退到A节点(跨两层网关)
    1. 此时需要额外处理B21,C1,C21,C22 四个节点

前提2:如果此时流程走到E节点时 (图省略)

此时可能需求场景:

  1. 需要从E节点回退到任意分支里,比如 B11
    1. 从图上看,没有额外处理,直接跳转就行
  2. 需要从E节点回退到分支外,即回退到A节点
    1. 从图上看,没有额外处理,直接跳转就行

须知:我这边因为没有用到子流程,所有没有考虑子流程情况!!!

3.2 层级标识处理

对于场景1里面提及的跨网关跳转,需要处理平行节点带来的问题,这个之前是一只困扰我

然后我去网上找了看是否别人跟我有一样的需求,结果真只让我找到一个 Flowable并行网关跳转

简单描述下它的方式:他是通过给每一个层网关的节点都打上层级标识,然后通过层级标识来处理被影响的平行节点

但是我思考下来,如果只是通过给每层打层级标识,会导致另一个问题


用这个场景说明:从B12回退到B节点(跨一层网关)

按照打层级标签的方式:B11、B12、B21、B22 、C1、C2都属于同一层级,C21、C22属于同一层级

规定:

  • A、E属于第一层级
  • B、C、D属于第二层级
  • B11、B12、B21、B22 、C1、C2属于第三层级
  • C21、C22属于第四层级

当第三层级往第二层级跳,要不要额外处理同层级的,甚至更下一层(第四层级)?这就是个问题了

如果是 从B12回退到B节点(从第三层跳到第二层)。B21属于第三层,需要处理, C1属于第三层级的不需要处理,C21、C22属于第四层级也不需要处理

如果是 从B12回退到A节点(从第三层跳到第 一层)。B21属于第三层,需要处理,属于 C1属于第三层级的需要处理,C21、C22属于第四层级也需要处理

所以只有层级标识,是处理不了这样复杂的情况,当然如果能结合树的结构+层级就可解决这个问题

3.3 层级结构处理

而我目前给出的解决方案很简单,就是把层级结构找出来就行

我最终实现了这个一个东西

back-parent-child.png

把每一层下面的直属节点找到就行。然后找到起始节点到目标节点会影响到哪些节点就可以了

核心代码


	/**
	 * 找到回退到目标节点是 受影响的范围的节点
	 *
	 * @param procDefId
	 * @param targetTaskKey 目标节点
	 * @return 受影响的范围的节点列表
	 */
	public List<String> findNodeListFallBackScope(String procDefId, String targetTaskKey) {
		Process mainProcess = this.getProcess(procDefId);
		this.getProcessAndCheck(mainProcess);

		Collection<FlowElement> flowElementList = mainProcess.getFlowElements();

		// 记录网关跟前面节点的关系
		Map<String, String> gatewayBeforeElementMap = new HashMap<>();
		// 将线连接的前后两个任务记录下来
		Map<String, String> userTaskAfterElementMap = new HashMap<>();

		// 正向记录 父级节点下面有哪些直接子节点
		Map<String, List<String>> parentChildrenMap = new HashMap<>();
		// 方向记录,每个子节点的父级是谁
		Map<String, String> childrenParentMap = new HashMap<>();

		Map<String, String> nameElementMap = new HashMap<>();

		for (FlowElement flowElement : flowElementList) {
			log.info("{}, {}", flowElement.getName(), flowElement.getId());
			nameElementMap.put(flowElement.getId(), flowElement.getName());
		}


		// 拿到第一个用户任务节点的负责人配置
		for (FlowElement flowElement : flowElementList) {
			if (flowElement instanceof UserTask) {
				UserTask userTask = (UserTask) flowElement;

				// 将任务归在各层的网关下
				String beforeElementName = userTaskAfterElementMap.get(userTask.getName());
				if (StringUtils.isNotEmpty(beforeElementName)) {
					String parentName = childrenParentMap.get(beforeElementName);
					List<String> childNames = parentChildrenMap.get(parentName);
					if (childNames != null) {
						childNames.add(userTask.getName());
					} else {
						log.error("父级别存在预期之外的空元素");
					}
				}

			} else if (flowElement instanceof Gateway) {
				Gateway gateway = (Gateway) flowElement;
				// 记录网关的上一个节点
				if (gateway.getIncomingFlows().size() == 1) {
					gatewayBeforeElementMap.put(flowElement.getName(), nameElementMap.get(gateway.getIncomingFlows().get(0).getSourceRef()));
				}

				// 处理网关的出口
				List<SequenceFlow> sequenceFlowList = gateway.getOutgoingFlows();
				if (sequenceFlowList.size() > 1) {
					// 这种直接认为是分叉 TODO 可能存在问题
					List<String> childrenName = new ArrayList<>();
					String parent = gatewayBeforeElementMap.get(gateway.getName());

					for (SequenceFlow sequenceFlow : sequenceFlowList) {
						String child = nameElementMap.get(sequenceFlow.getTargetRef());
						childrenName.add(child);
						// 将分叉后的每个子节点反向跟父节点关联起来
						childrenParentMap.put(child, parent);
					}
					// 把分叉的的所有节点全部关联到网关之前的节点
					parentChildrenMap.put(parent, childrenName);
				}
			} else if (flowElement instanceof SequenceFlow) {
				String sourceId = ((SequenceFlow) flowElement).getSourceRef();
				String targetRefId = ((SequenceFlow) flowElement).getTargetRef();
				if (sourceId.startsWith("Activity_") && targetRefId.startsWith("Activity_")) {
					// 将线连接的前后两个任务记录下来,后续判断是否在同一分支
					// B11 - > B12. 存的时候 B12作为key, B11作为value
					userTaskAfterElementMap.put(nameElementMap.get(targetRefId), nameElementMap.get(sourceId));
				}
			}
		}

		String targetTaskName = nameElementMap.get(targetTaskKey);
		List<String> result = new ArrayList<>();
		List<String> childrenList = parentChildrenMap.get(targetTaskName);
		nestHandler(childrenList, result, parentChildrenMap);

		return result;
	}

	/**
	 * 嵌套处理
	 * @param childrenList
	 * @param result
	 * @param parentChildrenMap
	 */
	private void nestHandler(List<String> childrenList, List<String> result, Map<String, List<String>> parentChildrenMap) {
		if (childrenList != null) {
			for (String child : childrenList) {
				result.add(child);
				List<String> tmpChildrenList = parentChildrenMap.get(child);
				nestHandler(tmpChildrenList, result, parentChildrenMap);
			}
		}
	}
	

博文更改日志

  • 2022-11-23 初步开始基于调研写本篇文章
  • 2022-11-29 写完核心部分,基本完成本篇文章