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