什么是工作流

工作流(Workflow),就是通过计算机对业务流程自动化执行管理。它主要解决的事:使多个参与者之间按照某种预定的规则自动进行传递文档、信息或任务的过程,从而实现某个预期的业务目标,或者促使此目标的实现。通俗来讲,就是业务上一个完整的审批流程。例如员工的请假,出差,外出采购,合同审核等等,这些过程,都是一个工作流。

工作流引擎

对于工作流的处理,如果采用原始的方式,我们需要拿着各种文件到各个负责人那里去签字,需要在多个部门之间不断审批,这种方式费时费力。而我们可以借助软件系统来协助我们处理这些审批流程,这样就出现了工作流系统,使用工作流系统后可以极大的提高工作效率。

在学习工作流的过程中,我们肯定看到过这个模型:填写请假单->部门经理审批->总经理审批->人事备案

(1)要实现上述的流程,我们自己可以通过字段标识来实现这个审批效果,在业务表中加个字段,比如填写请假单用1标识,部门经理用2标识,总经理用3标识,人事备案用4标识,好像看起来没啥问题,也实现了审批效果。可是一旦我们的流程出现了变化,这个时候我们就需要改动我们的代码了,这显然是不可取的,那么有没有专业的方式来实现工作流的管理呢? 并且可以做到业务流程变化之后,我们的程序可以不用改变,如果可以实现这样的效果,那么我们的业务系统的适应能力就得到了极大提升。在这样的背景下,就出现了工作流引擎

为什么使用工作流引擎,能实现业务流程改变,不用修改代码,流程还能自动推进?

(1)我们先来说说为什么流程改变,不用修改代码:我们的工作流引擎都实现了一个规范,这个规范要求我们的流程管理与状态字段无关,始终都是读取业务流程图的下一个节点。当业务更新的时候我们只需要更新业务流程图就行了。这就实现了业务流程改变,不用修改代码。

(2)再来说说流程自动推进,这个原理就更简单了,就拿上面的请假模型来说,工作流引擎会用一张表来记录当前处在的节点。当填写完请假单后肯定是要轮到部门经理来审批了,所以我们一旦完成了请假单填写那么这条记录将会被从这张表删除掉,并且会把下一个节点部门经理的信息插入到这张表中,当我们用部门经理的信息去这张表中查询的时候就能查出部门经理相关的审批的信息了,以此类推,这样层层递进,就实现了流程的自动递交了。

常见的工作流引擎

主流的框架有:Activiti、jBPM、Camunda、Flowable、盘古BPM、云程

Activiti 7概述

Activiti介绍

activiti是一个工作流引擎,可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN进行定义,业务流程按照预先定义的流程进行执行。实现了系统的流程由activti进行管理,减少业务系统由于流程变更进行系统升级改造的工作流量,从而提高系统的健壮性,同时也减少了系统开发维护成本。

官方网站: https://www.activiti.org

建模语言BPMN

BPM (Business Process Management) 即业务流程管理,是一种规范化的构造端到端的业务流程,以持续提高
组织业务效率。

BPM 软件就是根据企业中业务环境的变化,推进人与人之间、人与系统之间以及系统与系统之间的整理及调整的经营方法与解决方案的 T 工具。使用 BPM 软件对企业内部及外部的业务流程的整个生命周期进行建模、自动化、管理监控和优化,可以降低企业成本,提高利润。

BPMN (Business Process Model AndNotation) 即业务流程模型和符号,是一套标准的业务流程建模符号,使用BPMN 提供的符号可以创建业务流程。Activit 就是使用 BPMN 进行流程建模、流程执行管理的。

BPMN2.0 是业务流程建模符号 2.0 的缩写,它由 Business Process Management nitiative 这个非营利协会创建并不断发展。BPMN2.0 是使用一些符号来明确业务流程设计流程图的一套符号规范,能增进业务建模时的沟通效率。目前 BPMN2.0 是最新的版本,它用于在 BPM 上下文中进行布局和可视化的沟通。

BPMN2.0 的基本符号主要包含:

  • 事件 Event
    开始:表示一个流程的开始
    中间:发生的开始和结束事件之间,影响处理的流程
    结束:表示该过程结束
  • 活动 Activities
    活动是工作或任务的一个通用术语。一个活动可以是一个任务,还可以是一个当前流程的子处理流程;其次,你还可以为活动指定不同的类型。常见的活动如下
    • 用户任务(User Task)
    • 服务任务(Service Task)
    • 子流程(Sub Process)
  • 网管 GateWay
    用于表示流程的分支与合并,有几种常用的网关需要了解
    • 排他网关(Exclusive Gateway)
    • 并行网关(Parallel Gateway)
    • 包容网关(Inclusive Gateway)
    • 事件网关(Event gateway)

Activiti 使用流程

第一步: 引入依赖并初始化数据库

既然activiti是一个框架,那么我们肯定是需要引入对应的iar包坐标的,具体参考代码中的。

第二步: 通过工具绘画流程图

使用activiti 流程建模工具(activity-designer)定义业务流程(.bpmn 文件)。bpmn 文件就是业务流程定义文件通过 xml 定义业务流程。

第三步: 流程定义部署

向activiti 部署业务流程定义 (.bpmn 文件),使用 activiti 提供的 api向activiti 中部署.bpmn 文件通俗来讲,就是让activiti认识要使用的流程

第四步: 启动一个流程实例 (ProcessInstance)

启动一个流程实例表示开始一次业务流程的运行,比如员工请假流程部署完成,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响,就好比定义一个 java 类,实例化两个对象一样,部署的流程就好比 iava 类,启动一个流程实例就好比 new 一个iava 对象

第五步: 用户查询待办任务(Task)

因为现在系统的业务流程已经交给 activiti 管理,通过 activiti 就可以查询当前流程执行到哪了,当前用户需要办理什么任务了,这些 activiti都我们管理了。实际上我们学习activiti也只是学习它的API怎么使用,因为很多功能activiti都已经封装好了,我们会调用就行了。

第六步:用户办理任务

用户查询待办任务后,就可以办理某个任务,如果这个任务办理完成还需要其他用户办理,比如请假单创建后还需要部分经历审批,这个过程也是由 activiti 帮我们完成了,不需要我们在代码中指定。

第七步:流程结束

当任务办理完成没有下一个任务节点了,这个流程实例就完成了。

Activiti Modeler

Activiti官网:Get started | Activiti,下载Activiti 5.x

Activiti setup

使用Activiti 在线设计,需要安装Java 运行环境和Apache Tomcat。确保你已经正确设置了 JAVA_HOME 系统变量,设置方式取决于操作系统。

要让 Activiti Explore 和 REST Web 应用程序运行,只需将从 Activiti 下载页面下载的 WAR 复制到 Tomcat 安装目录中的文件夹中即可。默认情况下,Explore 使用内存中数据库以及示例进程、用户和组运行。

测试账号:

UserId Password Security roles
kermit kermit admin
gonzo gonzo manager
fozzie fozzie user

现在您可以访问以下Web应用程序

Webapp Name URL Description
Activiti Explorer http://localhost:8080/activiti-explorer 进程引擎用户控制台。使用此工具启动新流程、分配任务、查看和声明任务等。该工具还允许管理Activiti引擎。

设计请假流程

创建流程

来到流程设计工作区,点击右上角的新建模型

  • 名称:leave
    描述:请假

设计如图的流程定义,注意图中红色部分的ID

设置审批人

点击王经理审批,找到下面的Assignment,在弹出窗户中的Assignee下填入wjlsys_user表的用户昵称)

以同样的方式设置李人事经理Assigneelrsjl

保存并退出设计器

导出模型

流程设计工作区的右上角,处理模型中选择导出模型

leave.bpmn20.xml

Activiti 7

我们将activiti 7引入项目

Activiti环境

配置依赖

pom.xml
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M6</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>

说明:Activiti 7与Spring Boot整合后,默认集成了Spring Security安全框架,当前我们项目已经集成过了Spring Security,后续案例设置审批人时都必须是系统用户,Activiti会检查用户是否存在。

添加配置

spring:
activiti:
database-schema-update: true
db-history-used: true
history-level: full
check-process-definitions: true
async-executor-activate: false

启动项目

项目启动后,会自动创建数据库表

Activiti的运行支持必须要有这25张表的支持,主要是在业务流程运行过程中,记录参与流程的用户主体,用户组信息,以及流程的定义,流程执行时的信息,和流程的历史信息等等。

部署流程定义

创建resources/processes,将我们设计的请假模型拷贝到此处,将leave.bpmn20.xml压缩成zip文件,名称为leave.zip,其中leave是流程定义的Key

# Mac使用下面命令压缩,防止加入隐藏文件
zip leave.zip leave.bpmn20.xml

可使用repositoryService进行流程部署

  • 在上传流程文件zip包时,zip包名为流程定义Key
ProcessServiceImpl.java
@Override
public void publish(Long id) {
// 修改状态
ProcessTemplate processTemplate = baseMapper.selectById(id);
// 获取定义文件的路径
String path = processTemplate.getProcessDefinitionPath();
if (StringUtils.isEmpty(path)) {
throw new BizException("文件路径为空");
}
deployByZip(path);
processTemplate.setStatus(1);
baseMapper.updateById(processTemplate);
}

private void deployByZip(String path) {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(path);
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
// 流程部署
Deployment deployment = repositoryService.createDeployment()
.addZipInputStream(zipInputStream)
.deploy();
}

其中processTemplate为流程模板实体类,存放了流程定义文件的路径

对应的Controller接口为:ProcessTemplateController.publish

对应的发起请求的前端页面为:template.vue | 219行

启动流程实例

启动流程实例就是新建一个审批流程,比如某个用户发起一个请假审批,这个时候就需要启动一个流程实例。

除了使用runtimeService启动流程实例外,还需要进行流程信息的保存等操作。

  • 获取流程定义的Key,其被保存在processTemplate中。其实就是设计时指定的,也就是zip包的名称。
  • 流程实例可以绑定一个业务ID,这里将我们的Process.id设定为业务ID
  • 流程参数被设置为了用户填写的表单值。
  • 流程实例启动成功,获取下一个审批人信息。可能有多个,循环做消息推送。
  • 发起审批也是流程的一环节,需要记录到ProcessRecord表中。
ProcessServiceImpl.java
@Override
public void startUp(ProcessFormVo processFormVo) {
// 获取用户信息
SysUser sysUser = sysUserService.getById(LoginUserInfoHelper.getUserId());
// 根据审批模版id,查询模板信息
LambdaQueryWrapper<ProcessTemplate> wrapper = new LambdaQueryWrapper<>();
wrapper.select(ProcessTemplate::getId, ProcessTemplate::getName, ProcessTemplate::getProcessDefinitionKey)
.eq(ProcessTemplate::getId, processFormVo.getProcessTemplateId());
ProcessTemplate processTemplate = processTemplateService.getOne(wrapper);
// 保存流程信息
Process process = new Process();
BeanUtils.copyProperties(processFormVo, process);
process.setStatus(1);
String workNo = String.valueOf(System.currentTimeMillis());
process.setProcessCode(workNo);
process.setUserId(LoginUserInfoHelper.getUserId());
process.setFormValues(processFormVo.getFormValues());
process.setTitle(sysUser.getName() + "发起" + processTemplate.getName() + "申请");
baseMapper.insert(process);
// 启动流程实例
// 流程定义key
String key = processTemplate.getProcessDefinitionKey();
// 业务流程id
String businessId = String.valueOf(process.getId());
// 流程参数
HashMap<String, Object> variables = new HashMap<>();
JSONObject jsonObject = JSON.parseObject(processFormVo.getFormValues());
JSONObject formData = jsonObject.getJSONObject("formData");
HashMap<String, Object> map = new HashMap<>(formData);
variables.put("data", map);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, businessId, variables);
// 查询下一个审批人
List<Task> taskList = this.getCurrentTaskList(processInstance.getId());
String names = taskList.stream().map(task -> {
String assignee = task.getAssignee();
SysUser user = sysUserService.getUserByUsername(assignee);
// 推送消息
messageService.pushPendingMessage(process, user, task.getId());
return user.getName();
}).collect(Collectors.joining(","));
// 业务流程关联
process.setProcessInstanceId(processInstance.getId());
process.setDescription("等待" + names + "审批");
baseMapper.updateById(process);
processRecordService.record(process.getId(), 1, "发起申请");
}

其中 LoginUserInfoHelper 是 ThreadLocal 变量,负责记录登陆用户的信息。

其中 processFormVo 是前端传过来的参数,保存了审批模板ID,表单值等数据。

其中 process 为流程实体类,存放了审批发起人,请假的表单数据,审批状态等过程数据。

其中 processRecord 为流程记录表,负责记录每一次的审批流程。例如请假有三个流程,则审批通过后会有三条记录。

其中 messageService 负责推送消息给下一步的处理人。

对应的Controller接口为:ProcessApiController.startUp

对应的发起请求的前端页面为:apply.vue | 40行

查看审批任务

流程参与者均可查看审批任务详情,包括填写的表单数据、审批状态、审批进度等信息。

通过processInstanceId,即流程实例ID可以获取到当前进行的任务,从中可以获取到当前所有任务的审批人,通过循环与当前登陆用户比对,可以确定登陆用户是否是任务审批人。

ProcessServiceImpl.java
@Override
public Map<String, Object> show(Long id) {
// 获取审批信息
Process process = baseMapper.selectById(id);
if (process == null) return null;
// 获取审批记录信息
LambdaQueryWrapper<ProcessRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ProcessRecord::getProcessId, id);
List<ProcessRecord> processRecords = processRecordService.list(wrapper);
// 获取审批模版
ProcessTemplate processTemplate = processTemplateService.getById(process.getProcessTemplateId());
// 是否可以审批
List<Task> taskList = this.getCurrentTaskList(process.getProcessInstanceId());
boolean isApprove = false;
for (Task task : taskList) {
String username = LoginUserInfoHelper.getUsername();
if (task.getAssignee().equals(username)) {
isApprove = true;
break;
}
}
// 封装map数据
HashMap<String, Object> map = new HashMap<>();
map.put("process", process);
map.put("processRecordList", processRecords);
map.put("processTemplate", processTemplate);
map.put("isApprove", isApprove);
return map;
}

其中processTemplate为流程模板实体类,存放了表单模版信息,便于前端展示数据用。

其中 process 为流程实体类,存放了审批发起人,请假的表单数据,审批状态等数据。

其中 processRecord 为流程记录表,负责记录每一次的审批流程。例如请假有三个流程,则审批通过后会有三条记录。用于展示审批进度。

对应的Controller接口为:ProcessApiController.show

对应的发起请求的前端页面为:show.vue | 84行

处理审批任务

负责审批的人可以选择通过和拒绝当前审批任务

  • 首先,判断审批人是通过还是拒绝了当前审批任务,如果通过则直接complete,否则执行驳回操作;
  • 同时,将审批消息推送给发起人;
  • 更新 Process 信息,同时记录本次流程到 oa_process_record表;
  • 查询下一步的审批人,推送审批消息。
ProcessServiceImpl.java
@Override
public void approve(ApprovalVo approvalVo) {
// 获取流程变量
String taskId = approvalVo.getTaskId();
Process process = baseMapper.selectById(approvalVo.getProcessId());
Map<String, Object> variables = taskService.getVariables(taskId);
if (approvalVo.getStatus() == 1) {
// 审批通过
taskService.complete(taskId);
} else {
// 驳回
this.endTask(taskId);
}
// 推送给发起人
messageService.pushProcessedMessage(process, approvalVo.getStatus());
// 记录审批过程信息
String description = approvalVo.getStatus() == 1 ? "已通过" : "已拒绝";
processRecordService.record(approvalVo.getProcessId(), approvalVo.getStatus(), description);
// 查询下一个审批人
List<Task> taskList = this.getCurrentTaskList(process.getProcessInstanceId());
if (!CollectionUtils.isEmpty(taskList)) {
String names = taskList.stream().map(task -> {
String assignee = task.getAssignee();
SysUser user = sysUserService.getUserByUsername(assignee);
// 推送消息
messageService.pushPendingMessage(process, user, task.getId());
return user.getName();
}).collect(Collectors.joining(","));
process.setDescription("等待" + names + "审批");
process.setStatus(1);
} else {
if (approvalVo.getStatus() == 1) {
process.setDescription("审批完成(通过)");
process.setStatus(2);
} else {
process.setDescription("审批完成(通过)");
process.setStatus(-1);
}
}
baseMapper.updateById(process);
}

// 结束流程
private void endTask(String taskId) {
// 获取任务对象
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
// 获取流程定义模型
BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
// 获取结束流程节点
List<EndEvent> endEventList = bpmnModel.getMainProcess().findFlowElementsOfType(EndEvent.class);
if (CollectionUtils.isEmpty(endEventList)) {
return;
}
FlowNode endFlowNode = (FlowNode) endEventList.get(0);
// 当前流向节点
FlowNode currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(task.getTaskDefinitionKey());

// 临时保留当前活动的原始流向
// List<SequenceFlow> originalSequenceFlowList = new ArrayList<>(currentFlowNode.getOutgoingFlows());
// 清理当前流向节点
currentFlowNode.getOutgoingFlows().clear();

// 创建新流向
SequenceFlow newSequenceFlow = new SequenceFlow();
newSequenceFlow.setId("newSequenceFlow");
newSequenceFlow.setSourceFlowElement(currentFlowNode);
newSequenceFlow.setTargetFlowElement(endFlowNode);

// 当前节点指向新方向
ArrayList<SequenceFlow> newSequenceFlows = new ArrayList<>();
newSequenceFlows.add(newSequenceFlow);
currentFlowNode.setOutgoingFlows(newSequenceFlows);

taskService.complete(taskId);
}

其中 approvalVo 记录 流程ID,任务ID,审批状态。

对应的Controller接口为:ProcessApiController.approve

对应的发起请求的前端页面为:show.vue | 106行

查询已发起流程

流程实例启动时,会保存到oa_process中,其中的用户就是发起人,直接查询oa_process表即可。

@Override
public IPage<Process> pageStarted(Page<Process> pageParam) {
LambdaQueryWrapper<Process> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Process::getUserId, LoginUserInfoHelper.getUserId());
return baseMapper.selectPage(pageParam, wrapper);
}

其中 pageParam 是分页信息。

对应的Controller接口为:ProcessApiController.pageStarted

对应的发起请求的前端页面为:list.vue | 172行

查询已处理流程

根据当前登陆用户的uername(审批人)查询其任务,在流程设计时指定了审批人为sys_user.username

通过historyService指定审批人,可查询当前审批人已经处理的任务;通过 Task 的流程实例ID关联查询流程表(oa_process),并转换为 ProcessVo 便于前端展示。

ProcessServiceImpl.java
@Override
public IPage<ProcessVo> pageProcessed(Page<Process> pageParam) {
// 封装查询信息
HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery()
.taskAssignee(LoginUserInfoHelper.getUsername())
.finished()
.orderByTaskCreateTime().desc();
// 分页查询已完成的任务
int begin = (int) ((pageParam.getCurrent() - 1) * pageParam.getSize());
int size = (int) pageParam.getSize();
List<HistoricTaskInstance> list = query.listPage(begin, size);
List<ProcessVo> processVoList = new ArrayList<>();
for (HistoricTaskInstance item : list) {
// 根据流程实例ID获取流程信息
LambdaQueryWrapper<Process> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Process::getProcessInstanceId, item.getProcessInstanceId());
Process process = baseMapper.selectOne(wrapper);
// 转换
ProcessVo processVo = new ProcessVo();
BeanUtils.copyProperties(process, processVo);
processVo.setTaskId(item.getId());
processVoList.add(processVo);
}
IPage<ProcessVo> page = new Page<>(pageParam.getCurrent(), pageParam.getSize(), query.count());
page.setRecords(processVoList);
return page;
}

其中 pageParam 是分页信息。

对应的Controller接口为:ProcessApiController.pageProcessed

对应的发起请求的前端页面为:list.vue | 167行

查询待处理流程

根据username分页查询当前用户的待处理任务,通过任务的流程实例ID关联查询业务Key,即oa_process.id,随后查询出 Process 信息。

ProcessServiceImpl.java
@Override
public IPage<ProcessVo> pagePending(Page<Process> pageParam) {
TaskQuery query = taskService.createTaskQuery()
.taskAssignee(LoginUserInfoHelper.getUsername())
.orderByTaskCreateTime()
.desc();
int begin = (int) ((pageParam.getCurrent() - 1) * pageParam.getSize());
int size = (int) pageParam.getSize();
List<Task> taskList = query.listPage(begin, size);
List<ProcessVo> processVoList = new ArrayList<>();
for (Task task : taskList) {
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.singleResult();
String businessKey = processInstance.getBusinessKey();
if (businessKey == null) {
continue;
}
Process process = baseMapper.selectById(Long.parseLong(businessKey));
ProcessVo processVo = new ProcessVo();
BeanUtils.copyProperties(process, processVo);
processVo.setTaskId(task.getId());
processVoList.add(processVo);
}
IPage<ProcessVo> page = new Page<>(pageParam.getCurrent(), pageParam.getSize(), query.count());
page.setRecords(processVoList);
return page;
}

其中 pageParam 是分页信息。

对应的Controller接口为:ProcessApiController.pagePending

对应的发起请求的前端页面为:list.vue | 162行