工作流-flowable

news2024/10/5 13:49:08

1. 工作流概述

1.1 概念

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

工作流的作用是对业务流程进行自动化管理,归根结底,工作流只是协助业务管理流程,即使没有工作流业务系统也可以开发运行,只不过有了工作流可以更好的管理业务流程,提高系统的可扩展性。

1.2 具体应用

  1. 以正在开发中的CCPC来举例,我们对赛事的报名就可以看做是一个工作流,在学生报名参赛时,需要经过报名,审核,缴费,缴费审核,完成报名这5个阶段
  2. 关键业务流程:订单、报价处理、合同审核、客户电话处理、供应链管理等
  3. 行政管理类:出差申请、加班申请、请假申请、用车申请、各种办公用品申请、购买申请、日报周报等凡是原来手工流转处理的行政表单。
  4. 人事管理类:员工培训安排、绩效考评、职位变动处理、员工档案信息管理等。
  5. 财务相关类:付款请求、应收款处理、日常报销处理、出差报销、预算和计划申请等。
  6. 客户服务类:客户信息管理、客户投诉、请求处理、售后服务管理等。
  7. 特殊服务类:ISO系列对应流程、质量管理对应流程、产品数据信息管理、贸易公司报关处理、物流公司货物跟踪处理等各种通过表单逐步手工流转完成的任务均可应用工作流软件自动规范地实施。

1.3 实现方式

在没有专门的工作流引擎之前,我们之前为了实现流程控制,通常的做法就是**采用状态字段的值来跟踪流程的变化情况。**这样不用角色的用户,通过状态字段的取值来决定记录是否显示。 eg:我们在CCPC中采用status字段来跟踪报名参赛的流程

针对有权限可以查看的记录,当前用户根据自己的角色来决定审批是否合格的操作。如果合格将状态字段设置一个值,来代表合格;当然如果不合格也需要设置一个值来代表不合格的情况。

这是一种最为原始的方式。通过状态字段虽然做到了流程控制,但是当我们的流程发生变更的时候,这种方式所编写的代码也要进行调整。(Hard Coding)

那么有没有专业的方式来实现工作流的管理呢?并且可以做到业务流程变化之后,我们的程序可以不用改变,如果可以实现这样的效果,那么我们的业务系统的适应能力就得到了极大提升。请接着看…

2. Flowable工作流引擎

Flowable is a light-weight business process engine written in Java. The Flowable process engine allows you to deploy BPMN 2.0 process definitions (an industry XML standard for defining processes), creating process instances of those process definitions, running queries, accessing active or historical process instances and related data, plus much more. –官方文档

翻译:Flowable是一个用Java编写的轻量级业务流程引擎。Flowable流程引擎允许您部署BPMN 2. 0流程定义(定义流程的行业XML标准),创建这些流程定义的流程实例,运行查询,访问活动或历史流程实例和相关数据等等。

2.1 BPMN 2.0介绍

详细规范请参考官方文档

BPMN 2.0是行业广泛接受的一种XML标准。在Flowable术语中,我们称之为流程定义。从一个流程定义中,可以启动多个流程实例。可以把流程定义看作是流程执行的蓝图。

image-20230908142106943

以这张图举例:

  • 左边的圆圈为开始事件,是流程实例的起点

  • 第一个矩形为用户任务,这是执行流程中的第一个步骤,在此例中,经理需要去批准或拒绝请求

  • 根据经理的决定,独占网关(带有X字的菱形)会将流程实例路由到审批路径或拒绝路径

  • 如果获得批准,我们必须在某个外部系统(即编写一个外部类)中注册请求,然后再次为原始员工执行用户任务,通知他们该结果。 当然,这可以替换为电子邮件。

  • 如果拒绝,则会向员工发送电子邮件,通知他们

  • 右边的加粗的圆圈是结束事件,是流程实例的结束点

3. 入门示例(java api)

源码位于:learning/flowable/flowable-quickstart at master · JiuYou2020/learning (github.com)

在源码中,使用tudo标记了代码的编写流程,因此,我们可以通过idea的工具去查找编写流程,如下:

image-20230908001711964

  1. 建模流程定义(有些流程定义是可以使用可视化建模工具建模的,但是这里使用xml以属性bpmn 2.0 及其概念)

将以下 XML 保存在名为 holiday-request.bpmn20.xml 的文件中,该文件位于 src/main/resources 文件夹中。

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:flowable="http://flowable.org/bpmn"
             typeLanguage="http://www.w3.org/2001/XMLSchema"
             expressionLanguage="http://www.w3.org/1999/XPath"
             targetNamespace="http://www.flowable.org/processdef">
    <!--    流程定义的编写-->
    <!--id为holidayRequest,isExecutable为true表示该流程可以被执行-->
    <process id="holidayRequest" name="Holiday Request" isExecutable="true">
        <!--startEvent为开始节点,流程的开始-->
        <startEvent id="startEvent"/>

        <!--sequenceFlow为连线,sourceRef为开始节点的id,targetRef为下一个节点的id-->
        <sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>
        <!--userTask为用户任务,即需要用户来完成的任务,需要指定id和name-->
        <userTask id="approveTask" name="Approve or reject request"/>
        <sequenceFlow sourceRef="approveTask" targetRef="decision"/>

        <!--exclusiveGateway为排他网关,即判断条件,需要指定id-->
        <exclusiveGateway id="decision"/>
        <!--sequenceFlow为连线,sourceRef为排他网关的id,targetRef为下一个节点的id,conditionExpression为条件表达式,即判断条件-->
        <sequenceFlow sourceRef="decision" targetRef="externalSystemCall">
            <!-- 这里是判断条件,${approved}表示流程变量approved的值,如果approved为true,则流程走向externalSystemCall,否则走向sendRejectionMail
             此处作为表达式编写的条件是${approved}形式,是${approved==true}的简写-->
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
          ${approved}
        ]]>
            </conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="decision" targetRef="sendRejectionMail">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
          ${!approved}
        ]]>
            </conditionExpression>
        </sequenceFlow>
        <!--serviceTask为服务任务,这里是==true时的步骤即需要调用服务来完成的任务,需要指定id,name和class,这里的class为自定义的类-->
        <serviceTask id="externalSystemCall" name="Enter holidays in external system"
                     flowable:class="cn.learning.CallExternalSystemDelegate"/>
        <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>
        <!--userTask为用户任务,即需要用户来完成的任务,这里表达管理人员统一审核,需要指定id和name-->
        <userTask id="holidayApprovedTask" name="Holiday approved"/>
        <sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>
        <!--serviceTask为服务任务,这里是==false时的步骤即需要调用服务来完成的任务,需要指定id,name和class,这里的class为自定义的类-->
        <serviceTask id="sendRejectionMail" name="Send out rejection email"
                     flowable:class="org.flowable.SendRejectionMail"/>
        <sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>
        <!--endEvent为结束节点,流程的结束-->
        <endEvent id="approveEnd"/>

        <endEvent id="rejectEnd"/>

    </process>

</definitions>

虽然长度有点让人生畏,但是都有比较详细的注释,可以结合 [BPMN 2.0介绍](# 2.1 BPMN 2.0介绍) 中给出的图片一起食用~

  1. 现在,我们有了BPMN 2.0 XML文件,我们需要将其部署到引擎中,这意味着
  • 进程引擎会将 XML 文件存储在数据库中,以便在需要时可以检索它
  • 进程定义被解析为内部的可执行对象模型,以便可以从中启动进程实例。(这里就像mybatis将mapper.xml解析为MapperStatement对象)

若要进程定义部署到可流动引擎,需要使用RepositoryService,可以从进程引擎对象中检索该服务

public class HolidayRequest {
    public static void main(String[] args) {
        //这段代码主要作用就是使用Flowable提供的API完成流程引擎的配置、创建工作,为后续的流程部署与执行等操作做准备。
        //通过构建ProcessEngine对象,我们就可以进一步地使用Flowable的流程引擎来开发业务流程应用程序了。
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration().setJdbcUrl("jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1").setJdbcUsername("sa").setJdbcPassword("").setJdbcDriver("org.h2.Driver").setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);

        ProcessEngine processEngine = cfg.buildProcessEngine();
        //先去完成日志文件的设置和流程图的编写,再接下来看下面的内容
        //进程引擎会将xml文件存储到数据库中,以便需要时获取,接下来将流程部署到引擎中,即使用数据库服务
        RepositoryService repositoryService = processEngine.getRepositoryService();
        Deployment deployment = repositoryService.createDeployment().addClasspathResource("holiday-request.bpmn20.xml").deploy();
        //现在,我们可以通过 API 查询流程定义来验证引擎是否知道流程定义(并了解一些有关 API 的信息)。
        //这是通过 RepositoryService 创建一个新的 ProcessDefinitionQuery 对象来完成的。
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();
        System.out.println("流程定义如下 : " + processDefinition.getName());
        //现在,我们已将流程定义部署到流程引擎,因此可以使用此流程定义作为“蓝图”启动流程实例
        //要启动流程实例,我们需要提供一些初始流程变量。通常,当流程被自动触发时,将通过呈现给用户的表单或通过REST API获得这些信息。
        //在本例中,我们将保持简单,并使用java. util. Scanner类在命令行上简单地输入一些数据
        Scanner scanner = new Scanner(System.in);

        System.out.println("你的名字是?");
        String employee = scanner.nextLine();

        System.out.println("你需要请假多少天?");
        Integer nrOfHolidays = Integer.valueOf(scanner.nextLine());

        System.out.println("你为什么需要请假?");
        String description = scanner.nextLine();
        //接下来,我们可以通过RuntimeService启动一个流程实例。收集的数据作为java. util. Map实例传递,其中key是将用于稍后检索变量的标识符。
        //使用key启动流程实例。此键与BPMN 2. 0 XML文件中设置的id属性匹配,在本例中为holidayRequest。除了使用密钥之外,还有许多启动流程实例的方法
        //当流程实例启动时,会创建一个执行并放入start事件中。从那里开始,这个执行遵循序列流程到用户任务,以获得管理员的批准,并执行用户任务行为。
        //此行为将在数据库中创建一个任务,以后可以通过查询找到该任务。用户任务处于等待状态,引擎将停止执行任何进一步操作,返回API调用。
        RuntimeService runtimeService = processEngine.getRuntimeService();
        Map<String, Object> variables = new HashMap<>();
        variables.put("employee", employee);
        variables.put("nrOfHolidays", nrOfHolidays);
        variables.put("description", description);
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holidayRequest", variables);
    }
}

到这里,启动流程实例时,将创建一个执行并放入启动事件中。此执行遵循工作流到用户任务以供经理批准,并执行用户任务行为。此行为将在数据库中创建一个任务,稍后可以查询找到该任务。用户任务是一种等待状态,工作流引擎将停止执行任何进一步的操作,并返回 API 调用。

ps:事务性

  1. 在Flowable中,数据库事务对保证数据一致性和解决并发问题起着至关重要的作用。
  2. 当调用Flowable API时,默认情况下所有操作都是同步的,在同一事务中执行。也就是当方法返回时,事务会启动并提交。
  3. 当启动一个流程实例时,从流程开始到下一个等待状态(第一个用户任务)会在一个数据库事务中完成。当引擎执行到用户任务时,状态会持久化到数据库,事务提交,API调用返回。
  4. 在Flowable中继续执行流程实例时,总是从前一个等待状态到下一个等待状态会有一个数据库事务。一旦持久化,数据可以在数据库中停留很长时间,直到有API调用继续推进流程实例。注意在此等待状态下不消耗计算和内存资源。
  5. 在本例中,当第一个用户任务完成时,从用户任务通过排他网关(自动逻辑)到第二个用户任务会在一个事务中完成。通过另一条路径直接结束流程也是如此。
  1. 在现实的应用程序中,将有一个用户界面,员工和经理可以在其中登录并查看他们的任务列表,有了这些,他们可以对这些流程执行具体的操作如批准请假,等等.在此示例中,我们将通过执行通常位于驱动 UI 的服务调用后面的 API 调用来模拟任务列表。

我们尚未为用户任务配置分配。我们希望第一个任务转到“经理”组,第二个用户任务分配给假期的原始请求者。为此,请将候选组属性添加到第一个任务:

<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>

以及第二个任务的被分派人属性,如下所示。请注意,我们没有使用像上面的“managers”值这样的静态值,而是基于我们在启动流程实例时传递的流程变量的动态赋值:

<userTask id="holidayApprovedTask" name="Holiday approved" flowable:assignee="${employee}"/>

再在HolidayRequest继续编写代码

//todo 10.
        //获取实际的任务列表,通过TaskService创建一个新的TaskQuery对象,并使用taskCandidateGroup方法过滤出候选组为managers的任务
        TaskService taskService = processEngine.getTaskService();
        List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
        System.out.println("你有 " + tasks.size() + " 个任务:");
        for (int i = 0; i < tasks.size(); i++) {
            System.out.println((i + 1) + ") " + tasks.get(i).getName());
        }
        //使用任务标识符,我们现在可以获取特定任务的详细信息,并将其打印到控制台
        System.out.println("你想要处理哪个任务?");
        int taskIndex = Integer.parseInt(scanner.nextLine());
        Task task = tasks.get(taskIndex - 1);
        Map<String, Object> processVariables = taskService.getVariables(task.getId());
        System.out.println(processVariables.get("employee") + " 想要请假 " + processVariables.get("nrOfHolidays") + " 天,你是否同意?");
        //todo 11.
        //现在,我们可以使用TaskService完成任务。实际上,这通常意味着表单是由用户去提交的,然后,表单中的数据将作为流程传递变量使用
        //在这里,我们将通过在任务完成时传递带有“approved”变量的映射来模拟这一点(注意,名称很重要,因为它稍后会在序列流的条件中使用!
        boolean approved = "y".equalsIgnoreCase(scanner.nextLine());
        variables = new HashMap<>();
        variables.put("approved", approved);
        taskService.complete(task.getId(), variables);
        //任务已经完成,并根据"已批准"流程变量选择离开独占网关的序列流程

这样,就完成了经理同意审核这一网关,此时,需要路由到后面的操作,也就是请求获得批准时将执行的自动逻辑,还记得我们写的xml语句吗?

<serviceTask id="externalSystemCall" name="Enter holidays in external system"
                     flowable:class="cn.learning.CallExternalSystemDelegate"/>

它会路由到CallExternalSystemDelegate类去执行后面的操作

public class CallExternalSystemDelegate implements JavaDelegate {
    public void execute(DelegateExecution execution) {
        System.out.println("为 " + execution.getVariable("employee") + " 申请了 " + execution.getVariable("nrOfHolidays") + " 天假期。");
    }
}

这时,我们可以看到这样的效果

image-20230907235512928

当然,使用Flowable还有一个很方便的地方,就是我们可以去获取流程实例的审核数据历史数据

//查询历史数据
        HistoryService historyService = processEngine.getHistoryService();
        List<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstance.getId()).finished().orderByHistoricActivityInstanceEndTime().asc().list();

        for (HistoricActivityInstance activity : activities) {
            System.out.println(activity.getActivityId() + " 花了 " + activity.getDurationInMillis() + " 毫秒");
        }

可以看到如下的效果

image-20230907235555192

4. SpringBoot集成Flowable

源码GitHub地址:learning/flowable/flowable-springboot at master · JiuYou2020/learning (github.com)

# 项目目录:
PS D:\JAVA\Project\private\learning\flowable\flowable-springboot\src> tree /F
卷 Data 的文件夹 PATH 列表
卷序列号为 8632-9174
D:.
└─main
    ├─java
    │  └─cn
    │      └─learning
    │          └─flowable
    │              └─springboot
        │
        ├─processess
        │      vacationRequest.bpmn20.xml
        │
        ├─sql
        │      flowable.sql
        │
        └─templates
                list.html
                search.html
                vacation.html

一、pom中引入Flowable相关框架

具体依赖,父项pom文件依赖管理以及依赖版本管理

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!--         flowable-springboot      -->
    <springboot.version>2.7.14</springboot.version>
    <flowable.springboot.version>6.8.0</flowable.springboot.version>
    <mysql.version>8.0.11</mysql.version>
    <mybatis.version>2.2.2</mybatis.version>
    <lombok.version>1.18.8</lombok.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${springboot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter</artifactId>
            <version>${flowable.springboot.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

子项目(即该项目)具体依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 工作流flowable架包 -->
    <dependency>
        <groupId>org.flowable</groupId>
        <artifactId>flowable-spring-boot-starter</artifactId>
    </dependency>
    <!-- mysql数据库连接架包 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- mybatis ORM 架包 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <!-- thymeleaf架包 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

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

二、相关配置文件

1.application.yml配置文件

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/flowable?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

logging:
  level:
    org:
    flowable: debug

# 业务流程设计的表自动创建
flowable:
  database-schema-update: true
  async-executor-activate: false

2.审批流程xml文件,默认放置在resources下的processess文件夹下

名称为vacationRequest.bpmn20.xml

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:flowable="http://flowable.org/bpmn"
             typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath"
             targetNamespace="http://www.activiti.org/processdef">
    <!-- -请假条流程图 -->
    <process id="vacationRequest" name="请假条流程" isExecutable="true">
        <!-- -流程的开始 -->
        <startEvent id="startEvent"/>
        <sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>
        <!-- -流程的节点 -->
        <userTask id="approveTask" name="开始请假" flowable:candidateGroups="managers"/>
        <!-- -流程节点间的线条,上一个节点和下一个节点-->
        <sequenceFlow sourceRef="approveTask" targetRef="decision"/>
        <!-- -排他性网关 -->
        <exclusiveGateway id="decision"/>
        <!-- -同意时 -->
        <sequenceFlow sourceRef="decision" targetRef="holidayApprovedTask">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[${approved}]]>
            </conditionExpression>
        </sequenceFlow>
        <!-- -拒绝时 -->
        <sequenceFlow sourceRef="decision" targetRef="rejectEnd">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[${!approved}]]>
            </conditionExpression>
        </sequenceFlow>
        <!-- -外部服务 -->
         <serviceTask id="externalSystemCall" name="Enter holidays in external system"
                     flowable:class="org.javaboy.flowable02.flowable.Approve"/>
        <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>

        <userTask id="holidayApprovedTask" flowable:assignee="${employee}" name="同意请假"/>
        <sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>

         <serviceTask id="rejectLeave" name="Send out rejection email"
                     flowable:class="org.javaboy.flowable02.flowable.Reject"/>
        <sequenceFlow sourceRef="rejectLeave" targetRef="rejectEnd"/>

        <endEvent id="approveEnd"/>

        <endEvent id="rejectEnd"/>
        <!-- -流程的结束 -->
    </process>
</definitions>

3. flowable.sql文件

请执行下面语句😄

create database if not exists flowable default character set utf8 collate utf8_bin;

三、控制层代码块

package cn.learning.flowable.springboot.controller;

import cn.learning.flowable.springboot.pojo.ResponseBean;
import cn.learning.flowable.springboot.pojo.VacationApproveVo;
import cn.learning.flowable.springboot.pojo.VacationRequestVo;
import cn.learning.flowable.springboot.service.VacationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

/**
 * {@code @Author: } JiuYou2020
 * <br>
 * {@code @Date: } 2023/9/8
 * <br>
 * {@code @Description: } 请假流程测试
 */
@RequestMapping("vacation")
@RestController
public class VacationController {
    @Autowired
    VacationService vacationService;

    /**
     * 请假条新增页面
     *
     * @return ModelAndView
     */
    @GetMapping("/add")
    public ModelAndView add() {
        return new ModelAndView("vacation");
    }

    /**
     * 请假条审批列表
     *
     * @return ModelAndView
     */
    @GetMapping("/aList")
    public ModelAndView aList() {
        return new ModelAndView("list");
    }

    /**
     * 请假条查询列表
     *
     * @return ModelAndView
     */
    @GetMapping("/sList")
    public ModelAndView sList() {
        return new ModelAndView("search");
    }

    /**
     * 请假请求方法
     *
     * @param vacationRequestVO 请假请求参数
     * @return ModelAndView
     */
    @PostMapping
    public ResponseBean askForLeave(@RequestBody VacationRequestVo vacationRequestVO) {
        return vacationService.askForLeave(vacationRequestVO);
    }

    /**
     * 获取待审批列表
     *
     * @param identity 用户名
     * @return ResponseBean
     */
    @GetMapping("/list")
    public ResponseBean leaveList(String identity) {
        return vacationService.leaveList(identity);
    }

    /**
     * 拒绝或同意请假
     *
     * @param vacationVO 请假请求参数
     * @return ResponseBean
     */
    @PostMapping("/handler")
    public ResponseBean askForLeaveHandler(@RequestBody VacationApproveVo vacationVO) {
        return vacationService.askForLeaveHandler(vacationVO);
    }

    /**
     * 请假查询
     *
     * @param name 请假人
     * @return ResponseBean
     */
    @GetMapping("/search")
    public ResponseBean searchResult(String name) {
        return vacationService.searchResult(name);
    }
}

四、Service层,请假条新增、审批、查询的业务处理

package cn.learning.flowable.springboot.service;

import cn.learning.flowable.springboot.pojo.ResponseBean;
import cn.learning.flowable.springboot.pojo.VacationApproveVo;
import cn.learning.flowable.springboot.pojo.VacationInfo;
import cn.learning.flowable.springboot.pojo.VacationRequestVo;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.task.api.Task;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * {@code @Author: } JiuYou2020
 * <br>
 * {@code @Date: } 2023/9/8
 * <br>
 * {@code @Description: } 请假流程测试
 */
@Service
public class VacationService {
    @Autowired
    RuntimeService runtimeService;

    @Autowired
    TaskService taskService;

    @Autowired
    HistoryService historyService;

    @Autowired
    private RepositoryService repositoryService;

    @PostConstruct
    public void init() {
        //部署流程
        repositoryService.createDeployment().addClasspathResource("processess/vacationRequest.bpmn20.xml").deploy();
    }

    /**
     * 申请请假
     *
     * @param vacationRequestVO 请假请求参数
     * @return ResponseBean
     */
    @Transactional
    public ResponseBean askForLeave(VacationRequestVo vacationRequestVO) {
        Map<String, Object> variables = new HashMap<>();
        variables.put("name", vacationRequestVO.getName());
        variables.put("days", vacationRequestVO.getDays());
        variables.put("reason", vacationRequestVO.getReason());
        try {
            //指定业务流程
            runtimeService.startProcessInstanceByKey("vacationRequest", vacationRequestVO.getName(), variables);
            return ResponseBean.ok("已提交请假申请");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ResponseBean.error("提交申请失败");
    }

    /**
     * 审批列表
     *
     * @param identity 审批人
     * @return ResponseBean
     */
    public ResponseBean leaveList(String identity) {
        List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup(identity).list();
        List<Map<String, Object>> list = new ArrayList<>();
        for (int i = 0; i < tasks.size(); i++) {
            Task task = tasks.get(i);
            Map<String, Object> variables = taskService.getVariables(task.getId());
            variables.put("id", task.getId());
            list.add(variables);
        }
        return ResponseBean.ok("加载成功", list);
    }

    /**
     * 操作审批
     *
     * @param vacationVO 审批参数
     * @return ResponseBean
     */
    public ResponseBean askForLeaveHandler(VacationApproveVo vacationVO) {
        try {
            boolean approved = vacationVO.getApprove();
            Map<String, Object> variables = new HashMap<String, Object>();
            variables.put("approved", approved);
            variables.put("employee", vacationVO.getName());
            Task task = taskService.createTaskQuery().taskId(vacationVO.getTaskId()).singleResult();
            taskService.complete(task.getId(), variables);
            if (approved) {
                //如果是同意,还需要继续走一步
                Task t = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).singleResult();
                taskService.complete(t.getId());
            }
            return ResponseBean.ok("操作成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ResponseBean.error("操作失败");
    }

    /**
     * 请假列表
     *
     * @param name 请假人
     * @return ResponseBean
     */
    public ResponseBean searchResult(String name) {
        List<VacationInfo> vacationInfos = new ArrayList<>();
        List<HistoricProcessInstance> historicProcessInstances = historyService
                .createHistoricProcessInstanceQuery()
                .processInstanceBusinessKey(name)
                .finished()
                .orderByProcessInstanceEndTime()
                .desc()
                .list();
        for (HistoricProcessInstance historicProcessInstance : historicProcessInstances) {
            VacationInfo vacationInfo = new VacationInfo();
            Date startTime = historicProcessInstance.getStartTime();
            Date endTime = historicProcessInstance.getEndTime();
            List<HistoricVariableInstance> historicVariableInstances = historyService.createHistoricVariableInstanceQuery()
                    .processInstanceId(historicProcessInstance.getId())
                    .list();
            for (HistoricVariableInstance historicVariableInstance : historicVariableInstances) {
                String variableName = historicVariableInstance.getVariableName();
                Object value = historicVariableInstance.getValue();
                if ("reason".equals(variableName)) {
                    vacationInfo.setReason((String) value);
                } else if ("days".equals(variableName)) {
                    vacationInfo.setDays(Integer.parseInt(value.toString()));
                } else if ("approved".equals(variableName)) {
                    vacationInfo.setStatus((Boolean) value);
                } else if ("name".equals(variableName)) {
                    vacationInfo.setName((String) value);
                }
            }
            vacationInfo.setStartTime(startTime);
            vacationInfo.setEndTime(endTime);
            vacationInfos.add(vacationInfo);
        }
        return ResponseBean.ok("ok", vacationInfos);
    }
}

五、POJO相关类

package cn.learning.flowable.springboot.pojo;

import lombok.Data;
/**
 * 响应类
 * @Date
 */
@Data
public class ResponseBean {
  
    private Integer status;
    
    private String msg;
    
    private Object data;

    public static ResponseBean ok(String msg, Object data) {
        return new ResponseBean(200, msg, data);
    }


    public static ResponseBean ok(String msg) {
        return new ResponseBean(200, msg, null);
    }


    public static ResponseBean error(String msg, Object data) {
        return new ResponseBean(500, msg, data);
    }


    public static ResponseBean error(String msg) {
        return new ResponseBean(500, msg, null);
    }

    private ResponseBean() {
    }

    private ResponseBean(Integer status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }
}



package cn.learning.flowable.springboot.pojo;

import lombok.Data;

import java.util.Date;

/**
 * 请假条DO
 * @Date
 */
@Data
public class VacationInfo {

  private String name;
  
  private Date startTime;
  
  private Date endTime;
  
  private String reason;
  
  private Integer days;
  
  private Boolean status;
}


package cn.learning.flowable.springboot.pojo;

import lombok.Data;

/**
 * 请假条申请
 * @Date
 */
@Data
public class VacationRequestVo {

    private String name;
    
    private Integer days;
    
    private String reason;
}


package cn.learning.flowable.springboot.pojo;

import lombok.Data;

/**
 * 请假条审批
 *
 * @Date 2023/9/8
 */
@Data
public class VacationApproveVo {

    private String taskId;

    private Boolean approve;

    private String name;
}

六、页面代码,页面文件放在resources的templates文件夹下

1.提交请假条申请页面vacation.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>提交请假条申请页面</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!-- Import style -->
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"/>
    <script src="https://unpkg.com/vue@3"></script>
    <!-- Import component library -->
    <script src="//unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
    <h1>开始一个请假流程</h1>
    <table>
        <tr>
            <td>请输入姓名:</td>
            <td>
                <el-input type="text" v-model="afl.name"/>
            </td>
        </tr>
        <tr>
            <td>请输入请假天数:</td>
            <td>
                <el-input type="text" v-model="afl.days"/>
            </td>
        </tr>
        <tr>
            <td>请输入请假理由:</td>
            <td>
                <el-input type="text" v-model="afl.reason"/>
            </td>
        </tr>
    </table>
    <el-button type="primary" @click="submit">提交请假申请</el-button>
</div>
<script>
    Vue.createApp(
        {
            data() {
                return {
                    afl: {
                        name: 'test',
                        days: 3,
                        reason: '测试'
                    }
                }
            },
            methods: {
                submit() {
                    let _this = this;
                    axios.post('/vacation', this.afl)
                        .then(function (response) {
                            if (response.data.status == 200) {
                                //提交成功
                                _this.$message.success(response.data.msg);
                            } else {
                                //提交失败
                                _this.$message.error(response.data.msg);
                            }
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                }
            }
        }
    ).use(ElementPlus).mount('#app')
</script>
</body>
</html>

2.审批请假条页面list.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>审批请假条页面</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!-- Import style -->
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"/>
    <script src="https://unpkg.com/vue@3"></script>
    <!-- Import component library -->
    <script src="//unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
    <div>
        <div>请选择你的身份:</div>
        <div>
            <el-select name="" id="" v-model="identity" @change="initTasks">
                <el-option :value="iden" v-for="(iden,index) in identities" :key="index" :label="iden"></el-option>
            </el-select>
            <el-button type="primary" @click="initTasks">刷新一下</el-button>
        </div>

    </div>
    <el-table border strip :data="tasks">
        <el-table-column prop="name" label="姓名"></el-table-column>
        <el-table-column prop="days" label="请假天数"></el-table-column>
        <el-table-column prop="reason" label="请假原因"></el-table-column>
        <el-table-column lable="操作">
            <template #default="scope">
                <el-button type="primary" @click="approveOrReject(scope.row.id,true,scope.row.name)">批准</el-button>
                <el-button type="danger" @click="approveOrReject(scope.row.id,false,scope.row.name)">拒绝</el-button>
            </template>
        </el-table-column>
    </el-table>
</div>
<script>
    Vue.createApp(
        {
            data() {
                return {
                    tasks: [],
                    identities: [
                        'managers'
                    ],
                    identity: ''
                }
            },
            methods: {
                initTasks() {
                    let _this = this;
                    axios.get('/vacation/list?identity=' + this.identity)
                        .then(function (response) {
                            _this.tasks = response.data.data;
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                },
                approveOrReject(taskId, approve,name) {
                    let _this = this;
                    axios.post('/vacation/handler', {taskId: taskId, approve: approve,name:name})
                        .then(function (response) {
                            _this.$message.success("审批成功");
                            _this.initTasks();

                        })
                        .catch(function (error) {
                            _this.$message.error("操作失败");
                            console.log(error);
                        });
                }
            }
        }
    ).use(ElementPlus).mount('#app')
</script>
</body>
</html>

3.已审批请假条查询页面search.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>已审批请假条查询页面</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!-- Import style -->
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"/>
    <script src="https://unpkg.com/vue@3"></script>
    <!-- Import component library -->
    <script src="//unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
    <div style="margin-top: 50px">
        <el-input v-model="name" style="width: 300px" placeholder="请输入用户名"></el-input>
        <el-button type="primary" @click="search">查询</el-button>
    </div>

    <div>
        <el-table border strip :data="historyInfos">
            <el-table-column prop="name" label="姓名"></el-table-column>
            <el-table-column prop="startTime" label="提交时间"></el-table-column>
            <el-table-column prop="endTime" label="审批时间"></el-table-column>
            <el-table-column prop="reason" label="事由"></el-table-column>
            <el-table-column prop="days" label="天数"></el-table-column>
            <el-table-column label="状态">
                <template #default="scope">
                    <el-tag type="success" v-if="scope.row.status">已通过</el-tag>
                    <el-tag type="danger" v-else>已拒绝</el-tag>
                </template>
            </el-table-column>
        </el-table>
    </div>
</div>
<script>
    Vue.createApp(
        {
            data() {
                return {
                    historyInfos: [],
                    name: 'zhangsan'
                }
            },
            methods: {
                search() {
                    let _this = this;
                    axios.get('/vacation/search?name=' + this.name)
                        .then(function (response) {
                            if (response.data.status == 200) {
                                _this.historyInfos=response.data.data;
                            } else {
                                _this.$message.error(response.data.msg);
                            }
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                }
            }
        }
    ).use(ElementPlus).mount('#app')
</script>
</body>
</html>

七、启动并测试

image-20230908140522244

1.第一次运行,系统会自动创建flowable需要数据表结构

image-20230908140815081

2.输入url地址:localhost:8081/vacation/add,建立几个请假条

image-20230908140929994

3.请假条建立好了,审批处理一下

image-20230908141001778

注意:第一次运行这个demo,权限暂且不管,角色也先写死,先把demo跑起来再说。四个请假条两个通过,两个拒绝,操作完成后,在待审批列表不在出现

4.作为请假人,查询一下自己提交的假条审批了.

image-20230908141045975

通过查询结果得知,两个通过,两个拒绝。至此,一个简单的请假条审批流程走完了!!!

八、API梳理说明

1.FormService

表单数据的管理; 是可选服务,也就是说Flowable没有它也能很好地运行,而不必牺牲任何功能。这个服务引入了开始表单(start form)与任务表单(task form)的概念。 开始表单是在流程实例启动前显示的表单,

而任务表单是用户完成任务时显示的表单。Flowable可以在BPMN 2.0流程定义中定义这些表单。表单服务通过简单的方式暴露这些数据。再次重申,表单不一定要嵌入流程定义,因此这个服务是可选的

formService.getStartFormKey() // 获取表单key
formService.getRenderedStartForm()  // 查询表单json(无数据)

2.RepositiryService

提供了在编辑和发布审批流程的api。主要是模型管理和流程定义的业务api

这个服务提供了管理与控制部署(deployments)与流程定义(process definitions)的操作

查询引擎现有的部署与流程定义。 暂停或激活部署中的某些流程,或整个部署。暂停意味着不能再对它进行操作,激活刚好相反,重新使它可以操作。 获取各种资源,比如部署中保存的文件,

或者引擎自动生成的流程图。 获取POJO版本的流程定义。它可以用Java而不是XML的方式查看流程。

1.提供了带条件的查询模型流程定义的api
repositoryService.createXXXQuery()
例如:
repositoryService.createModelQuery().list() 模型查询 
repositoryService.createProcessDefinitionQuery().list() 流程定义查询

repositoryService.createXXXXQuery().XXXKey(XXX) (查询该key是否存在)

2.提供一大波模型与流程定义的通用方法
模型相关
repositoryService.getModel()  (获取模型)
repositoryService.saveModel()  (保存模型)
repositoryService.deleteModel() (删除模型)
repositoryService.createDeployment().deploy(); (部署模型)
repositoryService.getModelEditorSource()  (获得模型JSON数据的UTF8字符串)
repositoryService.getModelEditorSourceExtra()  (获取PNG格式图像)

3.流程定义相关
repositoryService.getProcessDefinition(ProcessDefinitionId);  获取流程定义具体信息
repositoryService.activateProcessDefinitionById() 激活流程定义
repositoryService.suspendProcessDefinitionById()  挂起流程定义
repositoryService.deleteDeployment()  删除流程定义
repositoryService.getProcessDiagram()获取流程定义图片流
repositoryService.getResourceAsStream()获取流程定义xml流
repositoryService.getBpmnModel(pde.getId()) 获取bpmn对象(当前进行到的那个节点的流程图使用)

4.流程定义授权相关
repositoryService.getIdentityLinksForProcessDefinition() 流程定义授权列表
repositoryService.addCandidateStarterGroup()新增组流程授权
repositoryService.addCandidateStarterUser()新增用户流程授权
repositoryService.deleteCandidateStarterGroup() 删除组流程授权
repositoryService.deleteCandidateStarterUser()  删除用户流程授权

3.RuntimeService

处理正在运行的流程

runtimeService.createProcessInstanceBuilder().start() 发起流程
runtimeService.deleteProcessInstance() 删除正在运行的流程
runtimeService.suspendProcessInstanceById() 挂起流程定义
runtimeService.activateProcessInstanceById() 激活流程实例
runtimeService.getVariables(processInstanceId); 获取表单中填写的值
runtimeService.getActiveActivityIds(processInstanceId)获取以进行的流程图节点 (当前进行到的那个节点的流程图使用)
runtimeService.createChangeActivityStateBuilder().moveExecutionsToSingleActivityId(executionIds, endId).changeState(); 终止流程

4.HistoryService

在用户发起审批后,会生成流程实例。historyService为处理流程实例的api,但是其中包括了已完成的和未完成的流程实例; 如果是处理正在运行的流程实例,请使用runtimeService;

**暴露Flowable引擎收集的所有历史数据。**当执行流程时,引擎会保存许多数据(可配置),例如流程实例启动时间、谁在执行哪个任务、完成任务花费的事件、每个流程实例的执行路径,等等。这个服务主要提供查询这些数据的能力

historyService.createHistoricProcessInstanceQuery().list() 查询流程实例列表(历史流程,包括未完成的)
historyService.createHistoricProcessInstanceQuery().list().foreach().getValue() 可以获取历史中表单的信息
historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); 根绝id查询流程实例
historyService.deleteHistoricProcessInstance() 删除历史流程
historyService.deleteHistoricTaskInstance(taskid); 删除任务实例
historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).list()  流程实例节点列表 (当前进行到的那个节点的流程图使用)
    
flowable 有api查看act_hi_varinst里面的数据吗
HistoricVariableInstanceQuery query = historyService.createHistoricVariableInstanceQuery().processInstanceId(instance.getId());
HistoricVariableInstance operate = query.variableName("operate").singleResult();
if (operate.getValue().toString().equals("1")) {             
    // 表示流程同意          
}

5.TaskService

对流程实例的各个节点的审批处理

流转的节点审批

taskService.createTaskQuery().list() 待办任务列表
taskService.createTaskQuery().taskId(taskId).singleResult();  待办任务详情
taskService.saveTask(task); 修改任务
taskService.setAssignee() 设置审批人
taskService.addComment() 设置审批备注
taskService.complete() 完成当前审批
taskService.getProcessInstanceComments(processInstanceId); 查看任务详情(也就是都经过哪些人的审批,意见是什么)
taskService.delegateTask(taskId, delegater); 委派任务
taskService.claim(taskId, userId);认领任务
taskService.unclaim(taskId); 取消认领
taskService.complete(taskId, completeVariables); 完成任务

任务授权
taskService.addGroupIdentityLink()新增组任务授权
taskService.addUserIdentityLink() 新增人员任务授权
taskService.deleteGroupIdentityLink() 删除组任务授权
taskService.deleteUserIdentityLink() 删除人员任务授权

6.ManagementService

主要是执行自定义命令

managementService.executeCommand(new classA())  执行classA的内部方法 

在自定义的方法中可以使用以下方法获取repositoryService

ProcessEngineConfiguration processEngineConfiguration =
            CommandContextUtil.getProcessEngineConfiguration(commandContext);
RepositoryService repositoryService = processEngineConfiguration.getRepositoryService();

也可以获得流程定义方法集合

ProcessEngineConfigurationImpl processEngineConfiguration =
            CommandContextUtil.getProcessEngineConfiguration(commandContext);
        ProcessDefinitionEntityManager processDefinitionEntityManager =
            processEngineConfiguration.getProcessDefinitionEntityManager();
如 findById/findLatestProcessDefinitionByKey/findLatestProcessDefinitionByKeyAndTenantId 等。

7.IdentityService

用于身份信息获取和保存,这里主要是获取身份信息

用于管理(创建,更新,删除,查询……)组与用户。请注意,Flowable实际上在运行时并不做任何用户检查。

例如任务可以分派给任何用户,而引擎并不会验证系统中是否存在该用户。这是因为Flowable有时要与LDAP、Active Directory等服务结合使用

identityService.createUserQuery().userId(userId).singleResult();  获取审批用户的具体信息
identityService.createGroupQuery().groupId(groupId).singleResult(); 获取审批组的具体信息

8.DynamicBpmnService

可用于修改流程定义中的部分内容,而不需要重新部署它。例如可以修改流程定义中一个用户任务的办理人设置,或者修改一个服务任务中的类名。

九、数据库表说明(34张表)

表分类表名表说明
一般数据(2)ACT_GE_BYTEARRAY通用的流程定义和流程资源
ACT_GE_PROPERTY系统相关属性
流程历史记录(8)ACT_HI_ACTINST历史的流程实例
ACT_HI_ATTACHMENT历史的流程附件
ACT_HI_COMMENT历史的说明性信息
ACT_HI_DETAIL历史的流程运行中的细节信息
ACT_HI_IDENTITYLINK历史的流程运行过程中用户关系
ACT_HI_PROCINST历史的流程实例
ACT_HI_TASKINST历史的任务实例
ACT_HI_VARINST历史的流程运行中的变量信息
用户用户组表(9)ACT_ID_BYTEARRAY二进制数据表
ACT_ID_GROUP用户组信息表
ACT_ID_INFO用户信息详情表
ACT_ID_MEMBERSHIP人与组关系表
ACT_ID_PRIV权限表
ACT_ID_PRIV_MAPPING用户或组权限关系表
ACT_ID_PROPERTY属性表
ACT_ID_TOKEN系统登录日志表
ACT_ID_USER用户表
流程定义表(3)ACT_RE_MODEL模型信息
ACT_RE_DEPLOYMENT部署单元信息
ACT_RE_PROCDEF已部署的流程定义
运行实例表(10)ACT_RU_DEADLETTER_JOB正在运行的任务表
ACT_RU_EVENT_SUBSCR运行时事件
ACT_RU_EXECUTION运行时流程执行实例
ACT_RU_HISTORY_JOB历史作业表
ACT_RU_IDENTITYLINK运行时用户关系信息
ACT_RU_JOB运行时作业表
ACT_RU_SUSPENDED_JOB暂停作业表
ACT_RU_TASK运行时任务表
ACT_RU_TIMER_JOB定时作业表
ACT_RU_VARIABLE运行时变量表
其他表(2)ACT_EVT_LOG事件日志表
ACT_PROCDEF_INFO流程定义信息

参考:

[1].官方文档

D_TOKEN | 系统登录日志表 |
| | ACT_ID_USER | 用户表 |
| 流程定义表(3) | ACT_RE_MODEL | 模型信息 |
| | ACT_RE_DEPLOYMENT | 部署单元信息 |
| | ACT_RE_PROCDEF | 已部署的流程定义 |
| 运行实例表(10) | ACT_RU_DEADLETTER_JOB | 正在运行的任务表 |
| | ACT_RU_EVENT_SUBSCR | 运行时事件 |
| | ACT_RU_EXECUTION | 运行时流程执行实例 |
| | ACT_RU_HISTORY_JOB | 历史作业表 |
| | ACT_RU_IDENTITYLINK | 运行时用户关系信息 |
| | ACT_RU_JOB | 运行时作业表 |
| | ACT_RU_SUSPENDED_JOB | 暂停作业表 |
| | ACT_RU_TASK | 运行时任务表 |
| | ACT_RU_TIMER_JOB | 定时作业表 |
| | ACT_RU_VARIABLE | 运行时变量表 |
| 其他表(2) | ACT_EVT_LOG | 事件日志表 |
| | ACT_PROCDEF_INFO | 流程定义信息 |
| | | |

参考:

[1].官方文档

[2].遇见你真好的博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/988906.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图解系列 图解直播推拉流流程

文章目录 流程推流流程扩展 拉流流程 文件加密流程 常用开源流媒体服务器为SRS和MTX 流程 涉及到的组件 主播&#xff08;推流端&#xff09;观众&#xff08;播放器&#xff09;业务服务【持有一些私有Key&#xff0c;如rtmpKey等】流媒体服务器【SRS/MTX】CDN【持有公钥】 …

2023-大数据应用开发-电商可视化结果汇总

电商可视化 任务一&#xff1a;用柱状图展示消费额最高的省份 编写Vue工程代码&#xff0c;根据接口&#xff0c;用柱状图展示2020年消费额最高的5个省份&#xff0c;同时将用于图表展示的数据结构在浏览器的console中进行打印输出&#xff0c;将图表可视化结果和浏览器conso…

JavaWeb_LeadNews_Day11-KafkaStream实现实时计算文章分数

JavaWeb_LeadNews_Day11-KafkaStream实现实时计算文章分数 KafkaStream概述案例-统计单词个数SpringBoot集成 实时计算文章分值来源Gitee KafkaStream 概述 Kafka Stream: 提供了对存储与Kafka内的数据进行流式处理和分析的功能特点: Kafka Stream提供了一个非常简单而轻量的…

使用python爬取网站html代码

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言一、Requests是什么&#xff1f;二、使用步骤1.引入库2.创建一个Faker对象&#xff1a;3.设置要访问的目标网站的URL&#xff1a;4.定义HTTP请求头部&#xff1a;6.…

【python手写算法】正则化在线性回归和逻辑回归中的应用

多元线性回归&#xff1a; # codingutf-8 import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.mplot3d import Axes3Dif __name__ __main__:X1 [12.46, 0.25, 5.22, 11.3, 6.81, 4.59, 0.66, 14.53, 15.49, 14.43,2.19, 1.35, 10.02, 12.93, 5.93, 2.92,…

day 51 |● 503.下一个更大元素II ● 42. 接雨水

503.下一个更大元素II 显示的是循环链表&#xff0c;所以需要遍历两遍。 用i%len(nums)保证循环两遍即可。 func nextGreaterElements(nums []int) []int {res : make([]int, len(nums))for i : 0; i < len(nums); i{res[i] -1}stack : make([]int, 0)stack append(stac…

VoxWeekly|The Sandbox 生态周报|20230904

欢迎来到由 The Sandbox 发布的《VoxWeekly》。我们会在每周发布&#xff0c;对上一周 The Sandbox 生态系统所发生的事情进行总结。 如果你喜欢我们内容&#xff0c;欢迎与朋友和家人分享。请订阅我们的 Medium 、关注我们的 Twitter&#xff0c;并加入 Discord 社区&#xf…

便利连锁店这波操作,赚麻了!

在当今数字化时代&#xff0c;零售业经历了前所未有的革命性变革。消费者期望在购物时获得更多的便捷性、个性化体验和高度智能化的服务。为了满足这些需求&#xff0c;零售商们不得不重新思考他们的经营模式&#xff0c;并将目光投向了新零售模式的未来。 新零售模式不再局限于…

来看看什么是:编译型语言和解释型语言的区别

通过高级语言编写的源码&#xff0c;我们能够轻松理解&#xff0c;但对于计算机来说&#xff0c;它只认识二进制指令&#xff0c;源码就是天书&#xff0c;根本无法识别。源码要想执行&#xff0c;必须先转换成二进制指令。 所谓二进制指令&#xff0c;也就是由 0 和 1 组成的…

非插座式水壶/插座式水壶:SOR/2016-181(水壶法规)和CSA 22.1标准解读!

电水壶作为一种常见的小家电&#xff0c;受到了广大消费者的喜爱。然而&#xff0c;由于安全问题的日益重视&#xff0c;亚马逊加拿大站决定加强对电水壶产品的审核&#xff0c;以确保消费者的安全和权益。 近日&#xff0c;亚马逊平台发布公告&#xff0c;要求在加拿大站销售…

C/C++苹果和虫子 2019年9月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C苹果和虫子 一、题目要求 1、编程实现 2、输入输出 二、解题思路 1、案例分析 三、程序代码 四、程序说明 五、运行结果 六、考点分析 C/C苹果和虫子 2019年9月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 你买了一箱n个苹果&#xff0c;很不幸…

printf scanf

目录 printf scanf printf 把十的二进制代码放进去了&#xff0c;i对的是二进制代码&#xff0c;指定这一串0101代码以什么样的格式输出。 为什么要输出控制符&#xff0c;因为里面放的是二进制&#xff0c;必须控制输出的格式&#xff0c;指定这一串二进制以什么样的格式输出…

当妈妈们开始精致悦已,母婴品牌该怎么做营销?媒介盒子告诉你

近年来&#xff0c;二孩政策全面放开&#xff0c;母婴行业产品溢价高利润可观&#xff0c;优质品牌层出不穷&#xff0c;那么母婴行业该如何宣传产品呢&#xff1f; 试试软文推广吧&#xff01;软文推广是企业宣传最有效且投入成本最低的宣传方式了。 母婴行业做软文推广可以达…

[uni-app] 海报图片分享方案 -canvas绘制

文章目录 canvas使用记录先看下实际效果图绘制流程及思路1. 绘制头像, 通过drawImage来绘制2.绘制文字部分 具体代码 分享海报图片的方式,以前再RN端采用的是截图方案, 我记得组件好像是 react-native-view-shot 现在要处理uni-app的海报图片分享, 一般也有 html2canvas的相关插…

二维码怎么做调查问卷?二维码统计数据的技巧

小伙伴们一定发现&#xff0c;现在用来收集用户数据时&#xff0c;通过扫码填表的方式在很多场景中都被应用&#xff0c;这种方式有效的提升制作者获取用户数据的速度&#xff0c;而且对于填表者而言也更加的便捷。那么用二维码生成器&#xff08;免费在线二维码生成器-二维码在…

Docker网络功能

基本网络功能 Docker 允许通过外部访问容器或容器互联的方式来提供网络服务。使用docker network子命令来管理Docker网络。 外部访问容器可通过端口映射实现&#xff0c;启动容器时使用-p参数指定映射关系。-p可多次使用来绑定多个端口。使用docker port命令查看当前映射的端…

VR全景拍摄发展如何?在各行业应用中有优势吗?

现如今&#xff0c;虚拟现实技术正在以惊人的速度改变着我们的生活&#xff0c;而VR全景拍摄作为一种创新的拍摄方式&#xff0c;可以为大家带来全新的视觉体验。通过VR全景拍摄&#xff0c;可以将平面画面变得更加逼真、更具沉浸感&#xff0c;让人仿佛置身于真实场景之中。 近…

蚂蚁发布金融大模型:两大应用产品支小宝2.0、支小助将在完成备案后

9月8日&#xff0c;在上海举办的外滩大会上&#xff0c;蚂蚁集团正式发布金融大模型。据了解&#xff0c;蚂蚁金融大 模型基于蚂蚁自研基础大模型&#xff0c;针对金融产业深度定制&#xff0c;底层算力集群达到万卡规模。该大 模型聚焦真实的金融场景需求&#xff0c;在“认知…

恭贺弘博创新2023下半年软考(中/高级)认证课程顺利举行

为迎接2023年下半年软考考试&#xff0c;弘博创新于2023年9月2日举行了精品的软考中/高级认证课程&#xff0c;线下线上学员都积极参与学习。 在课程开始之前&#xff0c;弘博创新的老师为学员们提供了详细的学习资料和准备建议&#xff0c;以确保学员们在课程中能够跟上老师的…