功能说明:类似钉钉发起流程,发起人填单子过程中,可自动预判该流程走向,方便发起人填单子时候就可以看到这个单子的审批流转情况。
注意:预判的流程走向,并不是固定的,只能根据当时的表单数据做预判,因为表单值在审批过程中是可以被修改的,修改之后流程走向会变,具体的解释请查看本系列文章的第十五章节《springboot+activiti7+react实现模仿钉钉功能的审批流(十五、流程表单操作权限设计)》;
图的相关算法,可参考我的另外一篇文章《《算法4》无向图、有向图 (一、深度优先 | 广度优先 | 连通分量 | 可达性分析 | 最短路径)》;
本人并未实际具体实现此功能,只是写下自己考虑实现该功能的大致实现思路,具体细节还需要打磨,以供交流,欢迎提出更优方案;话不多的,直接举一个简单栗子:
流程预判,钉钉是这样玩的:
- 第1步 表单设计:
- 第2步 流程设计:
- 第3步 用户发起审批,并自动预判流程走向
条件满足 “请假天数 < 3” 走B审批:
条件不满足 “请假天数 < 3” 走C审批:
具体实现步骤:
先简化下钉钉的这个demo图,就是这个样子:
之前我的这个系列文章里面<实现仿钉钉流程设计器>章节,写过bpmn就是一个有向图结构,所以可以用有向图广度优先遍历算法解决该问题,没看过之前章节的可以先扫盲看一下。
1. 获取流程图bpmn信息
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
这个bpmnModel对象里面,可以拿到有整个流程图信息,包括所有节点、网关、网关线上的el表达式、还有各种其他信息,找不到的可以打断点dubug查看;
2. 将activiti的bpmn流程图,转换为一个有向图结构,这里用邻接表数据结构存储(当然你用邻接矩阵也行);
用以上面钉钉的演示为例,实现该邻接表数据结构可以有两种方式:
方式1:Map实现
S: [A]
A: [G]
G: [B, C]
B: [D]
C: [D]
D: [E]
E: []
方式1:邻接表数据结构实现(推荐)
字母换数字List邻接表实现,算法实现比map结构更简单而且省了空间:
以这种做映射 0:S 1:A 2:G 3:B 4:C 5:D 6:E,结构如下:
0: [1]
1: [2]
2: [3, 4]
3: [5]
4: [5]
5: [6]
6: []
来源《算法4》一书中的有向图邻接表结构;
代码上一波:
@Test
public void digraph() {
/**
* map结构存储有向图:
* S: [A]
* A: [G]
* G: [B, C]
* B: [D]
* C: [D]
* D: [E]
* E: []
* 左边key是节点,右边的value是节点的后续节点
*/
//为了顺序,用下LinkedHashMap
Map<String, List<String>> map = new LinkedHashMap<>();
map.put("S", Arrays.asList("A"));
map.put("A", Arrays.asList("G"));
map.put("G", Arrays.asList("B", "C"));
map.put("B", Arrays.asList("D"));
map.put("C", Arrays.asList("D"));
map.put("D", Arrays.asList("E"));
map.put("E", Arrays.asList());
//输出邻接表结构
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
System.out.println();
/**
* 邻接表结构存储有向图(推荐!!!):
* 0: [1]
* 1: [2]
* 2: [3, 4]
* 3: [5]
* 4: [5]
* 5: [6]
* 6: []
* 左边既是数组下标,也是节点,右边是节点的后续节点,算法实现比map结构更简单而且省了空间
*/
//初始化邻接表,7是节点数量,这里只是简易演示下,详细的有向图结构和算法请查阅资料
List<Integer>[] adj = new List[7];
for (int i = 0; i < 7; i++) {
adj[i] = new ArrayList<>();
}
adj[0] = Arrays.asList(1);
adj[1] = Arrays.asList(2);
adj[2] = Arrays.asList(3, 4);
adj[3] = Arrays.asList(5);
adj[4] = Arrays.asList(5);
adj[5] = Arrays.asList(6);
//输出邻接表结构
for (int i = 0; i < 7; i++) {
System.out.println(i + ": " + adj[i]);
}
}
3. 模拟发起流程,构造一个Map对象formData,如:{"day": 3, "reason": "有事回家几天"},然后使用有向图的广度优先(这里主要是为了兼容并行网关,没有并行网关也可以用深度优先)遍历算法,从开始(startEvent)节点遍历节点,遇到网关时候用下面代码的gatewayJuelExpression(String el, Map<String, Object> formData)方法,判断网关节点的后续节点;备注:el表达式字符串在网关节点的出线属性上面;
4. 补充第3步,使用有向图广度优先(这里主要是为了兼容并行网关,没有并行网关也可以用深度优先)遍历算法从开始(startEvent)节点遍历节点,如从上图中的S(startEvent)节点开始走,走到A(userTask)节点,然后走到G(exclusiveGateway)节点,网关节点G这里根据el表达式和map参数判断走B(userTask)节点还是C(userTask)节点,然后走D(userTask)节点,最后走E(userTask)节点结束;
遍历网关节点出口要注意:bpmn的排他网关出口线是有顺序的,要按网关的条件顺序依次执行该方法判断,直到为true的条件成立即找到网关的出口,比如a、b、c 是网关G的3个出口线,如果不按顺序可能会走错出口,比如a、b两条线都满足el条件表达式,走错顺序就会有问题);
一个el表达式判断网关走向的代码demo(注意看注释):
/**
* 计算Activiti排他网关(ExclusiveGateway)的条件表达式(EL表达式)的值
* ---网关(exclusiveGateway)节点的走向判断---
* uel表达式解析,该方法的作用是类似activiti的uel表达式,网关(exclusiveGateway)节点根据el表达式和流程变量variables判断走向, 方法可以改造为:
* Boolean gatewayJuelExpression(String el, Map<String, Object> formData)
* el为uel表达式,formData为activiti的Map类型的流程变量variables
* <p>
* demo:
* Boolean res = gatewayJuelExpression("${day < 3}", {"day": 3, "reason": "有事回家几天"});
* 根据el和变量经过uel解析得到的res可以判断网关的走向
* <p>
* 注意:bpmn的网关条件(一般网关出口至少2个以上)是有顺序的,要按网关的条件顺序依次执行该方法判断,直到为true的条件成立即找到网关的出口(下一个节点);
* 另:bpmn网关的最后一个条件可不写条件判断表达式作为默认保底的出口,比如G网关节点有"A/B/C"3个出口节点,依次匹配判断A->B->C条件是否满足,如果匹配不成功则继续进行匹配下一优先级的条件;
* 当3个条件都不满足时抛异常(事实上activiti就是这么干的,没有满足条件的出口就抛异常,本人就遇到这个坑,发起流程就报异常,原因是网关没有出口!);C不写表达式时,当A/B条件不满足时,默认最后这里会走C出口;
*/
@Test
public void gatewayJuelExpression() {
//el表达式
String el = "${day < 3}";
//模拟提交activiti的变量
Map<String, Object> formData = new HashMap();
formData.put("day", 3);
formData.put("reason", "有事回家几天");
ExpressionFactory factory = new ExpressionFactoryImpl();
SimpleContext context = new SimpleContext();
for (Object k : formData.keySet()) {
if (formData.get(k) != null) {
context.setVariable(k.toString(), factory.createValueExpression(formData.get(k), formData.get(k).getClass()));
}
}
ValueExpression e = factory.createValueExpression(context, el, Boolean.class);
//el表达式和variables得到的结果
Boolean res = (Boolean) e.getValue(context);
//输出结果 false
System.out.println(res);
}
5.将上面遍历过的节点,根据节点走向(遍历顺序),关联到Node,并将Node加到一个List<Node>里面,Node是一个普通对象,里面包含userTask节点的信息(节点名、会签信息...)和审批人(审批人list...)信息;
6.将List<Node>返给前端,前端像钉钉一样,从上到下展示出来即可;
以上遍历过程中的用户节点(userTask),也适用于服务节点(serviceTask);
我之前的这个系列文章里面,写了有借助服务节点(serviceTask)实现抄送功能,有开发中需要实现抄送功能的可以借鉴;
注意事项:
钉钉的流程是比较简单的流程,都是有向无环图,图中不会存在有向环情况,所以暂时不用判断是否存在有向环情况;
若是复杂的流程图,如activiti modeler和bpmnjs设计出来的图,会有各种情况,需要注意用marked[]数组标记走过的节点,判断是否存在有向环的情况,避免遍历节点时候出现死循环或者栈溢出,如下图这种:
最后补一段 <计算Activiti用户(userTask)节点表达式(EL表达式)的值> 的代码:
/**
* 计算Activiti用户(userTask)节点表达式(EL表达式)的值
* ---用户(userTask)节点从流程变量中得到值---
* uel表达式解析,该方法的作用是类似activiti的uel表达式,用户(userTask)节点根据el表达式和流程变量variables得到该节点上的值, 比如得到这个节点的审批人list,方法可以改造为:
* List<String> assignJuelExpression(String el, Map formData)
* el为uel表达式,formData为activiti的Map类型的流程变量variables
* <p>
* demo:
* List<String> assigns = assignJuelExpression("${assigns}", {"assigns": users集合});
* 根据el和变量经过uel解析得到的res可以判断网关的走向
*/
@Test
public void assignJuelExpression() {
//el表达式
String el = "${assigns}";
//模拟提交activiti的变量
List<String> users = Arrays.asList(new String[]{"admin", "lisi", "zhangsan"});
Map<String, Object> formData = new HashMap();
formData.put("assigns", users);
ExpressionFactory factory = new ExpressionFactoryImpl();
SimpleContext context = new SimpleContext();
for (Object k : formData.keySet()) {
if (formData.get(k) != null) {
context.setVariable(k.toString(), factory.createValueExpression(formData.get(k), formData.get(k).getClass()));
}
}
ValueExpression e = factory.createValueExpression(context, el, List.class);
//输出结果 [admin, lisi, zhangsan]
List<String> assigns = (List) e.getValue(context);
System.out.println(assigns);
}