Application development platform integrated workflow - design and implementation of process modeling function routing branch conversion

background

For the problem of unfriendly process settings, domestic DingTalk designed and implemented a set of process modeling mode separately, which has nothing to do with the bpmn specification. Someone imitated it and made it open source ( https://github.com/StavinLi/Workflow -Vue3 ), the effect diagram is as follows:

the general principle of implementation is based on infinitely nested child nodes, output json data, and pass it to the backend. After the backend parses, it calls the api of the Camunda engine, converts it into a process model, and then persists it.

The previous part introduced the conversion design and implementation of the processing node. Although the processing node is the main part of the process, the actual business process still needs some logical branches. For example, in the process of asking for leave, the approval of the department manager is enough if the number of leave days is less than 3 days, and the approval of the vice president is required for more than 3 days. At this time, the conditional branch is needed. For example, an infrastructure plan for a municipal bureau will involve several subordinate county bureaus, and the process needs to go to the relevant subordinate county bureaus. At this time, parallel branches are needed.

basic concept

Workflow-based process processing requires branch processing in addition to the common linear flow.
There are two cases of branching, one is parallel branching, all branches will be executed; the other is conditional selection branching, which will be executed only when the set conditions are met.
Camunda uses gateways to handle branches, mainly in the following three types:

  1. Exclusive Gateway: Only one branch is allowed to execute, and the next node is selected according to conditional expressions or rules.
  2. Parallel Gateway (Parallel Gateway): Execute multiple branches at the same time, and continue to execute the next node when all branches are completed.
  3. Compatible Gateway (Inclusive Gateway): Multiple branches are allowed to execute, and the next node is selected according to conditional expressions or rules, but if none of the branches meets the conditions, the default branch is selected.

The parallel gateway is that all branches will be executed without setting conditions or rules.

The difference between a compatible gateway and an exclusive gateway is as follows:
the number of executions is different: the compatible gateway allows multiple branches to be executed; the exclusive gateway only allows one branch to be executed.
The selection methods are different: compatible gateways select the next node according to conditional expressions or rules; exclusive gateways execute the default branch according to the first branch that meets the conditions.
The processing methods are different: Compatible gateways will calculate all egress sequence flows; exclusive gateways only process egress sequence flows that evaluate to true.

Design

Is there any need to use an exclusive gateway?

Functionally, a compatible gateway includes an exclusive gateway, or in other words, an exclusive gateway is a special case of a compatible gateway, that is, only one branch meets the condition. Considering the difference from a performance point of view, the main difference between the two lies in whether the calculation stops when a branch that meets the conditions is found, or all branches are calculated. In the case of limited process branches (generally three or five, no more than single-digit range at most), partial calculation is not much different from full calculation, and the resources and performance are not bad.
From a functional point of view and user experience, the logic of exclusive gateways is clearer, and users clearly know that they can only choose one of multiple conditions, but the improvement of user experience is also very limited.

Is it necessary to use parallel gateway?

In terms of function, compatible gateways actually include parallel gateways. After testing, no conditions are set on the branches, and Camunda considers them to meet the conditions by default, which means that all branches can go. Intuitively, it feels a bit overused to use compatible gateways. , will cause a certain degree of unclear logic of parallel branch and conditional branch, but for users, it is not necessarily better to distinguish between the two concepts of condition and parallel than to have only one concept without distinction.

plan selection

Looking specifically at the process model of DingTalk, there is no dedicated exclusive gateway, there are only two types, one is parallel branch and the other is conditional branch. These two names are more in line with the business meaning and easy for users to understand.

From the point of view of business use, it is necessary to retain a concept, whether only one branch, several branches or all branches can be taken depends on whether the conditions set on the branch are met. It is not necessary to clearly distinguish whether it is a conditional branch or a parallel branch. , which is more friendly to business users.

Based on the above considerations, the exclusive gateways and parallel gateways are removed, and only compatible gateways are used. The front-ends are all described by routes and branches.

Program realization

The Workflow-vue3 open source project that the platform is used for integration has built-in conditional branches and is modified to meet your own needs. Let's implement a relatively simple leave approval process. The flow chart is as follows:
image.png

Front-end implementation

Modify nodeWrap.vue, handle the routing branch separately, and set the type encoding to INCLUSIVE_GATEWAY

  <!-- 路由分支 -->
  <div class="branch-wrap" v-else-if="modelValue.type == 'INCLUSIVE_GATEWAY'">
    <div class="branch-box-wrap">
      <div class="branch-box">
        <button class="add-branch" @click="addCondition">添加条件</button>
        <div class="col-box" v-for="(item, index) in modelValue.branchList" :key="index">
          <div class="condition-node">
            <div class="condition-node-box">
              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
                <div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)">&lt;</div>
                <div class="title-wrapper">
                  <input
                    v-if="isInputList[index]"
                    type="text"
                    class="ant-input editable-title-input"
                    @blur="blurEvent(index)"
                    @focus="$event.currentTarget.select()"
                    v-focus
                    v-model="item.name"
                  />
                  <span v-else class="editable-title" @click="clickEvent(index)">{
   
   {
                    item.name
                  }}</span>
                  <i class="anticon anticon-close close" @click="removeCondition(index)"></i>
                </div>
                <div
                  class="sort-right"
                  v-if="index != modelValue.branchList.length - 1"
                  @click="arrTransfer(index)"
                  >&gt;</div
                >
                <div class="content" @click="setConditionNode(item)">{
   
   {
                  $func.conditionStr(modelValue, index)
                }}</div>
                <div class="error_tip" v-if="isTried && item.error">
                  <i class="anticon anticon-exclamation-circle"></i>
                </div>
              </div>
              <addNode v-model:childNodeP="item.child" />
            </div>
          </div>
          <nodeWrap v-if="item.child" v-model:modelValue="item.child" />
          <template v-if="index == 0">
            <div class="top-left-cover-line"></div>
            <div class="bottom-left-cover-line"></div>
          </template>
          <template v-if="index == modelValue.branchList.length - 1">
            <div class="top-right-cover-line"></div>
            <div class="bottom-right-cover-line"></div>
          </template>
        </div>
      </div>
      <addNode v-model:childNodeP="modelValue.child" />
    </div>
  </div>

The original open source project has a priority setting on the conditional branch, which means that the edge with a higher priority is calculated first. Once the condition is met, the conditions on other edges are not calculated. Therefore, it actually corresponds to an exclusive gateway, that is, only one branch will be taken. In actual business scenarios, there will be a need to take multiple conditional branches at the same time. Therefore, the platform needs to implement a compatible gateway to calculate all the conditions on the side. In this case, the priority is actually meaningless and should be removed.

Modify addNode.vue, set the default routing data, and set the conditional node type code to 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)
}

Regarding the setting of conditions, there are actually two modes. One is for business users and needs to provide visual configuration. For example, in the contract approval process, select the contract amount attribute and set the condition to be greater than 1 million. At the same time, it is possible to add other and or An OR operation, converted by the platform into an expression that can be processed by the platform. The other is for developers. After all, the processing logic of the process link still depends on the developers to write, especially the variables used in the conditional expressions are still processed in the logic of the process link. The current positioning of the platform is actually oriented to developers, that is, low-code configuration is the main focus, improving development efficiency and reducing development costs, supplemented by source code development, ensuring the realization of business logic, especially complex logic, and good scalability. Based on the positioning of the platform, set the conditions here, just put a text box, and the developer can set the final conditional expression, such as ${contractMoney>1000000}, which is simple and practical.

Added conditional expression setting component

<template>
  <el-drawer
    :append-to-body="true"
    title="条件设置"
    v-model="visible"
    :show-close="false"
    :size="550"
    :before-close="close"
    destroy-on-close
  >
    <el-form
      ref="form"
      :model="entityData"
      :rules="rules"
      label-width="120px"
      label-position="right"
      style="width: 90%; margin: 0px auto"
    >
      <!--表单区域 -->

      <el-form-item label="表达式" prop="expression">
        <el-input v-model="entityData.expression" type="textarea" rows="4" />
      </el-form-item>
      <el-form-item style="float: right; margin-top: 20px">
        <el-button type="primary" @click="save">确 定</el-button>
        <el-button @click="close">取 消</el-button>
      </el-form-item>
    </el-form>
  </el-drawer>
</template>
<script>
import { useStore } from '../../stores/index'
let store = useStore()

export default {
  data() {
    return {
      entityData: {},
      rules: {
        //前端验证规则
       
      }
    }
  },
  computed: {
    visible() {
      return store.conditionNodeConfigVisible
    },
    conditionNodeConfig() {
      return store.conditionNodeConfig
    }
  },
  watch: {
    conditionNodeConfig(value) {
      this.entityData = value
    }
  },
  methods: {
    close() {
      store.setConditionNodeConfigVisible(false)
    },
    save() {
      const nodeConfig = Object.assign(
        store.conditionNodeConfig,
        { ...this.entityData },
        { flag: true }
      )
      store.setConditionNodeConfig(nodeConfig)
      this.close()
    }
  }
}
</script>
<style scoped></style>

backend implementation

conversion logic

In the Camunda model, there are actually no specific branch nodes and sink nodes, they are all gateway nodes.
Therefore, there is a certain gateway node, which not only plays the role of convergence, but also plays the role of branch.
When the front-end and back-end models are implemented independently, the front-end routing node corresponds to the Camunda gateway, and the condition node corresponds to the Camunda condition edge.
In the DingTalk process model, the conditional branch does not have an explicit converging node, and the backend needs to judge and supplement it by itself.

In the case of conditional branching, the front-end self-built process model is substantially different from the back-end Camunda model.
For the front end only, a conditional branch node is a combination of multiple nodes, including routing nodes, conditional nodes, and processing nodes contained in the branch, and even nested conditional branch nodes in the branch. The front-end data structure is a nested object. For conditional branching, the example is as follows:

{
    
    
	"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": []
		}
	]
}

Its branch data is placed in the attribute branchList, and the type is a node array. For the backend, it is necessary to convert the node whose type is INCLUSIVE_GATEWAY into a compatible gateway, then read the branchList property of the node, convert each element of the corresponding node array into a short process, and then connect the first node to the compatible gateway. The node docks with an automatically added sink node.

Core conversion logic implementation

For the core model conversion, see the case INCLUSIVE_GATEWAY branch of the method below. For the complete code, see the open source project.

  /**
     * 将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;
        }

    }


How to set conditional edges?

The setting of the condition side is a difficult point. It is speculated that there should be an API to complete this work. The edge object SequenceFlow has a property to set the ConditionExpression property, but this property is not a String, but an interface.

public interface ConditionExpression extends FormalExpression {
    
    
    String getType();

    void setType(String var1);

    String getCamundaResource();

    void setCamundaResource(String var1);
}

It implements the class construction method, and the parameter required to be passed in is another strange class object 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);
    }
}

After searching for a long time and trying for a long time, I couldn't find how the API can build this object. While staring at the code and thinking, an inspiration suddenly flashed, and I used modelInstance.newInstance(ConditionExpression.class) to construct it. I tried it, and it worked, and the problem was solved.
In addition, because of the use of recursion, the expression parameter should be empty after it is used once to avoid setting conditions for unrelated edges.

Transformed XML data

According to the above operation, after several rounds of debugging, the complex conversion is finally completed, the model verification is passed, and the xml model of Camunda is output, as follows:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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-8382bec8d3dc</outgoing>
    </startEvent>
    <userTask camunda:assignee="${firstNodeAssignee}" id="node72a9790c-a534-4dee-879b-32ea648d6a34" name="填报">
      <incoming>SequenceFlowff1bca6e-fe64-46ec-86d0-8382bec8d3dc</incoming>
      <outgoing>SequenceFlowc6d1d96a-24b4-4dcd-b587-da5d8631c317</outgoing>
    </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-da5d8631c317</incoming>
      <outgoing>SequenceFlow6493c88b-381f-438d-974b-cc3480acee5c</outgoing>
    </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-cc3480acee5c</incoming>
      <outgoing>SequenceFlow34963022-a892-44ee-9d47-bd4cf157c77d</outgoing>
      <outgoing>SequenceFlowf5da3c9f-849e-4d37-9768-465f65755a82</outgoing>
    </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-fdb7ee07036c</incoming>
      <incoming>SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860f</incoming>
      <outgoing>SequenceFlowf12d18b3-96fe-4b22-b872-b96c07e1e5bb</outgoing>
    </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-bd4cf157c77d</incoming>
      <outgoing>SequenceFlowa876a2e0-d9e4-4253-bc42-fdb7ee07036c</outgoing>
    </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&lt;=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-465f65755a82</incoming>
      <outgoing>SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860f</outgoing>
    </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&gt;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-b96c07e1e5bb</incoming>
    </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>

Take two test data, the number of leave days is set to 3 days and 5 days respectively, the circulation is normal, and the test is passed.
Delete the conditional expression setting that is greater than 3 days, and the number of leave days is 3 days, then both branches will flow to it, and the test will pass.

Development platform information

Platform name: One Two Three Development Platform
Introduction: Enterprise-level general development platform
Design information: csdn column
Open source address: Gitee
open source protocol: MIT
open source is not easy, welcome to favorite, like, comment.

Guess you like

Origin blog.csdn.net/seawaving/article/details/132099555