Activiti7工作流流程详解

前言

工作流程是我们日常开发项目中常见的功能,本文主要springboot整合activiti7,梳理activiti在工作中的一些常见用法和功能

一、工作流介绍

1.1 概述

工作流(Workflow):就是“业务过程的部分或整体在计算机应用环境下的自动化”,它主要解决的是“使在多个参与者之间按照某种预定义的规则传递文档、信息或任务的过程自动进行,从而实现某个预期的业务目标,或者促使此目标的实现”。

1.2 常见工作流

采用工作流有以下优点
1、提高系统的柔性,适应业务流程的变化

2、实现更好的业务过程控制,提高顾客服务质量

3、降低系统开发和维护成本

常见工作流:Activiti、JBPM、OSWorkflow、ActiveBPEL、YAWL等。本文主要采用activiti7工作流开发

1.3 Activiti7

Alfresco软件在2010年5月17日宣布Activiti业务流程管理(BPM)开源项目的正式启动。Activiti是一个工作流引擎, activiti可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由activiti进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。

官方地址https://www.activiti.org/
在这里插入图片描述

Activiti6.0开发手册https://www.activiti.org/userguide/#bpmnMultiInstance

GitHub Activiti7开发手册地址https://github.com/Activiti/activiti-7-developers-guide

二、工作流术语

2.1 工作流引

ProcessEngine对象: 这是Activiti工作的核心.负责生成流程运行时的各种实例及数据,监控和管理流程的运行

2.2 BPM

BPM(业务流程管理):是一种以规范化的构造端到端的卓越业务流程为中心,以持续的提高组织业务绩效为目的的系统化方法

2.3 BPMN

BPMN(Business Process Model and Notation) 业务流程建模与标注,描述流程的基本符号,包括这些图元如何组合成一个业务流程图(Business Process Diagram)bpmn文件又可以叫做流程定义文件,它需要遵循BPMN语言规。

2.4 流对象

流对象(process engine)通过它可以获得我们需要的一切activiti服务

一个业务流程图有三个流对象的核心元素

  • 事件
    一个事件用圆圈来描述,表示一个业务流程期间发生的东西。事件影响流程的流动,一般有一个原因(触发器)或一个影响(结果),基于它们对流程的影响,有三种事件:开始事件,中间事件,终止事件。
    在这里插入图片描述
  • 活动
    用圆角矩形表示,一个流程由一个活动或多个活动组成
  • 条件
    条件用菱形表示,用于控制序列流的分支与合并,可以作为选择,包括路径的分支与合,内部的标记会给出控制流的类型

三、Activiti结构

3.1 Activiti系统服务结构图

在这里插入图片描述

  • 核心类:ProcessEngine: 流程引擎的抽象,可以通过此类获取需要的所有服务
  • 服务类:XxxService: 通过ProcessEngine获取,Activiti将不同生命周期的服务封装在不同Service中,包括定义,部署,运行.通过服务类可获取相关生命周期中的服务信息
    • RepositoryService
      Repository Service提供了对repository的存取服,Activiti中每一个不同版本的业务流程的定义都需要使用一些定义文件,部署文件和支持数据(例如BPMN2.0XML文件,表单定义文件,流程定义图像文件等),这些文件都存储在Activiti内建的Repository中
    • RuntimeService
      Runtime Service提供了启动流程,查询流程实例,设置获取流程实例变量等功能.此外它还提供了对流程部署,流程定义和流程实例的存取服务
    • TaskService
      Task Service提供了对用户Task和Form相关的操作.它提供了运行时任务查询,领取,完成,删除以及变量设置等功能
    • HistoryService
      History Service用于获取正在运行或已经完成的流程实例的信息,与Runtime Service获取的流程信息不同,历史信息包含已经持久化存储的永久信息,并已经被针对查询优化
    • FormService
      使用Form Service可以存取启动和完成任务所需的表单数据并且根据需要来渲染表单
      Activiti中的流程和状态Task均可以关联业务相关的数据
    • IdentityService
      Identity Service提供了对Activiti系统中的用户和组的管理功,Activiti中内置了用户以及组管理的功能,必须使用这些用户和组的信息才能获取到相应的Task
    • ManagementService
      Management Service提供了对Activiti流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用。主要用于 Activiti 系统的日常维护核心业务对象:org.activiti.engine.impl.persistence.entity包下的类,Task,ProcessInstance,Execution等根据不同职责实现相应接口的方法(如需要持久化则继承PersistentObject接口),与传统的实体类不同
      (注:Activiti7删除了FormService和IdentityService接口),工作流程与业务解耦,结合具体业务自由的配置用户和用户组。

3.2 Activiti数据库表结构

Activiti7工作流总共包含25张数据表(Activiti6包含23张表),Activiti会自动帮你生成这25张表,所有的表名默认以“ACT_”开头。

在这里插入图片描述
Activiti 使用到的表都是 ACT_ 开头的。表名的第二部分用两个字母表明表的用途:

  • ACT_GE_ (GE) 表示 general 全局通用数据及设置,各种情况都使用的数据。
  • ACT_HI_ (HI) 表示 history 历史数据表,包含着程执行的历史相关数据,如结束的流程实例,变量,任务,等等
  • ACT_RE_ (RE) 表示 repository 存储,包含的是静态信息,如,流程定义,流程的资源(图片,规则等)。
  • ACT_RU_ (RU) 表示 runtime 运行时,运行时的流程变量,用户任务,变量,职责(job)等运行时的数据。Activiti 只存储实例执行期间的运行时数据,当流程实例结束时,将删除这些记录。这就保证了这些运行时的表小且快。

Activiti7 25张表含义

表名介绍
act_evt_log流程引擎通用日志表
act_ge_bytearray二进制表,存储通用的流程资源
act_ge_property系统存储表,存储整个流程引擎数据,默认存储三条数据
act_hi_actinst历史节点表
act_hi_attachment历史附件表
act_hi_comment历史意见表
act_hi_detail历史详情表
act_hi_identitylink历史用户信息表
act_hi_procinst历史流程实例表
act_hi_taskinst历史任务实例表
act_hi_varinst历史变量表
act_procdef_info流程定义的动态变更信息
act_re_deployment部署信息表
act_re_model流程设计实体表
act_re_procdef流程定义数据表
act_ru_deadletter_job作业失败表,失败次数>重试次数
act_ru_event_subscr运行时事件表
act_ru_execution运行时流程执行实例表
act_ru_identitylink运行时用户信息表
act_ru_integration运行时综合表
act_ru_job作业表
act_ru_suspended_job作业暂停表
act_ru_task运行时任务信息表
act_ru_timer_job运行时定时器表
act_ru_variable运行时变量表

四、流程步骤

4.1 部署Activiti

Activiti是一个工作流引擎(其实就是一堆jar包API),业务系统访问(操作)activiti的接口,就可以方便的操作流程相关数据,这样就可以把工作流环境与业务系统的环境集成在一起。

4.2 流程定义

工作流要有流程模型图,使用activiti流程建模工具(activity-designer)定义业务流程(.bpmn文件) 绘制好流程模型,.bpmn文件就是业务流程定义文件,通过xml定义业务流程。

4.3 流程定义部署

将画好的流程图(activiti部署业务流程定义(.bpmn文件)),使用activiti提供的api把流程定义内容存储起来,在Activiti执行过程中可以查询定义的内容,Activiti执行把流程定义内容存储在数据库中

4.4 启动一个流程实例

流程实例也叫:ProcessInstance,启动一个流程实例表示开始一次业务流程的运行。在启动流程实例之前可以配置相应的业务需求,将某个业务绑定到当前流程上

4.5 用户查询待办任务(Task)

将系统的业务流程已经交给activiti管理,通过activiti就可以查询当前流程执行到哪里了,当前用户需要办理什么任务,activiti帮我们管理执行操作

4.6 用户已办任务历史记录

用户可以查询已经办理的业务和正在处理的任务,查询历史任务表获得具体流程执行细节,当一个流程没有下一节点时,整个流程实例就执行结束。

五、springboot整合Activiti

5.1 springboot整合Activiti环境依赖

1)maven环境

<?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>

    <groupId>com.mengxuegu</groupId>
    <artifactId>activiti-boot</artifactId>
    <version>1.0-SNAPSHOT</version>


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

    <properties>
        <activiti.version>7.1.0.M6</activiti.version>
        <mybatis-plus.version>3.3.1</mybatis-plus.version>
    </properties>

    <dependencies>
        <!-- Activiti -->
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-spring-boot-starter</artifactId>
            <version>${activiti.version}</version>
        </dependency>
        <!-- java绘制activiti流程图 -->
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-image-generator</artifactId>
            <version>${activiti.version}</version>
        </dependency>
        <!-- activiti json转换器-->
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-json-converter</artifactId>
            <version>${activiti.version}</version>
        </dependency>
        <!-- svg转png图片工具-->
        <dependency>
            <groupId>org.apache.xmlgraphics</groupId>
            <artifactId>batik-all</artifactId>
            <version>1.10</version>
        </dependency>

        <!-- web启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--mybatis-plusǷ-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <!-- log start -->
    	<dependency>
        	<groupId>log4j</groupId>
        	<artifactId>log4j</artifactId>
        	<version>${log4j.version}</version>
    	</dependency>
    	<dependency>
        	<groupId>org.slf4j</groupId>
        	<artifactId>slf4j-api</artifactId>
        	<version>${slf4j.version}</version>
    	</dependency>
    	<dependency>
        	<groupId>org.slf4j</groupId>
        	<artifactId>slf4j-log4j12</artifactId>
        	<version>${slf4j.version}</version>
    	</dependency>
		
    </dependencies>

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

</project>

2)添加log4j日志配置
我们使用log4j日志包,可以对日志进行配置,在resources 下创建log4j.properties

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=f:\act\activiti.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n

3)添加activiti配置文件

  • 官方采用默认方式的要求是在 resources 下创建 activiti.cfg.xml 文件,注意:默认方式目录和文件名不能修改,因为activiti的源码中已经设置,到固定的目录读取固定文件名的文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
                    http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

我们这里不做采用,直接配置application.yaml

server:
  port: 9889
  servlet:
    context-path: /com/act

spring:
  mvc:
    static-path-pattern: /**
  cloud:
    nacos:
      discovery:
        server-addr: 填写自己地址
  datasource:
    druid:
     配置自己数据库连接
      # 初始化大小,最小,最大
      initial-size: 1
      min-idle: 3
      max-active: 20
      # 配置获取连接等待超时的时间
      max-wait: 6000

  # activiti配置
  activiti:
    #自动更新数据库结构
    # true:适用开发环境,默认值。activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建
    # false:适用生产环境。activiti在启动时,对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出异常
    # create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)
    # drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)
    database-schema-update: false
    # activiti7与springboot整合后默认不创建历史表,需要手动开启
    db-history-used: true
    # 记录历史等级 可配置的历史级别有none, activity, audit, full
    # none:不保存任何的历史数据,因此,在流程执行过程中,这是最高效的。
    # activity:级别高于none,保存流程实例与流程行为,其他数据不保存。
    # audit:除activity级别会保存的数据外,还会保存全部的流程任务及其属性。
    # full:保存历史数据的最高级别,除了会保存audit级别的数据外,还会保存其他全部流程相关的细节数据,包括一些流程参数等。
    history-level: full
    # 是否自动检查resources下的processes目录的流程定义文件
    check-process-definitions: false
#    # smtp服务器地址
#    mail-server-host:
#    # SSL端口号
#    mail-server-port:
#    # 开启ssl协议
#    mail-server-use-ssl: true
#    # 默认的邮件发送地址(发送人),如果activiti流程定义中没有指定发送人,则取这个值
#    mail-server-default-from:
#    # 邮件的用户名
#    mail-server-user-name:
    deployment-mode: never-fail

logging:
  level:
    com:
      oneconnect:
        sg: debug
    # 日志级别是debug才能显示SQL日志
    org.activiti.engine.impl.persistence.entity: inf
mybatis-plus:
  type-aliases-package: com.act.entity
  # xxxMapper.xml 路径
  mapper-locations: classpath*:com/act/mapper/**/*.xml

5.2 流程操作—流程模型

1)绘制流程模型编辑器

我们这里用java绑定activity-designer模型编辑器绘制模型,导入官方编辑器文件

  • ModelEditorJsonRestResource
package com.oneconnect.sg.act.activiti;

import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Model;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * @author Tijs Rademakers
 */
@RestController
public class ModelEditorJsonRestResource implements ModelDataJsonConstants {

    protected static final Logger LOGGER = LoggerFactory.getLogger(ModelEditorJsonRestResource.class);

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private ObjectMapper objectMapper;

    @RequestMapping(value="/model/{modelId}/json", method = RequestMethod.GET, produces = "application/json")
    public ObjectNode getEditorJson(@PathVariable String modelId) {
        ObjectNode modelNode = null;

        Model model = repositoryService.getModel(modelId);

        if (model != null) {
            try {
                if (StringUtils.isNotEmpty(model.getMetaInfo())) {
                    modelNode = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
                } else {
                    modelNode = objectMapper.createObjectNode();
                    modelNode.put(MODEL_NAME, model.getName());
                }
                modelNode.put(MODEL_ID, model.getId());
                ObjectNode editorJsonNode = (ObjectNode) objectMapper.readTree(
                        new String(repositoryService.getModelEditorSource(model.getId()), "utf-8"));
                modelNode.put("model", editorJsonNode);

            } catch (Exception e) {
                LOGGER.error("Error creating model JSON", e);
                throw new ActivitiException("Error creating model JSON", e);
            }
        }
        return modelNode;
    }
}
  • ModelSaveRestResource
package com.oneconnect.sg.act.activiti;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Model;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;

/**
 * @author Tijs Rademakers
 */
@RestController
public class ModelSaveRestResource implements ModelDataJsonConstants {

    protected static final Logger LOGGER = LoggerFactory.getLogger(ModelSaveRestResource.class);

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private ObjectMapper objectMapper;

    @RequestMapping(value="/model/{modelId}/save", method = RequestMethod.PUT)
    @ResponseStatus(value = HttpStatus.OK)
    public void saveModel(@PathVariable String modelId, @RequestParam MultiValueMap<String, String> values) {
        try {

            Model model = repositoryService.getModel(modelId);

            ObjectNode modelJson = (ObjectNode) objectMapper.readTree(model.getMetaInfo());

            modelJson.put(MODEL_NAME, values.getFirst("name"));
            modelJson.put(MODEL_DESCRIPTION, values.getFirst("description"));
            model.setMetaInfo(modelJson.toString());
            model.setName(values.getFirst("name"));
            // 每次修改模型保存后,将版本号+1
            model.setVersion(model.getVersion()+1);

            ObjectNode jsonXml = (ObjectNode) objectMapper.readTree(values.get("json_xml").get(0));

            model.setKey(jsonXml.get("properties").get("process_id").textValue());

            repositoryService.saveModel(model);

            repositoryService.addModelEditorSource(model.getId(), values.getFirst("json_xml").getBytes("utf-8"));

            InputStream svgStream = new ByteArrayInputStream(values.getFirst("svg_xml").getBytes("utf-8"));
            TranscoderInput input = new TranscoderInput(svgStream);

            PNGTranscoder transcoder = new PNGTranscoder();
            // Setup output
            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
            TranscoderOutput output = new TranscoderOutput(outStream);

            // Do the transformation
            transcoder.transcode(input, output);
            final byte[] result = outStream.toByteArray();
            repositoryService.addModelEditorSourceExtra(model.getId(), result);
            outStream.close();

        } catch (Exception e) {
            LOGGER.error("Error saving model", e);
            throw new ActivitiException("Error saving model", e);
        }
    }
}

  • StencilsetRestResource
package com.oneconnect.sg.act.activiti;

import org.activiti.engine.ActivitiException;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.InputStream;

/**
 * @author Tijs Rademakers
 */
@RestController
public class StencilsetRestResource {

    @RequestMapping(value="/editor/stencilset", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
    public @ResponseBody String getStencilset() {
        InputStream stencilsetStream = this.getClass().getClassLoader().getResourceAsStream("static/stencilset.json");
        try {
            return IOUtils.toString(stencilsetStream, "utf-8");
        } catch (Exception e) {
            throw new ActivitiException("Error while loading stencil set", e);
        }
    }
}

将编辑器静态资源文件放在resources下面

2)创建模型

/**
     * 新增流程模型
     * @param req
     * @return
     * @throws Exception
     */
    @Test
    public String add(ModelAddREQ req) throws Exception {
        int version = 0;

        // 1. 初始空的模型
        Model model = repositoryService.newModel();
        model.setName(req.getName());
        model.setKey(req.getKey());
        model.setVersion(version);

        // 封装模型json对象
        ObjectNode objectNode  = objectMapper.createObjectNode();
        objectNode.put(ModelDataJsonConstants.MODEL_NAME, req.getName());
        objectNode.put(ModelDataJsonConstants.MODEL_REVISION, version);
        objectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, req.getDescription());
        model.setMetaInfo(objectNode.toString());
        // 保存初始化的模型基本信息数据
        repositoryService.saveModel(model);

        // 封装模型对象基础数据json串
        // {"id":"canvas","resourceId":"canvas","stencilset":{"namespace":"http://b3mn.org/stencilset/bpmn2.0#"},"properties":{"process_id":"未定义"}}
        ObjectNode editorNode = objectMapper.createObjectNode();
        ObjectNode stencilSetNode = objectMapper.createObjectNode();
        stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");
        editorNode.replace("stencilset", stencilSetNode);
        // 标识key
        ObjectNode propertiesNode = objectMapper.createObjectNode();
        propertiesNode.put("process_id", req.getKey());
        editorNode.replace("properties", propertiesNode);

        repositoryService.addModelEditorSource(model.getId(), editorNode.toString().getBytes("utf-8"));

        return model.getId();
    }

将具体要绑定的业务bussinekey和routes绑定到此模型上

3) 查询流程模型模板

/**
     * 查询所有流程定义模板
     */
    @Test
    public void modelList() {
        ModelQuery modelQuery = repositoryService.createModelQuery();
        List<Model> modelList = modelQuery.orderByCreateTime().desc().list();
        modelList.stream().forEach(m -> {
            System.out.print(" 模型id: " + m.getId());
            System.out.print(", 模型名称: " + m.getName());
            System.out.print(", 模型描述: " + m.getMetaInfo());
            System.out.print(", 模型标识key: " + m.getKey());
            System.out.print(", 模型版本号: " + m.getVersion());
        });
    }

4)删除流程定义模板

/**
     * 删除流程定义模板
     */
    @Test
    public void deleteModel() {
        String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
        repositoryService.deleteModel(modelId);
        //ACT_RE_MODEL、ACT_GE_BYTEARRAY
        System.out.println("删除成功");
    }

5)导出模型zip方式

 /**
     * 将模型以zip的方式导出
     * @param modelId
     * @param response
     */
    @Override
    public void exportZip(HttpServletResponse response) {
    
	    String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
        ZipOutputStream zipos = null;
        try {
            // 实例化zip输出流
            zipos = new ZipOutputStream(response.getOutputStream());

            // 压缩包文件名
            String zipName = "模型不存在";

            // 1. 查询模型基本信息
            Model model = repositoryService.getModel(modelId);
            if(model != null) {
                // 2. 查询流程定义模型的json字节码
                byte[] bpmnJsonBytes = repositoryService.getModelEditorSource(modelId);
                // 2.1 将json字节码转换为xml字节码
                byte[] xmlBytes = bpmnJsonXmlBytes(bpmnJsonBytes);
                if(xmlBytes == null) {
                    zipName = "模型数据为空-请先设计流程定义模型,再导出";
                }else {
                    // 压缩包文件名
                    zipName = model.getName() + "." + model.getKey() + ".zip";

                    // 将xml添加到压缩包中(指定xml文件名:请假流程.bpmn20.xml )
                    zipos.putNextEntry(new ZipEntry(model.getName() + ".bpmn20.xml"));
                    zipos.write(xmlBytes);

                    // 3. 查询流程定义模型的图片字节码
                    byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);
                    if(pngBytes != null) {
                        // 图片文件名(请假流程.leaveProcess.png)
                        zipos.putNextEntry(new ZipEntry(model.getName() + "." + model.getKey() + ".png"));
                        zipos.write(pngBytes);
                    }

                }
            }

            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition",
                    "attachment; filename=" + URLEncoder.encode(zipName, "UTF-8") + ".zip");
            // 刷出响应流
            response.flushBuffer();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(zipos != null) {
                try {
                    zipos.closeEntry();
                    zipos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

6)部署流程

 /**
     * 通过模型数据部署流程定义
     * @param modelId
     * @return
     * @throws Exception
     */
    @Test
    public String deploy() throws Exception {
    
		String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
        // 1. 查询流程定义模型json字节码
        byte[] jsonBytes = repositoryService.getModelEditorSource(modelId);
        if(jsonBytes == null) {
            return "模型数据为空,请先设计流程定义模型,再进行部署";
        }
        // 将json字节码转为 xml 字节码,因为bpmn2.0规范中关于流程模型的描述是xml格式的,而activiti遵守了这个规范
        byte[] xmlBytes = bpmnJsonXmlBytes(jsonBytes);
        if(xmlBytes == null) {
            return "数据模型不符合要求,请至少设计一条主线流程";
        }
        // 2. 查询流程定义模型的图片
        byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);

        // 查询模型的基本信息
        Model model = repositoryService.getModel(modelId);

        // xml资源的名称 ,对应act_ge_bytearray表中的name_字段
        String processName = model.getName() + ".bpmn20.xml";
        // 图片资源名称,对应act_ge_bytearray表中的name_字段
        String pngName = model.getName() + "." + model.getKey() + ".png";

        // 3. 调用部署相关的api方法进行部署流程定义
        Deployment deployment = repositoryService.createDeployment()
                .name(model.getName()) // 部署名称
                .addString(processName, new String(xmlBytes, "UTF-8")) // bpmn20.xml资源
                .addBytes(pngName, pngBytes) // png资源
                .deploy();

        // 更新 部署id 到流程定义模型数据表中
        model.setDeploymentId(deployment.getId());
        repositoryService.saveModel(model);

        return "部署成功";
    }

5.3 流程操作—流程部署

一个流程部署有两种方式,一是通过程定义模型数据,部署流程定义;二是通过 .zip 流程压缩包进行部署的流程定义。

  • 通过流程定义模型数据,部署流程定义
	@Test
    public void deploy() throws Exception {
        //1、查询流程定义模型的json字节码
        // 将json字节码转为xml字节码,因为bpmn2.0规范中关于流程模型的描述是xml格式的,而activiti遵守了这个规范
        String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
        byte[] jsonBytes = repositoryService.getModelEditorSource(modelId);
        if (jsonBytes == null) {
            System.out.println("模型数据为空,请先设计流程定义模型,再进行部署");
            return;
        }
        JsonNode jsonNode = objectMapper.readTree(jsonBytes);

        BpmnModel bpmnModel = new BpmnJsonConverter().convertToBpmnModel(jsonNode);
        byte[] xmlByte = null;
        if (bpmnModel.getProcesses().size() != 0) {
            xmlByte = new BpmnXMLConverter().convertToXML(bpmnModel);
            if (xmlByte == null) {
                System.out.println("数据模型不符合要求,请至少设计一条主线流程");
                return;
            }
        }
        //2、查询流程定义模型的图片
        byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);
        // 查询模型的基本信息
        Model model = repositoryService.getModel(modelId);

        // xml资源的名称,对应ACT_GE_BYTEARRAY表中的name_字段
        String processName = model.getName() + ".bpmn20.xml";
        // 图片资源名称,对应ACT_GE_BYTEARRAY表中的name_字段
        String pngName = model.getName() + "." + model.getKey() + ".png";

        //3、调用部署相关的api方法进行部署流程定义
        // 操作一下表
        // ACT_RE_PROCDEF 新增数据: 流程定义数据
        // ACT_RE_DEPLOYMENT 新增数据: 流程部署数据
        // ACT_GE_BYTEARRAY 新增数据:将当前流程图绑定到此流程定义部署数据上
        Deployment deployment = repositoryService.createDeployment()
                .name(model.getName())  //部署名称
                .addString(processName, new String(xmlByte)) //bpmn2.0资源
                .addBytes(pngName, pngBytes) //png资源
                .deploy();

        // 更新 部署id 到流程定义模型表中
        // ACT_RE_MODEL 更新部署id
        model.setDeploymentId(deployment.getId());
        repositoryService.saveModel(model);

        System.out.println("部署成功");
    }
  • 通过 .zip 流程压缩包进行部署的流程定义
	@Test
    public void deployByZip() throws Exception {
        File file = new File("D:/请假流程test01.leave.zip");
        String filename = file.getName();
        // 压缩包输入流
        ZipInputStream zipis = new ZipInputStream(new FileInputStream(file));
        // 创建部署实例
        DeploymentBuilder deployment = repositoryService.createDeployment();
        // 添加zip流
        deployment.addZipInputStream(zipis);
        // 部署名称
        deployment.name(filename.substring(0, filename.indexOf(".")));
        // 执行部署流程定义
        deployment.deploy();
        System.out.println("zip压缩包方式部署流程定义完成");
    }

2)删除部署信息

	/**
     * 根据部署ID删除流程定义部署信息:
     * ACT_GE_BYTEARRAY、
     * ACT_RE_DEPLOYMENT、
     * ACT_RE_PROCDEF、
     * -----下面是流程实例时产生的数据会被一起删除
     * ACT_RU_IDENTITYLINK、
     * ACT_RU_EVENT_SUBSCR
     */
    @Test
    public void delete() {
        try {
            // 部署ID ACT_RE_DEPLOYMENT
            String deploymentId = "d298a2f5-1db4-11ec-9048-28d0ea7310b3";
            // 不带级联的删除:如果有正在执行的流程,则删除失败抛出异常;不会删除 ACT_HI_和 历史表数据
            repositoryService.deleteDeployment(deploymentId);
            // 级联删除:不管流程是否启动,都能可以删除;并删除历史表数据。
            //repositoryService.deleteDeployment(deploymentId, true);
            System.out.println("删除流程定义部署信息成功");
        } catch (Exception e) {
            e.printStackTrace();
            if (e.getMessage().indexOf("a foreign key constraint fails") > 0) {
                System.out.println("有正在执行的流程,不允许删除");
            } else {
                System.out.println("删除失败,原因:" + e.getMessage());
            }
        }
    }

3)查询部署的流程定义数据

/**
     * 查询部署的流程定义数据 ACT_RE_PROCDEF
     * 需求:如果有多个相同流程定义标识key的流程时,只查询其最新版本
     */
    @Test
    public void getProcessDefinitionList() {
        // 1. 获取 ProcessDefinitionQuery
        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
        // 条件查询
        query.processDefinitionNameLike("%请假%");
        // 有多个相同标识key的流程时,只查询其最新版本
        query.latestVersion();
        // 按流程定义key升序排列
        query.orderByProcessDefinitionKey().asc();
        // 当前查询第几页
        int current = 1;
        // 每页显示多少条数据
        int size = 5;
        // 当前页第1条数据下标
        int firstResult = (current - 1) * size;
        // 开始分页查询
        List<ProcessDefinition> definitionList = query.listPage(firstResult, size);
        for (ProcessDefinition pd : definitionList) {
            System.out.print("流程部署ID:" + pd.getDeploymentId());
            System.out.print(",流程定义ID:" + pd.getId());
            System.out.print(",流程定义Key:" + pd.getKey());
            System.out.print(",流程定义名称:" + pd.getName());
            System.out.print(",流程定义版本号:" + pd.getVersion());
            System.out.println(",状态:" + (pd.isSuspended() ? "挂起(暂停)" : "激活(开启)"));
        }

        // 用于前端显示页面,总记录数
        long total = query.count();
        System.out.println("满足条件的流程定义总记录数:" + total);
    }

4)挂起或者激活流程定义

/**
     * 通过流程定义id,挂起或激活流程定义
     */
    @Test
    public void updateProcessDefinitionState() {
        // 流程定义ID ACT_RE_PROCDEF
        String definitionId = "leave:2:b9227310-c8f4-11eb-acbf-aa2e2e85fc05";
        // 流程定义对象
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                .processDefinitionId(definitionId)
                .singleResult();
        // 获取当前状态是否为:挂起
        boolean suspended = processDefinition.isSuspended();
        //对应 act_re_procdef 表中的 SUSPENSION_STATE_ 字段,1是激活,2是挂起
        if (suspended) {
            // 如果状态是:挂起,将状态更新为:激活,
            // 参数1: 流程定义id;参数2:是否级联激活该流程定义下的流程实例;参考3:设置什么时间激活这个流程定义,如果 null 则立即激活)
            repositoryService.activateProcessDefinitionById(definitionId, true, null);
        } else {
            // 如果状态是:激活,将状态更新为:挂起
            // 参数 (流程定义id,是否挂起,激活时间)
            repositoryService.suspendProcessDefinitionById(definitionId, true, null);
        }
    }

5)导出流程定义相关文件

/**
     * 导出下载流程定义相关的文件(.bpmn20.xml流程描述或.png图片资源)
     * @throws Exception
     */
    @Test
    public void exportProcDefFile() throws Exception {
        // 流程定义id ACT_RE_PROCDEF
        String processDefinitionId = "leave:2:4ccffb81-1b68-11ec-9d87-28d0ea7310b3";
        ProcessDefinition processDefinition = repositoryService.getProcessDefinition(processDefinitionId);

        //获取的是 xml 资源名
        String resourceName = processDefinition.getResourceName();
        //获取的是 png 资源名
        String diagramResourceName = processDefinition.getDiagramResourceName();
        //查询到相关的资源输入流
        InputStream input = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), resourceName);

        File file = new File("D:/" + resourceName);
        FileOutputStream out = new FileOutputStream(file);
        // 输入流,输出流的转换
        IOUtils.copy(input, out);
        // 关闭流
        out.close();
        input.close();
        System.out.println("下载流程定义 资源(xml、png) 文件成功");
    }

5.4 流程操作—流程实例

1)启动流程

/**
     * 启动流程实例
     * ACT_HI_TASKINST 任务实例
     * ACT_HI_PROCINST 流程实例
     * ACT_HI_ACTINST  节点实例
     * ACT_HI_IDENTITYLINK  流程实例相关办理人
     * ACT_RU_EXECUTION  运行时流程执行实例表
     * ACT_RU_TSK 运行时流程任务实例
     * ACT_RU_IDENTITYLINK 运行时流程实例相关办理人
     */
    @Test
    public void startProcessInstance() {
        // 流程定义唯一标识key  ACT_RE_PROCDEF 字段KEY_
        String processKey = "leave";
        //业务id
        String businessKey = "10000";
        //启动当前流程实例的用户
        Authentication.setAuthenticatedUserId("zhangsan");

        //启动流程实例(流程定义唯一标识key,业务id),采用流程key对应的最新版本的流程定义数据
        ProcessInstance pi = runtimeService.startProcessInstanceByKey(processKey, businessKey);
        //将流程定义名称 作为 流程实例名称
        runtimeService.setProcessInstanceName(pi.getProcessInstanceId(), pi.getProcessDefinitionName());
        System.out.println("启动流程实例成功: " + pi.getProcessInstanceId());
    }

启动流程实例时可以将业务配置绑定到当前流程上,办理人以实际业务办理人或候选人

2)查询正在运行的流程实例

/**
     * 查询正在运行中的流程实例
     */
    @Test
    public void getProcInstListRunning() {
//        //查询正在运行的流程实例 所有
//        List<ProcessInstance> list = runtimeService.createProcessInstanceQuery().list();
        //查询正在运行的流程实例 按照条件查询
        List<ProcessInstance> list = runtimeService.createProcessInstanceQuery().processInstanceNameLike("%请假%").list();
        list.stream().forEach(l -> {
            System.out.println("流程定义key: " + l.getProcessDefinitionKey());
            System.out.println("流程定义版本号: " + l.getProcessDefinitionVersion());
            System.out.println("流程实例Id: " + l.getProcessInstanceId());
            System.out.println("流程实例名称: " + l.getName());
            System.out.println("业务key(业务主键id): " + l.getBusinessKey());
            System.out.println("发起人: " + l.getStartUserId());
            System.out.println("流程实例状态: " + (l.isSuspended() ? "已挂起(暂停)" : "已激活(启动)"));
        });
    }

当前运行实例只有一个任务在运行

3)挂起或者激活流程实例

/**
     * 挂起或激活流程实例
     */
    @Test
    public void updateProInstState() {
        //流程实例id ACT_RU_EXECUTION 字段 ROOT_PROC_INST_ID_
        String proInstId = "d2f5ca6c-1c01-11ec-ba31-28d0ea7310b3";

        //查询流程实例对象
        ProcessInstance processInstance =
                runtimeService.createProcessInstanceQuery().processInstanceId(proInstId).singleResult();

        //获取当前流程实例状态是否为:挂起(暂停)
        boolean suspended = processInstance.isSuspended();

        //判断
        if (suspended) {
            //如果状态是:挂起,则更新为激活状态 ACT_RU_EXECUTION 字段 SUSPENSION_STATE_  1 激活
            runtimeService.activateProcessInstanceById(proInstId);
            System.out.println("激活流程实例");
        } else {
            //如果状态是:激活,则更新为挂起状态 CT_RU_EXECUTION 字段 SUSPENSION_STATE_  2 挂起
            runtimeService.suspendProcessInstanceById(proInstId);
            System.out.println("挂起流程实例");
        }
    }

4)删除流程实例

/**
     * 删除流程实例
     * ACT_RU_IDENTITYLINK
     * ACT_RU_INTEGRATION
     * ACT_RU_TASK
     */
    @Test
    public void deleteProcInst() {
        //流程实例id ACT_RU_EXECUTION 字段 ROOT_PROC_INST_ID_
        String proInstId = "7bf4bfe2-1db6-11ec-ba03-28d0ea7310b3";

        //查询流程实例对象
        ProcessInstance processInstance =
                runtimeService.createProcessInstanceQuery().processInstanceId(proInstId).singleResult();
        if (processInstance == null) {
            System.out.println("该实例不存在");
            return;
        }

        //删除流程实例(流程实例id,删除原因),不会删除流程实例相关历史数据
        runtimeService.deleteProcessInstance(proInstId, "XXX作废了当前流程申请");

        //删除流程实例相关历史数据
        historyService.deleteHistoricProcessInstance(proInstId);
    }

5)查询已结束流程实例

/**
     * 查询已结束的流程实例
     *
     * @param req
     * @return
     */
    @Test
    public void getProcInstFinish() {

        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery()
                .finished() //已结束
                .orderByProcessInstanceEndTime()
                .desc();
                
		List<HistoricProcessInstance> list =
                query.list();
        list.stream().forEach(l -> {
            System.out.println(l.getId());//流程实例id
            System.out.println(l.getName());//流程名称
            System.out.println(l.getProcessDefinitionKey());//流程定义key
            System.out.println(l.getProcessDefinitionVersion());//流程定义版本
            System.out.println(l.getStartUserId());//流程发起人
            System.out.println(l.getBusinessKey());//业务ID
            System.out.println(l.getStartTime()));//流程实例开始时间
            System.out.println(l.getEndTime()));//流程实例结束时间
            System.out.println(l.getDeleteReason());//删除原因
        });
    }

5.5 流程操作—流程任务

1)查询当前用户办理人或候选人任务

 /**
     * 查询当前用户是办理人或候选人的待办任务 pc
     *
     * @param req
     * @return
     */
    @Test
    public void findWaitTask() {
        //办理人(当前用户)
        String assignee = "1132";
        TaskQuery query = taskService.createTaskQuery()
                .taskCandidateOrAssigned(assignee)// 作为办理人或候选人
                .orderByTaskCreateTime().desc();

        if (StringUtils.isNotEmpty(req.getTaskName())) {
            query.taskNameLike("%" + req.getTaskName() + "%");
        }

        //分页查询
        List<Task> taskList = query.list;
        
		taskList.stream().forEach(l -> {
            System.out.println(l.getId());//任务ID
            System.out.println(l.getName());//任务名称
            System.out.println(l.isSuspended() );//状态
            System.out.println(l.getAssignee());//流程定义版本
            System.out.println(l..getAssignee());//办理人
            System.out.println(l.getProcessInstanceId());//流程实例ID
            System.out.println(l.getCreateTime()));//任务创建时
            System.out.println(l.getExecutionId());//执行对象ID
            System.out.println(l.getProcessDefinitionId());//流程定义ID
	
        });

2)获得下一节点任务

@Tes
public List<Map<String, Object>> getNextNodeInfo() {
		String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3"
        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
        if (task == null) {
            return null;
        }

        //获取当前模型
        BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());

        //根据任务节点id获取当前节点
        FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());

        //封装下一个节点信息
        List<Map<String, Object>> nextNodes = new ArrayList<>();
        getNextNodes(flowElement, nextNodes);
        return nextNodes;
        
}

获取当前审批节点,用来设置下一节点办理人,如果有排他或并行网关,需要获取其集合

/**
     * 判断当前节点的下一节点是人工任务的集合
     *
     * @param flowElement 当前节点
     * @param nextNodes   下节点名称集合
     */
    public void getNextNodes(FlowElement flowElement, List<Map<String, Object>> nextNodes) {
        //获取当前节点的连线信息
        List<SequenceFlow> outgoingFlows = ((FlowNode) flowElement).getOutgoingFlows();

        for (SequenceFlow outgoingFlow : outgoingFlows) {
            //下一节点
            FlowElement nextFlowElement = outgoingFlow.getTargetFlowElement();
            if (nextFlowElement instanceof EndEvent) {
                //结束节点
                break;
            } else if (nextFlowElement instanceof UserTask) {
                Map<String, Object> node = new HashMap<>();
                //用户任务 获取节点id和名称
                node.put("id", nextFlowElement.getId());
                node.put("name", nextFlowElement.getName());
                nextNodes.add(node);
            } else if (nextFlowElement instanceof ParallelGateway ||  //并行网关
                    nextFlowElement instanceof ExclusiveGateway) { //排他网关
                //递归 继续找下一节点
                getNextNodes(nextFlowElement, nextNodes);
            }
        }
        
}

3)拾取任务

/**
     * 签收(拾取)任务
     */
    @Test
    public String claimTask() {
 		    String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3";
			String userId = "zhangsan";
            Task task = taskService.createTaskQuery()
                    .taskId(taskId)
                    .singleResult();
            if (StringUtils.isBlank(task.getAssignee())) {
                taskService.claim(taskId, userId);
                return "操作成功";
            } else {
                return "操作失败";
            }
    }

这里取消拾取任务只需要在claim时将user置为null,就可以取消拾取任务

4)任务转交

/**
     * 任务转交
     */
    @Test
    public void turnTask() {
    	String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3";
        String assigneeUserKey = "校长";
        String userId = "zhangsan";
        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
        taskService.setAssignee(taskId, assigneeUserKey)
        //添加处理意见
        taskService.addComment(taskId, task.getProcessInstanceId(), message);
    }

5)完成任务

 /**
     * 执行任务
     */
    @Test
    public void completeTask() {
        //每个节点就是一个任务, ACT_RU_TASK  ID_
        String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3";
        //执行任务 complete 这个方法还有别的参数, complete(String taskId, Map<String, Object> variables);
        //variables 表示流程变量,可以修改,覆盖之前流程实例初始化时的流程变量
        taskService.complete(taskId);
    }

6)设置下一节点人

/**
     * 完成当前节点时,设置下一节点任务办理人(上面输入框指定的那个办理人)
     */
    @Test
    public void completeTaskSetNextAssignee() {
        String taskId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
        if (task == null) {
            System.out.println("任务id错误,无法查询到相关任务");
        } else {
            //执行任务
            taskService.complete(taskId);
            //查询下一个任务
            List<Task> taskList = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list();

            //下一个节点任务
            if (!CollectionUtils.isEmpty(taskList)) {
                //针对每个任务分配审批人
                for (Task t : taskList) {
                    //当前任务有审批人,则不设置新的审批人
                    if (StringUtils.isNotEmpty(t.getAssignee())) {
                        System.out.println("当前任务有审批人,不设置新的审批人");
                        continue;
                    }
                    //分配新的审批人
                    String assignee = "lisi";
                    taskService.setAssignee(t.getId(), assignee);
                }
            }
        }
        
}

7)获取已完成任务用于节点跳转

	@Test
    public List<Map<String, Object>> getBackNodes() {
		String taskId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
        String userId = "zhangsan";
        Task task = taskService.createTaskQuery()
                .taskId(taskId)
                .taskAssignee(username)
                .singleResult();

        if (task == null) {
            return null;
        }

        // 不把当前节点查询出来, 没有办理完的节点不查询,每条数据都有一个唯一值,我们使用随机数
        String sql = "select rand() AS ID_, t2.* from " +
                " ( select distinct t1.TASK_DEF_KEY_, t1.NAME_ from " +
                "  ( select ID_, RES.TASK_DEF_KEY_, RES.NAME_, RES.START_TIME_, RES.END_TIME_ " +
                "   from ACT_HI_TASKINST RES " +
                "   WHERE RES.PROC_INST_ID_ = #{processInstanceId} and TASK_DEF_KEY_ != #{taskDefKey}" +
                "   and RES.END_TIME_ is not null order by RES.START_TIME_ asc " +
                "  ) t1 " +
                " ) t2";

        List<HistoricTaskInstance> list =
                historyService.createNativeHistoricTaskInstanceQuery()
                        .sql(sql)
                        .parameter("processInstanceId", task.getProcessInstanceId())
                        .parameter("taskDefKey", task.getTaskDefinitionKey())
                        .list();
        List<Map<String, Object>> list = new ArrayList<>();

        list.forEach(hit -> {
            Map<String, Object> data = new HashMap<>();
            data.put("activityId", hit.getTaskDefinitionKey());
            data.put("activityName", hit.getName());
            list.add(data);
        });

        return list;
        
}

8)节点跳转

/**
     * 1. 取得当前节点信息
     * 2. 获取驳回的目标节点信息
     * 要考虑并行网关:从选中的目标节点的上级节点(如:并行网关),找到其上级节点的所有子节点,并行 网关就会有多条子节点
     * 3. 将当前节点出口指定为驳回的目标节点,(并行网关是多条)
     * 4. 完成当前节点任务,删除执行表 is_active_=0数据,不然并行汇聚不向后流转;删除其他并行任务,
     * 5. 分配目标节点原办理人。
     *
     * @param taskId           当前任务id
     * @param targetActivityId 回滚节点 TaskDefinitionKey  (getBackNodes这个方法设置)
     * @param userId
     * @return
     */
    @Test
    public void backProcess(String taskId, , String userId) {
		String taskId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
        String userId = "zhangsan";
        String targetActivityId = "fb7643c2-01ea-08ac-b098-82c0da3710f3"
        //----------------------第一部分
        // 1. 查询当前任务信息
        Task task = taskService.createTaskQuery()
                .taskId(taskId)
                .taskAssignee(username)
                .singleResult();
        if (task == null) {
            return "Error:当前任务不存在或你不是任务办理人";
        }

        // 2. 获取流程模型实例 BpmnModel
        BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
        // 3. 当前节点信息
        FlowNode curFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(task.getTaskDefinitionKey());

        // 4. 获取当前节点的原出口连线
        List<SequenceFlow> sequenceFlowList = curFlowNode.getOutgoingFlows();

        // 5. 临时存储当前节点的原出口连线
        List<SequenceFlow> oriSequenceFlows = new ArrayList<>();
        oriSequenceFlows.addAll(sequenceFlowList);

        // 6. 将当前节点的原出口清空
        sequenceFlowList.clear();

        //----------------------第二部分
        // 7. 获取目标节点信息
        FlowNode targetFlowNode = (FlowNode) bpmnModel.getFlowElement(targetActivityId);
        // 8. 获取驳回的新节点
        // 获取目标节点的入口连线
        List<SequenceFlow> incomingFlows = targetFlowNode.getIncomingFlows();
        // 存储获取驳回的新的流向
        List<SequenceFlow> allSequenceFlow = new ArrayList<>();
        for (SequenceFlow incomingFlow : incomingFlows) {
            // 找到入口连线的源头(获取目标节点的父节点)
            FlowNode source = (FlowNode) incomingFlow.getSourceFlowElement();
            // 获取目标节点的父组件的所有出口,
            List<SequenceFlow> sequenceFlows;

            if (source instanceof ParallelGateway) {
                // 并行网关的出口有多条连线:根据目标入口连线的父节点的出口连线,其所有出口连线才是驳回的真实节点
                sequenceFlows = source.getOutgoingFlows();
            } else {
                // 其他类型,将目标入口作为当前节点的出口
                sequenceFlows = targetFlowNode.getIncomingFlows();
            }
            // 找到后把它添加到集合作为新方向
            allSequenceFlow.addAll(sequenceFlows);
        }

        // 9. 将当前节点的出口设置为新节点
        curFlowNode.setOutgoingFlows(allSequenceFlow);

        //----------------------第三部分
        //流程实例id
        String procInstId = task.getProcessInstanceId();

        // 10. 完成当前任务,流程就会流向目标节点创建新目标任务
        //    删除已完成任务,删除已完成并行任务的执行数据 act_ru_execution
        List<Task> list = taskService.createTaskQuery().processInstanceId(procInstId).list();
        list.forEach(t -> {
            // 当前任务id
            if (taskId.equals(t.getId())) {
                // 当前任务,完成当前任务
                String message = String.format("【%s 驳回任务 %s => %s】",
                        username, task.getName(), targetFlowNode.getName());
                taskService.addComment(t.getId(), procInstId, message);
                // 完成任务,就会进行驳回到目标节点,产生目标节点的任务数据
                taskService.complete(taskId);
                // 删除执行表中 is_active_ = 0的执行数据, 使用command自定义模型
                DelelteExecutionCommand deleteExecutionCMD = new DelelteExecutionCommand(task.getExecutionId());
                managementService.executeCommand(deleteExecutionCMD);
            } else {
                // 删除其他未完成的并行任务
                // taskService.deleteTask(taskId); // 注意这种方式删除不掉,会报错:流程正在运行中无法删除。
                // 使用command自定义命令模型来删除,直接操作底层的删除表对应的方法,对应的自定义是否删除
                DeleteTaskCommand deleteTaskCMD = new DeleteTaskCommand(t.getId());
                managementService.executeCommand(deleteTaskCMD);
            }
        });

        // 11. 查询目标任务节点历史办理人
        List<Task> newTaskList = taskService.createTaskQuery().processInstanceId(procInstId).list();
        for (Task newTask : newTaskList) {
            // 取之前的历史办理人
            HistoricTaskInstance oldTargerTask = historyService.createHistoricTaskInstanceQuery()
                    .taskDefinitionKey(newTask.getTaskDefinitionKey()) // 节点id
                    .processInstanceId(procInstId)
                    // .finished() // 已经完成才是历史
                    .processFinished()
                    .orderByTaskCreateTime().desc() // 最新办理的在最前面
                    .list().get(0);
            taskService.setAssignee(newTask.getId(), oldTargerTask.getAssignee());
        }

        //----------------------第四部分
        // 12. 完成驳回功能后,将当前节点的原出口方向进行恢复
        curFlowNode.setOutgoingFlows(oriSequenceFlows);
        
}

5.6 流程操作—流程历史

1)查询审批历史记录

@Test
public void getHistoryInfoList() {

		String procInstId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
        //查询流程人工任务历史数据
        List<HistoricActivityInstance> historicActivityInstances =
                historyService.createHistoricActivityInstanceQuery()
                        .processInstanceId(processInstanceId)
                        .orderByHistoricActivityInstanceStartTime().asc()
                        .list();
        for (HistoricTaskInstance hti : historicTaskInstanceList) 
            System.out.println(hti.getId()); //任务id
            System.out.println(hti.getName()); //任务名称
            System.out.println(hti.getProcessInstanceId()); //流程实例ID
            System.out.println(hti.getStartTime()); //开始时间
            System.out.println(hti.getEndTime()); //结束时间
            System.out.println(hti.getAssignee()); //办理人
           
    }

这里也可以配置业务的businessKey来查询相应的历史任务
2)获取审批历史记录图

/**
     * 获取流程实例审批历史
     */
    @Test
    public void getHistoryProcessImage(HttpServletResponse response) {
        String procInstId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3", 
        InputStream imageStream = null;
        try {
            // 通过流程实例ID获取历史流程实例
            HistoricProcessInstance historicProcessInstance =
                    historyService.createHistoricProcessInstanceQuery()
                            .processInstanceId(procInstId)
                            .singleResult();
            // 获取流程定义Model对象
            BpmnModel bpmnModel =
                    repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());

            // 创建流程图生成器
            CustomProcessDiagramGenerator generator = new CustomProcessDiagramGenerator();

            //通过流程实例ID获取流程中已经执行的节点,按照执行先后顺序排序
            List<HistoricActivityInstance> historicActivityInstances =
                    historyService.createHistoricActivityInstanceQuery()
                            .processInstanceId(procInstId)
                            .orderByHistoricActivityInstanceStartTime().desc()
                            .list();

            // 将已经执行的节点id放入高亮显示节点集合
            List<String> highLightedActivityIdList =
                    historicActivityInstances.stream().map(HistoricActivityInstance::getActivityId)
                            .collect(Collectors.toList());

            // 通过流程实例ID获取流程中正在执行的节点
            List<Execution> runningActivityInstanceList =
                    runtimeService.createExecutionQuery().processInstanceId(procInstId).list();

            List<String> runningActivityIdList = new ArrayList<>();
            for (Execution execution : runningActivityInstanceList) {
                if (!StringUtils.isEmpty(execution.getActivityId())) {
                    runningActivityIdList.add(execution.getActivityId());
                }
            }

            // 获取已经流经的流程线,需要高亮显示流程已经发生流转的线id集合
            List<String> highLightedFlowsIds =
                    generator.getHighLightedFlows(bpmnModel, historicActivityInstances);

            // 使用自定义配置获得流程图表生成器,并生成追踪图片字符流
            imageStream =
                    generator.generateDiagramCustom(bpmnModel,
                            highLightedActivityIdList,
                            runningActivityIdList,
                            highLightedFlowsIds,
                            "宋体",
                            "微软雅黑",
                            "黑体");

            // 输出资源内容到相应对象
            response.setContentType("image/svg+xml");
            byte[] bytes = IOUtils.toByteArray(imageStream);
            OutputStream outputStream = response.getOutputStream();
            outputStream.write(bytes);
            outputStream.flush();
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (imageStream != null) {
                try {
                    imageStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

3)查询指定用户的已处理任务

	@Test
    public void findCompleteTask() {
        //用户id
        String userId = "meng";
        List<HistoricTaskInstance> historicTaskInstances =
                historyService.createHistoricTaskInstanceQuery()
                        .taskAssignee(userId)
                        .orderByTaskCreateTime().desc()
                        .finished()
                        .list();

        historicTaskInstances.forEach(h -> {
            System.out.println("任务id:" + h.getId());
            System.out.println("任务名称:" + h.getName());
            System.out.println("任务的办理任:" + h.getAssignee());
            System.out.println("任务的开始时间:" + h.getStartTime());
            System.out.println("任务的结束时间" + h.getEndTime());
            System.out.println("流程实例ID:" + h.getProcessInstanceId());
            System.out.println("流程定义ID:" + h.getProcessDefinitionId());
            System.out.println("业务唯一标识:" + h.getBusinessKey());
        });
    }

4)删除已结束的流程实例

/**
     * 删除已结束的流程实例
     * ACT_HI_DETAIL
     * ACT_HI_VARINST
     * ACT_HI_TASKINST
     * ACT_HI_PROCINST
     * ACT_HI_ACTINST
     * ACT_HI_IDENTITYLINK
     * ACT_HI_COMMENT
     */
    @Test
    public void deleteFinishProcInst() {
        String procInstId = "4c772281-1c75-11ec-b888-28d0ea7310b3";

        //查询流程实例是否已结束
        HistoricProcessInstance historicProcessInstance =
                historyService.createHistoricProcessInstanceQuery()
                        .processInstanceId(procInstId)
                        .finished()
                        .singleResult();

        if (historicProcessInstance == null) {
            System.out.println("流程实例不存在或未结束");
            return;
        }

        historyService.deleteHistoricProcessInstance(procInstId);
    }

六、Activiti监听器

6.1 监听器分配

监听器分配,分配的是什么,是任务执行人的分配。

  • Event(Create、Assignment、Delete、All)
  • Create: 任务创建后触发
  • Assignment:任务分配后触发
  • Delete: 任务完成后触发
  • All: 所有事件发生都触发在这里插入图片描述
    在我们绘制流程图时,可以对这个岗位进行事件监听,设置它触发条件,当节点任务发生变化时做出相应的处理
    在这里插入图片描述
  • 使用监听器需要实现TaskListener接口
public class ManagementTakListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        //任务id
        String taskId = delegateTask.getId();
        //监听事件 create\delete\assignment\complete
        String eventName = delegateTask.getEventName();

        HistoryService historyService =
                SpringUtil.getBean(HistoryServiceImpl.class);

        //任务创建时执行
        if ("班主任".equals(delegateTask.getName())
                && "create".equalsIgnoreCase(delegateTask.getEventName())) 
            String processInstanceId = delegateTask.getProcessInstanceId()
            HistoricProcessInstance historicProcessInstance =
                    historyService.createHistoricProcessInstanceQuery()
                          .processInstanceId(delegateTask.getProcessInstanceId())
                          .singleResult();


            String userId = historicProcessInstance.getStartUserId();
            delegateTask.addCandidateUser(userId);
            }
        }

        //任务完成时
        if ("班主任".equals(delegateTask.getName())
                && "complete".equalsIgnoreCase(delegateTask.getEventName())) {
            HistoricProcessInstance historicProcessInstance =
                    historyService.createHistoricProcessInstanceQuery()
                          .processInstanceId(delegateTask.getProcessInstanceId())
                            .singleResult();
                            
		//TODO审批意见回调
	    }
	
}

七、Activiti安全验证

7.1 去除activiti内置的sercurity

acitiviti模型设计器整合到Spring boot原工程中时需要排除security安全验证,启动项目后访问所有界面都被拦截到登录界面

  • 没有排除security安全验证出现错误如下
Caused by: java.io.FileNotFoundException: class path 
resource [org/springframework/security/config/annotation/authentication
/configurers/GlobalAuthenticationConfigurerAdapter.class] 
cannot be opened because it does not exist
  • 去除方法
    1)在启动类上加上如下注解
    2)继承WebSecurityConfigurerAdapter

@SpringBootApplication(exclude ={
        org.activiti.spring.boot.SecurityAutoConfiguration.class,
        SecurityAutoConfiguration.class,
        ManagementWebSecurityAutoConfiguration.class})

public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests().antMatchers("/**").permitAll();
}

八、总结

总结来说工作流的工作流程从流程模型—>流程部署—>流程启动。在配置模型时可以配置业务路由绑定实际业务,在流程启动时可以设置业务办理人。通过模型ID到流程定义ID,从流程定义ID到流程实例ID,从流程实例ID获取当前实例任务ID,历史节点信息,从而了解整个流程的流动方向和流动状态,流程定义绑定业务businessKey,从而嵌入实际业务逻辑。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是开心的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值