Flowable做会签和或签

考虑如下场景:

  • 某一流程进行到下一步需要指派给多人,当每个被指派的人都选择完成任务后,流程继续
  • 某一流程进行到下一步需要指派给多人,但是当某一个人选择完成任务后,流程继续
  • 指派任务时,其人数是不固定的,可能是2-5个人,在流程创建时是未知的;只有在启动流程实例时,指派人的具体信息可以确认

其中需求1就是常说的会签,需求2就是常说的或签;在AI和互联网的帮助下搜到了一些解决方案:

  • 多实例
  • 多实例 + 任务监听器 + 执行监听器
  • 网关 + 子流程
  • 使用candidateGroup的(或签)

这篇文章记录一下用最简单的多实例完成以上需求的过程,FLOWABLE版本7.0.0.M1
具体主要是参考官方代码中的单元测试类MultiInstanceTest中的testParallelUserTasksBasedOnCollection方法。

先看下具体代码:

BPMN

<?xml version="1.0" encoding="UTF-8"?>
<definitions id="definition" 
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:activiti="http://activiti.org/bpmn"
  targetNamespace="Examples">
  
  <process id="miParallelUserTasksBasedOnCollection">
  
    <startEvent id="theStart" />
    <sequenceFlow id="flow1" sourceRef="theStart" targetRef="miTasks" />
    
    <userTask id="miTasks" name="My Task ${loopCounter}" activiti:assignee="${assignee}">
      <multiInstanceLoopCharacteristics isSequential="false">
        <loopDataInputRef>assigneeList</loopDataInputRef>
        <inputDataItem name="assignee" />
        <completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
      </multiInstanceLoopCharacteristics>
    </userTask>
    
    <sequenceFlow id="flow3" sourceRef="miTasks" targetRef="theEnd" />
    <endEvent id="theEnd" />
    
  </process>

</definitions>

单元测试方法

@Test
@Deployment
public void testParallelUserTasksBasedOnCollection() {
    List<String> assigneeList = Arrays.asList("kermit", "gonzo", "mispiggy", "fozzie", "bubba");
    String procId = runtimeService.startProcessInstanceByKey("miParallelUserTasksBasedOnCollection", CollectionUtil.singletonMap("assigneeList", assigneeList)).getId();

    List<org.flowable.task.api.Task> tasks = taskService.createTaskQuery().orderByTaskAssignee().asc().list();
    assertThat(tasks)
            .extracting(Task::getAssignee)
            .containsExactly("bubba", "fozzie", "gonzo", "kermit", "mispiggy");

    // Completing 3 tasks will trigger completioncondition
    taskService.complete(tasks.get(0).getId());
    taskService.complete(tasks.get(1).getId());
    taskService.complete(tasks.get(2).getId());
    assertThat(taskService.createTaskQuery().count()).isZero();
    assertProcessEnded(procId);
}

打开AI,解释下此段代码,

总结

这段代码测试了一个基于集合的并行用户任务的完成条件。具体步骤如下:

  1. 创建一个包含多个分配者的列表 assigneeList
  2. 启动一个流程实例,并传递 assigneeList 作为变量。
  3. 查询并断言任务的分配者顺序。
  4. 依次完成前3个任务。
  5. 验证所有任务都已完成,任务数量为0。
  6. 断言流程实例已经结束。

解释下工作流:

工作流解释

定义

  • 该BPMN文件定义了一个名为 miParallelUserTasksBasedOnCollection 的流程。

起始事件

  • 流程从一个起始事件 theStart 开始。

并行用户任务

  • 从起始事件通过一个顺序流 flow1 连接到一个并行用户任务 miTasks
  • 该用户任务 miTasks 的名称为 My Task ${loopCounter},其中 ${loopCounter} 是一个循环计数器,表示当前任务的索引。
  • 用户任务的分配者通过表达式 ${assignee} 动态设置。
  • 该用户任务配置了多实例循环特性:
    • isSequential="false" 表示这些任务是并行执行的。
    • loopDataInputRef="assigneeList" 指定了一个集合变量 assigneeList,用于生成多个任务实例。
    • inputDataItem name="assignee" 表示每个任务实例的分配者从 assigneeList 中获取。
    • completionCondition="${nrOfCompletedInstances/nrOfInstances >= 0.6 }" 表示当已完成的任务实例数占总任务实例数的比例达到或超过60%时,整个多实例任务将完成。

结束事件

  • 多实例用户任务完成后,通过一个顺序流 flow3 连接到结束事件 theEnd,表示流程结束。

总结

该BPMN工作流定义了一个基于集合的并行用户任务流程。流程从起始事件开始,生成多个并行用户任务,每个任务的分配者从 assigneeList 中获取。当已完成的任务实例数达到总任务实例数的60%时,多实例任务完成,流程结束。

在看下FLOWABLE官方文档对多实例的说明。

至此,掌握的事实如下,已基本可以满足需求,总结如下:

  • 可以使用loopDataInputRef和inputDataItem指定的变量创建多实例以及使用的参数名
  • 可以使用completionCondition以及内置变量nrOfCompletedInstances,nrOfInstances等来判断何时终结多实例。比如nrOfCompletedInstances == nrOfInstances即所有多实例都完成,即为会签;nrOfCompletedInstances >= 1即多实例有一个以上完成了,即为或签;当然也可以和例子中类似使用nrOfCompletedInstances/nrOfInstances >= 0.6来表示60%以上的人完成后,流程继续。
  • 如果loopDataInputRef和inputDataItem比较难以记住,可以使用标签flowable:collection="assigneeList" flowable:elementVariable="assignee"来达到同等效果

我们开始自己的流程,期望建立一个流程,第一个任务做会签,然后进行一步Service Task,第二个任务做或签,

流程代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:flowable="http://flowable.org/bpmn"
             xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
             xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
             xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
             typeLanguage="http://www.w3.org/2001/XMLSchema"
             expressionLanguage="http://www.w3.org/1999/XPath"
             targetNamespace="http://www.flowable.org/processdef">
    <process id="simpleProcess" isExecutable="true">
        <startEvent id="startEvent1"/>
        <sequenceFlow id="flow1" sourceRef="startEvent1" targetRef="userTask1"/>
        <userTask id="userTask1" name="My User Task ${loopCounter}" flowable:assignee="${userId}">
            <multiInstanceLoopCharacteristics isSequential="false"
                                              flowable:collection="${userIds}"
                                              flowable:elementVariable="userId">
                <loopCardinality>${userIds.size()}</loopCardinality>
                <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
            </multiInstanceLoopCharacteristics>
        </userTask>
        <sequenceFlow id="flow2" sourceRef="userTask1" targetRef="serviceTask1"/>
        <serviceTask id="serviceTask1" name="Print Variables" flowable:class="com.suoshi.astralstream.PrintVariablesDelegate"/>
        <sequenceFlow id="flow4" sourceRef="serviceTask1" targetRef="approvalTask"/>
        <userTask id="approvalTask" name="Approval Task ${loopCounter}" flowable:assignee="${approver}">
            <multiInstanceLoopCharacteristics isSequential="false"
                                              flowable:collection="${approvers}"
                                              flowable:elementVariable="approver">
                <loopCardinality>${approvers.size()}</loopCardinality>
                <completionCondition>${nrOfCompletedInstances >= 1}</completionCondition>
            </multiInstanceLoopCharacteristics>
        </userTask>
        <sequenceFlow id="flow5" sourceRef="approvalTask" targetRef="endEvent1"/>
        <endEvent id="endEvent1"/>
    </process>
    <bpmndi:BPMNDiagram id="BPMNDiagram_a">
        <bpmndi:BPMNPlane bpmnElement="simpleProcess" id="BPMNPlane_a">
            <bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
                <omgdc:Bounds height="36.0" width="36.0" x="100.0" y="100.0"/>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="userTask1" id="BPMNShape_userTask1">
                <omgdc:Bounds height="80.0" width="100.0" x="200.0" y="80.0"/>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="serviceTask1" id="BPMNShape_serviceTask1">
                <omgdc:Bounds height="80.0" width="100.0" x="350.0" y="80.0"/>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="approvalTask" id="BPMNShape_approvalTask">
                <omgdc:Bounds height="80.0" width="100.0" x="500.0" y="80.0"/>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="endEvent1" id="BPMNShape_endEvent1">
                <omgdc:Bounds height="36.0" width="36.0" x="700.0" y="100.0"/>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
                <omgdi:waypoint x="136.0" y="118.0"/>
                <omgdi:waypoint x="200.0" y="120.0"/>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
                <omgdi:waypoint x="300.0" y="120.0"/>
                <omgdi:waypoint x="350.0" y="120.0"/>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
                <omgdi:waypoint x="450.0" y="120.0"/>
                <omgdi:waypoint x="500.0" y="120.0"/>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
                <omgdi:waypoint x="600.0" y="120.0"/>
                <omgdi:waypoint x="700.0" y="118.0"/>
            </bpmndi:BPMNEdge>
        </bpmndi:BPMNPlane>
    </bpmndi:BPMNDiagram>
</definitions>

单元测试代码

@Test
    public void testStartSimpleProcess() {
        // 部署流程定义
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("simple-process.bpmn20.xml")
                .deploy();

        // 设置任务变量,包括 assignee 列表
        List<String> userIds = new ArrayList<>();
        userIds.add("johnDoe");
        userIds.add("janeDoe");
        userIds.add("alice");

        // 设置审批者列表
        List<String> approvers = new ArrayList<>();
        approvers.add("bob");
        approvers.add("charlie");

        Map<String, Object> variables = new HashMap<>();
        variables.put("userIds", userIds);
        variables.put("approvers", approvers);

        // 启动流程实例
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess", variables);

        // 断言流程实例是否成功启动
        assertNotNull(processInstance);
        System.out.println("流程实例ID: " + processInstance.getId());

        // 查询用户任务
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
        assertEquals(userIds.size(), tasks.size());

        // 验证每个任务是否正确分配给指定的用户
        for (Task task : tasks) {
            System.out.println("用户任务ID: " + task.getId());
            System.out.println("任务名称: " + task.getName());
            System.out.println("任务分配给的执行人员: " + task.getAssignee());

            // 验证任务分配给的执行人员
            assertTrue(userIds.contains(task.getAssignee()));
        }

        // 模拟两个用户完成任务
        for (int i = 0; i < 2; i++) {
            Task task = tasks.get(i);
            taskService.complete(task.getId());
        }

        // 检查流程实例是否仍然处于活动状态
        boolean isEnded = runtimeService.createProcessInstanceQuery()
                .processInstanceId(processInstance.getId())
                .singleResult() == null;
        assertFalse(isEnded);

        // 处理最后一个人
        Task lastTask = tasks.get(tasks.size() - 1);
        taskService.complete(lastTask.getId());

        List<Task> approvalTasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
        // 模拟只有一个人完成任务
        Task approvalTask = approvalTasks.get(0);
        taskService.complete(approvalTask.getId());

        // 检查流程实例是否已经结束
        boolean isEnded2 = runtimeService.createProcessInstanceQuery()
                .processInstanceId(processInstance.getId())
                .singleResult() == null;
        assertTrue(isEnded2);
    }

输出日志

流程实例ID: 3b57f0ad-b76a-11ef-bc19-2ea2d97adec2
用户任务ID: 3b5e5963-b76a-11ef-bc19-2ea2d97adec2
任务名称: My User Task 0
任务分配给的执行人员: johnDoe
用户任务ID: 3b5ea788-b76a-11ef-bc19-2ea2d97adec2
任务名称: My User Task 1
任务分配给的执行人员: janeDoe
用户任务ID: 3b5ece9d-b76a-11ef-bc19-2ea2d97adec2
任务名称: My User Task 2
任务分配给的执行人员: alice
Service Task 'Print Variables' triggered.
Variable 'userIds' = [johnDoe, janeDoe, alice]
Variable 'approvers' = [bob, charlie]

目前看已经可以完成第一个任务会签,第二个任务或签的需求。

FLOWABLE还有很多其他实现方式,待后续慢慢研究。Enjoy Coding!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容