Activiti工作流使用之SpringBoot整合Activiti

一、springboot整合Activiti环境依赖
1.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>

1.2 添加日志配置
##我们使用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

1.3 添加activiti配置文件

直接使用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
二、流程操作–流程模型
2.1 绘制流程模型编辑器
  • 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;

/**
 * 
 */
@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;

/**
 * 
 */
@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;

/**
 * 
 */
@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.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绑定到此模型上

2.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());
       });
   }

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

2.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();
               }
           }
       }
   }

2.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 "部署成功";
   }

三、流程操作–流程部署
3.1 部署流程
  • 通过流程定义模型数据,部署流程定义
	@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压缩包方式部署流程定义完成");
    }

3.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.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);
   }

3.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);
       }
   }

3.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) 文件成功");
   }

四、流程操作–流程实例
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());
   }


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

4.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() ? "已挂起(暂停)" : "已激活(启动)"));
       });
   }

4.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.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);
   }

4.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.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
	
       });

5.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;
       
}

5.3 获取当前审批节点,用来设置下一节点办理人,如果有排他或并行网关,需要获取其集合
/**
    * 判断当前节点的下一节点是人工任务的集合
    *
    * @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);
           }
       }
       
}

5.4 签收任务
/**
    * 签收(拾取)任务
    */
   @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,就可以取消拾取任务

5.5 任务转交
/**
    * 任务转交
    */
   @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.6 完成任务
/**
    * 执行任务
    */
   @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);
   }

5.7 设置下一个节点人
/**
    * 完成当前节点时,设置下一节点任务办理人(上面输入框指定的那个办理人)
    */
   @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);
               }
           }
       }
       
}

5.8 获取已完成任务用于节点跳转
	@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;
       
}

5.9 节点跳转
/**
    * 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);
       
}

六、流程操作–流程历史
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来查询相应的历史任务

6.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();
               }
           }
       }
   }

6.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());
       });
   }

6.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);
   }

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐