【工作流】Spring Boot 项目与 Camunda 的整合

【工作流】Spring Boot 项目与 Camunda 的整合

【一】Camunda 和主流流程引擎的对比

官网:https://camunda.com/
中文网站:http://camunda-cn.shaochenfeng.com/

【1】核心引擎对比

(1)Camunda

(1)技术定位:企业级 BPMN 2.0 引擎,强调高性能与扩展性
(2)核心优势:
企业级功能:支持流程实例迁移、双异步机制、分布式定时器等高级特性,适合复杂业务场景
工具链完善:提供 Modeler(流程设计)、Tasklist(任务管理)、Cockpit(监控)等开箱即用工具,降低实施成本
性能表现:百万级实例并发处理能力,高并发场景下稳定性优于 Flowable 10%-39%
开源限制:社区版不包含流程监控预警、多人协作设计等功能,企业版需商业授权
适用场景:大型企业复杂流程管理、金融级审批系统、跨国集团多分支协同

(2)Flowable

(1)技术定位:轻量级 BPMN 2.0 引擎,侧重快速集成与灵活性
(2)核心优势:
轻量级架构:启动速度快,适合中小规模项目快速落地
开源友好:Apache V2 协议允许自由商用,无功能限制
生态兼容性:与 Activiti API 高度兼容,便于旧系统迁移
性能瓶颈:高并发场景下吞吐量低于 Camunda,复杂流程易出现内存泄漏
适用场景:中小型企业 OA 系统、电商订单处理、快速原型开发

(3)Temporal

(1)技术定位:云原生工作流引擎,支持长时间运行流程
(2)核心优势:
云原生架构:无锁设计、弹性扩展,适合 Kubernetes 环境
持久化状态:自动处理故障恢复,保障任务可靠性
编程模型灵活:支持 Go/Java 多语言,适合微服务编排
学习成本:基于 SDK 开发,需适应工作流编程范式
适用场景:物联网设备管理、游戏服务器状态机、跨服务事务协调

(4)JBPM

(1)技术定位:规则引擎驱动的流程管理平台
(2)核心优势:
规则与流程结合:集成 Drools 规则引擎,适合需要动态决策的场景
Red Hat 生态:与 WildFly、Openshift 深度集成,适合 Java EE 环境
市场局限:国内社区活跃度低,文档资源匮乏
适用场景:电信运营商计费流程、医疗行业合规性审批

(5)Activiti

(1)技术定位:轻量级 BPMN 2.0 引擎,历史悠久但维护停滞
(2)核心优势:
Eclipse 插件:早期开发者熟悉的可视化设计工具
低成本集成:适合快速搭建简单流程(如请假审批)
技术风险:官方已停止维护 Activiti 6,7 版本无实质改进
适用场景:遗留系统维护、教育行业教学案例

【2】选型决策框架

(1)需求优先级评估

在这里插入图片描述

(2)行业实践建议

(1)金融 / 保险:选择 Camunda,其流程实例迁移和审计功能满足合规要求
(2)互联网 / 电商:优先 Temporal,支持微服务架构和弹性扩展
(3)制造业:考虑 Flowable,轻量级部署降低产线改造成本
(4)政府 / 医疗:推荐 JBPM,结合 Drools 规则引擎实现动态政策适配

(3)技术栈适配

(1)Java 生态:Camunda(推荐)、Flowable、JBPM
(2)云原生架构:Temporal(推荐)、Camunda 8.x
(3)微服务:Temporal(推荐)、Flowable Spring Boot Starter
(4)低代码平台:Camunda Modeler + Zeebe,Flowable Form Engine

(4)成本模型

(1)开源成本:Flowable(全功能免费) > Camunda 社区版(基础功能) > Temporal(核心免费)
(2)商业成本:Camunda 企业版(按 CPU 核数计费) > JBPM(Red Hat 订阅) > Temporal(云服务按使用量)

【3】典型案例参考

(1)某跨国银行信贷审批系统
选择 Camunda,利用其流程实例迁移和分布式定时器功能,实现全球分支机构流程版本统一管理
(2)某电商平台订单处理
采用 Temporal,通过持久化状态和无锁架构,支撑百万级日活订单的异步处理
(3)某汽车制造企业供应链协同
部署 Flowable,快速集成 ERP 系统,实现采购、生产、物流流程自动化

【4】选型验证步骤

(1)概念验证(PoC)
用目标引擎实现核心业务流程(如审批、订单处理)
测试高并发场景下的吞吐量和响应时间
验证与现有系统的集成复杂度
(2)成本测算
开源版:评估功能缺口及二次开发成本
商业版:获取报价并对比 ROI(如 Camunda 企业版可提升流程效率 30%)
(3)长期维护
社区支持:查看 GitHub 活跃度、Stack Overflow 响应速度
商业支持:评估供应商服务能力(如 Camunda 提供 7×24 小时技术支持)

【5】为什么要引入流程引擎

(1)引入原因

(1)自动化流程管理:能将企业业务流程中的各个环节自动化,按照预设规则自动流转,减少人工干预,提高工作效率。
(2)提高流程透明度:使业务流程可视化,让参与者清楚了解流程进展、当前状态和下一步操作,便于监控和管理。
(3)便于流程优化:通过对流程数据的分析,发现流程中的瓶颈、浪费和不合理之处,为优化提供依据,持续改进业务流程。

(2)解决的问题和痛点

(1)流程复杂难以管理:当企业业务流程复杂,涉及多个部门和环节时,人工管理易出现混乱、延误和错误。Camunda 能清晰定义和管理复杂流程,确保各环节有序执行。
(2)流程变更困难:业务需求变化时,传统方式修改流程成本高、耗时长。Camunda 等流程引擎提供了灵活的流程定义和配置,方便快速修改和部署新流程。
(3)信息孤岛问题:不同系统间数据难以共享和交互,流程引擎可作为桥梁,集成多个系统,实现数据流通,确保流程中各环节信息及时准确传递。
(4)缺乏流程监控和分析:企业需实时了解流程运行状况,如流程执行时间、节点通过率等。Camunda 提供监控和分析功能,帮助企业及时发现问题,做出决策。
(5)人力成本高且效率低:人工处理流程易疲劳、出错,且受工作时间和精力限制。引入流程引擎可自动化处理重复、规律性任务,节省人力成本,提高效率和质量。

【二】概念介绍

【1】Camunda 概念:

(1)流程(PROCESS): 通过工具建模最终生成的BPMN文件,里面有整个流程的定义
(2)流程实例(Instance):流程启动后的实例
(3)流程变量(Variables):流程任务之间传递的参数
(4)任务(TASK):流程中定义的每一个节点
(5)流程部署:将之前流程定义的.bpmn文件部署到工作流平台

【2】BPMN 概念

【三】环境准备

提前安装Java1.8以上的JRE或JDK

【1】安装流程设计器CamundaModeler【画图工具】

CamundaModeler是官方提供的一个流程设计器,用于编辑流程图和其他模型【表单】,也就是一个流程图的绘图工具。
注意:下载解压以后安装到非中文目录中,否则路径可以访问失败

(1)下载安装

(1)下载地址:https://camunda.com/download/modeler/
下载并解压
(2)安装
启动页面如下,Modeler支持Camunda7与Camunda8,在这里我们可以根据自己需求开展BPMN、DMN建模。
在这里插入图片描述

【2】CamundaModeler如何使用

(1)模型分类

选择不同的模型,例如BPMN,DMN, CMMN,它们展示的画布界面是不一样的
(1)BPMN(Business Process Model and Notation)
这是一种图形表示法,用于在业务流程模型中指定业务流程的业务步骤。BPMN的目标是支持业务流程的管理,无论是那些流程已经被自动化,还是尚未被自动化。BPMN创建了一个标准化的桥梁,通过公共视觉语言填补了业务策略与业务实施之间的鸿沟。
(2)DMN(Decision Model and Notation)
这是一种标准化的模型和表示法,用于定义和管理业务决策。DMN的目标是使决策模型变得更加透明和可理解,同时也能够自动化决策过程。
(3)Form
在Camunda中,表单用于在业务流程中收集和显示数据。Camunda支持两种类型的表单:生成的表单和自定义表单。生成的表单是由Camunda自动创建的,基于流程变量的类型和结构。自定义表单则允许开发人员自定义表单的外观和行为。

(2)事件分类(Event)

事件代表流程生命周期中发生的特定时刻或条件,它们用于触发流程中的动作或改变流程的执行路径。事件通常用圆形表示,并根据其功能分为三类:
(1)开始事件:标记流程的起点,可以是普通开始事件、定时开始事件或信号开始事件等。
(2)中间事件:发生在流程执行过程中,如消息事件、定时器事件、错误事件等,会影响流程的流转。
(3)结束事件:标志着流程或某个流程分支的完成。
(4)边界事件:附着在活动或网关上,当特定条件满足时触发,影响所附着元素的行为。
(5)终止事件:特殊类型的结束事件,它不仅结束当前流程,还会取消任何未完成的并行分支或子流程。

在这里插入图片描述

(3)活动分类(Activity)

活动是工作或任务的一个通用术语,一个活动可以是一个任务,还可以是一个当前流程的子处理流程;其次,你还可以为活动指定不听的类型
在这里插入图片描述

只介绍最常用的两种
(1)用户任务 (User Task)具体来说就是需要手动执行的任务,即需要我们写完业务代码后,调用代码 taskService.complete(taskId, variables); 才会完成的任务
在这里插入图片描述

(2)系统任务(Service Task)系统会自动帮我们完成的任务
在这里插入图片描述

(4)网关分类(GateWay)

网关用来处理决策,有几种常用的网关
在这里插入图片描述

分为这么几类,会根据我们传入的流程变量及设定的条件走
在这里插入图片描述

(1)排他网关(exclusive gateway)
这个网关只会走一个,我们走到这个网关时,会从上到下找第一个符合条件的任务往下走
(2)并行网关(Parallel Gateway)
这个网关不需要设置条件,会走所有的任务
(3)包含网关(Inclusive Gateway)
这个网关会走一个或者多个符合条件的任务

在这里插入图片描述
如上图包含网关,需要在网关的连线初设置表达式 condition,参数来自于流程变量

两个参数:switch2d 、 switch3d

如果 都为true,则走任务1,3
如果 switch2d 为true switch3d为false,则只走任务1
如果 switch3d 为true switch2d为false,则只走任务3
如果都为false,则直接走网关,然后结束

(5)表单传参(Form)

(6)监听器

(1)Execution Listener(执行监听器)

执行监听器可以在流程执行期间发生某些事件时执行额外的Java代码或者计算表达式,这些事件包含:

(1)流程的开始与结束
(2)活动的开始与结束
(3)网关的开始与结束
(4)中间事件的开始与结束

执行监听器的触发事件有:
(1)start
(2)end
(3)take

其中节点有start、end两种事件,而连线则有take事件。连线也称之为过渡,take事件也就是在进行过渡

针对监听器类型首先演示一下Expression,在Start Event节点设置执行监听器。
在这里插入图片描述与其他表达式一样,执行变量可以被解析使用。因为执行实现对象有一个公开事件名称的属性,所以可以使用execution. eventName将事件名称传递给方法。

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1z0li53" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.19.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.20.0">
  <bpmn:process id="Process_0ic6df5" isExecutable="true" camunda:historyTimeToLive="180">
    <bpmn:extensionElements />
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:extensionElements>
        <camunda:executionListener expression="${StartEvent.hello(execution.eventName)}" event="start" />
      </bpmn:extensionElements>
      <bpmn:outgoing>Flow_16zen96</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="Flow_16zen96" sourceRef="StartEvent_1" targetRef="Activity_0im4bve">
      <bpmn:extensionElements />
    </bpmn:sequenceFlow>
    <bpmn:endEvent id="Event_1w6waod">
      <bpmn:extensionElements />
      <bpmn:incoming>Flow_1yfgw6n</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_1yfgw6n" sourceRef="Activity_0im4bve" targetRef="Event_1w6waod" />
    <bpmn:userTask id="Activity_0im4bve" name="审批人" camunda:assignee="${assignee}">
      <bpmn:extensionElements />
      <bpmn:incoming>Flow_16zen96</bpmn:incoming>
      <bpmn:outgoing>Flow_1yfgw6n</bpmn:outgoing>
    </bpmn:userTask>
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0ic6df5">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="179" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_1w6waod_di" bpmnElement="Event_1w6waod">
        <dc:Bounds x="432" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_02axx51_di" bpmnElement="Activity_0im4bve">
        <dc:Bounds x="270" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_16zen96_di" bpmnElement="Flow_16zen96">
        <di:waypoint x="215" y="117" />
        <di:waypoint x="270" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1yfgw6n_di" bpmnElement="Flow_1yfgw6n">
        <di:waypoint x="370" y="117" />
        <di:waypoint x="432" y="117" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

在代码中如下处理

@Slf4j
@Component("StartEvent")
public class StartEvent {
    public void hello(String eventName){
        log.info("挑战Camunda,eventName--{}",eventName);
    }
}

发起流程实力后:
在这里插入图片描述

(2)Task Listener(任务监听器)

任务监听器的触发事件类型如下:

(1)create :在create事件之前不会触发其他与任务相关的事件
(2)assignment:专门跟踪assignee 属性的更改
(3)complete:在任务从运行时数据中删除之前,发生complete事件,该事件的成功执行代表任务事件生命周期的结束。
(4)delete:删除事件发生在从运行时数据中删除任务之前,在delete事件之后不会触发其他事件,因为它也代表任务事件生命周期的结束。这意味着delete事件与complete事件是互斥的
(5)update:当修改任务属性时触发,任务属性包含分配人,所有者,注释,任务局部变量等
(6)timeout:当与此任务监听器关联的计时器到期时,将发生超时事件。注意,这需要定义一个Timer。

设计流程定义,在用户任务节点设置任务监听器的六种事件
在这里插入图片描述
这里比较特殊的是Timeout,此处的ID就是唯一标识符,仅在事件设置为timeout时才需要。

在这里插入图片描述
选择不同的Type就要设置不同格式的值:

(1)Date: 格式为ISO 8601格式的固定时间和日期 比如 2024-03-11T12:13:14Z
(2)Duration:两种格式分别为PnYnMnDTnHnMnS、PnW。比如P10D 【间隔10天】
(3)Cycle:两种格式分别为ISO 8601重复间隔标准规定的重复持续时间格式,比如R3/PT10H【3 次重复间隔,每次持续 10 小时】、第二种就是cron表达式

注意下面流程定义中的任务监听器 Java class路径 一定要根据自己的项目做调整

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1s60m5d" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.19.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.20.0">
  <bpmn:process id="Process_144a42e" isExecutable="true" camunda:historyTimeToLive="180">
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:extensionElements />
      <bpmn:outgoing>Flow_1qgw3o5</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="Flow_1qgw3o5" sourceRef="StartEvent_1" targetRef="Activity_0gpxgwy">
      <bpmn:extensionElements />
    </bpmn:sequenceFlow>
    <bpmn:userTask id="Activity_0gpxgwy" name="测试任务监听器" camunda:assignee="${assignee}">
      <bpmn:extensionElements>
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="create" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="assignment" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="complete" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="delete" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="update" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="timeout" id="listenerOne">
          <bpmn:timerEventDefinition id="TimerEventDefinition_1kyckfx">
            <bpmn:timeCycle xsi:type="bpmn:tFormalExpression">5 * * * * ?</bpmn:timeCycle>
          </bpmn:timerEventDefinition>
        </camunda:taskListener>
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_1qgw3o5</bpmn:incoming>
      <bpmn:outgoing>Flow_0589xmv</bpmn:outgoing>
      <bpmn:multiInstanceLoopCharacteristics camunda:collection="${assigneeList}" camunda:elementVariable="assignee">
        <bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances == 1}</bpmn:completionCondition>
      </bpmn:multiInstanceLoopCharacteristics>
    </bpmn:userTask>
    <bpmn:endEvent id="Event_0ybej7n">
      <bpmn:incoming>Flow_0589xmv</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0589xmv" sourceRef="Activity_0gpxgwy" targetRef="Event_0ybej7n" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_144a42e">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="179" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_087xqfi_di" bpmnElement="Activity_0gpxgwy">
        <dc:Bounds x="270" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0ybej7n_di" bpmnElement="Event_0ybej7n">
        <dc:Bounds x="432" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_1qgw3o5_di" bpmnElement="Flow_1qgw3o5">
        <di:waypoint x="215" y="117" />
        <di:waypoint x="270" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0589xmv_di" bpmnElement="Flow_0589xmv">
        <di:waypoint x="370" y="117" />
        <di:waypoint x="432" y="117" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

相关代码:

/**
 * 流程实例相关接口
 *
 * @author lonewalker
 */
@RequestMapping("/process/instance")
@RequiredArgsConstructor
@RestController
public class ProcessInstanceController {
 
    private final RuntimeService runtimeService;
 
    private final TaskService taskService;
 
    /**
     * 根据流程定义key发起流程实例
     *
     * @param requestParam 请求参数
     * @return 流程实例id
     */
    @PostMapping("/startProcessInstanceByKey")
    public String startProcessInstanceByKey(@RequestBody StartProcessRequest requestParam) {
        Map<String, Object> paramMap = new HashMap<>(8);
        List<String> assigneeList = new ArrayList<>();
        assigneeList.add("10086");
        assigneeList.add("10087");
        assigneeList.add("10088");
        paramMap.put("assigneeList", assigneeList);
 
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(requestParam.getProcessDefinitionKey(), requestParam.getBusinessKey(), paramMap);
        return processInstance.getProcessInstanceId();
    }
 
    /**
     * 完成单个任务
     *
     * @param requestParam 请求参数
     * @return {@code true 成功}
     */
    @PostMapping("/completeSingleTask")
    public Boolean completeSingleTask(@RequestBody @Validated CompleteTaskRequest requestParam) {
        taskService.complete(requestParam.getTaskId());
        return true;
    }
 
    /**
     * 转交任务
     *
     * @param requestParam 请求参数
     * @return {@code true 成功}
     */
    @PostMapping("/transferTask")
    public Boolean transferTask(@RequestBody TransferTaskRequest requestParam){
        taskService.setAssignee(requestParam.getTaskId(), requestParam.getUserId());
        return true;
    }
}
@Data
public class TransferTaskRequest {
 
    private String taskId;
 
    private String userId;
}

自定义任务监听器:

@Slf4j
@Component
public class CustomTaskListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        log.info("Event Type--{}",delegateTask.getEventName());
    }
}

发起流程实例后,等待超过设置的时间,控制台打印如下:

在这里插入图片描述

调用新增的【转交任务】接口,可以看到又执行了update和assignment,符合预期。

在这里插入图片描述由于我们设置的是三个人或签,当调用完成任务接口后:第一个任务触发complete,另外两个触发delete

在这里插入图片描述

(3)执行监听器和任务监听器的区别?

Camunda的执行监听器和任务监听器是用于添加自定义逻辑的监听器,它们的区别在于作用对象和触发事件的不同。

(1)执行监听器
是与BPMN流程中的各种流程元素(例如开始事件、用户任务、服务任务、网关等)相关联的。执行监听器可以在流程元素执行前、执行后或抛出异常时添加自定义逻辑,例如在服务任务执行前进行参数验证、在网关执行后进行决策评估。

(2)任务监听器
是与任务节点相关联的,用于监听任务的创建、分配和完成等事件。任务监听器可以在任务节点的生命周期中添加自定义逻辑,例如在任务完成时发送通知邮件、在任务创建时设置任务优先级。

下面列举一些应用场景,以说明何时使用执行监听器和任务监听器:

(1)适用于执行监听器的场景:

在服务任务执行前对参数进行验证,以确保输入的正确性;
在网关执行后对决策进行评估,以决定下一步应该执行哪个分支;
在用户任务执行前对权限进行验证,以确保用户有权执行该任务;
在抛出异常时记录异常信息,以便后续处理。

(2)适用于任务监听器的场景:
在任务完成时发送通知邮件,以通知相关人员任务已经完成;
在任务创建时设置任务优先级,以指定任务的紧急程度;
在任务分配时更新任务信息,例如设置任务截止时间、设置任务处理人等。
总之,执行监听器和任务监听器可以根据具体的业务需求进行灵活使用,以添加自定义的逻辑和行为,提高流程的可扩展性和可重用性。

(7)指派任务处理人

(8)根绝角色动态获取审批人

实际项目中可能会设置节点审批人是角色,那这个角色对应的人是动态变化的,假如流程被驳回到中间的某个节点再回到这个节点,需要保证获取角色对应的人员是最新的。

前文的演示中都是在发起流程实例时直接传入审批人参数,这里演示一下如何通过执行监听器 take获取审批人,介绍Execution Listener时也提到take是在连线【过渡】上设置的。当然也有朋友疑惑为何不在Task Listener上设置审批人,首先我们知道assignment事件是在assignee属性变更时触发,此时我们要设置assignee肯定要在它之前就要拿到值,那还剩个create事件,我们测试一下:

在执行监听器完成后就报错了
在这里插入图片描述
也就是一定要在创建任务之前就拿到审批人数据,流程引擎是根据传入的数据以及配置来判断创建几个任务

修改流程定义,在用户任务节点前的连线【过渡】上设置执行监听器take事件

在这里插入图片描述
我的路径是com.lonewalker.demo.listener.CustomExecutionListener,各位根据自己的项目修改

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1s60m5d" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.19.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.20.0">
  <bpmn:process id="Process_144a42e" isExecutable="true" camunda:historyTimeToLive="180">
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:extensionElements />
      <bpmn:outgoing>Flow_1qgw3o5</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="Flow_1qgw3o5" sourceRef="StartEvent_1" targetRef="Activity_0gpxgwy">
      <bpmn:extensionElements>
        <camunda:executionListener class="com.lonewalker.demo.listener.CustomExecutionListener" event="take" />
      </bpmn:extensionElements>
    </bpmn:sequenceFlow>
    <bpmn:userTask id="Activity_0gpxgwy" name="测试任务监听器" camunda:assignee="${assignee}">
      <bpmn:extensionElements>
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="create" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="assignment" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="complete" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="delete" />
        <camunda:taskListener class="com.lonewalker.demo.listener.CustomTaskListener" event="update" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_1qgw3o5</bpmn:incoming>
      <bpmn:outgoing>Flow_0589xmv</bpmn:outgoing>
      <bpmn:multiInstanceLoopCharacteristics camunda:collection="${assigneeList}" camunda:elementVariable="assignee">
        <bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances == 1}</bpmn:completionCondition>
      </bpmn:multiInstanceLoopCharacteristics>
    </bpmn:userTask>
    <bpmn:endEvent id="Event_0ybej7n">
      <bpmn:incoming>Flow_0589xmv</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0589xmv" sourceRef="Activity_0gpxgwy" targetRef="Event_0ybej7n" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_144a42e">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="179" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_087xqfi_di" bpmnElement="Activity_0gpxgwy">
        <dc:Bounds x="270" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0ybej7n_di" bpmnElement="Event_0ybej7n">
        <dc:Bounds x="432" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_1qgw3o5_di" bpmnElement="Flow_1qgw3o5">
        <di:waypoint x="215" y="117" />
        <di:waypoint x="270" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0589xmv_di" bpmnElement="Flow_0589xmv">
        <di:waypoint x="370" y="117" />
        <di:waypoint x="432" y="117" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

代码中将发起流程实例时传入的审批人参数转到执行监听器中:

@Slf4j
@Component
public class CustomExecutionListener implements ExecutionListener {
    @Override
    public void notify(DelegateExecution execution) {
        log.info("【执行监听器】Event Type--{}", execution.getEventName());
        List<String> assigneeList = new ArrayList<>();
        assigneeList.add("10086");
        assigneeList.add("10087");
        assigneeList.add("10088");
        execution.setVariable("assigneeList", assigneeList);
    }
}

重新部署、发起流程实例
在这里插入图片描述

(9)创建任务节点没有指定处理人行不行?

(10)

【3】安装web应用CamundaBPM【管理平台】

(1)介绍

Camunda BPM Run 是一个用于部署、运行和管理 Camunda BPM 平台的独立程序。它集成了 Camunda 引擎和 Web 应用程序,提供了一个方便的方式来启动和管理 Camunda BPM 平台。

具体来说,Camunda BPM Run 有以下作用:
(1)部署和管理 Camunda BPM 平台:Camunda BPM Run 提供了一个独立的运行时环境,您可以使用它来部署和管理 Camunda BPM 平台。它集成了 Camunda 引擎、Camunda Web 应用程序和数据库,使您可以方便地启动和配置整个平台。
(2)运行工作流和流程引擎:Camunda BPM Run 提供了一个工作流引擎,可以执行和管理业务流程。您可以使用 Camunda Modeler 创建和编辑 BPMN 流程模型,然后将其部署到 Camunda BPM Run 中进行执行。Camunda BPM Run 提供了任务管理、流程实例跟踪、变量管理等功能,使您可以轻松地追踪和管理工作流的执行状态。
(3)提供 Web 应用界面:Camunda BPM Run 包含一个集成了 Camunda Web 应用程序的 Web 服务器。通过该界面,您可以轻松地管理和监控流程的执行,查看任务列表、处理用户任务、查看流程实例的状态等。Web 应用程序还提供了一个用户任务表单引擎,可以定义和渲染用户任务的表单。
(4)支持扩展和集成:Camunda BPM Run 允许您利用 Camunda 引擎的扩展能力,通过编写插件或自定义扩展来满足特定的需求。它还提供了一些集成选项,可以与其他系统进行集成,例如数据库、消息队列和外部服务。

总之,Camunda BPM Run 提供了一个独立的运行时环境,使您可以轻松地部署、运行和管理 Camunda BPM 平台。它集成了 Camunda 引擎和 Web 应用程序,提供了丰富的功能和界面,支持管理工作流、流程实例和用户任务,并支持扩展和集成以满足特定需求。

(2)下载启动

(1)下载地址:https://downloads.camunda.cloud/release/camunda-bpm/run/7.17/
7.17支持jdk1.8,7.20不支持

(2)启动 Camunda BPM
进入解压后的目录,找到 bin 文件夹,使用以下命令启动 Camunda BPM:

cd /Library/Java/AllenCamunda/camunda-bpm-run-7.17.0
sh start.sh

在这里插入图片描述
(3)进入页面
账号密码:都是demo
在这里插入图片描述

(3)配置本地mysql数据库

(1)建库

连接本地mysql,为Camunda平台创建一个数据库模式,名称为camunda

(2)导入SQL脚本

将camunda 自带的40张数据库表初始化到本地mysql的库里,执行创建所有必需的表和默认索引的SQL DDL脚本。这些脚本可以在configuration/sql/create文件夹中找到。共2个脚本,都需要导入。
在这里插入图片描述
在这里插入图片描述

(3)配置数据源

找到安装目录下的camunda-bpm-run-7.15.0\configuration\default.yml文件,修改datasource的配置为mysql,将JDBC URL和登录凭据添加到配置文件中,如下:
在这里插入图片描述

  url: jdbc:mysql://127.0.0.1:3306/camunda715?characterEncoding=UTF-8&useUnicode=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
  driver-class-name: com.mysql.cj.jdbc.Driver
  username: root
  password: root
(4)替换数据库驱动包

找到安装目录下的camunda-bpm-run-7.15.0\configuration\ userlib下,删除h2的驱动包,放置mysql的驱动包。
在这里插入图片描述

(5)重新启动camunda

关闭应用。然后启动

(6)登录验证

启动完成后,登录http://127.0.0.1:8080/camunda/app/admin/default/#/login,输入demo/demo账号登录
在这里插入图片描述
查看数据库act_id_user表,一条默认数据已经初始化了,说明camunda已经连接mysql成功了。
在这里插入图片描述

【四】BPMN设计基本案例

【1】入门案例

(1)绘制流程图

选择BPMN diagram
在这里插入图片描述然后保存bpmn文件

(2)外置任务创建

基于java实现
(1)Maven引入相关依赖。

	<properties>
		<camunda.external-task-client.version>7.17.0</camunda.external-task-client.version>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>
 
	<dependencies>
		<dependency>
			<groupId>org.camunda.bpm</groupId>
			<artifactId>camunda-external-task-client</artifactId>
			<version>${camunda.external-task-client.version}</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.7.36</version>
		</dependency>
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.3.1</version>
		</dependency>
	</dependencies>

(2)创建外部主题订阅类
主题名应与上面创建的模板中配置的相同。

package com.zeeboomdog.camundatest.IntroductionDemo;
 
import java.util.logging.Logger;
import java.awt.Desktop;
import java.net.URI;
 
import org.camunda.bpm.client.ExternalTaskClient;
 
/**
 * 主题任务订阅
 */
public class ChargeCardWorker {
    private final static Logger LOGGER = Logger.getLogger(ChargeCardWorker.class.getName());
 
    public static void main(String[] args) {
        ExternalTaskClient client = ExternalTaskClient.create()
                .baseUrl("http://localhost:8080/engine-rest")//web应用
                .asyncResponseTimeout(10000)//超时时间
                .build();
 
        // 订阅外部主题任务
        client.subscribe("charge-card")
                .lockDuration(1000) // 超时时间 默认:20s
                .handler((externalTask, externalTaskService) -> {
                    try {
                        // 业务逻辑
                        businessFun();
 
                        // 取得流程信息
                        String item = externalTask.getVariable("item");
                        Integer amount = externalTask.getVariable("amount");
 											// 完成以后打开这个页面
                        Desktop.getDesktop().browse(new URI("https://docs.camunda.org/get-started/quick-start/complete"));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
 
                    // Complete the task
                    externalTaskService.complete(externalTask);
                }).open();
    }
 
    /**
     * 业务逻辑方法
     */
    private static void businessFun() {
        // do nothing
    }
}

(3)流程部署操作

在这里插入图片描述
在这里插入图片描述

部署成功后,可在Camunda中的cockpit查看

在这里插入图片描述

(4)发起流程

使用postman发起请求到BPM,触发请求,后台就会根据配置的流程完成

在这里插入图片描述
调用成功以后就会打开代码的配置页面:https://docs.camunda.org/get-started/quick-start/complete

【2】用户任务案例

(1)添加审批节点

Assignee表示处理人,现在使用默认的用户demo,groups指的是群组,users表示的是多个审批人
在这里插入图片描述

(2)配置表单

在这里插入图片描述在这里插入图片描述

(3)部署流程

deploy部署,然后到camunda平台的cockpit查看,确认流程已经创建

在这里插入图片描述

进入tasklist,点击start process启动流程,选择要启动的流程
在这里插入图片描述

填写流程里配置的key,没有就不写,然后把流程form配置的3个参数加上,点击start
在这里插入图片描述
底部出现以下提示就表示启动成功
在这里插入图片描述
点击all tasks刷新一下,就能看到新启动的任务了
在这里插入图片描述
点击任务看一下,就能看到表单的参数信息,还有处理的历史记录history,还有diagram是流程信息(高亮的部分就是当前正处于的节点)
在这里插入图片描述
点击complete就代表对此节点进行审批,但是还没有配置网关,所以没法无论是否勾选同意都会走到刷卡的节点,接着看下一个案例

(4)测试流程

【3】网关任务实例

使用排他网关
在这里插入图片描述
判断付款金额的网关判断条件
在这里插入图片描述
付款审批的网关判断条件
在这里插入图片描述

在这里插入图片描述

【4】梳理最终的逻辑

(1)首先发起付款
(2)节点判断金额大小,决定是直接刷卡付款还是走审批
(3)如果是走付款审批,就会给指定的审批人创建任务,并把申请的参数发给审批人,审批人在后台进行审批
(4)如果审批同意,就会走到刷卡付款,触发项目里的外置任务进行付款,最终返回付款完成
(5)如果审批不同意,就会返回付款失败

【5】监听器使用

首先监听器分为ExecutionListener【执行监听器】和TaskListener【任务监听器】,我们知道不管是连线还是任务节点底层都有对应的执行器,所以ExecutionListener是可以设置在连线和节点上的。

哪些场景下可以使用监听器去完成:
(1)动态分配节点处理人
(2)流程运行到某个节点时通知待办人
(3)统计任务处理时间
(4)处理业务逻辑

(1)执行监听器

执行监听器的触发事件有:start、end、take;其中节点有start、end两种事件,而连线则有take事件。

给或签节点设置了开始事件和结束事件的执行监听器

在这里插入图片描述开启一条流程实例测试一下,这里我们在发起流程时就把所有节点审批人参数设置好。

在这里插入图片描述发起人节点调用审批通过后就触发或签节点的执行监听器开始事件,两次是因为该监听事件是设置在节点上的,而或签节点设置了两个处理人,所以它会创建两个待办任务,每个待办任务都有对应的执行器,等到或签节点有人审批通过了,就会触发执行监听器结束事件。

在这里插入图片描述
接下来我们改变一下设置审批人的方式,通过任务节点上的执行监听器去设置

@Component
public class CustomExecutionListener implements ExecutionListener {
    @Override
    public void notify(DelegateExecution delegateExecution) throws Exception {
        System.out.println("ExecutionListener--事件:【"+delegateExecution.getEventName()+"】--触发了");
        if ("start".equals(delegateExecution.getEventName())){
            List<String> userOneList = new ArrayList<>();
            userOneList.add("zhangsan");
            userOneList.add("lisi");
           delegateExecution.setVariable("userOneList",userOneList);
        }
    }
}

当发起人节点审批后,会报错

在这里插入图片描述
说明在触发该监听器之前,流程引擎就需要知道该节点上有几个待办任务从而创建对应数量的执行器。这也是上一篇动态设置审批人时在节点前连线上设置执行监听器的原因。

当前你可以选择在节点上的执行监听器结束事件触发时去设置下一节点审批人,不过我感觉麻烦一点。

(2)任务监听器

任务监听器的触发事件有:create, assignment, update, complete, delete or timeout。

这里我们演示常用的create、assignment、complete事件

在这里插入图片描述重新部署后,把审批人设置方式再改回到发起流程时设置,然后发起流程实例,这里如果执行监听器的触发在任务监听器之前,那就更不能在任务监听器上动态设置审批人了。

在这里插入图片描述先看红框标记部分,当任务监听器的完成事件触发后才会触发节点上执行监听器的结束事件,然后触发下一个节点的执行监听器开始事件,接着就是创建任务和分配任务时触发任务监听器。

绿框标记部分说明待办任务调用审批通过接口后会触发任务监听器的完成事件,如果满足了该节点完成条件,就会连续触发的待办任务对应的执行监听器结束事件。

所以动态设置审批人的条件就是要在节点执行监听器的开始事件触发之前就设置好审批人参数。

【6】会签、或签、比例签

无论是或签还是比例签都是会签,都是多实例节点,只是该节点通过的规则不同。演示会签通过的三种规则:【所有人审批通过】、【一人审批通过】、【按比例投票】。而会签又可以分为并行与串行。 三条垂直线表示实例将并行执行,而三条水平线表示顺序【串行】执行。

在这里插入图片描述

注意:设置的是通过规则,对于驳回操作均为一人驳回则驳回。

用户任务可以直接分配给单个用户、用户列表或组列表,本文将演示分配给单个用户、用户列表两种方式,用户组在后续文章中也会演示。

以下变量名是在整个过程中比较重要的,结合示例理解并使用:

(1)nrOfInstances : 实例总数
(2)nrOfActiveInstances:当前活动的实例的数量。对于串行而言该值始终为1
(3)nrOfCompletedInstances:已经完成的实例数
(4)loopCounter :循环计数器
(5)Loop cardinality:循环基数。可选项。可以直接填整数,表示会签的人数。
(6)Collection:会签人数的集合,通常为list 如图assigneeList,在添加变量时名字要对应上
(7)Element variable:变量元素。选择Collection时必选,为collection集合每次遍历的元素。这个要和下图的assignee内容对应上
(8)Completion condition:节点的完成条件

使用案例
在这里插入图片描述

(1)或签

指同一个审批节点设置多个人,如ABC三人,三人会同时收到审批,只要其中任意一人审批即可到下一审批节点。

用户任务分配给单个用户可按如下图所示设置:
在这里插入图片描述一人通过 通常被称为【或签】,设置完成条件:

${nrOfCompletedInstances == 1}

还需注意变量元素名和Assignee中设置的变量名保持一致。类似于在Java中的fori循环,变量名是i,使用该变量时也应该用i。
在这里插入图片描述

(2)比例通过

//已完成的实例数占总实例数的三成以上就算通过
${nrOfCompletedInstances/nrOfInstances > 0.3}

(3)全部通过(会签)

指同一个审批节点设置多个人,如ABC三人,三人会同时收到审批,需全部同意之后,审批才可到下一审批节点。

//已完成的实例数 等于 总实例数才算通过
${nrOfCompletedInstances == nrOfInstances}

(4)测试实例

(1)流程定义

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1o78fuh" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.19.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.20.0">
  <bpmn:process id="Process_19w1rrm" isExecutable="true" camunda:historyTimeToLive="180">
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:outgoing>Flow_0g1nmt1</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="Flow_0g1nmt1" sourceRef="StartEvent_1" targetRef="root" />
    <bpmn:userTask id="root" name="发起人" camunda:assignee="${initiator}">
      <bpmn:incoming>Flow_0g1nmt1</bpmn:incoming>
      <bpmn:outgoing>Flow_178rknz</bpmn:outgoing>
    </bpmn:userTask>
    <bpmn:sequenceFlow id="Flow_178rknz" sourceRef="root" targetRef="Activity_0163wxf" />
    <bpmn:userTask id="Activity_0163wxf" name="一人通过" camunda:assignee="${assignee}">
      <bpmn:incoming>Flow_178rknz</bpmn:incoming>
      <bpmn:outgoing>Flow_1t1uand</bpmn:outgoing>
      <bpmn:multiInstanceLoopCharacteristics camunda:collection="${userOneList}" camunda:elementVariable="assignee">
        <bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances == 1}</bpmn:completionCondition>
      </bpmn:multiInstanceLoopCharacteristics>
    </bpmn:userTask>
    <bpmn:sequenceFlow id="Flow_1t1uand" sourceRef="Activity_0163wxf" targetRef="Activity_1hgbacv" />
    <bpmn:userTask id="Activity_1hgbacv" name="比例通过" camunda:assignee="${assignee}">
      <bpmn:incoming>Flow_1t1uand</bpmn:incoming>
      <bpmn:outgoing>Flow_1giv9ue</bpmn:outgoing>
      <bpmn:multiInstanceLoopCharacteristics camunda:collection="${userTwoList}" camunda:elementVariable="assignee">
        <bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances/nrOfInstances &gt; 0.3 }</bpmn:completionCondition>
      </bpmn:multiInstanceLoopCharacteristics>
    </bpmn:userTask>
    <bpmn:userTask id="Activity_14cwgmh" name="全部通过" camunda:assignee="${assignee}">
      <bpmn:incoming>Flow_1giv9ue</bpmn:incoming>
      <bpmn:outgoing>Flow_0xb5gog</bpmn:outgoing>
      <bpmn:multiInstanceLoopCharacteristics camunda:collection="${userThreeList}" camunda:elementVariable="assignee">
        <bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances == nrOfInstances}</bpmn:completionCondition>
      </bpmn:multiInstanceLoopCharacteristics>
    </bpmn:userTask>
    <bpmn:endEvent id="Event_0a7muzc">
      <bpmn:incoming>Flow_0xb5gog</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0xb5gog" sourceRef="Activity_14cwgmh" targetRef="Event_0a7muzc" />
    <bpmn:sequenceFlow id="Flow_1giv9ue" sourceRef="Activity_1hgbacv" targetRef="Activity_14cwgmh" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_19w1rrm">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="179" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0fh3xaa_di" bpmnElement="root">
        <dc:Bounds x="270" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_06ae8hl_di" bpmnElement="Activity_0163wxf">
        <dc:Bounds x="430" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0edrq19_di" bpmnElement="Activity_1hgbacv">
        <dc:Bounds x="590" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1j25q3g_di" bpmnElement="Activity_14cwgmh">
        <dc:Bounds x="760" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0a7muzc_di" bpmnElement="Event_0a7muzc">
        <dc:Bounds x="912" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_0g1nmt1_di" bpmnElement="Flow_0g1nmt1">
        <di:waypoint x="215" y="117" />
        <di:waypoint x="270" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_178rknz_di" bpmnElement="Flow_178rknz">
        <di:waypoint x="370" y="117" />
        <di:waypoint x="430" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1t1uand_di" bpmnElement="Flow_1t1uand">
        <di:waypoint x="530" y="117" />
        <di:waypoint x="590" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0xb5gog_di" bpmnElement="Flow_0xb5gog">
        <di:waypoint x="860" y="117" />
        <di:waypoint x="912" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1giv9ue_di" bpmnElement="Flow_1giv9ue">
        <di:waypoint x="690" y="117" />
        <di:waypoint x="760" y="117" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

(2)部署流程定义

在resources下新建目录bpmn用于存放流程定义文件,启动项目后调用部署接口:

/**
 * 流程定义相关接口
 * @author lonewalker
 */
@RequestMapping("/process/definition")
@AllArgsConstructor
@RestController
public class ProcessDefinitionController {
 
    private final RepositoryService repositoryService;
 
    /**
     * 部署流程定义
     *
     * @return 提示信息
     */
    @PostMapping("/deploy")
    public String deployProcessDefinition(){
        repositoryService.createDeployment()
                .addClasspathResource("bpmn/2.bpmn")
                .name("演示")
                .deploy();
        return "部署成功";
    }
}

(3)流程实例测试

/**
 * 流程实例相关接口
 *
 * @author lonewalker
 */
@RequestMapping("/process/instance")
@RequiredArgsConstructor
@RestController
public class ProcessInstanceController {
 
    private final RuntimeService runtimeService;
 
    private final TaskService taskService;
 
    /**
     * 根据流程定义key发起流程实例
     *
     * @param requestParam 请求参数
     * @return 流程实例id
     */
    @PostMapping("/startProcessInstanceByKey")
    public String startProcessInstanceByKey(@RequestBody StartProcessRequest requestParam) {
        Map<String, Object> paramMap = new HashMap<>(8);
        List<String> userOneList = new ArrayList<>();
        List<String> userTwoList = new ArrayList<>();
        List<String> userThreeList = new ArrayList<>();
 
        //一人通过节点的审批人
        userOneList.add("10086");
        userOneList.add("10087");
 
        //比例通过节点的审批人
        userTwoList.add("10087");
        userTwoList.add("10088");
        userTwoList.add("10089");
        userTwoList.add("10090");
        userTwoList.add("10091");
 
        //全部通过节点的审批人
        userThreeList.add("10090");
        userThreeList.add("10091");
 
        paramMap.put("initiator", "10086");
        paramMap.put("userOneList", userOneList);
        paramMap.put("userTwoList", userTwoList);
        paramMap.put("userThreeList", userThreeList);
 
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(requestParam.getProcessDefinitionKey(), requestParam.getBusinessKey(), paramMap);
        return processInstance.getProcessInstanceId();
    }
 
    /**
     * 完成单个任务
     *
     * @param requestParam 请求参数
     * @return 任务所在节点信息
     */
    @PostMapping("/completeSingleTask")
    public Boolean completeSingleTask(@RequestBody @Validated CompleteTaskRequest requestParam) {
        taskService.complete(requestParam.getTaskId());
        return true;
    }
}

(4)发起流程实例后让流程来到【一人通过】节点
在这里插入图片描述
(5)比例通过
在【比例通过】节点设置完成条件是通过人数占总人数的三成,所以只需两个人审批通过即通过:
在这里插入图片描述
两人审批通过后是符合预期来到最后一个节点
在这里插入图片描述
查看任务的历史表【act_hi_taskinst】,两个任务被完成,其他任务则被删除了。

(5)串行或者并行的扩展说明

多实例节点可以配置串行或并行。三条垂直线表示实例将并行执行,而三条水平线表示顺序执行。

在这里插入图片描述

【7】驳回

(1)简单的流程实现驳回

先演示一个简单的驳回并且动态获取审批人的案例

和之前的一样,把执行监听器的路径换成自己的

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1i3dpos" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.19.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.20.0">
  <bpmn:process id="Process_18ein8i" isExecutable="true" camunda:historyTimeToLive="180">
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:outgoing>Flow_1e2ksci</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="Flow_1e2ksci" sourceRef="StartEvent_1" targetRef="Activity_0t0na59">
      <bpmn:extensionElements />
    </bpmn:sequenceFlow>
    <bpmn:userTask id="Activity_0t0na59" name="发起人" camunda:assignee="${initiator}">
      <bpmn:incoming>Flow_1e2ksci</bpmn:incoming>
      <bpmn:outgoing>Flow_048c77g</bpmn:outgoing>
    </bpmn:userTask>
    <bpmn:sequenceFlow id="Flow_048c77g" sourceRef="Activity_0t0na59" targetRef="Activity_1gi2y5i">
      <bpmn:extensionElements>
        <camunda:executionListener class="com.lonewalker.demo.listener.CustomExecutionListener" event="take" />
      </bpmn:extensionElements>
    </bpmn:sequenceFlow>
    <bpmn:userTask id="Activity_1gi2y5i" name="经理" camunda:assignee="${assignee}">
      <bpmn:incoming>Flow_048c77g</bpmn:incoming>
      <bpmn:outgoing>Flow_1iykwgr</bpmn:outgoing>
      <bpmn:multiInstanceLoopCharacteristics camunda:collection="${assigneeList}" camunda:elementVariable="assignee">
        <bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances == 1}</bpmn:completionCondition>
      </bpmn:multiInstanceLoopCharacteristics>
    </bpmn:userTask>
    <bpmn:endEvent id="Event_0ksobhd">
      <bpmn:incoming>Flow_1iykwgr</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_1iykwgr" sourceRef="Activity_1gi2y5i" targetRef="Event_0ksobhd" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_18ein8i">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="179" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0tb19ah_di" bpmnElement="Activity_0t0na59">
        <dc:Bounds x="270" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0psrsxg_di" bpmnElement="Activity_1gi2y5i">
        <dc:Bounds x="430" y="77" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0ksobhd_di" bpmnElement="Event_0ksobhd">
        <dc:Bounds x="592" y="99" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_1e2ksci_di" bpmnElement="Flow_1e2ksci">
        <di:waypoint x="215" y="117" />
        <di:waypoint x="270" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_048c77g_di" bpmnElement="Flow_048c77g">
        <di:waypoint x="370" y="117" />
        <di:waypoint x="430" y="117" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1iykwgr_di" bpmnElement="Flow_1iykwgr">
        <di:waypoint x="530" y="117" />
        <di:waypoint x="592" y="117" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

新增驳回任务接口

/**
 * 流程实例相关接口
 *
 * @author lonewalker
 */
@RequestMapping("/process/instance")
@RequiredArgsConstructor
@RestController
public class ProcessInstanceController {
 
    private final RuntimeService runtimeService;
 
    private final TaskService taskService;
 
    /**
     * 根据流程定义key发起流程实例
     *
     * @param requestParam 请求参数
     * @return 流程实例id
     */
    @PostMapping("/startProcessInstanceByKey")
    public String startProcessInstanceByKey(@RequestBody StartProcessRequest requestParam) {
        Map<String, Object> paramMap = new HashMap<>(8);
        paramMap.put("initiator", requestParam.getInitiator());
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(requestParam.getProcessDefinitionKey(), requestParam.getBusinessKey(), paramMap);
        return processInstance.getProcessInstanceId();
    }
 
    /**
     * 完成单个任务
     *
     * @param requestParam 请求参数
     * @return {@code true 成功}
     */
    @PostMapping("/completeSingleTask")
    public Boolean completeSingleTask(@RequestBody @Validated CompleteTaskRequest requestParam) {
        taskService.complete(requestParam.getTaskId());
        return true;
    }
 
    /**
     * 转交任务
     *
     * @param requestParam 请求参数
     * @return {@code true 成功}
     */
    @PostMapping("/transferTask")
    public Boolean transferTask(@RequestBody TransferTaskRequest requestParam){
        taskService.setAssignee(requestParam.getTaskId(), requestParam.getUserId());
        return true;
    }
 
    /**
     * 驳回任务
     *
     * @param requestParam 请求参数
     * @return {@code true 成功}
     */
    @PostMapping("/rejectTask")
    public Boolean rejectTask(@RequestBody RejectTaskRequest requestParam){
        String processInstanceId = requestParam.getProcessInstanceId();
 
        //获取当前环节实例 这是个树结构
        ActivityInstance activity = runtimeService.getActivityInstance(processInstanceId);
        runtimeService.createProcessInstanceModification(processInstanceId)
        //取消现有的活动实例
        .cancelActivityInstance(activity.getId())
        //设置备注
        .setAnnotation("驳回")
        //让流程实例从目标活动重新开始
        .startBeforeActivity(requestParam.getTargetNodeId())
        .execute();
        return true;
    }
}

驳回接口的请求参数类

@Data
public class RejectTaskRequest {
    private String processInstanceId;
    /**
     * 驳回的目标节点id
     */
    private String targetNodeId;
 
    private String taskId;
}

自定义执行监听器

@Slf4j
@Component
public class CustomExecutionListener implements ExecutionListener {
 
    @Resource
    private RepositoryService repositoryService;
 
 
    @Override
    public void notify(DelegateExecution execution) {
        ExecutionEntity executionEntity = (ExecutionEntity) execution;
        TransitionImpl transition = executionEntity.getTransition();
        //连线的目标节点id
        String targetNodeId = transition.getDestination().getId();
        //针对多实例节点
        if (targetNodeId.contains(ActivityTypes.MULTI_INSTANCE_BODY)) {
            targetNodeId = targetNodeId.substring(0, targetNodeId.indexOf("#"));
        }
        //无论审批人是角色还是具体的人员,都是和节点关联并在保存流程定义时就入库的
        // 所以使用流程定义key+版本号+节点id 查询节点配置的审批人
 
        //流程定义id
        String processDefinitionId = executionEntity.getProcessDefinitionId();
        //流程定义key
        String processDefinitionKey = processDefinitionId.substring(0, processDefinitionId.indexOf(":"));
        //获取流程定义的版本
        final int version = repositoryService.getProcessDefinition(processDefinitionId).getVersion();
 
        String processInstanceId = executionEntity.getProcessInstanceId();
 
        //模拟不同的节点分配不同的审批人
        List<String> assigneeList = new ArrayList<>();
        if ("Activity_03k4yru".equals(targetNodeId)) {
            assigneeList.add("10087");
        } else if ("Activity_108tjp3".equals(targetNodeId)){
            assigneeList.add("10088");
        }
 
        //设置审批人
        execution.setVariable("assigneeList", assigneeList);
    }
}

部署流程定义并发起后,第一次经理节点是一个人
在这里插入图片描述在调用驳回接口之前,先去修改执行监听器中的审批人数据,模拟角色对应的人员变动:
在这里插入图片描述

可以看到流程已经回到发起人节点,发起人再提交其实就是把自己这个待办任务完成

再次回到该节点可以发现审批人是更新后的:
在这里插入图片描述

(2)经过执行监听器的驳回

上述那种可以看做是审批人不是通过执行监听器设置的情况,可以直接驳回到目标节点上。

当审批人是通过执行监听器设置的,就需要让流程实例从目标节点前的连线重新开始。

修改驳回接口:

    /**
     * 驳回任务
     *
     * @param requestParam 请求参数
     * @return {@code true 成功}
     */
    @PostMapping("/rejectTask")
    public Boolean rejectTask(@RequestBody RejectTaskRequest requestParam){
        String processInstanceId = requestParam.getProcessInstanceId();
        String targetNodeId = requestParam.getTargetNodeId();
 
        //获取当前环节实例 这是个树结构
        ActivityInstance activity = runtimeService.getActivityInstance(processInstanceId);
 
        //获取实例的流程定义 这里主要是为了拿到节点前的那条线的Id
        List<HistoricActivityInstance> hisActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).finished().list();
        List<String> nodeIds = hisActivityList.stream().map(HistoricActivityInstance::getActivityId).collect(Collectors.toList());
 
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
        BpmnModelInstance bpmnModel = repositoryService.getBpmnModelInstance(processInstance.getProcessDefinitionId());
        ModelElementInstance modelElement = bpmnModel.getModelElementById(targetNodeId);
        UserTask userTask = (UserTask) modelElement;
        Collection<SequenceFlow> incoming = userTask.getIncoming();
        String transitionId = "";
        for (SequenceFlow sequenceFlow : incoming) {
            FlowNode source = sequenceFlow.getSource();
            if (nodeIds.contains(source.getId())) {
                transitionId = sequenceFlow.getId();
                break;
            }
        }
 
        //注意这里的调整,取消的活动实例是驳回的任务所对应的
        runtimeService.createProcessInstanceModification(processInstanceId)
                .cancelActivityInstance(activity.getId())
                .setAnnotation("驳回")
                .startTransition(transitionId)
                .execute();
        return true;
    }

发起流程后让流程来到【主管】节点
在这里插入图片描述
此时调用驳回,会再次触发【经理】节点前的执行监听器
在这里插入图片描述

(3)扩展说明

在这里插入图片描述
假设上图的多实例节点处于活动状态,并且有两个实例,则它的活动树如下:

ProcessInstance
客户 - Multi-Instance Body
客户
客户

下面的修改在同一个多实例主体活动中启动了【客户】活动的第三个实例:

ProcessInstance processInstance = ...;
runtimeService.createProcessInstanceModification(processInstance.getId())
  .startBeforeActivity("客户")
  .execute();

但是凡事有利必有弊,流程实例修改是一个非常强大的工具,允许随意启动和取消活动。因此,很容易创建正常流程执行无法到达的情况,如下图:

在这里插入图片描述
当存在并行网关时,【经理2】已通过,【经理1】驳回,但是【经理2】通过后执行会卡在并行网关上,因为没有令牌会到达其他传入序列流,从而激活并行网关。【我们知道并行网关有个Join机制,也就是到达并行网关的所有并发执行在网关等待,直到每个传入序列流的执行到达为止。】

然而当发起人再次发起,原先卡在并行网关的执行反而会通过,流程走到【主管节点】

【经理1】审批通过后还是会卡在并行网关

在这里插入图片描述正是因为允许随意启动和取消活动,过于灵活自然会带来一些问题,取决于具体的流程模型。

【五】原理解析

【1】库表设计

(1)介绍

Camunda bpm流程引擎的数据库由多个表组成,表名都以ACT开头,第二部分是说明表用途的两字符标识。笔者在工作中用的Camunda7.11版本共47张表。体验环境:http://www.yunchengxc.com

(1)ACT_RE_:'RE’表示流程资源存储,这个前缀的表包含了流程定义和流程静态资源(图片,规则等),共5张表。
(2)ACT_RU_
:'RU’表示流程运行时。 这些运行时的表,包含流程实例,任务,变量,Job等运行中的数据。 Camunda只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录, 这样运行时表的数据量最小,可以最快运行。共15张表。
(3)ACT_ID_:'ID’表示组织用户信息,比如用户,组等,共6张表。
(4)ACT_HI_
:'HI’表示流程历史记录。 这些表包含历史数据,比如历史流程实例,变量,任务等,共18张表。
(5)ACT_GE_*:‘GE’表示流程通用数据, 用于不同场景下,共3张表。

数据表的清单如下

流程资源存储	act_re_case_def	CMMN案例管理模型定义表
流程资源存储	act_re_decision_def	DMN决策模型定义表
流程资源存储	act_re_decision_req_def	待确定
流程资源存储	act_re_deployment	流程部署表
流程资源存储	act_re_procdef	BPMN流程模型定义表
流程运行时	act_ru_authorization	流程运行时收取表
流程运行时	act_ru_batch	流程执行批处理表
流程运行时	act_ru_case_execution	CMMN案例运行执行表
流程运行时	act_ru_case_sentry_part	待确定
流程运行时	act_ru_event_subscr	流程事件订阅表
流程运行时	act_ru_execution	BPMN流程运行时记录表
流程运行时	act_ru_ext_task	流程任务消息执行表
流程运行时	act_ru_filter	流程定义查询配置表
流程运行时	act_ru_identitylink	运行时流程人员表
流程运行时	act_ru_incident	运行时异常事件表
流程运行时	act_ru_job	流程运行时作业表
流程运行时	act_ru_jobdef	流程作业定义表
流程运行时	act_ru_meter_log	流程运行时度量日志表
流程运行时	act_ru_task	流程运行时任务表
流程运行时	act_ru_variable	流程运行时变量表
组织用户信息	act_id_group	群组信息表
组织用户信息	act_id_info	用户扩展信息表
组织用户信息	act_id_membership	用户群组关系表
组织用户信息	act_id_tenant	租户信息表
组织用户信息	act_id_tenant_member	用户租户关系表
组织用户信息	act_id_user	用户信息表
流程历史记录	act_hi_actinst	历史的活动实例表
流程历史记录	act_hi_attachment	历史的流程附件表
流程历史记录	act_hi_batch	历史的批处理记录表
流程历史记录	act_hi_caseactinst	历史的CMMN活动实例表
流程历史记录	act_hi_caseinst	历史的CMMN实例表
流程历史记录	act_hi_comment	历史的流程审批意见表
流程历史记录	act_hi_dec_in	历史的DMN变量输入表
流程历史记录	act_hi_dec_out	历史的DMN变量输出表
流程历史记录	act_hi_decinst	历史的DMN实例表
流程历史记录	act_hi_detail	历史的流程运行时变量详情记录表
流程历史记录	act_hi_ext_task_log	历史的流程任务消息执行表
流程历史记录	act_hi_identitylink	历史的流程运行过程中用户关系
流程历史记录	act_hi_incident	历史的流程异常事件记录表
流程历史记录	act_hi_job_log	历史的流程作业记录表
流程历史记录	act_hi_op_log	待确定
流程历史记录	act_hi_procinst	历史的流程实例
流程历史记录	act_hi_taskinst	历史的任务实例
流程历史记录	act_hi_varinst	历史的流程变量记录表
流程通用数据	act_ge_bytearray	流程引擎二进制数据表
流程通用数据	act_ge_property	流程引擎属性配置表
流程通用数据	act_ge_schema_log	数据库脚本执行日志表

(2)表之间的关系

流程引擎的最核心表是流程定义、流程执行、流程任务、流程变量和事件订阅表。它们之间的关系见下面的UML模型。
在这里插入图片描述

(1)BPMN流程引擎
BPMN引擎共20张表,它们的实体定义和表关系如下:
在这里插入图片描述
BPMN引擎数据库表说明:
在这里插入图片描述

(2)DMN决策引擎
在这里插入图片描述
DMN引擎数据库表说明:

在这里插入图片描述
(3)CMMN案例引擎
CMMN(案例)引擎共5张表,其中待办任务和变量表,复用流程引擎的ACT_RU_TASK和ACT_RU_VARLABLE表。
在这里插入图片描述
CMMN(案例)引擎数据库表说明:
在这里插入图片描述

(4)实例历史记录表
camunda流程实例历史记录表共18张表,为了允许不同的配置并使表更加灵活,历史表不包含外键约束。
在这里插入图片描述
流程实例历史记录数据库表说明:
在这里插入图片描述
(5)组织用户身份表
在这里插入图片描述
用户身份数据库表说明:

在这里插入图片描述

(3)核心表

由于Camunda的表比较多,其中一部分是企业版功能需要的,比如批量操作功能、流程监控预警功能等,还有一部分是CMMN案例管理模型和DMN决策模型相关的表,本文仅介绍跟BPMN流程引擎相关的表。

【2】BPM流程引擎封装的接口

Camunda 7.17 流程引擎封装了多个核心服务接口,这些接口涵盖了流程定义、流程实例、任务、历史数据、用户与组管理以及管理监控等多个方面。以下为你详细介绍各主要接口、其参数作用和返回值。

在这里插入图片描述

(1)RepositoryService:管理流程定义和部署相关操作

该接口主要用于管理流程定义和部署相关操作。

1DeploymentBuilder createDeployment()
参数:无。
作用:创建一个新的部署构建器,用于构建一个部署对象,可添加流程定义文件、资源等。
返回值:DeploymentBuilder 对象,通过该对象可进一步配置部署信息。

(2ProcessDefinitionQuery createProcessDefinitionQuery()
参数:无。
作用:创建一个流程定义查询对象,可用于根据各种条件筛选流程定义。
返回值:ProcessDefinitionQuery 对象,用于后续链式调用设置查询条件。

(3DeploymentQuery createDeploymentQuery()
参数:无。
作用:创建一个部署查询对象,用于根据条件查询部署信息。
返回值:DeploymentQuery 对象,可用于链式设置查询条件。

(4void deleteDeployment(String deploymentId, boolean cascade)
参数:
deploymentId:要删除的部署的唯一标识符。
cascade:布尔值,若为 true,则会级联删除该部署下的所有流程实例、历史数据等;若为 false,仅删除部署本身。
作用:删除指定的部署。
返回值:无。

(2)RuntimeService:管理流程实例的运行时操作

此接口用于管理流程实例的运行时操作。
(1)启动流程实例相关接口

1ProcessInstance startProcessInstanceByKey(String processDefinitionKey)
参数:
processDefinitionKey:流程定义的键,是在 BPMN 文件中定义的流程定义的唯一标识,用于区分不同的流程定义。例如在 BPMN 文件里 <process id="myProcess"> ,myProcess 就是这个流程定义的键。
作用:根据流程定义的键启动一个新的流程实例。系统会根据该键找到对应的最新版本的流程定义,并创建一个新的流程实例开始执行。
返回值:ProcessInstance 对象,该对象包含了新启动的流程实例的相关信息,如流程实例 ID、业务键、所属的流程定义 ID 等。

(2ProcessInstance startProcessInstanceByKey(String processDefinitionKey, String businessKey)
参数:
processDefinitionKey:流程定义的键,作用同上。
businessKey:业务键,用于关联业务系统中的业务数据,是业务系统中业务对象的唯一标识。例如在一个订单处理流程中,业务键可以是订单号。
作用:根据流程定义的键启动一个新的流程实例,并关联一个业务键。这样在后续的操作中可以通过业务键来查询和管理相关的流程实例。
返回值:ProcessInstance 对象,包含新启动流程实例的详细信息。

(3ProcessInstance startProcessInstanceByKey(String processDefinitionKey, Map<String, Object> variables)
参数:
processDefinitionKey:流程定义的键。
variables:一个包含流程实例启动时所需变量的映射。这些变量可以在流程执行过程中被使用,例如可以传递一些初始数据,像订单金额、客户信息等。
作用:根据流程定义的键启动一个新的流程实例,并传入初始变量。这些变量会在流程实例的整个生命周期内可用。
返回值:ProcessInstance 对象,包含新启动流程实例的详细信息。

(2)查询流程实例相关接口

ProcessInstanceQuery createProcessInstanceQuery()
参数:无。
作用:创建一个流程实例查询对象,用于根据各种条件筛选和查询流程实例。通过这个查询对象可以链式调用其他方法来设置不同的查询条件。
返回值:ProcessInstanceQuery 对象,后续可以通过该对象的方法设置查询条件,如按流程实例 ID、业务键、流程定义 ID 等进行查询。

(3)管理流程实例相关接口

void deleteProcessInstance(String processInstanceId, String deleteReason)
参数:
processInstanceId:要删除的流程实例的唯一标识符。
deleteReason:删除流程实例的原因描述,该描述会被记录到历史数据中,方便后续审计和查看。
作用:删除指定的流程实例。删除后,该流程实例将不再存在于运行时环境中,但相关的历史数据可能仍然保留。
返回值:无。

(4)其他

1ProcessInstance startProcessInstanceById(String processDefinitionId, Map<String, Object> variables)
参数:
processDefinitionId:流程定义的唯一标识符。
variables:一个包含流程实例启动时所需变量的映射,可为空。
作用:根据流程定义的 ID 启动一个新的流程实例,并传入初始变量。
返回值:ProcessInstance 对象,代表新启动的流程实例。

(3)TaskService:管理用户任务

该接口主要用于管理用户任务。
主要方法及参数、返回值

(1)查询任务相关接口

TaskQuery createTaskQuery()
参数:无。
作用:创建一个任务查询对象,用于根据各种条件筛选和查询任务。可以通过该对象的链式调用方法设置查询条件,如按任务 ID、任务负责人、任务所属流程实例等进行查询。
返回值:TaskQuery 对象,用于后续设置查询条件并执行查询操作。

(2)认领和分配任务相关接口

1void claim(String taskId, String userId)
参数:
taskId:要认领的任务的唯一标识符。
userId:认领任务的用户的唯一标识符。
作用:将指定任务认领给指定用户。认领后,该任务就属于该用户,其他用户无法再认领该任务。
返回值:无。

(2void setAssignee(String taskId, String userId)
参数:
taskId:要分配的任务的唯一标识符。
userId:要分配给的用户的唯一标识符。
作用:将指定任务分配给指定用户。与 claim 方法不同的是,setAssignee 可以直接分配任务,不需要任务处于可认领状态。
返回值:无。

(3)完成任务相关接口

1void complete(String taskId)
参数:
taskId:要完成的任务的唯一标识符。
作用:完成指定任务。任务完成后,流程会继续向下执行到下一个节点。
返回值:无。

(2void complete(String taskId, Map<String, Object> variables)
参数:
taskId:要完成的任务的唯一标识符。
variables:一个包含任务完成时所需变量的映射。这些变量可以影响流程后续的执行逻辑,例如根据变量的值决定流程的分支走向。
作用:完成指定任务,并传入相关变量。任务完成后,流程会继续向下执行,同时这些变量会在后续流程中可用。
返回值:无。

(4)HistoryService:查询和管理流程引擎产生的历史数据

此接口用于查询和管理流程引擎产生的历史数据。
主要方法及参数、返回值

1HistoricProcessInstanceQuery createHistoricProcessInstanceQuery()
参数:无。
作用:创建一个历史流程实例查询对象,用于根据条件查询历史流程实例。
返回值:HistoricProcessInstanceQuery 对象,可用于链式设置查询条件。

(2HistoricTaskInstanceQuery createHistoricTaskInstanceQuery()
参数:无。
作用:创建一个历史任务实例查询对象,用于根据条件查询历史任务实例。
返回值:HistoricTaskInstanceQuery 对象,可用于链式设置查询条件。

(3HistoricVariableInstanceQuery createHistoricVariableInstanceQuery()
参数:无。
作用:创建一个历史变量实例查询对象,用于根据条件查询历史变量实例。
返回值:HistoricVariableInstanceQuery 对象,可用于链式设置查询条件。

(5)IdentityService:管理用户和组信息

该接口用于管理用户和组信息。
主要方法及参数、返回值

(1)User createUser()
参数:无。
作用:创建一个新的用户对象,后续可对该对象设置用户信息。
返回值:User 对象,代表新创建的用户。

(2)Group createGroup()
参数:无。
作用:创建一个新的组对象,后续可对该对象设置组信息。
返回值:Group 对象,代表新创建的组。

(3)UserQuery createUserQuery()
参数:无。
作用:创建一个用户查询对象,用于根据条件查询用户。
返回值:UserQuery 对象,可用于链式设置查询条件。

(4)GroupQuery createGroupQuery()
参数:无。
作用:创建一个组查询对象,用于根据条件查询组。
返回值:GroupQuery 对象,可用于链式设置查询条件。

(6)ManagementService:管理和监控流程引擎

该接口提供了一些管理和监控流程引擎的功能。
主要方法及参数、返回值

(1)JobQuery createJobQuery()
参数:无。
作用:创建一个作业查询对象,用于根据条件查询作业。
返回值:JobQuery 对象,可用于链式设置查询条件。

(2)void executeCommand(Command<T> command)
参数:
command:实现了 Command 接口的自定义命令对象。
作用:执行一个自定义的命令。
返回值:命令执行的结果,类型取决于 Command 接口的泛型 T。

(7)FormService:用于获取和提交表单数据。

(8)ProcessEngine:表示一个流程引擎实例

可以通过ProcessEngines.getDefaultProcessEngine()方法获取默认的流程引擎实例。

(9)示例代码

以下是一个简单的示例,展示如何使用部分接口:

import org.camunda.bpm.engine.*;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Task;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CamundaExample {
    public static void main(String[] args) {
        // 获取流程引擎实例
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();

        // 获取服务接口
        RepositoryService repositoryService = processEngine.getRepositoryService();
        RuntimeService runtimeService = processEngine.getRuntimeService();
        TaskService taskService = processEngine.getTaskService();

        // 启动流程实例
        Map<String, Object> variables = new HashMap<>();
        variables.put("inputVariable", "exampleValue");
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess", variables);

        // 查询任务
        List<Task> tasks = taskService.createTaskQuery()
               .processInstanceId(processInstance.getId())
               .list();

        // 认领并完成任务
        if (!tasks.isEmpty()) {
            Task task = tasks.get(0);
            taskService.claim(task.getId(), "testUser");
            Map<String, Object> taskVariables = new HashMap<>();
            taskVariables.put("outputVariable", "resultValue");
            taskService.complete(task.getId(), taskVariables);
        }

		// HistoryService 示例
        // 查询历史流程实例
        HistoricProcessInstanceQuery historicProcessInstanceQuery = processEngine.getHistoryService().createHistoricProcessInstanceQuery();
        List<org.camunda.bpm.engine.history.HistoricProcessInstance> historicInstances = historicProcessInstanceQuery.processDefinitionKey(processDefinitionKey).list();

        // 关闭流程引擎
        processEngine.close();
    }
}

【3】工作流操作流程和对应表的关系

(1)部署流程定义

(1)操作流程:开发人员将 BPMN 流程定义文件(.bpmn)部署到 Camunda 引擎。这个过程会把流程定义的相关信息存储到数据库中,以便后续启动流程实例使用。

(2)涉及表

  • ACT_RE_PROCDEF:流程定义表,存储已部署的流程定义的元数据,包括流程定义的 ID、名称、版本、部署 ID 等信息。
  • ACT_RE_DEPLOYMENT:部署表,记录每次部署的信息,如部署 ID、部署名称、部署时间等。
  • ACT_GE_BYTEARRAY:二进制资源表,存储部署的 BPMN 文件和其他相关资源的二进制数据。

(2)启动流程实例

(1)操作流程:当满足特定条件时,通过 Camunda 引擎启动一个新的流程实例。启动流程实例时,会创建一个新的流程实例记录,并根据流程定义中的初始步骤创建相应的任务。

(2)涉及表

  • ACT_RU_EXECUTION:运行时执行实例表,存储当前正在运行的流程实例和执行实例的信息。每个流程实例都有一个对应的执行实例,执行实例可以包含多个子执行实例,用于表示流程中的并行分支等情况。
  • ACT_RU_TASK:运行时任务表,存储当前正在运行的任务信息,如任务 ID、任务名称、任务的办理人、所属流程实例 ID 等。
  • ACT_RU_VARIABLE:运行时变量表,存储流程实例和任务在运行过程中使用的变量信息,包括变量名、变量值、变量类型等。

(3)任务处理

(1)操作流程:流程实例在运行过程中会产生各种任务,用户或系统根据任务的分配规则获取任务并进行处理。处理任务时,可能会更新任务的状态、完成任务并推动流程继续执行。

(2)涉及表

  • ACT_RU_TASK:运行时任务表会在任务处理过程中更新任务的状态,如任务的办理人、任务的完成时间等。当任务完成后,该任务记录会从该表中删除。
  • ACT_HI_TASKINST:历史任务实例表,记录任务的历史信息,包括任务的创建时间、完成时间、办理人、任务结果等。任务完成后,相关信息会从运行时任务表转移到历史任务实例表中。
  • ACT_RU_VARIABLE:运行时变量表可能会在任务处理过程中更新变量的值,以反映任务处理的结果或中间状态。
  • ACT_HI_VARINST:历史变量实例表,记录变量的历史信息,包括变量的创建时间、更新时间、变量值等。

(4)流程实例结束

(1)操作流程:当流程实例执行到结束节点时,流程实例结束。此时,会清理运行时相关的记录,并将流程实例的历史信息保存到历史表中。

(2)涉及表

  • ACT_RU_EXECUTION:运行时执行实例表中的对应记录会被删除,因为流程实例已经结束,不再有运行中的执行实例。
  • ACT_RU_TASK:运行时任务表中的所有相关任务记录会被删除,因为任务已经全部完成。
  • ACT_HI_PROCINST:历史流程实例表,记录流程实例的历史信息,包括流程实例的启动时间、结束时间、流程定义 ID、流程实例的状态等。
  • ACT_HI_ACTINST:历史活动实例表,记录流程实例中每个活动(如任务、网关等)的执行历史信息,包括活动的开始时间、结束时间、活动类型等。

(5)流程监控和查询

(1)操作流程:管理员或业务人员可以通过 Camunda 提供的 API 或监控界面查询流程实例、任务、变量等信息,以便对流程的执行情况进行监控和分析。

(2)涉及表

  • ACT_HI_PROCINST:用于查询流程实例的历史信息,如流程实例的启动时间、结束时间、流程定义 ID 等。
  • ACT_HI_TASKINST:用于查询任务的历史信息,如任务的创建时间、完成时间、办理人等。
  • ACT_HI_VARINST:用于查询变量的历史信息,如变量的创建时间、更新时间、变量值等。
  • ACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_VARIABLE:如果需要查询正在运行的流程实例、任务和变量信息,则会从这些运行时表中获取数据。

(6)流程定义的挂起和激活

(1)操作流程:可以对流程定义进行挂起和激活操作。挂起流程定义后,将无法再启动该流程定义的新实例,但已启动的流程实例会继续执行;激活流程定义后,可以再次启动新的流程实例。

(2)涉及表

  • ACT_RE_PROCDEF:流程定义表中的 SUSPENSION_STATE_ 字段会被更新,用于表示流程定义的挂起状态(1 表示激活,2 表示挂起)。

(7)流程实例的挂起和激活

(1)操作流程:可以对单个流程实例进行挂起和激活操作。挂起流程实例后,该流程实例将暂停执行,直到被激活;激活后,流程实例将继续执行。
(2)涉及表

  • ACT_RU_EXECUTION:运行时执行实例表中的 SUSPENSION_STATE_ 字段会被更新,用于表示流程实例的挂起状态(1 表示激活,2 表示挂起)。

【4】常用表

(1)ACT_RU_EXECUTION
该表用于存储运行中的流程实例信息。当一个流程实例被启动时,会在此表中创建一条记录,当流程实例结束时,对应的记录被删除
(2)ACT_RU_TASK
该表用于存储运行中的任务信息。每个任务都有一个对应的流程实例和流程定义,这些信息都可以在该表中找到。
(3)ACT_HI_PROCINST
该表用于存储历史流程实例信息。当一个流程实例结束时,其信息会被移动到该表中,以便于查询和报表生成。
(4)ACT_HI_TASKINST
该表用于存储历史任务信息。当一个任务完成时,其信息会被移动到该表中,以便于查询和报表生成。
(5)ACT_HI_ACTINST
该表用于存储历史流程实例中的活动(如任务、网关、事件)信息。每个活动都有一个对应的流程实例和流程定义,这些信息都可以在该表中找到。
(6)ACT_ID_USER和ACT_ID_GROUP
这两个表用于存储用户和用户组信息。在Camunda中,用户和用户组可以用于任务的分配和权限管理。

【5】常用的REST API

(1)流程定义API

GET /process-definition:查询流程定义列表
GET /process-definition/{id}/xml:查询流程定义的BPMN XML
POST /process-definition/key/{key}/start:根据流程定义的key启动流程实例
GET /process-definition/key/{key}/startForm:查询启动表单
GET /process-definition/{id}/diagram:查询流程定义的流程图

(2)流程实例API

GET /process-instance:查询流程实例列表
GET /process-instance/{id}:查询流程实例详情
DELETE /process-instance/{id}:删除流程实例
POST /process-instance/{id}/modification:修改流程实例
POST /process-instance/{id}/suspended:暂停或激活流程实例

(3)任务API

GET /task:查询任务列表
GET /task/{id}:查询任务详情
POST /task/{id}/complete:完成任务
POST /task/{id}/assignee:指定任务的受理人
POST /task/{id}/unclaim:将任务的受理人设置为null

(4)历史记录API

GET /history/process-instance:查询流程实例历史记录
GET /history/task:查询任务历史记录
GET /history/variable-instance:查询流程变量历史记录
GET /history/activity-instance:查询活动历史记录

【6】问题汇总

(1)流程引擎启动时候 如果不传入设置的动态变量能正常启动吗?

如果在流程中定义了变量相关的,必须在流程启动前把变量传入进去,否则camunda会再启动流程实例时候报 PropertyNotFoundException: Cannot resolve identifier ‘***’ 错误。

(2)如何通过监听器动态实现设置审批人?

(3)如果是会签或者或签有多个审批人的场景,那么 ExecutionListener【执行监听器】和TaskListener【任务监听器】 会触发多次还是一次?

多次。每个任务指派人都会执行一次

(4)监听器是异步执行还是同步执行的?

同步执行

(5)如何在审批任务中,到达那个审批节点的时候才去动态设置审批人呢?

动态设置审批人的条件就是要在节点执行监听器的开始事件触发之前就设置好审批人参数。

在连线上使用 ExecutionListener【执行监听器】的take监听。

在上一个节点的 ExecutionListener【执行监听器】的 end 或者是 TaskListener【任务监听器】 的complete 事件监听。

也可以通过加签的方式动态设置审批人(提交流程全部节点的审批人传入一个指定的默认审批人,再通过待办任务加签

【六】封装Camunda的工具类方法

ProcessException为自定义的运行时异常,被全局异常处理器拦截处理

package com.allen.learn_camunda.utils;

import com.allen.learn_camunda.common.exception.ProcessException;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.*;
import org.camunda.bpm.engine.history.HistoricActivityInstance;
import org.camunda.bpm.engine.history.HistoricActivityInstanceQuery;
import org.camunda.bpm.engine.history.HistoricProcessInstance;
import org.camunda.bpm.engine.history.HistoricTaskInstance;
import org.camunda.bpm.engine.repository.Deployment;
import org.camunda.bpm.engine.repository.DeploymentBuilder;
import org.camunda.bpm.engine.repository.ProcessDefinition;
import org.camunda.bpm.engine.runtime.ActivityInstance;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Task;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;
import org.camunda.bpm.model.bpmn.instance.FlowNode;
import org.camunda.bpm.model.bpmn.instance.SequenceFlow;
import org.camunda.bpm.model.bpmn.instance.UserTask;
import org.camunda.bpm.model.xml.instance.ModelElementInstance;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @ClassName: CamundaUtils
 * @Author: AllenSun
 * @Date: 2025/2/20 下午11:42
 */
// Camunda 工具类,封装了所有用到的 Camunda 方法
@Slf4j
public class CamundaUtils {
    private static final ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
    private static final RuntimeService runtimeService = processEngine.getRuntimeService();
    private static final TaskService taskService = processEngine.getTaskService();
    private static final RepositoryService repositoryService = processEngine.getRepositoryService();
    private static final HistoryService historyService = processEngine.getHistoryService();

    /**
     * 通过类路径部署流程
     * @param deploymentName 部署名称
     * @param classpathResource 类路径下的资源名称
     * @return 部署对象
     */
    public static Deployment deployProcessByClasspath(String deploymentName, String classpathResource) {
        Deployment deploy = null;
        try {
            DeploymentBuilder deploymentBuilder = repositoryService.createDeployment()
                    .name(deploymentName)
                    .addClasspathResource(classpathResource);
            deploy = deploymentBuilder.deploy();
        } catch (Exception e) {
            log.info("流程部署失败:{}",e.getMessage());
            throw new ProcessException("流程部署失败");
        }
        log.info("流程部署成功:{}",deploy.getId());

        return deploy;
    }


    /**
     * 根据部署key查询部署的流程
     * @MethodName: listProcessInstanceByKey
     * @Author: AllenSun
     * @Date: 2025/2/22 下午11:56
     */
    public static ProcessDefinition selectProcessDefinitionByKey(String processDefinitionKey) {
        // 根据流程定义键和业务键查询流程实例
        ProcessDefinition processDefinition = null;
        try {
            processDefinition = repositoryService.createProcessDefinitionQuery()
                    .processDefinitionKey(processDefinitionKey)
                    .latestVersion()
                    .singleResult();
        } catch (Exception e) {
            log.info("查询部署流程信息失败:{}",e.getMessage());
            throw new ProcessException("查询失败");
        }
        return processDefinition;
    }

    /**
     * 判断流程是否已部署
     * @MethodName: isProcessDefinitionExists
     * @Author: AllenSun
     * @Date: 2025/2/22 下午11:59
     */
    public static boolean isProcessDefinitionExists(String processDefinitionKey) {
        return selectProcessDefinitionByKey(processDefinitionKey) != null;
    }


    /**
     * 启动流程实例
     * @param processDefinitionKey 流程定义的 Key,对应 BPMN 文件中的流程定义 ID
     * @param variables 流程变量,用于传递业务数据
     * @return 流程实例 ID
     */
    public static String startProcessInstance(String processDefinitionKey, Map<String, Object> variables) {
        if(!isProcessDefinitionExists(processDefinitionKey)){
            throw new ProcessException("流程未部署");
        }
        ProcessInstance processInstance = null;
        try {
            processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey, variables);
        } catch (Exception e) {
            log.info("流程启动失败:{}:{}",processDefinitionKey,e.getMessage());
            throw new ProcessException("流程启动失败");
        }
        log.info("流程启动成功:{}",processDefinitionKey);
        return processInstance.getId();
    }

    /**
     * 判断指定流程定义键和业务键的流程实例是否已经创建
     * @param processDefinitionKey 流程定义键
     * @return 如果流程实例已存在返回 true,否则返回 false
     */
    public static boolean isProcessInstanceExists(String processDefinitionKey) {
        // 根据流程定义键和业务键查询流程实例
        List<ProcessInstance> processInstances = runtimeService.createProcessInstanceQuery()
                .processDefinitionKey(processDefinitionKey)
                .list();
        return !processInstances.isEmpty();
    }

    /**
     * 完成任务
     * @param taskId 任务 ID
     * @param variables 任务变量,用于传递审批结果和理由等信息
     */
    public static void completeTask(String taskId, Map<String, Object> variables) {
        taskService.complete(taskId, variables);
    }

    /**
     * 获取指定用户的待办任务列表
     * @param assignee 任务负责人的工号
     * @param pageNum 页码
     * @param pageSize 每页数量
     * @return 待办任务列表
     */
    public static List<Task> getTasksByAssignee(String assignee, int pageNum, int pageSize) {
        List<Task> tasks = taskService.createTaskQuery()
                .taskAssignee(assignee)
                .listPage((pageNum - 1) * pageSize, pageSize);
        return tasks;
    }

    /**
     * 获取指定用户的已办任务列表
     * @param assignee 任务负责人的工号
     * @param pageNum 页码
     * @param pageSize 每页数量
     * @return 已办任务列表
     */
    public static List<HistoricTaskInstance> getCompletedTasksByAssignee(String assignee, int pageNum, int pageSize) {
        return historyService
                .createHistoricTaskInstanceQuery()
                .taskAssignee(assignee)
                .finished()
                .listPage((pageNum - 1) * pageSize, pageSize);
    }

    /**
     * 获取指定流程实例的历史活动实例列表
     * @param processInstanceId 流程实例 ID
     * @return 历史活动实例列表
     */
    public static List<HistoricActivityInstance> getHistoricActivityInstances(String processInstanceId) {
        HistoricActivityInstanceQuery query = historyService
                .createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId);
        return query.list();
    }

    /**
     * 获取指定流程实例的历史流程实例
     * @param processInstanceId 流程实例 ID
     * @return 历史流程实例
     */
    public static HistoricProcessInstance getHistoricProcessInstance(String processInstanceId) {
        return historyService
                .createHistoricProcessInstanceQuery()
                .processInstanceId(processInstanceId)
                .singleResult();
    }

    /**
     * 终止流程实例
     * @param processInstanceId 流程实例 ID
     * @param deleteReason 终止原因
     */
    public static void terminateProcessInstance(String processInstanceId, String deleteReason) {
        runtimeService.deleteProcessInstance(processInstanceId, deleteReason);
    }

    /**
     * 获取指定流程实例的当前任务
     * @param processInstanceId 流程实例 ID
     * @return 当前任务
     */
    public static Task getCurrentTask(String processInstanceId) {
        return taskService.createTaskQuery()
                .processInstanceId(processInstanceId)
                .singleResult();
    }

    /**
     * 转交任务
     * @MethodName: transferTask
     * @Author: AllenSun
     * @Date: 2025/2/23 下午3:37
     */
    public static Boolean transferTask(String taskId, String assigneeId){
        try {
            taskService.setAssignee(taskId, assigneeId);
        } catch (Exception e) {
            log.info("任务转交失败:{}",e.getMessage());
            throw new ProcessException("任务转交失败");
        }
        return true;
    }


    /**
     * 驳回处理
     * @MethodName: rejectTask
     * @Author: AllenSun
     * @Date: 2025/2/23 下午9:06
     */
    public static Boolean rejectTask(String processInstanceId, String targetNodeId){
        try {
            //获取当前环节实例 这是个树结构
            ActivityInstance activity = runtimeService.getActivityInstance(processInstanceId);

            //获取实例的流程定义 这里主要是为了拿到节点前的那条线的Id
            List<HistoricActivityInstance> hisActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).finished().list();
            List<String> nodeIds = hisActivityList.stream().map(HistoricActivityInstance::getActivityId).collect(Collectors.toList());

            ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
            BpmnModelInstance bpmnModel = repositoryService.getBpmnModelInstance(processInstance.getProcessDefinitionId());
            ModelElementInstance modelElement = bpmnModel.getModelElementById(targetNodeId);
            UserTask userTask = (UserTask) modelElement;
            Collection<SequenceFlow> incoming = userTask.getIncoming();
            String transitionId = "";
            for (SequenceFlow sequenceFlow : incoming) {
                FlowNode source = sequenceFlow.getSource();
                if (nodeIds.contains(source.getId())) {
                    transitionId = sequenceFlow.getId();
                    break;
                }
            }

            runtimeService.createProcessInstanceModification(processInstanceId)
                    //取消现有的活动实例
                    .cancelActivityInstance(activity.getId())
                    //设置备注
                    .setAnnotation("驳回")
                    .startTransition(transitionId)
                    .execute();
        } catch (Exception e) {
            log.info("驳回处理失败:{}",e.getMessage());
            throw new ProcessException("驳回处理失败");
        }
        return true;
    }


}

【七】贷款授信审批流程

【1】需求分析

【2】难点分析

(1)根据角色来动态设置审批人
(2)支持或签
(3)支持会签
(4)支持比例签
(5)需要创建业务表和接口和前端联调,对camunda封装的接口进行二次封装
(6)支持驳回,返回上一个节点

【3】

【八】Spring Boot 项目整合 Camunda 的搭建流程

springboot项目整合camunda7.17,将camunda的数据源改成mysql并完成创建表初始化,添加pom依赖,配置yml,并且设计bpmn,使用mvc架构实现用户审批请假流程,请假必须给出请假人工号、请假开始时间、请假结束时间、请假天数、工作交接人、请假原因等信息,根据员工工号查出员工的所有上级,然后要求3天以内上级领导审批,3天以上职能上级审批,最终都要经过行政上级审批,每一层审批都可以选择通过或拒绝,给出完整可用的案例和代码架构

【1】创建 Spring Boot 项目

可以使用 Spring Initializr(https://start.spring.io/)来创建一个基础的 Spring Boot 项目,添加以下依赖:

    <project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>camunda-leave-approval</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
    </parent>

    <dependencies>
        <!-- Camunda Spring Boot Starter -->
        <dependency>
            <groupId>org.camunda.bpm.springboot</groupId>
            <artifactId>camunda-bpm-spring-boot-starter</artifactId>
            <version>7.17.0</version>
        </dependency>
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

【2】配置数据库和Camunda

在 src/main/resources 目录下创建或修改 application.yml 文件,配置 MySQL 数据源和 Camunda 相关信息:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/camunda_leave_approval?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

camunda.bpm:
  admin-user:
    id: demo
    password: demo
    firstName: Demo
    lastName: User
  filter:
    create: All tasks

Camunda 会在启动时根据配置自动创建所需的数据库表。

【3】创建 BPMN 流程定义文件

使用 Camunda Modeler 设计请假审批流程,以下是对应的 BPMN XML 代码(保存为 leave_approval.bpmn 并放在 src/main/resources/processes 目录下):

(1)启动事件:添加一个起始事件表示请假流程开始。
(2)员工提交请假申请:添加一个用户任务,员工在此任务中输入请假时间和原因。
(3)天数判断网关:使用排他网关根据请假天数判断是由上级领导审批还是职能上级审批。
(4)上级领导审批:当请假天数小于等于 3 天,进入此用户任务,审批人可选择通过或拒绝。
(5)职能上级审批:当请假天数大于 3 天,进入此用户任务,审批人可选择通过或拒绝。
(6)行政上级审批:无论前面审批结果如何,最终都进入此用户任务进行审批。
(7)结束事件:根据最终审批结果,流程结束。

<?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:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
             xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
             xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
             id="Definitions_0"
             targetNamespace="http://bpmn.io/schema/bpmn">
    <process id="leaveApprovalProcess" name="Leave Approval Process" isExecutable="true">
        <startEvent id="StartEvent_1"></startEvent>
        <userTask id="SubmitLeaveRequest" name="Submit Leave Request">
            <extensionElements>
                <camunda:formData>
                    <camunda:formField id="employeeId" label="Employee ID" type="string"></camunda:formField>
                    <camunda:formField id="startDate" label="Start Date" type="date"></camunda:formField>
                    <camunda:formField id="endDate" label="End Date" type="date"></camunda:formField>
                    <camunda:formField id="leaveDays" label="Leave Days" type="long"></camunda:formField>
                    <camunda:formField id="handoverPerson" label="Handover Person" type="string"></camunda:formField>
                    <camunda:formField id="leaveReason" label="Leave Reason" type="string"></camunda:formField>
                </camunda:formData>
            </extensionElements>
        </userTask>
        <exclusiveGateway id="ExclusiveGateway_1" name="Check Leave Days"></exclusiveGateway>
        <userTask id="SupervisorApproval" name="Supervisor Approval">
            <extensionElements>
                <camunda:formData>
                    <camunda:formField id="approvalResult" label="Approval Result" type="string">
                        <camunda:value id="approved" name="Approved"></camunda:value>
                        <camunda:value id="rejected" name="Rejected"></camunda:value>
                    </camunda:formField>
                </camunda:formData>
            </extensionElements>
        </userTask>
        <userTask id="FunctionalSupervisorApproval" name="Functional Supervisor Approval">
            <extensionElements>
                <camunda:formData>
                    <camunda:formField id="approvalResult" label="Approval Result" type="string">
                        <camunda:value id="approved" name="Approved"></camunda:value>
                        <camunda:value id="rejected" name="Rejected"></camunda:value>
                    </camunda:formField>
                </camunda:formData>
            </extensionElements>
        </userTask>
        <userTask id="AdministrativeApproval" name="Administrative Approval">
            <extensionElements>
                <camunda:formData>
                    <camunda:formField id="approvalResult" label="Approval Result" type="string">
                        <camunda:value id="approved" name="Approved"></camunda:value>
                        <camunda:value id="rejected" name="Rejected"></camunda:value>
                    </camunda:formField>
                </camunda:formData>
            </extensionElements>
        </userTask>
        <endEvent id="EndEvent_1"></endEvent>
        <endEvent id="EndEvent_2"></endEvent>
        <sequenceFlow id="Flow_09m4z8h" sourceRef="StartEvent_1" targetRef="SubmitLeaveRequest"></sequenceFlow>
        <sequenceFlow id="Flow_112x5n4" sourceRef="SubmitLeaveRequest" targetRef="ExclusiveGateway_1"></sequenceFlow>
        <sequenceFlow id="Flow_00479m7" sourceRef="ExclusiveGateway_1" targetRef="SupervisorApproval">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[${leaveDays <= 3}]]></conditionExpression>
        </sequenceFlow>
        <sequenceFlow id="Flow_03y4x5a" sourceRef="ExclusiveGateway_1" targetRef="FunctionalSupervisorApproval">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[${leaveDays > 3}]]></conditionExpression>
        </sequenceFlow>
        <sequenceFlow id="Flow_070m4pz" sourceRef="SupervisorApproval" targetRef="AdministrativeApproval"></sequenceFlow>
        <sequenceFlow id="Flow_094639c" sourceRef="FunctionalSupervisorApproval" targetRef="AdministrativeApproval"></sequenceFlow>
        <sequenceFlow id="Flow_1abcdef" sourceRef="AdministrativeApproval" targetRef="EndEvent_1">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approvalResult == 'approved'}]]></conditionExpression>
        </sequenceFlow>
        <sequenceFlow id="Flow_2abcdef" sourceRef="AdministrativeApproval" targetRef="EndEvent_2">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approvalResult == 'rejected'}]]></conditionExpression>
        </sequenceFlow>
    </process>
    <bpmndi:BPMNDiagram id="BPMNDiagram_1">
        <!-- 图形布局信息,可忽略 -->
    </bpmndi:BPMNDiagram>
</definitions>

【6】请假审批流程

(1)项目结构

src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── camundaleaveapproval
│ │ ├── controller
│ │ │ └── LeaveApprovalController.java
│ │ ├── entity
│ │ │ └── LeaveRequest.java
│ │ ├── service
│ │ │ ├── EmployeeService.java
│ │ │ ├── LeaveApprovalService.java
│ │ │ └── impl
│ │ │ ├── EmployeeServiceImpl.java
│ │ │ └── LeaveApprovalServiceImpl.java
│ │ └── CamundaLeaveApprovalApplication.java
│ └── resources
│ ├── application.yml
│ └── processes
│ └── leave_approval.bpmn
└── test
└── java
└── com
└── example
└── camundaleaveapproval
└── CamundaLeaveApprovalApplicationTests.java

(2)实体类

package com.example.camundaleaveapproval.entity;

import lombok.Data;

import java.util.Date;

@Data
public class LeaveRequest {
    private String employeeId;
    private Date startDate;
    private Date endDate;
    private int leaveDays;
    private String handoverPerson;
    private String leaveReason;
}

(3)服务层接口

package com.example.camundaleaveapproval.service;

import com.example.camundaleaveapproval.entity.LeaveRequest;

public interface LeaveApprovalService {
    void startLeaveApprovalProcess(LeaveRequest leaveRequest);
}

(4)服务层实现类

package com.example.camundaleaveapproval.service.impl;

import com.example.camundaleaveapproval.entity.LeaveRequest;
import com.example.camundaleaveapproval.service.LeaveApprovalService;
import org.camunda.bpm.engine.RuntimeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class LeaveApprovalServiceImpl implements LeaveApprovalService {

    @Autowired
    private RuntimeService runtimeService;

    @Override
    public void startLeaveApprovalProcess(LeaveRequest leaveRequest) {
        Map<String, Object> variables = new HashMap<>();
        variables.put("employeeId", leaveRequest.getEmployeeId());
        variables.put("startDate", leaveRequest.getStartDate());
        variables.put("endDate", leaveRequest.getEndDate());
        variables.put("leaveDays", leaveRequest.getLeaveDays());
        variables.put("handoverPerson", leaveRequest.getHandoverPerson());
        variables.put("leaveReason", leaveRequest.getLeaveReason());

        runtimeService.startProcessInstanceByKey("leaveApprovalProcess", variables);
    }
}

(5)控制器类

package com.example.camundaleaveapproval.controller;

import com.example.camundaleaveapproval.entity.LeaveRequest;
import com.example.camundaleaveapproval.service.LeaveApprovalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/leave")
public class LeaveApprovalController {

    @Autowired
    private LeaveApprovalService leaveApprovalService;

    @PostMapping("/submit")
    public String submitLeaveRequest(@RequestBody LeaveRequest leaveRequest) {
        leaveApprovalService.startLeaveApprovalProcess(leaveRequest);
        return "Leave request submitted successfully.";
    }
}

(6)员工上级查询逻辑

package com.example.camundaleaveapproval.service;

import java.util.ArrayList;
import java.util.List;

public interface EmployeeService {
    List<String> getSupervisors(String employeeId);
}

package com.example.camundaleaveapproval.service.impl;

import com.example.camundaleaveapproval.service.EmployeeService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class EmployeeServiceImpl implements EmployeeService {
    @Override
    public List<String> getSupervisors(String employeeId) {
        List<String> supervisors = new ArrayList<>();
        // 这里可以实现从数据库查询上级信息的逻辑
        supervisors.add("Supervisor1");
        supervisors.add("FunctionalSupervisor1");
        supervisors.add("AdministrativeSupervisor1");
        return supervisors;
    }
}

(7)测试流程

(1)发起申请
可以使用 Postman 等工具发送 POST 请求到 http://localhost:8080/leave/submit,请求体示例如下:

{
    "employeeId": "123",
    "startDate": "2024-10-01",
    "endDate": "2024-10-05",
    "leaveDays": 5,
    "handoverPerson": "John",
    "leaveReason": "Family event"
}

(2)审批人登录到CamundaBPM进行审批
登录 Camunda Cockpit(http://localhost:8080/camunda/app/cockpit/default/)使用用户名 demo 和密码 demo 查看流程执行情况,并在 Camunda Tasklist 中完成各个审批任务。

【九】springboot整合Camunda测试实例项目

git地址:https://gitee.com/allensun03/learn_camunda

【十】审批流程案例【待完成】

【1】创建本地camunda项目

当前项目只包含流程的业务代码,最终打成jar包供其他项目调用,实现代码的解耦

项目结构使用ddd分层
在这里插入图片描述

【2】添加pom依赖

本项目是父子module的微服务项目,需要管理父子module的pom依赖

(1)父级pom

进行全局的依赖管理,注意Camunda的版本是7.15

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.hzyatop.framework.boot</groupId>
        <artifactId>damp-boot-parent</artifactId>
        <version>2.7.18-20241119</version>
    </parent>

    <groupId>com.hzyatop.damp.camunda</groupId>
    <artifactId>damp-base-camunda</artifactId>
    <version>${damp-base-camunda.version}</version>
    <packaging>pom</packaging>

    <modules>
        <module>api</module>
        <module>application</module>
        <module>domain</module>
        <module>infrastructure</module>
        <module>sdk</module>
    </modules>

     <!-- 版本号配置 -->
    <properties>
        <damp-base-camunda.version>4.7.0-SNAPSHOT</damp-base-camunda.version>
        <camunda.version>7.15.0</camunda.version>
        <camunda-spin.version>1.10.0</camunda-spin.version>
        <groovy-jsr223.version>4.0.13</groovy-jsr223.version>
    </properties>

	<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.camunda.bpm</groupId>
                <artifactId>camunda-engine</artifactId>
                <version>${camunda.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.mybatis</groupId>
                        <artifactId>mybatis</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.camunda.bpm.springboot</groupId>
                <artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
                <version>${camunda.version}</version>
            </dependency>
            <dependency>
                <groupId>org.camunda.bpm</groupId>
                <artifactId>camunda-engine-plugin-spin</artifactId>
                <version>${camunda.version}</version>
            </dependency>
            <dependency>
                <groupId>org.camunda.spin</groupId>
                <artifactId>camunda-spin-core</artifactId>
                <version>${camunda-spin.version}</version>
            </dependency>
			<dependency>
                <groupId>org.camunda.spin</groupId>
                <artifactId>camunda-spin-dataformat-json-jackson</artifactId>
                <version>${camunda-spin.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.groovy</groupId>
                <artifactId>groovy-jsr223</artifactId>
 			</dependency>

            <dependency>
                <groupId>com.hzyatop.damp.camunda</groupId>
                <artifactId>damp-base-camunda-application</artifactId>
                <version>${damp-base-camunda.version}</version>
            </dependency>
			<dependency>
                <groupId>com.hzyatop.damp.camunda</groupId>
                <artifactId>damp-base-camunda-domain</artifactId>
                <version>${damp-base-camunda.version}</version>
            </dependency>
            <dependency>
                <groupId>com.hzyatop.damp.camunda</groupId>
                <artifactId>damp-base-camunda-infrastructure</artifactId>
                <version>${damp-base-camunda.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 依赖 -->
    <dependencies>
        <dependency>
            <groupId>com.hzyatop.framework.cloud</groupId>
            <artifactId>damp-cloud-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.hzyatop.framework</groupId>
            <artifactId>damp-commons</artifactId>
        </dependency>

        <!-- easyexcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>flatten-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

(2)api层

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.hzyatop.damp.camunda</groupId>
        <artifactId>damp-base-camunda</artifactId>
        <version>${damp-base-camunda.version}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>damp-base-camunda-api</artifactId>
    <description>接口</description>

    <dependencies>
        <dependency>
            <groupId>com.hzyatop.damp.camunda</groupId>
            <artifactId>damp-base-camunda-infrastructure</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-mysql</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <includes>
                        <include>com/**</include>
                    </includes>
                    <!--&lt;!&ndash; 不打包资源文件(配置文件和依赖包分开) &ndash;&gt;-->
                    <excludes>
                        <exclude>**/*.xml</exclude>
                        <exclude>**/*.properties</exclude>
                        <exclude>**/*.yml</exclude>
                    </excludes>
                    <outputDirectory>${project.build.directory}</outputDirectory>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>timestamp-property</id>
                        <goals>
                            <goal>timestamp-property</goal>
                        </goals>
                        <configuration>
                            <name>build.time</name>
                            <timeZone>GMT+8</timeZone>
                            <pattern>yyyyMMddHHmmss</pattern>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <appendAssemblyId>false</appendAssemblyId>
                    <finalName>${project.parent.artifactId}-${build.time}</finalName>
                    <descriptors>
                        <descriptor>assembly/assembly.xml</descriptor>
                    </descriptors>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

(3)application层

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.hzyatop.damp.camunda</groupId>
        <artifactId>damp-base-camunda</artifactId>
        <version>${damp-base-camunda.version}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>damp-base-camunda-application</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.hzyatop.damp.camunda</groupId>
            <artifactId>damp-base-camunda-domain</artifactId>
        </dependency>
    </dependencies>
</project>

(4)domain层

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.hzyatop.damp.camunda</groupId>
        <artifactId>damp-base-camunda</artifactId>
        <version>${damp-base-camunda.version}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>damp-base-camunda-domain</artifactId>
    <description>领域</description>

    <dependencies>
        <dependency>
            <groupId>org.camunda.bpm</groupId>
            <artifactId>camunda-engine</artifactId>
        </dependency>
    </dependencies>
</project>

(5)infrastructure层

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.hzyatop.damp.camunda</groupId>
        <artifactId>damp-base-camunda</artifactId>
        <version>${damp-base-camunda.version}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>damp-base-camunda-infrastructure</artifactId>
    <description>基础设施</description>

    <dependencies>
        <dependency>
            <groupId>com.hzyatop.damp.camunda</groupId>
            <artifactId>damp-base-camunda-application</artifactId>
        </dependency>
        <dependency>
            <groupId>com.hzyatop.damp.camunda</groupId>
            <artifactId>damp-base-camunda-domain</artifactId>
        </dependency>

        <!-- camunda -->
        <dependency>
            <groupId>org.camunda.bpm.springboot</groupId>
            <artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.groovy</groupId>
            <artifactId>groovy-jsr223</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.camunda.bpm</groupId>
            <artifactId>camunda-engine-plugin-spin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.camunda.spin</groupId>
            <artifactId>camunda-spin-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.camunda.spin</groupId>
            <artifactId>camunda-spin-dataformat-json-jackson</artifactId>
        </dependency>
    </dependencies>
</project>
        

【3】添加配置信息

通过springCloud实现的微服务,使用Nacos实现注册中心和配置管理
damp-base-camunda-api.yml

damp:
  db:
    schema: damp_base_camunda
    type: mysql
camunda:
  bpm:
    # 配置账户密码来访问Camunda自带的管理界面
    admin-user:
      id: admin
      password: admin@Yatop
      first-name: admin
    #禁止自动部署resources下面的bpmn文件
    auto-deployment-enabled: false
    #指定数据库类型
    database:
      type: ${damp.db.type}
yt:
  auth:
    excludes:         # [默认] 不开启鉴权路径
      - /4.2.0/approve_user
    app:
      token: 2a9b01494d5d422cb11eca265902b241

【4】建表

除了Camunda基本的表以外,还有额外新增的表

在这里插入图片描述

(1)业务流程定义关系

CREATE TABLE `yatop_procdef` (
  `ID` bigint NOT NULL COMMENT '主键',
  `MOD_MENU_CD` varchar(40) DEFAULT NULL COMMENT '模块菜单编号',
  `PROC_DEF_KEY` varchar(255) DEFAULT NULL COMMENT '流程定义key',
  `PROC_DEF_NM` varchar(255) DEFAULT NULL COMMENT '流程定义名',
  `PROC_DEPLOY_ID` varchar(64) DEFAULT NULL COMMENT '流程部署ID',
  `PROC_DEF_VER` int DEFAULT NULL COMMENT '部署版本',
  `STATUS` char(1) DEFAULT NULL COMMENT '流程状态',
  `TENANT_ID` bigint DEFAULT NULL COMMENT '租户ID',
  `CRT_USER_ID` bigint DEFAULT NULL COMMENT '创建人',
  `UPD_USER_ID` bigint DEFAULT NULL COMMENT '更新人',
  `CRT_DT_TM` datetime DEFAULT NULL COMMENT '创建时间',
  `UPD_DT_TM` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`ID`),
  UNIQUE KEY `YATOP_PROCDEF_UK` (`PROC_DEF_KEY`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='业务流程定义关系';

在这里插入图片描述

(2)业务-流程定义关系表

-- uat1_damp_base_camunda.yatop_procdef_biz definition

CREATE TABLE `yatop_procdef_biz` (
  `ID` bigint NOT NULL COMMENT '主键',
  `MODULE` varchar(32) DEFAULT NULL COMMENT '所属业务模块',
  `BIZ_NAME` varchar(256) DEFAULT NULL COMMENT '业务名称',
  `BIZ_TYPE` varchar(20) DEFAULT NULL COMMENT '业务类型',
  `PROC_DEF_KEY` varchar(255) DEFAULT NULL COMMENT '流程key',
  `DEPLOYMENT_ID` varchar(64) DEFAULT NULL COMMENT '部署ID',
  `VERSION` int DEFAULT NULL COMMENT '部署版本',
  `STATE` int DEFAULT NULL COMMENT '状态',
  `CRT_USER_ID` bigint DEFAULT NULL COMMENT '创建人',
  `UPD_USER_ID` bigint DEFAULT NULL COMMENT '更新人',
  `CRT_DT_TM` datetime DEFAULT NULL COMMENT '创建时间',
  `UPD_DT_TM` datetime DEFAULT NULL COMMENT '更新时间',
  `TENANT_ID` bigint DEFAULT NULL COMMENT '租户ID',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='业务-流程定义关系表';

(3)业务-流程实例关联表

-- uat1_damp_base_camunda.yatop_procinst_biz_relation definition

CREATE TABLE `yatop_procinst_biz_relation` (
  `ID` bigint NOT NULL COMMENT '主键',
  `MODULE` varchar(32) DEFAULT NULL COMMENT '所属业务模块',
  `BIZ_NO` varchar(128) DEFAULT NULL COMMENT '业务编码',
  `BIZ_NAME` varchar(256) DEFAULT NULL COMMENT '业务名称',
  `BIZ_TYPE` varchar(64) DEFAULT NULL COMMENT '业务类型',
  `BIZ_PRIORITY` int NOT NULL COMMENT '任务优先级',
  `BIZ_EXTEND_INFO` varchar(500) DEFAULT NULL COMMENT '业务扩展信息',
  `PROC_DEF_KEY` varchar(255) DEFAULT NULL COMMENT '流程定义key',
  `PROC_INST_ID` varchar(64) DEFAULT NULL COMMENT '审核流程实例id',
  `SUBMITTER_ID` bigint DEFAULT NULL COMMENT '审核提交人id',
  `SUBMITTER_NAME` varchar(64) DEFAULT NULL COMMENT '审核提交人',
  `PROC_STATE` varchar(50) DEFAULT NULL COMMENT '审核状态',
  `START_DT_TM` datetime DEFAULT NULL COMMENT '开始时间',
  `CHECK_DT_TM` datetime DEFAULT NULL COMMENT '审核时间',
  `STATE` int DEFAULT NULL COMMENT '状态',
  `CRT_USER_ID` bigint DEFAULT NULL COMMENT '创建人',
  `UPD_USER_ID` bigint DEFAULT NULL COMMENT '更新人',
  `CRT_DT_TM` datetime DEFAULT NULL COMMENT '创建时间',
  `UPD_DT_TM` datetime DEFAULT NULL COMMENT '更新时间',
  `TENANT_ID` bigint DEFAULT NULL COMMENT '租户ID',
  PRIMARY KEY (`ID`),
  UNIQUE KEY `INDEX_PROC_INST_ID` (`PROC_INST_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='业务-流程实例关联表';

【5】Camunda流程引擎接口二开

(1)application层

提供两类接口
(1)ProscessApi负责处理流程信息

(2)TaskApi负责处理任务信息

(2)domain层

(3)infrastruture层

在这里插入代码片

【6】在其他项目中引入使用

某项目中要使用工作流审批,则引入本项目的依赖

<dependency>
    <groupId>com.hzyatop.damp.camunda</groupId>
    <artifactId>damp-base-camunda-sdk</artifactId>
    <version>4.7.0-SNAPSHOT</version>
</dependency>

【7】流程配置页面

通过自建的表process来存储业务流程的基本信息,查询自建表process得到本项目的全部流程
在这里插入图片描述

发布流程
在这里插入图片描述

下线申请流程
在这里插入图片描述

使用申请流程
在这里插入图片描述

取消使用申请流程
在这里插入图片描述

【8】发布审批流程解析

(1)创建流程

(2)绘制bpmn

(3)发起流程

(4)实现审批

(5)实现回调

(6)完成审核

(7)总结

【9】重要接口流程分析

(1)业务流程涉及的表

(1)申请记录表

@Getter
@Setter
@ToString
public class IndApplyRecord extends BaseEntity<Long> {
    /**
     * 指标id
     */
    private Long indId;
    /**
     * 1上线2下线3使用4取消使用
     */
    private Integer applyType;
    /**
     * 1待审核 2驳回 3通过 0撤销
     */
    private Integer status;
    /**
     * 申请单号
     */
    private String applyNum;
    /**
     * 申请项名称
     */
    private String name;
    /**
     * 结束时间
     */
    private LocalDateTime finishDt;
    /**
     * 扩展属性
     */
    private String extendJson;
    /**
     * 实例id
     */
    private String procId;
    /**
     * 申请说明
     */
    private String applyDesc;

    /**
     * 用途
     */
    private String purPose;
    /**
     * 0 不可撤销 1可撤销
     */
    private Integer state;

    /**
     * 检查租户和当前用户租户是否一致
     *
     * @return true:一致;false:不一致
     */
    public boolean checkTenant() {
        return Objects.equals(this.getTenantId(), RequestContext.getTenantId());
    }
}

(2)申请记录详情表

@Getter
@Setter
@ToString
public class IndApplyRecordDetail extends BaseEntity<Long> {
    /**
     * 申请单id
     */
    private Long applyId;
    /**
     * 
     */
    private LocalDateTime approvedDt;
    /**
     * 1待审核 2驳回 3通过 0撤销
     */
    private Integer status;
    /**
     * 审核说明
     */
    private String auditDesc;

    /**
     * 检查租户和当前用户租户是否一致
     *
     * @return true:一致;false:不一致
     */
    public boolean checkTenant() {
        // 获取当前用户
        return Objects.equals(this.getTenantId(), RequestContext.getTenantId());
    }
}

(2)发起指标发布审批

(1)接口和参数
接口:/4.2.0/indaudit/apply
参数:
在这里插入图片描述

参数请求对象

package com.hzyatop.damp.ind.application.api.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

/**
 * 创建请求类
 *
 * @author zlx
 * @since 2024-06-26 08:53
 */
@Getter
@Setter
@ToString
@Schema(description = "创建请求类")
public class IndApplyRecordCreateRequest {

    /**
     * 指标id
     */
    @Schema(description = "指标id")
    private Long indId;
    @Schema(description = "指标库id")
    private Long indVaultId;

    /**
     * 1上线2下线3使用4取消使用
     */
    @Schema(description = "0上线2下线1使用3取消使用")
    private Integer applyType;

    /**
     * 申请项名称
     */
    @Schema(description = "申请项名称")
    private String name;

    @Schema(description = "指标市场 1 指标库0")
    private String judge;
    @Schema(description = "申请说明")
    private String applyDesc;
    /**
     * 用途场景:1-指标查询、2-BI分析、3-三方应用、4-其他
     */
    @Schema(description = "用途场景:1-指标查询、2-BI分析、3-三方应用、4-其他")
    private List<String> applyPurpose;

    /**
     * 权限范围
     */
    @Schema(description = "权限范围")
    private List<IndApllyPermissionScopeRequest> permissionScope;
    private String releaseNewVersion;
}

(2)接口逻辑

准备请求参数逻辑

            // 初始化审核状态为待审核
            indApplyRecord.setStatus(IndexApplyStateEnum.APPROVE.getCode());
            String today = DateUtils.date2String(new Date(), DateUtils.YYYYMMDD);
            redisTemplate.opsForValue().setIfAbsent(INDEX_APPLY_KEY + today, 0, 1, TimeUnit.DAYS);
            Long increment = redisTemplate.opsForValue().increment(INDEX_APPLY_KEY + today);
            DecimalFormat df = new DecimalFormat("0000");
            String formattedNumber = df.format(increment);
            // 申请单号
            indApplyRecord.setApplyNum(PREFIX + today + formattedNumber);
            // 0 不可撤销 1可撤销
            indApplyRecord.setState(1);
            // 申请项名称
            indApplyRecord.setName(index.getIndexName());
            indApplyRecord.setId(SnowflakeUtils.getId());
            batchCreateApplyRecordList.add(indApplyRecord);
            if (enable) {
                ProcessInstanceCreateRequest processInstanceCreateRequest = new ProcessInstanceCreateRequest();
                // 流程定义的key
                processInstanceCreateRequest.setProcessKey(key);
                // 业务ID,每次申请记录的id
                processInstanceCreateRequest.setBizId(indApplyRecord.getId().toString());
                // 业务系统编号
                processInstanceCreateRequest.setBizSysCd(FileEnum.INDEX_RELEASE.getBizSysCd());
                // 业务模块编号
                processInstanceCreateRequest.setBizModCd(FileEnum.INDEX_RELEASE.getBizModCd());

                // 发起流程的参数,处理人的唯一标识【工号】
                varMap.put("assigns1", Arrays.asList(userMap.get(index.getPrsnHandleId())));
                varMap.put("assigns2", Arrays.asList(userMap.get(index.getPrsnResponsibleId())));
                varMap.put("assigns4", Arrays.asList(userMap.get(index.getPrsnInChrgResponsibleId())));
                if (Objects.equals(indAuditApprovedQueryRequest.getJudge(), FlagConstants.TRUE) && applyTypeEnum.getCode().equals(ApplyTypeEnum.OFFLINE.getCode())) {
                    varMap.put(APPROVAL_RESULT, FlagConstants.FALSE);
                    varMap.put("assigns3", Arrays.asList(StringUtils.EMPTY));
                } else {
                    varMap.put("assigns3", Arrays.asList(userMap.get(index.getPrsnInChrgId())));
                }
                varMap.put("JUDGE", indAuditApprovedQueryRequest.getJudge());
                // 待审核的指标id
                varMap.put("IND_ID", indApplyRecord.getIndId());
                // 审核的业务类型:1上线2下线3使用4取消使用
                varMap.put("BIZ_TYPE", indApplyRecord.getApplyType());
                // 装填请求的表单数据参数
                processInstanceCreateRequest.setForm(varMap);
                processInstanceCreateRequests.add(processInstanceCreateRequest);
            }

创建和申请记录,发起申请流程,申请流程发起成功后,更新本次流程的实例id

		if (ObjectUtil.isNotEmpty(batchCreateApplyRecordList)) {
            // 创建申请记录
            indApplyRecordRepo.createBatch(batchCreateApplyRecordList);
        }
        // 发起流程申请
        if (ObjectUtil.isNotEmpty(processInstanceCreateRequests)) {
            // TODO 发起流程
            ApiResponse<List<TaskProcessStartResponse>> listApiResponse = camundaProcessFeignClient.batchProcessStart(processInstanceCreateRequests);
            Assert.isFalse(listApiResponse.getCode() != ExceptionCode.OK.getCode(), "发起流程异常");
            Map<Long, TaskProcessStartResponse> reponseMap = listApiResponse.getData().stream().collect(Collectors.toMap(it -> Long.valueOf(it.getBizId()), it -> it, (a, b) -> a));
            // 修改申请记录
            for (IndApplyRecord applyRecord : batchCreateApplyRecordList) {
                TaskProcessStartResponse response = reponseMap.get(applyRecord.getId());
                // 更新此次申请的请求实例id
                applyRecord.setProcId(response.getProcId());
            }
        }
        if (ObjectUtil.isNotEmpty(batchCreateApplyRecordList)) {
            // 更新申请记录
            indApplyRecordRepo.updateBatch(batchCreateApplyRecordList);
        }

(3)远程接口处理逻辑
请求类

package com.hzyatop.damp.camunda.application.api.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;



import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.util.Map;

/**
 * 流程启动请求
 *
 * @author YT-0274
 * @since 2023-08-10 20:01
 */
@Schema(description = "流程启动请求对象")
@Getter
@Setter
@ToString
public class ProcessInstanceCreateRequest {

    /**
     * 流程定义Key
     */
    @Schema(description = "流程定义Key")
    @NotBlank
    private String processKey;
    @NotBlank
    @Schema(
            description = "业务系统编号 参考sys_menu的一级menu_cd"
    )
    private @NotEmpty String bizSysCd;
    @NotBlank
    @Schema(
            description = "业务模块编号"
    )
    private @NotEmpty String bizModCd;

    /**
     * 业务ID
     */
    @Schema(description = "业务ID")
    @NotBlank
    private String bizId;


    /**
     * 表单信息
     */
    @Schema(description = "表单信息")
    private Map<String, Object> form;
}

发起流程的逻辑


    /**
     * 流程服务
     */
    private final ProcessService processService;

    public ApiResponse<List<TaskProcessStartResponse>> batchProcessStart(List<ProcessInstanceCreateRequest> list) {
        return transactionTemplate.execute(status -> {
            List<TaskProcessStartResponse> data = Lists.newArrayList();
            for (ProcessInstanceCreateRequest createRequest : list) {
                // TODO 启动流程
                DomainResponse<String> domainResponse = processService.startByKey(createRequest.getProcessKey(), createRequest.getBizSysCd(), createRequest.getBizModCd(), createRequest.getBizId(), createRequest.getForm());
                data.add(new TaskProcessStartResponse().setBizId(createRequest.getBizId()).setProcId(domainResponse.getData()));
            }
            // TODO 返回业务id和流程id
            return ApiResponse.ok(data);
        });
    }

(3)查询待审批列表

(1)我提交的

接口:/4.2.0/indaudit/submit_list?pageNumber=1&pageSize=20
参数:

查询的具体逻辑

    public ApiPageResponse<IndApplyRecordQueryResponse> submitList(IndAuditSubmitQueryRequest indAuditSubmitQueryRequest, Pagination pagination) {
        // 从本地的申请记录表分页查询申请记录信息
        List<IndApplyRecordQueryResponse> indApplyRecordQueryResponses = indApplyRecordReadModelRepo.submitList(pagination, indAuditSubmitQueryRequest);
        // 装填细节信息
        List<IndIndex> indIndices = indIndexService.queryByIds(indApplyRecordQueryResponses.stream().map(a -> Long.valueOf(a.getIndId())).collect(Collectors.toList())).getData();
        Map<String, IndIndex> indexMap = indIndices.stream().collect(Collectors.toMap(a -> a.getId().toString(), Function.identity()));
        for (IndApplyRecordQueryResponse indApplyRecordQueryRespons : indApplyRecordQueryResponses) {
            // 根据流程实例id查询全部任务节点
            ApiResponse<List<TaskResponse>> listApiResponse = camundaTaskFeignClient.incompleteTaskList(indApplyRecordQueryRespons.getProcId());
   			if (listApiResponse.isSuccess() && CollectionUtil.isNotEmpty(listApiResponse.getData())) {
                // 当前正在审批的流程节点
                indApplyRecordQueryRespons.setCurrentApproveRole(listApiResponse.getData().get(0).getTaskName());
            }
            if (indexMap.containsKey(indApplyRecordQueryRespons.getIndId())) {
                indApplyRecordQueryRespons.setIndexType(indexMap.get(indApplyRecordQueryRespons.getIndId()).getIndexType());
            }

        }
        return ApiPageResponse.ok(pagination, indApplyRecordQueryResponses);
    }

在这里插入图片描述

远程接口incompleteTaskList的逻辑

    public List<TaskResponse> incompleteTaskList(String procInstId) {
        List<Task> list = taskService.createTaskQuery().active().processInstanceId(procInstId).list();
        return taskPOAssembler.toQueryResponse(list);
    }
(2)我审核的(待审核)

接口:/4.2.0/indaudit/pend_list?pageNumber=1&pageSize=20

在这里插入图片描述

接口逻辑

    public ApiPageResponse<IndApplyRecordQueryResponse> pendList(IndAuditPendQueryRequest indAuditPendQueryRequest, Pagination pagination) {
        ProcessDefQueryRequest processDefQueryRequest = new ProcessDefQueryRequest();
        processDefQueryRequest.setModMenuCd("ind");
        processDefQueryRequest.setPageSize(1000);
        //查询指标平台下的全部业务流程
        ApiPageResponse<YatopProcdefQueryResponse> yatopProcdefQueryResponseApiPageResponse = camundaProcessFeignClient.defQuery(processDefQueryRequest);
        Assert.isFalse(yatopProcdefQueryResponseApiPageResponse.getCode() != ExceptionCode.OK.getCode(), "查询流程异常");
        List<YatopProcdefQueryResponse> list = yatopProcdefQueryResponseApiPageResponse.getData().getList();
        Assert.isFalse(CollectionUtil.isEmpty(list), "无指标流程部署");
        String keys = list.stream().map(YatopProcdefQueryResponse::getProcDefKey).collect(Collectors.joining(StringConstants.COMMA));
        TaskMineQueryRequest taskMineQueryRequest = new TaskMineQueryRequest();
        taskMineQueryRequest.setProcessKeys(keys);
        //根据全部流程key,查询待我审核的任务
        ApiResponse<List<TaskResponse>> listApiResponse = camundaTaskFeignClient.queryMine(taskMineQueryRequest);
        Assert.isFalse(listApiResponse.getCode() != ExceptionCode.OK.getCode(), "查询流程异常");
        if (CollectionUtil.isEmpty(listApiResponse.getData())) {
            pagination.setTotal(0);
            return ApiPageResponse.ok(pagination, Lists.newArrayList());
        }
        // 用来填充信息
        Map<String, TaskResponse> taskMap = listApiResponse.getData().stream().collect(Collectors.toMap(TaskResponse::getProcessInstanceId, Function.identity()));
        
// 根据实例id从本地记录表查询全部申请记录
        indAuditPendQueryRequest.setProcIds(listApiResponse.getData().stream().map(TaskResponse::getProcessInstanceId).collect(Collectors.toList()));
        List<IndApplyRecordQueryResponse> indApplyRecordQueryResponses = indApplyRecordReadModelRepo.pendList(pagination, indAuditPendQueryRequest);

        // 查询用户
        Set<Long> createUserIdSet = indApplyRecordQueryResponses.stream().map(IndApplyRecordQueryResponse::getCreateUserId).filter(ObjectUtils::isNotEmpty).collect(Collectors.toSet());
        UserBaseQuery userBaseQuery = new UserBaseQuery();
        userBaseQuery.setIdList(createUserIdSet);
        userBaseQuery.setSimple(Boolean.TRUE);
        List<UserInfoResp> userInfoResps = ytSysWrap.queryUser(userBaseQuery);
        Map<Long, String> userMap = userInfoResps.stream().collect(Collectors.toMap(UserInfo::getId, UserInfo::getUserName));

        List<IndIndex> indIndices = indIndexService.queryByIds(indApplyRecordQueryResponses.stream().map(a -> Long.valueOf(a.getIndId())).collect(Collectors.toList())).getData();
        Map<String, IndIndex> indexMap = indIndices.stream().collect(Collectors.toMap(a -> a.getId().toString(), Function.identity()));
        for (IndApplyRecordQueryResponse response : indApplyRecordQueryResponses) {
            response.setCreateUserNm(userMap.get(response.getCreateUserId()));
            response.setTaskId(taskMap.getOrDefault(response.getProcId(), new TaskResponse()).getId());
            if (indexMap.containsKey(response.getIndId())) {
                response.setIndexType(indexMap.get(response.getIndId()).getIndexType());
            }

        }
        return ApiPageResponse.ok(pagination, indApplyRecordQueryResponses);
    }

远程接口queryMine的逻辑

public List<TaskResponse> query(Pagination pagination, TaskQueryDTO taskQueryDTO) {
        TaskQuery taskQuery = taskService.createTaskQuery();
        if (ObjectUtils.isNotEmpty(taskQueryDTO.getProcessDefinitionKeys())) {
            taskQuery.processDefinitionKeyIn(taskQueryDTO.getProcessDefinitionKeys().toArray(new String[0]));
        }
        if (ObjectUtils.isNotEmpty(taskQueryDTO.getAssignee())) {
            taskQuery.taskAssigneeIn(taskQueryDTO.getAssignee().toArray(new String[0]));
        }
        taskQuery.tenantIdIn(taskQueryDTO.getTenantId().toString());

        // 按照任务创建时间倒序排序
        taskQuery.orderByTaskCreateTime().desc();

        // 分页查询
        List<Task> list = super.page(pagination, taskQuery);
        return taskPOAssembler.toQueryResponse(list);
    }
(3)我审核的(历史审核记录)

接口:/4.2.0/indaudit/approved_list?pageNumber=1&pageSize=20

在这里插入图片描述

接口逻辑:

    public ApiPageResponse<IndApplyRecordQueryResponse> approved(IndAuditApprovedQueryRequest indAuditApprovedQueryRequest, Pagination pagination) {
        ProcessDefQueryRequest processDefQueryRequest = new ProcessDefQueryRequest();
        processDefQueryRequest.setModMenuCd("ind");
        processDefQueryRequest.setPageSize(1000);
        //查询指标下的流程
        ApiPageResponse<YatopProcdefQueryResponse> yatopProcdefQueryResponseApiPageResponse = camundaProcessFeignClient.defQuery(processDefQueryRequest);
        Assert.isFalse(yatopProcdefQueryResponseApiPageResponse.getCode() != ExceptionCode.OK.getCode(), "查询流程异常");
        List<YatopProcdefQueryResponse> list = yatopProcdefQueryResponseApiPageResponse.getData().getList();
        Assert.isFalse(CollectionUtil.isEmpty(list), "无指标流程部署");
        String keys = list.stream().map(YatopProcdefQueryResponse::getProcDefKey).collect(Collectors.joining(StringConstants.COMMA));
        TaskMineQueryRequest taskMineQueryRequest = new TaskMineQueryRequest();
        taskMineQueryRequest.setProcessKeys(keys);
        //查询待我审核
        ApiResponse<List<TaskResponse>> listApiResponse = camundaTaskFeignClient.queryHistoryMine(taskMineQueryRequest);
        Assert.isFalse(listApiResponse.getCode() != ExceptionCode.OK.getCode(), "查询流程异常");
        if (CollectionUtil.isEmpty(listApiResponse.getData())) {
            pagination.setTotal(0);
            return ApiPageResponse.ok(pagination, Lists.newArrayList());
        }
        indAuditApprovedQueryRequest.setProcIds(listApiResponse.getData().stream().map(TaskResponse::getProcessInstanceId).distinct().collect(Collectors.toList()));
        List<IndApplyRecordQueryResponse> indApplyRecordQueryResponses = indApplyRecordReadModelRepo.approved(pagination, indAuditApprovedQueryRequest);
        if(CollectionUtil.isEmpty(indApplyRecordQueryResponses)){
            return ApiPageResponse.ok(pagination, indApplyRecordQueryResponses);
            }
        // 查询用户
        Set<Long> createUserIdSet = indApplyRecordQueryResponses.stream().map(IndApplyRecordQueryResponse::getCreateUserId).filter(ObjectUtils::isNotEmpty).collect(Collectors.toSet());
        UserBaseQuery userBaseQuery = new UserBaseQuery();
        userBaseQuery.setIdList(createUserIdSet);
        userBaseQuery.setSimple(Boolean.TRUE);
        List<UserInfoResp> userInfoResps = ytSysWrap.queryUser(userBaseQuery);
        Map<Long, String> userMap = userInfoResps.stream().collect(Collectors.toMap(UserInfo::getId, UserInfo::getUserName));

        List<IndIndex> indIndices = indIndexService.queryByIds(indApplyRecordQueryResponses.stream().map(a -> Long.valueOf(a.getIndId())).collect(Collectors.toList())).getData();
        Map<String, IndIndex> indexMap = indIndices.stream().collect(Collectors.toMap(a -> a.getId().toString(), Function.identity()));
        for (IndApplyRecordQueryResponse response : indApplyRecordQueryResponses) {
            response.setCreateUserNm(userMap.get(response.getCreateUserId()));
            if (indexMap.containsKey(response.getIndId())) {
                response.setIndexType(indexMap.get(response.getIndId()).getIndexType());
            }
        }
        return ApiPageResponse.ok(pagination, indApplyRecordQueryResponses);
    }

远程接口queryHistoryMine逻辑

    public ApiPageResponse<TaskResponse> queryHistoryMinePage(Pagination pagination, TaskMineQueryRequest queryRequest) {
        TaskHistoryQueryDTO.TaskHistoryQueryDTOBuilder builder = TaskHistoryQueryDTO.builder();
        builder.tenantId(RequestContext.getTenantId());
        builder.assignee(RequestContext.getUser().getUserAcct());
        builder.processDefinitionKeys(toList(queryRequest.getProcessKeys()));
        List<TaskResponse> list = taskReadModelRepo.queryHistoryPage(pagination, builder.build());
        return ApiPageResponse.ok(pagination, list);
    }


    public List<TaskResponse> queryHistoryPage(Pagination pagination, TaskHistoryQueryDTO taskHistoryQueryDTO) {
        HistoricTaskInstanceQuery taskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
        taskInstanceQuery.tenantIdIn(taskHistoryQueryDTO.getTenantId().toString());
        taskInstanceQuery.finished();

        if (StringUtils.isNotBlank(taskHistoryQueryDTO.getAssignee())) {
            taskInstanceQuery.taskAssignee(taskHistoryQueryDTO.getAssignee());
        }
        if (CollectionUtils.isNotEmpty(taskHistoryQueryDTO.getProcessDefinitionKeys())) {
            taskInstanceQuery.taskDefinitionKeyIn(taskHistoryQueryDTO.getProcessDefinitionKeys().toArray(new String[0]));
        }
        List<HistoricTaskInstance> list = super.page(pagination, taskInstanceQuery);
        return taskPOAssembler.toHistoryQueryResponse(list);
    }

(4)实例详情

信息详情接口:/4.2.0/indaudit/{id}
根据流程实例id流程图:/4.2.0/indaudit/ab8bda87-47d5-11ef-b039-00505698f44a/flow_chart

在这里插入图片描述

接口逻辑

    public ApiResponse<List<TaskApproveResultResponse>> flowChartDetail(String procId) {
        return camundaTaskFeignClient.listTasksByProcInstId(procId);

    }

远程接口逻辑

    public List<TaskApproveResultVO> allTasksByProcInstId(String procInstId) {
        boolean flag = Boolean.TRUE;
        //拿到所有审批过的节点
        Map<String, Object> map = listFinishTasksByProcInstId(procInstId, flag);
        List<TaskApproveResultVO> result = (List<TaskApproveResultVO>) map.get("result");
        List<String> activityNames = result.stream().map(TaskApproveResultVO::getActivityName).collect(Collectors.toList());
        flag = (boolean) map.get("flag");
        //拿流程定义id
        String definitionId = runTimeRepo.getProcessDefinitionIddByInstId(procInstId);
        ProcessDefinitionEntity processDefinitionEntity = repositoryRepo.getDeployedProcessDefinition(definitionId);
        //所有活动节点
        List<ActivityImpl> activities = processDefinitionEntity.getActivities();
        //所有活动节点变量
        List<VariableInstance> variableInstance = runTimeRepo.variableInstance(procInstId);
        Map<String, Object> variableNameValue = CollectionUtils.isEmpty(variableInstance) ? new HashMap<>(0) :
                variableInstance.stream().filter(distinctByKey(VariableInstance::getName))
                        .collect((Collectors.toMap(VariableInstance::getName, a -> Objects.nonNull(a.getValue()) ? a.getValue() : "")));
        //流程定义节点
        Map<String, TaskDefinition> taskDefinitionMap = processDefinitionEntity.getTaskDefinitions();
        taskDefinitionMap = MapUtil.isEmpty(taskDefinitionMap) ? new HashMap<>(0) : taskDefinitionMap;
        //截取没有审批过的节点
        activities = activities.subList(result.size() + 1, activities.size() - 1);
        //拿到节点中分配的审批人或者审批角色id
        for (ActivityImpl activity : activities) {
            TaskApproveResultVO taskApproveResultVO = new TaskApproveResultVO();
            taskApproveResultVO.setActivityName(CollectionUtil.isEmpty(activity.getActivities()) ? activity.getName() : activity.getActivities().get(0).getName());
            if(activityNames.contains(taskApproveResultVO.getActivityName())){
                continue;
            }
            result.add(taskApproveResultVO);
            taskApproveResultVO.setActivityId(replaceActivityId(activity.getActivityId()));
            TaskDefinition taskDefinition = taskDefinitionMap.get(replaceActivityId(activity.getActivityId()));
            if (null == taskDefinition) {
                continue;
            }
            Expression expression = taskDefinition.getAssigneeExpression();
            if (expression != null) {
                taskApproveResultVO.setAssignee((String) variableNameValue.get(replaceExpression(expression.getExpressionText())));
                if (flag) {
                    List<String> strings = handUser(procInstId);
                    taskApproveResultVO.setTaskHandleUserList(queryUserByAccount(strings));
                    flag = Boolean.FALSE;
                }

            }
            Set<Expression> expressions = taskDefinition.getCandidateGroupIdExpressions();
            if (CollectionUtils.isEmpty(expressions)) {
                continue;
            }
            expression = new ArrayList<>(expressions).get(0);
            taskApproveResultVO.setRoleCode((String) variableNameValue.get(replaceExpression(expression.getExpressionText())));
        }
        boolean contains = result.stream().map(TaskApproveResultVO::getActivityName).collect(Collectors.toList()).contains("统筹方修订");
        boolean existContains = result.stream().map(TaskApproveResultVO::getActivityName).collect(Collectors.toList()).contains("数据治理确认");
        if (contains && !existContains) {
            int i = result.stream().map(TaskApproveResultVO::getActivityName).collect(Collectors.toList()).indexOf("统筹方修订");
            TaskApproveResultVO taskApproveResultVO = new TaskApproveResultVO();
            taskApproveResultVO.setActivityName("数据治理确认");
            TaskApproveUserVO taskApproveUserVO = new TaskApproveUserVO();
            taskApproveUserVO.setTipMsg("无影响,自动通过");
            taskApproveResultVO.setTaskApproveUserList(Collections.singletonList(taskApproveUserVO));
            result.add(i, taskApproveResultVO);
        }
        return result;
    }

(5)审核通过

接口:/4.2.0/indaudit/approve
参数:
在这里插入图片描述

接口代码逻辑

    public ApiResponse<Void> approve(IndApplyApproveRequest request) {
        // 获取申请的详情
        IndApplyRecord indApplyRecord = indApplyRecordService.queryById(request.getAppLyId());
        TaskCompleteRequest taskCompleteRequest = new TaskCompleteRequest();
        IndApplyRecordDetail indApplyRecordDetail = new IndApplyRecordDetail();
        taskCompleteRequest.setId(request.getTaskId());
        taskCompleteRequest.setApprove(request.isApprove());
        // 审批的请求数据
        Map<String, Object> map = Maps.newHashMap();
        if (request.isApprove()) {
            map.put(APPROVAL_RESULT, FlagConstants.FALSE);
            indApplyRecordDetail.setStatus(IndexApplyStateEnum.APPROVED.getCode());
        } else {
            map.put(APPROVAL_RESULT, FlagConstants.TRUE);
            indApplyRecordDetail.setStatus(IndexApplyStateEnum.REJECT.getCode());
            indApplyRecord.setStatus(IndexApplyStateEnum.REJECT.getCode());
        }

        if (StringUtils.isNotBlank(indApplyRecord.getExtendJson())) {
            Map<String, Object> extendMap = JSON.parseObject(indApplyRecord.getExtendJson(), new TypeReference<Map<String, Object>>() {

            });
            extendMap.put("permissionScope", request.getPermissionScope());
            indApplyRecord.setExtendJson(JSON.toJSONString(extendMap));
        } else {
            Map<String, Object> extendMap = Maps.newHashMap();
            extendMap.put("permissionScope", request.getPermissionScope());
            indApplyRecord.setExtendJson(JSON.toJSONString(extendMap));
        }
        indApplyRecord.setState(0);
        indApplyRecordService.updateById(indApplyRecord);
        taskCompleteRequest.setVariables(map);
        taskCompleteRequest.setComments(request.getAuditDesc());
        TodoProcessRequest todoProcessRequest = new TodoProcessRequest();
        todoProcessRequest.setBizId(String.valueOf(indApplyRecord.getId()));
        todoFeignClient.processBatch(todoProcessRequest);
        // 提交审批
        ApiResponse<Void> complete = camundaTaskFeignClient.complete(taskCompleteRequest);
        Assert.isFalse(complete.getCode() != ExceptionCode.OK.getCode(), "审批失败");
        indApplyRecordDetail.setApplyId(request.getAppLyId());
        indApplyRecordDetail.setAuditDesc(request.getAuditDesc());
        indApplyRecordDetail.setApprovedDt(LocalDateTime.now());
        // 提交审批记录信息
        indApplyRecordDetailService.create(indApplyRecordDetail);
        return ApiResponse.ok();
    }

远程接口的逻辑:

    /**
     * 任务完成
     *
     * @param taskId      任务ID
     * @param approve     审核。true:同意;false:拒绝
     * @param variableMap 变量
     * @return 响应对象
     */
    public DomainResponse<Void> complete(String taskId, boolean approve, Map<String, Object> variableMap, String assign, String comments) {
        variableMap.put("approve", approve);
        // 签收任务
        taskRepo.claim(taskId, assign);
        TaskCommentVO comment = new TaskCommentVO();
        comment.setUserName(RequestContext.getUser() == null ? "" : RequestContext.getUser().getUserName());
        comment.setOrgCode(RequestContext.getUser() == null ? "" : RequestContext.getUser().getOrgCode());
        if (approve) {
            comment.setOpinion(comments);
            comment.setTaskExecState(TakStateEnum.PASS.getCode());
            comment.setTaskExecStateDesc(TakStateEnum.PASS.getDesc());
        } else {
            comment.setOpinion(comments);
            comment.setTaskExecState(TakStateEnum.REJECT.getCode());
            comment.setTaskExecStateDesc(TakStateEnum.REJECT.getDesc());
        }
  Task task = taskRepo.queryByTaskId(taskId);
        //添加备注信息
        taskRepo.createComment(taskId, task.getProcessInstanceId(), comment.toJsonStr());
        taskRepo.complete(taskId, assign, variableMap);
        return DomainResponse.ok();
    }
@Transactional(rollbackFor = Exception.class)
    @Override
    public void complete(String taskId, String assignee, Map<String, Object> variableMap) {
        // 完成任务
        taskService.complete(taskId, variableMap);
    }
@Override
    public void claim(String taskId, String loginAcct) {
        taskService.claim(taskId, loginAcct);
    }

在这里插入图片描述

(6)审核驳回

(7)监听器的使用

(1)定义节点类型的枚举
public interface TaskListener {
    String EVENTNAME_CREATE = "create";
    String EVENTNAME_ASSIGNMENT = "assignment";
    String EVENTNAME_COMPLETE = "complete";
    String EVENTNAME_UPDATE = "update";
    String EVENTNAME_DELETE = "delete";
    String EVENTNAME_TIMEOUT = "timeout";

    void notify(DelegateTask var1);
}
(2)通知监听器
@Slf4j
@Component
@AllArgsConstructor
public class IndCustomTaskListener implements TaskListener {
    /**
     * 用户feign客户端
     */
    private final UserFeignClient userFeignClient;
    private final TodoFeignClient todoFeignClient;
    private final Environment environment;
    @Override
    public void notify(DelegateTask delegateTask) {
        System.out.println("======== 任务监听:" + delegateTask.getEventName() + " ========");
        Map<String, Object> variables = delegateTask.getVariables();
        switch (delegateTask.getEventName()) {
            // 任务创建完成后
            // TODO 任务创建完和处理人指派完以后,调用feign接口创建待办任务
            case EVENTNAME_CREATE:
                todo(variables, delegateTask.getAssignee(), delegateTask.getName(),delegateTask.getProcessInstanceId());
                break;
            case EVENTNAME_ASSIGNMENT:
                todo(variables, delegateTask.getAssignee(), delegateTask.getName(),delegateTask.getProcessInstanceId());
                break;
            case EVENTNAME_COMPLETE:
                break;
            case EVENTNAME_UPDATE:
                break;
            case EVENTNAME_DELETE:
                break;
            case EVENTNAME_TIMEOUT:
                break;
        }
//        delegateTask.setVariable("gggg", Lists.newArrayList("ORG:111", "USER:2222"));
        System.out.println("======== 任务监听:" + delegateTask.getEventName() + " ========");
    }
    private void todo(Map<String, Object> variables, String userNm, String taskName,String procId) {
	// TODO
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值