应用开发平台集成工作流——流程建模功能路由分支转换设计与实现

背景

对于流程设置不友好的问题,国内钉钉另行设计与实现了一套流程建模模式,跟bpmn规范无关,有人仿照实现了下,并做了开源(https://github.com/StavinLi/Workflow-Vue3),效果图如下:
应用开发平台集成工作流——流程建模功能路由分支转换设计与实现_第1张图片
实现大致原理是基于无限嵌套的子节点,输出json数据,传给后端,后端进行解析后,调用Camunda引擎的api,转换成流程模型后持久化。

上篇介绍了办理节点的转换设计与实现。虽然办理节点是流程的主要组成部分,但实际业务流程,还需要一些逻辑分支。例如请假流程中,请假天数3天以内部门经理批准即可,3天以上需要副总审批,这时候就需要用到条件分支。在比如,一份市局的基建方案,会涉及到若干个下属县局,流程需要走到相关的下属县局,这时候就需要用到并行分支。

基础概念

基于工作流的流程处理,除了常见的线性流转外,还需要分支处理。
分支有两种情况,一是并行分支,所有分支都会执行;二是条件选择分支,满足设定的条件才会执行。
Camunda使用网关来处理分支,主要是以下三种:

  1. 排他网关(Exclusive Gateway):只允许一条分支执行,根据条件表达式或规则选择下一个节点。
  2. 并行网关(Parallel Gateway):同时执行多条分支,当所有分支完成后,才继续执行下一个节点。
  3. 兼容网关(Inclusive Gateway):允许多条分支执行,并根据条件表达式或规则选择下一个节点,但如果没有任何一个分支满足条件,则选择默认分支。

并行网关是所有分支都会执行,不需要设置条件或规则。

兼容网关和排他网关的区别如下:
执行数量不同:兼容网关允许多条分支执行;排他网关只允许一条分支执行。
选择方式不同:兼容网关根据条件表达式或规则选择下一个节点;排他网关根据第一条符合条件的分支执行默认分支。
处理方式不同:兼容网关会计算所有出口顺序流;排他网关只处理计算为true的出口顺序流。

方案设计

有没有必要使用排他网关?

从功能上,兼容网关是包含排他网关的,或者说,排他网关是兼容网关的一种特例,即只有一条分支满足条件。从性能角度考虑差别,二者差别主要在于找到一条满足条件的分支就停止计算,还是计算所有分支。在流程分支有限的情况下(一般也就三五条,最多不会超过个位数范围),部分计算跟全部计算没多少差别,不差那点资源和性能。
从功能角度和用户体验而言,有排他网关逻辑更清晰一些,用户很明确知道在多个条件中只能选择1个,不过这点用户体验提升也非常有限。

有没有必要使用并行网关?

从功能上,兼容网关实际也包含并行网关,经测试,分支上的不设置条件,Camunda默认视为满足条件,则意味着所有分支都能走,这么做,直观上感觉有点过度使用兼容网关了,会造成一定程度并行分支和条件分支逻辑不清,不过对于用户而言,区分条件和并行这两个概念,未必比只有一个概念不区分更好。

方案选择

专门看了下钉钉的流程模型,没有专门的排他网关,只有两类,一类是并行分支,另一类是条件分支,这两种称呼也更符合业务含义,便于用户理解。

从业务使用角度而言,就保留一个概念,分支,到底是只能走一条、走若干条还是全部走,取决于分支上设置条件是否满足即可,不需要明确区分到底是条件分支还是并行分支,对于业务用户而言更友好。

基于上述考虑,去除掉排他网关,并行网关,只使用兼容网关,前端一律使用路由和分支来描述。

方案实现

平台用于集成的Workflow-vue3开源项目,内置了条件分支,进行改造,来满足自己的需求,我们来实现一个相对简单的请假审批流程,流程图如下:
应用开发平台集成工作流——流程建模功能路由分支转换设计与实现_第2张图片

前端实现

修改nodeWrap.vue,对于路由分支单独处理,类型编码设置为INCLUSIVE_GATEWAY

  
  
<
{{ item.name }}
>
{{ $func.conditionStr(modelValue, index) }}

原开源项目,条件分支上有优先级的设置,意味了优先计算优先级高的边,一旦满足则不在计算其他边上的条件,因此实际对应的是排他网关,即只会走一条分支。在实际业务场景中,会出现同时走多条件分支的需求。因此平台需要实现兼容网关,计算所有边上的条件,这种情况下,优先级实际就没有意义,进行移除。

修改addNode.vue,设置默认的路由数据,条件节点类型编码设置为CONDITION

const addConditionBranch = () => {
  visible.value = false
  const data = {
    name: '路由',
    id: 'node' + uuid(),
    type: 'INCLUSIVE_GATEWAY',
    config: {},
    child: null,
    branchList: [
      {
        name: '条件1',
        id: 'condition' + uuid(),
        type: 'CONDITION',
        config: {},
        branchList: [],
        child: props.childNodeP
      },
      {
        name: '条件2',
        id: 'condition' + uuid(),
        type: 'CONDITION',
        config: {},
        branchList: []
      }
    ]
  }
  emits('update:childNodeP', data)
}

关于条件的设置,实际有两种模式,一种是面向业务用户,需要提供可视化的配置,如在合同审批流程中,选择合同金额属性,设置条件为大于100万,同时有可能增加其他与或者是或运算,由平台转换成可供平台处理的表达式。另外一种则是面向开发人员,毕竟流程环节的处理逻辑,还是依赖开发人员编写,特别是条件表达式里面使用到的变量,还是在流程环节中的逻辑中进行处理的。平台目前定位实际是面向开发人员的,即低代码配置为主,提升开发效率和降低开发成本,源码开发为辅,保障业务逻辑特别是复杂逻辑的实现,以及良好的扩展性。基于平台的定位,这里条件边设置,只放一个文本框,由开发人员设置最终的条件表达式就可以了,如${contractMoney>1000000},简便实用。

新增条件表达式设置组件





后端实现

转换逻辑

在Camunda模型中,实际没有具体的分支节点和汇聚节点,都是网关节点。
因此存在某个网关节点,即起到汇聚作用,又起到分支作用。
前后端模型独立实现的情况下,前端路由节点,对应着Camunda的网关,条件节点,对应着Camunda条件边。
钉钉流程模型中,条件分支并没有显性的汇聚节点,需要后端自行判断补充。

在条件分支这种场景下,前端自建的流程模型与后端Camunda模型实质上产生了一定的差异化。
对于前端而已,条件分支节点是多个节点的组合,包括路由节点、条件节点,以及分支包含的办理节点,甚至于在分支中再嵌套条件分支节点。前端的数据结构是一个嵌套的对象,对于条件分支,示例如下:

{
	"name": "路由",
	"id": "node3278_00b0_e238_a105",
	"type": "INCLUSIVE_GATEWAY",
	"config": {},
	"child": null,
	"branchList": [{
			"name": "3天以内",
			"id": "condition5914_12fb_e783_f171",
			"type": "CONDITION",
			"config": {
				"expression": "${total<=3}"
			},
			"branchList": []
		},
		{
			"name": "超过3天",
			"id": "condition10081_fd56_1fb6_f8ed",
			"type": "CONDITION",
			"config": {
				"expression": "${total>3}"
			},
			"branchList": []
		}
	]
}

其分支数据,是放在属性branchList中,类型是一个node数组。对于后端而言,需要将类型为INCLUSIVE_GATEWAY的节点转换为兼容网关,然后读取该节点的branchList属性,将对应的node数组,每个元素转换成一条短流程,然后首节点对接兼容网关,末节点对接一个自动添加的汇聚节点。

核心转换逻辑实现

核心的模型转换参见下面方法的case INCLUSIVE_GATEWAY 分支,完整代码见开源项目。

  /**
     * 将json转换为模型
     * 流程节点转换
     *
     * @param process       流程
     * @param parentElement 父元素
     * @param flowNode      流程节点
     * @param tempVersion   临时版本
     * @param expression    表达式
     * @return {@link FlowNode}
     */
    private  FlowNode convertJsonToModel(Process process, FlowNode parentElement,
                                     MyFlowNode flowNode,String tempVersion,String expression) {
        // 获取模型实例
        ModelInstance modelInstance = process.getModelInstance();
        // 构建节点
        FlowNode element=null;
        FlowCodeTypeEnum type = EnumUtils.getEnum(FlowCodeTypeEnum.class, flowNode.getType());
        switch (type){
            case ROOT:
                UserTask firstNode = modelInstance.newInstance(UserTask.class);
                firstNode.setName(flowNode.getName());
                firstNode.setCamundaAssignee("${firstNodeAssignee}");
                firstNode.setId("node"+ UUID.randomUUID().toString());
                process.addChildElement(firstNode);
                element=firstNode;
                // 构建边
                createSequenceFlow(process, parentElement, element);
                break;
            case HANDLE:
                UserTask userTask = modelInstance.newInstance(UserTask.class);
                // 基本属性设置
                userTask.setName(flowNode.getName());
                userTask.setId("node"+UUID.randomUUID().toString());
                // 环节配置
                String config=flowNode.getConfig();
                WorkflowNodeConfig userTaskNodeConfig =JSON.parseObject(config, WorkflowNodeConfig.class) ;
                userTask.setCamundaCandidateGroups(userTaskNodeConfig.getUserGroup());
                if (userTaskNodeConfig.getMode().equals(NodeModeEnum.COUNTERSIGN.name())) {
                    //会签模式
                    //设置处理人为变量
                    userTask.setCamundaAssignee("${assignee}");
                    //设置多实例
                    MultiInstanceLoopCharacteristics loopCharacteristics =
                            modelInstance.newInstance(MultiInstanceLoopCharacteristics.class);

                    loopCharacteristics.setSequential(false);
                    loopCharacteristics.setCamundaCollection("${assigneeList}");
                    loopCharacteristics.setCamundaElementVariable("assignee");
                    userTask.addChildElement(loopCharacteristics);
                } else {
                    //普通模式
                    //设置处理人为变量
                    userTask.setCamundaAssignee("${singleHandler}");
                }

                // 附加固化的人员指派监听器
                ExtensionElements extensionElements=modelInstance.newInstance(ExtensionElements.class);

                CamundaTaskListener listener=modelInstance.newInstance(CamundaTaskListener.class);
                listener.setCamundaEvent("create");
                listener.setCamundaClass("tech.abc.platform.workflow.listener.ApproverTaskListener");
                extensionElements.addChildElement(listener);
                userTask.setExtensionElements(extensionElements);

                process.addChildElement(userTask);
                element=userTask;
                // 构建边
                SequenceFlow sequenceFlow = createSequenceFlow(process, parentElement, element);
                // 如表达式不为空,则意味着需要设置条件边
                if(StringUtils.isNotBlank(expression)){
                    ConditionExpression conditionExpression= modelInstance.newInstance(ConditionExpression.class);
                    conditionExpression.setTextContent(expression);
                    sequenceFlow.setConditionExpression(conditionExpression);
                    // 使用一次后置空
                    expression=null;
                }

                // 生成环节配置
                userTaskNodeConfig.setProcessDefinitionId(tempVersion);
                userTaskNodeConfig.setName(userTask.getName());
                userTaskNodeConfig.setNodeId(userTask.getId());
                flowNodeConfigService.add(userTaskNodeConfig);

                break;
            case INCLUSIVE_GATEWAY:
                InclusiveGateway node = modelInstance.newInstance(InclusiveGateway.class);
                process.addChildElement(node);
                // 基本属性设置
                node.setName(flowNode.getName());
                node.setId(flowNode.getId());
                // 构建入边
                SequenceFlow inflow = createSequenceFlow(process, parentElement, node);
                // 如表达式不为空,则意味着需要设置条件边
                if(StringUtils.isNotBlank(expression)){
                    ConditionExpression conditionExpression= modelInstance.newInstance(ConditionExpression.class);
                    conditionExpression.setTextContent(expression);
                    inflow.setConditionExpression(conditionExpression);
                    // 使用一次后置空
                    expression=null;
                }
                // 生成虚拟的汇聚节点
                InclusiveGateway convergeNode = modelInstance.newInstance(InclusiveGateway.class);
                process.addChildElement(convergeNode);
                convergeNode.setName("汇聚节点");
                convergeNode.setId("convergeNode"+UUID.randomUUID().toString());
                element=convergeNode;

                // 分支处理
                List<MyFlowNode> branchList = flowNode.getBranchList();
                // 转换分支
                branchList.stream().forEach(item->{
                    // 分支首节点涉及到在边上设置条件表达式
                    MyConditionNode myConditionNode = JSON.parseObject(item.getConfig(), MyConditionNode.class);
                    String branchExpression=myConditionNode.getExpression();
                    log.info("expression:{}",branchExpression);

                    if(item.getChild()!=null && StringUtils.isNotBlank(item.getChild().getName())) {


                        FlowNode brachEndNode = convertJsonToModel(process, node,
                                item.getChild(), tempVersion,branchExpression);
                        // 附加汇聚节点
                        createSequenceFlow(process, brachEndNode, convergeNode);
                    }else{
                        // 附加汇聚节点
                        SequenceFlow endFlow = createSequenceFlow(process, node, convergeNode);
                        ConditionExpression conditionExpression= modelInstance.newInstance(ConditionExpression.class);                      
                        conditionExpression.setTextContent(branchExpression);
                        inflow.setConditionExpression(conditionExpression);

                    }

                });

                break;
            case SERVICE_TASK:
                // TODO
                // element = modelInstance.newInstance(ServiceTask.class);
                break;
            default:
                log.error("未找到合适的类型");

        }

        //递归处理子节点
        if(flowNode.getChild()!=null && StringUtils.isNotBlank(flowNode.getChild().getName())){
            return convertJsonToModel(process,element,flowNode.getChild(),tempVersion,expression);
        }else{
            return element;
        }

    }


如何设置条件边?

条件边的设置这地方是个难点,推测应该有个API来完成这工作。边对象SequenceFlow,有属性来设置ConditionExpression属性,但这个属性并不是一个String,而是一个接口。

public interface ConditionExpression extends FormalExpression {
    String getType();

    void setType(String var1);

    String getCamundaResource();

    void setCamundaResource(String var1);
}

其实现类构造方法,要求传入的参数是另外一个奇怪的类对象ModelTypeInstanceContext……

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.camunda.bpm.model.bpmn.impl.instance;

import org.camunda.bpm.model.bpmn.instance.ConditionExpression;
import org.camunda.bpm.model.bpmn.instance.FormalExpression;
import org.camunda.bpm.model.xml.ModelBuilder;
import org.camunda.bpm.model.xml.impl.instance.ModelTypeInstanceContext;
import org.camunda.bpm.model.xml.type.ModelElementTypeBuilder;
import org.camunda.bpm.model.xml.type.attribute.Attribute;

public class ConditionExpressionImpl extends FormalExpressionImpl implements ConditionExpression {
    protected static Attribute<String> typeAttribute;
    protected static Attribute<String> camundaResourceAttribute;

    public static void registerType(ModelBuilder modelBuilder) {
        ModelElementTypeBuilder typeBuilder = modelBuilder.defineType(ConditionExpression.class, "conditionExpression").namespaceUri("http://www.omg.org/spec/BPMN/20100524/MODEL").extendsType(FormalExpression.class).instanceProvider(new ModelElementTypeBuilder.ModelTypeInstanceProvider<ConditionExpression>() {
            public ConditionExpression newInstance(ModelTypeInstanceContext instanceContext) {
                return new ConditionExpressionImpl(instanceContext);
            }
        });
        typeAttribute = typeBuilder.stringAttribute("type").namespace("http://www.w3.org/2001/XMLSchema-instance").defaultValue("tFormalExpression").build();
        camundaResourceAttribute = typeBuilder.stringAttribute("resource").namespace("http://camunda.org/schema/1.0/bpmn").build();
        typeBuilder.build();
    }

    public ConditionExpressionImpl(ModelTypeInstanceContext instanceContext) {
        super(instanceContext);
    }

    public String getType() {
        return (String)typeAttribute.getValue(this);
    }

    public void setType(String type) {
        typeAttribute.setValue(this, type);
    }

    public String getCamundaResource() {
        return (String)camundaResourceAttribute.getValue(this);
    }

    public void setCamundaResource(String camundaResource) {
        camundaResourceAttribute.setValue(this, camundaResource);
    }
}

查了半天,也试了半天,没找到API如何来构建这个对象。盯着代码思考的时候,突然闪过一个灵感,拿 modelInstance.newInstance(ConditionExpression.class)来构造,试了下,果然可以,问题解决。
此外,因为使用了递归,因此表达式参数,使用一次后将其置空,避免为无关的边设置条件。

转换后的XML数据

按照上述操作,经过多轮调试,终于完成了复杂转换,通过了模型验证,输出Camunda的xml模型,如下:


<definitions xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="definitions_159e6873-e4fc-4ae3-83e5-0b4978edb636" targetNamespace="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">
  <process id="Leave" isExecutable="true" name="请假申请">
    <startEvent id="startEvent_339a89ba-eb8f-453e-a874-4d141233719f" name="流程开始">
      <outgoing>SequenceFlowff1bca6e-fe64-46ec-86d0-8382bec8d3dcoutgoing>
    startEvent>
    <userTask camunda:assignee="${firstNodeAssignee}" id="node72a9790c-a534-4dee-879b-32ea648d6a34" name="填报">
      <incoming>SequenceFlowff1bca6e-fe64-46ec-86d0-8382bec8d3dcincoming>
      <outgoing>SequenceFlowc6d1d96a-24b4-4dcd-b587-da5d8631c317outgoing>
    userTask>
    <sequenceFlow id="SequenceFlowff1bca6e-fe64-46ec-86d0-8382bec8d3dc" sourceRef="startEvent_339a89ba-eb8f-453e-a874-4d141233719f" targetRef="node72a9790c-a534-4dee-879b-32ea648d6a34"/>
    <userTask camunda:assignee="${singleHandler}" camunda:candidateGroups="99" id="nodefde7fee7-6cd2-4adc-a773-79716bd008e2" name="部门领导审批">
      <extensionElements>
        <camunda:taskListener class="tech.abc.platform.workflow.listener.ApproverTaskListener" event="create"/>
      extensionElements>
      <incoming>SequenceFlowc6d1d96a-24b4-4dcd-b587-da5d8631c317incoming>
      <outgoing>SequenceFlow6493c88b-381f-438d-974b-cc3480acee5coutgoing>
    userTask>
    <sequenceFlow id="SequenceFlowc6d1d96a-24b4-4dcd-b587-da5d8631c317" sourceRef="node72a9790c-a534-4dee-879b-32ea648d6a34" targetRef="nodefde7fee7-6cd2-4adc-a773-79716bd008e2"/>
    <inclusiveGateway id="node3278_00b0_e238_a105" name="条件路由">
      <incoming>SequenceFlow6493c88b-381f-438d-974b-cc3480acee5cincoming>
      <outgoing>SequenceFlow34963022-a892-44ee-9d47-bd4cf157c77doutgoing>
      <outgoing>SequenceFlowf5da3c9f-849e-4d37-9768-465f65755a82outgoing>
    inclusiveGateway>
    <sequenceFlow id="SequenceFlow6493c88b-381f-438d-974b-cc3480acee5c" sourceRef="nodefde7fee7-6cd2-4adc-a773-79716bd008e2" targetRef="node3278_00b0_e238_a105"/>
    <inclusiveGateway id="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c" name="汇聚节点">
      <incoming>SequenceFlowa876a2e0-d9e4-4253-bc42-fdb7ee07036cincoming>
      <incoming>SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860fincoming>
      <outgoing>SequenceFlowf12d18b3-96fe-4b22-b872-b96c07e1e5bboutgoing>
    inclusiveGateway>
    <userTask camunda:assignee="${singleHandler}" camunda:candidateGroups="99" id="node8ebc9ee6-bf0d-4542-b6b5-2509a9ea440d" name="HR审批">
      <extensionElements>
        <camunda:taskListener class="tech.abc.platform.workflow.listener.ApproverTaskListener" event="create"/>
      extensionElements>
      <incoming>SequenceFlow34963022-a892-44ee-9d47-bd4cf157c77dincoming>
      <outgoing>SequenceFlowa876a2e0-d9e4-4253-bc42-fdb7ee07036coutgoing>
    userTask>
    <sequenceFlow id="SequenceFlow34963022-a892-44ee-9d47-bd4cf157c77d" sourceRef="node3278_00b0_e238_a105" targetRef="node8ebc9ee6-bf0d-4542-b6b5-2509a9ea440d">
      <conditionExpression id="conditionExpression_42181437-38e5-48fb-8788-94c7af7b8790">${total<=3}conditionExpression>
    sequenceFlow>
    <sequenceFlow id="SequenceFlowa876a2e0-d9e4-4253-bc42-fdb7ee07036c" sourceRef="node8ebc9ee6-bf0d-4542-b6b5-2509a9ea440d" targetRef="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c"/>
    <userTask camunda:assignee="${singleHandler}" camunda:candidateGroups="99" id="node26d04870-685c-4ae9-9b83-971983d3016b" name="副总审批">
      <extensionElements>
        <camunda:taskListener class="tech.abc.platform.workflow.listener.ApproverTaskListener" event="create"/>
      extensionElements>
      <incoming>SequenceFlowf5da3c9f-849e-4d37-9768-465f65755a82incoming>
      <outgoing>SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860foutgoing>
    userTask>
    <sequenceFlow id="SequenceFlowf5da3c9f-849e-4d37-9768-465f65755a82" sourceRef="node3278_00b0_e238_a105" targetRef="node26d04870-685c-4ae9-9b83-971983d3016b">
      <conditionExpression id="conditionExpression_f9565f4c-ef57-4c9c-a394-dffb2f299089">${total>3}conditionExpression>
    sequenceFlow>
    <sequenceFlow id="SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860f" sourceRef="node26d04870-685c-4ae9-9b83-971983d3016b" targetRef="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c"/>
    <endEvent id="endEvent_ddd6bac7-1400-44c9-b54f-a53de7256c34">
      <incoming>SequenceFlowf12d18b3-96fe-4b22-b872-b96c07e1e5bbincoming>
    endEvent>
    <sequenceFlow id="SequenceFlowf12d18b3-96fe-4b22-b872-b96c07e1e5bb" sourceRef="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c" targetRef="endEvent_ddd6bac7-1400-44c9-b54f-a53de7256c34"/>
  process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_3d886d70-b6b3-4ad2-8311-9e1eaa6a4fca">
    <bpmndi:BPMNPlane bpmnElement="Leave" id="BPMNPlane_6e44812c-0b24-4f06-8d7f-c8e5425638d6">
      <bpmndi:BPMNShape bpmnElement="startEvent_339a89ba-eb8f-453e-a874-4d141233719f" id="BPMNShape_c1f6af6c-c5e3-4525-b39b-6c12e68e959c">
        <dc:Bounds height="36.0" width="36.0" x="100.0" y="100.0"/>
      bpmndi:BPMNShape>
    bpmndi:BPMNPlane>
  bpmndi:BPMNDiagram>
definitions>

走两条测试数据,请假天数分别设置为3天和5天,流转正常,测试通过。
删除大于3天的条件表达式设置,请假天数为3天,然后两个分支都会流转到,测试通过。

开发平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
开源不易,欢迎收藏、点赞、评论。

你可能感兴趣的:(#,工作流集成,应用开发平台,工作流集成,activiti,camunda,钉钉流程)