业务场景实战(三)工作流引擎
2022-02-09 本文已影响0人
后来丶_a24d
思维导图
思维导图.png系列总目录
背景
-
简单解释:请假,离职多个节点审批,多个流程,不同场景(请假,离职)流程不一样,这时候工作流引擎能够很好的承接
请假流程.png -
应用:
- 内部OA系统
- 一些内容设计比如PPT,海报模板设计时提供下载功能,下载时需要审批,不同租户审批的流程可设置成不一样
技术选型
对比项 | Activiti | Flowable | JBMP | Camunda |
---|---|---|---|---|
语言 | Java | Java | Java | Java |
活跃度 | 很高github 8.3K | 高4.9K | 一般 | 比较高2.3K |
优势 | 易上手,功能全面,文档丰富,支持云原生 | 基于activiti6,拓展一些新特性,有商业版 | 近年来新的文档少一些,应用和二次开发可能会比较吃力 | 基于activiti5,有商业版 |
- 从成熟度,活跃度,github标星,易上手,以及对云原生的支持,结合公司自身业务发展选择activiti
入门
Spring-activiti项目
- 参考gitee推荐的项目 https://gitee.com/shenzhanwang/Spring-activiti?_from=gitee_search,Spring-activiti项目被gitee推荐,目前star 2.4k
- Spring-activiti项目可以作为练手项目,熟悉并入门Activiti
使用
- Spring-activiti使用的springboot版本比较低,而且pom文件也没有使用parent标签,所以很多依赖包版需要自己改
- 由于我本地是使用mysql8版本所以我把pom的mysql-connector-java的version改为8.0.15
- application.yml中spring相关的配置改成
spring:
datasource:
url: jdbc:mysql://localhost:3306/activiti5?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&nullCatalogMeansCurrent=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: ****
activiti:
check-process-definitions: false
#Activiti记录历史任务数据级别,full是最全的,方便日后查询使用
history-level: full
#创建数据库历史数据表
db-history-used: true
database-schema-update: true
process-definition-location-prefix: classpath:/process/
thymeleaf :
mode: LEGACYHTML5
-
执行activiti.sql
-
启动
-
http://localhost:8888/login 账号xiaomi 密码 1234
-
将resource下两个bpmn例子上传
上传1.png
上传2.png -
然后就可以开始愉快的熟悉了
BPMN设计说明,流程图在线工具
- github开源项目 https://github.com/juzhizhang/springboot2-activiti-design
- 修改application.yml的spring配置
spring:
datasource: # 数据源的相关配置 注意加上nullCatalogMeansCurrent=true配置才能自动生成表
type: com.zaxxer.hikari.HikariDataSource # 数据源类型:HikariCP
driver-class-name: com.mysql.jdbc.Driver # mysql驱动
url: jdbc:mysql://localhost:3306/activiti6?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&nullCatalogMeansCurrent=true
username: root
password: 3876556+0
hikari:
connection-timeout: 30000 # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
minimum-idle: 5 # 最小连接数
maximum-pool-size: 20 # 最大连接数
auto-commit: true # 自动提交
idle-timeout: 600000 # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
pool-name: DateSourceHikariCP # 连接池名字
max-lifetime: 1800000 # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms
thymeleaf:
cache: false
activiti:
check-process-definitions: false
#Activiti记录历史任务数据级别,full是最全的,方便日后查询使用
history-level: full
#创建数据库历史数据表
db-history-used: true
database-schema-update: true
- 启动
- 浏览器输入http://localhost:8002 这边使用google打不开,使用safari能正常打开,不知道为啥
- 可以愉快地编辑了,编辑完之后浏览器请求路径上有modelId参数
-
我们可以在这里面加段代码,输入xml文件,这边我们编辑的流程图就可以在别的地方使用了
xml.png
xml设计以及解释
-
拿Spring-activiti的采购流程图解释
采购流程图.png - 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:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/test">
<process id="purchase" name="purchaseprocess" isExecutable="true">
<startEvent id="startevent1" name="Start" activiti:initiator="${starter}" />
<userTask id="purchaseAuditi" name="采购经理审批" activiti:formKey="test12138" activiti:assignee="${purchaseManager}" activiti:candidateGroups="采购经理">
<extensionElements>
<activiti:formProperty id="startDate" label="开始日期" type="string">
<activiti:properties>
<activiti:property id="author" value="code" />
</activiti:properties>
</activiti:formProperty>
<activiti:formProperty id="endDate" label="结束日期" type="string">
<activiti:properties>
<activiti:property id="author" value="code2" />
</activiti:properties>
</activiti:formProperty>
</extensionElements>
</userTask>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="purchaseAuditi" />
<exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway" />
<sequenceFlow id="flow2" sourceRef="purchaseAuditi" targetRef="exclusivegateway1" />
<userTask id="updateapply" name="调整采购申请" activiti:assignee="${changePurchase}" />
<sequenceFlow id="flow4" name="不通过" sourceRef="exclusivegateway1" targetRef="updateapply">
<conditionExpression xsi:type="tFormalExpression">${purchaseauditi=='false'}</conditionExpression>
</sequenceFlow>
<exclusiveGateway id="exclusivegateway2" name="是否重新申请" />
<sequenceFlow id="flow5" sourceRef="updateapply" targetRef="exclusivegateway2" />
<endEvent id="endevent1" name="End" />
<sequenceFlow id="flow6" name="不重新申请" sourceRef="exclusivegateway2" targetRef="endevent1">
<conditionExpression xsi:type="tFormalExpression">${updateapply=='false'}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow7" name="重新申请" sourceRef="exclusivegateway2" targetRef="purchaseAuditi">
<conditionExpression xsi:type="tFormalExpression">${updateapply=='true'}</conditionExpression>
</sequenceFlow>
<subProcess id="pay" name="付费子流程">
<startEvent id="startevent2" name="Start" />
<userTask xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="financeaudit" name="财务审批" activiti:candidateGroups="财务管理员" camunda:assignee="${finance}" />
<sequenceFlow id="flow9" sourceRef="startevent2" targetRef="financeaudit" />
<exclusiveGateway id="exclusivegateway3" name="Exclusive Gateway" />
<sequenceFlow id="flow10" sourceRef="financeaudit" targetRef="exclusivegateway3" />
<exclusiveGateway id="exclusivegateway4" name="Exclusive Gateway" />
<sequenceFlow id="flow11" name="通过" sourceRef="exclusivegateway3" targetRef="exclusivegateway4">
<conditionExpression xsi:type="tFormalExpression">${finance=='true'}</conditionExpression>
</sequenceFlow>
<userTask xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="manageraudit" name="总经理审批" activiti:candidateGroups="总经理" camunda:assignee="${ceo}" />
<sequenceFlow id="flow12" name="金额大于1万" sourceRef="exclusivegateway4" targetRef="manageraudit">
<conditionExpression xsi:type="tFormalExpression">${money>10000}</conditionExpression>
</sequenceFlow>
<userTask xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="paymoney" name="出纳付款" activiti:candidateGroups="出纳员" camunda:assignee="${cashier}" />
<sequenceFlow id="flow13" name="金额小于1万" sourceRef="exclusivegateway4" targetRef="paymoney">
<conditionExpression xsi:type="tFormalExpression">${money<10000}</conditionExpression>
</sequenceFlow>
<endEvent id="endevent2" name="End" />
<sequenceFlow id="flow14" sourceRef="paymoney" targetRef="endevent2" />
<exclusiveGateway id="exclusivegateway5" name="Exclusive Gateway" />
<sequenceFlow id="flow15" sourceRef="manageraudit" targetRef="exclusivegateway5" />
<endEvent id="errorendevent1" name="总经理不同意">
<errorEventDefinition />
</endEvent>
<sequenceFlow id="flow17" name="通过" sourceRef="exclusivegateway5" targetRef="paymoney">
<conditionExpression xsi:type="tFormalExpression">${manager=='true'}</conditionExpression>
</sequenceFlow>
<endEvent id="errorendevent2" name="财务不同意">
<errorEventDefinition />
</endEvent>
<sequenceFlow id="flow18" sourceRef="exclusivegateway3" targetRef="errorendevent2">
<conditionExpression xsi:type="tFormalExpression">${finance=='false'}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow23" sourceRef="exclusivegateway5" targetRef="errorendevent1">
<conditionExpression xsi:type="tFormalExpression">${manager=='false'}</conditionExpression>
</sequenceFlow>
</subProcess>
<boundaryEvent id="boundaryerror1" name="Error" attachedToRef="pay">
<errorEventDefinition />
</boundaryEvent>
<sequenceFlow id="flow19" name="捕获子流程异常" sourceRef="boundaryerror1" targetRef="updateapply" />
<sequenceFlow id="flow20" name="进入付费子流程" sourceRef="exclusivegateway1" targetRef="pay">
<conditionExpression xsi:type="tFormalExpression">${purchaseauditi=='true'}</conditionExpression>
</sequenceFlow>
<userTask xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="receiveitem" name="收货确认" activiti:assignee="${starter}" camunda:assignee="${receive}" />
<sequenceFlow id="flow21" sourceRef="pay" targetRef="receiveitem" />
<endEvent id="endevent3" name="End" />
<sequenceFlow id="flow22" sourceRef="receiveitem" targetRef="endevent3" />
</process>
<!-- 这部分只是帮助前端展示 -->
<bpmndi:BPMNDiagram id="BPMNDiagram_purchase">
</bpmndi:BPMNDiagram>
</definitions>
- process标签定义了整体流程的id name
<process id="purchase" name="purchaseprocess" isExecutable="true">
- startEvent标签定义了开始事件
<startEvent id="startevent1" name="Start" activiti:initiator="${starter}" />
- userTask用户事件, 表示用户事件节点,比如图示的采购经理审批,用户事件节点可以有表单信息承接,表单信息处理一般都是放在应用侧,流程引擎底层服务只是存储应用侧定义的表单信息
- sequenceFlow标签就是图示的箭头,节点流程箭头
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="purchaseAuditi" />
-
exclusiveGateway排他网关, 根据不同条件进入不同流程
排他网关.png - endEvent结束事件
-
subProcess标签标示子流程
image.png
- 还有一些常用标签
-
boundaryEvent边界事件可以定义timerEventDefinition超时时间
超时提醒.png - userTask中multiInstanceLoopCharacteristics标签可以设置会签规则,会签表示多人审批规则定义
原理
- 将业务流程的每个节点读取到数据库中,这样每个节点(包括开始节点和结束节点)就是数据库中的一条记录,当发生业务流程的时候,不断的从业务流程图中读取下一个节点,其实就相当于操作节点对应的数据库记录,这样就实现流程管理和状态字段无关
数据库
- activiti7流程引擎主要是跟25张表打交道, Activiti 数据库表结构中介绍了activiti6中各个表结构中字段,对于activiti7有参考意义
通用数据表
- act_ge ge表示 general 全局通用数据及设置,各种情况都使用的数据
- act_ge_bytearray: 二进制数据表,存储通用的流程定义和流程资源
- act_ge_property: 属性数据表。存储整个流程引擎级别的数据
流程存储表
- act_re re表示 repository 存储,包含的是静态信息,如,流程定义,流程的资源
- act_re_deployment : 部署流程定义时需要被持久化保存下来的信息
- act_re_model: 流程设计器设计流程后,保存数据到该表
- act_re_procdef: 业务流程定义数据表。此表和 ACT_RE_DEPLOYMENT 是多对一的关系,即,一个部署的bar包里可能包含多个流程定义文件,每个流程定义文件都会有一条记录在 ACT_REPROCDEF 表内,每个流程定义的数据,都会对于 ACT_GE_BYTEARRAY 表内的一个资源文件和 PNG 图片文件
运行数据表
- act_ru:'ru’表示 runtime,此前缀的表是记录运行时的数据,包含流程实例,任务,变量,异步任务等运行中的数据。Activiti只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录
- act_ru_deadletter_job 作业死亡信息表,作业失败超过重试次数
- act_ru_event_subscr 运行时事件表
- act_ru_execution 运行时流程执行实例表
- act_ru_identitylink 运行时用户信息表
- act_ru_integration 运行时积分表
- act_ru_job 运行时作业信息表
- act_ru_suspended_job 运行时作业暂停表
- act_ru_task 运行时任务信息表
- act_ru_timer_job 运行时定时器作业表
- act_ru_variable 运行时变量信息表
历史数据表
- act_hi:'hi’表示 history,此前缀的表包含历史数据,如历史(结束)流程实例,变量,任务等等
- act_hi_actinst 历史节点表
- act_hi_attachment 历史附件表
- act_hi_comment 历史意见表
- act_hi_detail 历史详情表,提供历史变量的查询
- act_hi_identitylink 历史流程用户信息表
- act_hi_procinst 历史流程实例表
- act_hi_taskinst 历史任务实例表
- act_hi_varinst 历史变量表
其他表
- act_evt_log 流程引擎的通用事件日志记录表
- act_procdef_info 流程定义的动态变更信息
拓展UserTask
- 使用activiti流程引擎时,有时候不能满足我们的需求,比如没有处理人时自动跳过任务,任务处理人是任务发起人时自动跳过任务等这种自定义需求时,需要拓展BPMN
- 拓展方式
- 在bpmn流程图的usertask属性中增加自定义标签,比如无处理人自动跳过skipNoProcess
<userTask id="UserTask_06y6qko" name="A" activiti:skipNoProcess="true"
- ProcessEngineConfigurationConfigurer中增加 BpmnXMLConverter.addConverter(继承UserTaskXMLConverter,拓展bpmn文件需要拓展转换能力)
- 增加后置处理器能力继承AbstractActivityBpmnParseHandler,ProcessEngineConfigurationConfigurer增加handler
功能
部署流程
- 例子
String fileName = "***".bpmn";
File file = new File("***".bpmn");
InputStream inputStream = new FileInputStream(file);
MultipartFile multipartFile = new MockMultipartFile(file.getName(), inputStream);
//实现流程部署
Deployment deployment = processEngine.getRepositoryService().createDeployment().addBytes(fileName, multipartFile.getBytes()).name("测试子流程加assignee测试" + RandomUtil.createRandomCharData(10)).key("test_" + RandomUtil.createRandomCharData(10)).deploy();
启动流程
// 开始流程 -------------------
Map<String, Object> variables = new HashMap<>();
variables.put("purchaseManager", "seeger");
ProcessInstance processInstance = processEngine.getRuntimeService().startProcessInstanceByKey(deployment.getKey(), variables);
完成/转办/回退/审批/取消/改派/任务
完成
processEngine.getTaskService().complete(task.getId())
转办
- 有些时候需要将节点的assign换成其他人, 重新set aasign即可
task.setAssignee(assigneeId)
表单
- 参考开始流程的variables设置
会签
- 涉及到多个审批人时会用到
超时任务
- 可监听ExecutionListener, 在bpmn设计流程时增加TimeoutListener
代码:
public class TimeoutListener implements ExecutionListener {
@Override
public void notify(final DelegateExecution execution) {
}
}
bpmn流程
<task id="****" name="超时提醒">
<extensionElements>
<activiti:executionListener class="***.TimeoutListener" event="start">
<activiti:field name="timeout">
<activiti:string>10</activiti:string>
</activiti:field>
</activiti:executionListener>
</extensionElements>
<incoming>****</incoming>
</task>
业务场景分层
- 工作流引擎底层只封装activiti能力,具体拓展还需要在工作流引擎架一层应用层
各个数据查询
- 可通过ManagementService查询各个表数据
private ManagementService managementService;
@Test
public void testInsertQueryTimeOutingTask() throws InterruptedException {
List<Job> jobs = managementService.executeCommand(new QueryTimeJobQueryCmd());
Thread.sleep(60000);
System.out.println("jobs");
}
public static class QueryTimeJobQueryCmd implements Command<List<Job>> {
@SneakyThrows
@Override
public List<Job> execute(CommandContext commandContext) {
TimerJobQueryImpl timerJobQuery = new TimerJobQueryImpl();
DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
Date date = dateFormat.parse("20220125");
Date date1 = dateFormat.parse("20220126");
return commandContext.getTimerJobEntityManager().findJobsByQueryCriteria(timerJobQuery, new org.activiti.engine.impl.Page(0, 10));
}
}
历史数据查询
- 查询act_hi_*表数据