背景
对于流程设置不友好的问题,国内钉钉另行设计与实现了一套流程建模模式,跟bpmn规范无关,有人仿照实现了下,并做了开源(https://github.com/StavinLi/Workflow-Vue3),效果图如下:
实现大致原理是基于无限嵌套的子节点,输出json数据,传给后端,后端进行解析后,调用Camunda引擎的api,转换成流程模型后持久化。
上篇介绍了办理节点的转换设计与实现。虽然办理节点是流程的主要组成部分,但实际业务流程,还需要一些逻辑分支。例如请假流程中,请假天数3天以内部门经理批准即可,3天以上需要副总审批,这时候就需要用到条件分支。在比如,一份市局的基建方案,会涉及到若干个下属县局,流程需要走到相关的下属县局,这时候就需要用到并行分支。
基础概念
基于工作流的流程处理,除了常见的线性流转外,还需要分支处理。
分支有两种情况,一是并行分支,所有分支都会执行;二是条件选择分支,满足设定的条件才会执行。
Camunda使用网关来处理分支,主要是以下三种:
- 排他网关(Exclusive Gateway):只允许一条分支执行,根据条件表达式或规则选择下一个节点。
- 并行网关(Parallel Gateway):同时执行多条分支,当所有分支完成后,才继续执行下一个节点。
- 兼容网关(Inclusive Gateway):允许多条分支执行,并根据条件表达式或规则选择下一个节点,但如果没有任何一个分支满足条件,则选择默认分支。
并行网关是所有分支都会执行,不需要设置条件或规则。
兼容网关和排他网关的区别如下:
执行数量不同:兼容网关允许多条分支执行;排他网关只允许一条分支执行。
选择方式不同:兼容网关根据条件表达式或规则选择下一个节点;排他网关根据第一条符合条件的分支执行默认分支。
处理方式不同:兼容网关会计算所有出口顺序流;排他网关只处理计算为true的出口顺序流。
方案设计
有没有必要使用排他网关?
从功能上,兼容网关是包含排他网关的,或者说,排他网关是兼容网关的一种特例,即只有一条分支满足条件。从性能角度考虑差别,二者差别主要在于找到一条满足条件的分支就停止计算,还是计算所有分支。在流程分支有限的情况下(一般也就三五条,最多不会超过个位数范围),部分计算跟全部计算没多少差别,不差那点资源和性能。
从功能角度和用户体验而言,有排他网关逻辑更清晰一些,用户很明确知道在多个条件中只能选择1个,不过这点用户体验提升也非常有限。
有没有必要使用并行网关?
从功能上,兼容网关实际也包含并行网关,经测试,分支上的不设置条件,Camunda默认视为满足条件,则意味着所有分支都能走,这么做,直观上感觉有点过度使用兼容网关了,会造成一定程度并行分支和条件分支逻辑不清,不过对于用户而言,区分条件和并行这两个概念,未必比只有一个概念不区分更好。
方案选择
专门看了下钉钉的流程模型,没有专门的排他网关,只有两类,一类是并行分支,另一类是条件分支,这两种称呼也更符合业务含义,便于用户理解。
从业务使用角度而言,就保留一个概念,分支,到底是只能走一条、走若干条还是全部走,取决于分支上设置条件是否满足即可,不需要明确区分到底是条件分支还是并行分支,对于业务用户而言更友好。
基于上述考虑,去除掉排他网关,并行网关,只使用兼容网关,前端一律使用路由和分支来描述。
方案实现
平台用于集成的Workflow-vue3开源项目,内置了条件分支,进行改造,来满足自己的需求,我们来实现一个相对简单的请假审批流程,流程图如下:
前端实现
修改nodeWrap.vue,对于路由分支单独处理,类型编码设置为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)"><</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)"
>></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>
原开源项目,条件分支上有优先级的设置,意味了优先计算优先级高的边,一旦满足则不在计算其他边上的条件,因此实际对应的是排他网关,即只会走一条分支。在实际业务场景中,会出现同时走多条件分支的需求。因此平台需要实现兼容网关,计算所有边上的条件,这种情况下,优先级实际就没有意义,进行移除。
修改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},简便实用。
新增条件表达式设置组件
<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>
后端实现
转换逻辑
在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模型,如下:
<?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<=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>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>
走两条测试数据,请假天数分别设置为3天和5天,流转正常,测试通过。
删除大于3天的条件表达式设置,请假天数为3天,然后两个分支都会流转到,测试通过。
开发平台资料
平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
开源不易,欢迎收藏、点赞、评论。