官方手册:https://tkjohn.github.io/flowable-userguide/#_introduction
逆向学习来源:
https://gitee.com/KonBAI-Q/ruoyi-flowable-plus
基于 RuoYi-Vue-Plus 进行二次开发扩展Flowable工作流功能,支持在线表单设计和丰富的工作流程设计能力。
1.和actitivi的区别
flowable和actitivi的区别如下图,从actitivi6开始,团队内部发生分歧,Rademakers离队带走了部分人手开发了Flowable,剩下的Salaboy开发出Activiti7之后也离开了。目前Flowable有开源版本和商业版本两部分。
2.ProcessEngine
ProcessEngine是Flowable和Activiti中最最核心的类,相当于JDBC中的Connection一样,必须要优先配置,由于当前逆向学习的版本是基于Springboot的,所以创建过程和原生版本不同,首先,引入依赖:
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
</dependency>
flowable中的service
- ProcessEngineConfiguration类通过读取xml文件配置流程引擎。
- 从ProcessEngine类中可以得到各种服务,值得注意的是,这些服务可以被认为是单例模式。
- RepositoryService是使用Flowable引擎时需要的第一个服务,它提供用于管理和操作部署和流程定义(BPMN )的操作,主要负责一些静态的配置。
- RuntimeService主要负责一些动态的任务,如启动一个新的实例,通常一个流程可以对应多个实例。它也用于检索和储存一些流程变量,比如在单向网关组件中需要传递之前的选择。
- TaskService主要和分配给人的任务相关,比如将任务分配给用户。
- IdentityService主要负责用户的管理。
- FormService是一个可选的服务,主要负责开始表单和结束表单。
- HistoryService保存历史信息。
- DynamicBpmnService可以动态的添加新的流程。
原生依赖是(和springboot整合不要引入这个)
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-engine</artifactId>
<version>6.3.0</version>
</dependency>
接下来创建ProcessEngine流程引擎实例。这是一个线程安全的对象,因此通常只需要在一个应用中初始化一次。 _ProcessEngine_由ProcessEngineConfiguration实例创建。本例子中通过
com/ruoyi/flowable/flow/FlowableConfig.java
配置完成。
其中的SpringProcessEngineConfiguration类继承了ProcessEngineConfigurationImpl,后者由继承了ProcessEngineConfiguration类,这个类有多个子类,原生应用中可以选择需要的子类进行实现,这里由于整合springboot,所以采用默认的ProcessEngineConfigurationImpl。
ProcessEngineConfigurationImpl中配置的持久化框架是mybatis,通过setDataSource方法注入配置文件中的数据库配置。
3.表结构
1、Flowable的所有数据库表都以ACT_开头。第二部分是说明表用途的两字符标示符。服务API的命名也大略符合这个规则。
2、ACT_RE_: 'RE’代表repository。带有这个前缀的表包含“静态”信息,例如流程定义与流程资源(图片、规则等)。
3、ACT_RU_: 'RU’代表runtime。这些表存储运行时信息,例如流程实例(process instance)、用户任务(user task)、变量(variable)、作业(job)等。Flowable只在流程实例运行中保存运行时数据,并在流程实例结束时删除记录。这样保证运行时表小和快。
4、ACT_HI_: 'HI’代表history。这些表存储历史数据,例如已完成的流程实例、变量、任务等。
5、ACT_GE_: “GE”代表“General”(通用),通用数据。在多处使用。
1)通用数据表
(2个)
- act_ge_bytearray:二进制数据表,如流程定义、流程模板、流程图的字节流文件;
- act_ge_property:属性数据表(不常用);
2)历史表
8个,HistoryService接口操作的表
- act_hi_actinst:历史节点表,存放流程实例运转的各个节点信息(包含开始、结束等非任务节点);
- act_hi_attachment:历史附件表,存放历史节点上传的附件信息(不常用);
- act_hi_comment:历史意见表;
- act_hi_detail:历史详情表,存储节点运转的一些信息(不常用);
- act_hi_identitylink:历史流程人员表,存储流程各节点候选、办理人员信息,常用于查询某人或部门的已办任务;
- act_hi_procinst:历史流程实例表,存储流程实例历史数据(包含正在运行的流程实例);
- act_hi_taskinst:历史流程任务表,存储历史任务节点;
- act_hi_varinst:流程历史变量表,存储流程历史节点的变量信息;
3)用户相关表
4个,IdentityService接口操作的表
- act_id_group:用户组信息表,对应节点选定候选组信息;
- act_id_info:用户扩展信息表,存储用户扩展信息;
- act_id_membership:用户与用户组关系表;
- act_id_user:用户信息表,对应节点选定办理人或候选人信息;
4)流程定义、流程模板相关表
3个,RepositoryService接口操作的表
- act_re_deployment:部署信息表,存储流程定义、模板部署信息;
- act_re_procdef:流程定义信息表,存储流程定义相关描述信息,流程定义的bpmn文件放在act_ge_bytearray表中,以字节形式存储;
- act_re_model:流程模板信息表,存储流程模板相关描述信息,流程定义的bpmn文件放在act_ge_bytearray表中,以字节形式存储;
5)流程运行时表
6个,RuntimeService,taskService接口操作的表
- act_ru_task:运行时流程任务节点表,存储运行中流程的任务节点信息,重要,常用于查询人员或部门的待办任务时使用;
- act_ru_event_subscr:监听信息表,不常用;
- act_ru_execution:运行时流程执行实例表,记录运行中流程运行的各个分支信息(当没有子流程时,其数据与act_ru_task表数据是一一对应的);
- act_ru_identitylink:运行时流程人员表,重要,常用于查询人员或部门的待办任务时使用;
- act_ru_job:运行时定时任务数据表,存储流程的定时任务信息;
- act_ru_variable:运行时流程变量数据表,存储运行中的流程各节点的变量信息;
4.ruoyi-flowable-plus
具体部署操作可参见其文档。
4.1项目结构
仅介绍工作流相关的代码结构
所有的controller都在ruoyi-admin中:
工作流相关的service以及impl以及对应的mapper,实体类都在ruoyi-system中:
工作流相关的核心类在ruoyi-flowable中:
4.2 流程分类
工作流表中的流程部署和流程模板表中有对应的categroy字段,用于在查询时作为筛选条件。act_re_deployment和act_re_model中会用到。本项目中创建流程模型之前必须有对应的分类才可以。
注意,flowable本身并没有专门针对流程分类的表结构,作者自己专门创建了该模块以及对应的表结构。
数据库中的表为:wf_category。可以从项目中找到对应的代码,就是一个基本的crud。
4.3 表单配置
本项目工作流启动时必须选择一个表单,该页面点击新增后进入表单设计界面。
设计界面中,一定要注意字段名,默认的字段名是随机起的,建议把以后可能在流程处理中用到的关键字段起一个有意义的名字。
表单设计源文件在view下的/tool/build/index
这个组件的名字叫做Form Generator,详情参见:
https://blog.csdn.net/weixin_42288182/article/details/108375824
这里简单提一下就行,重点是工作流,这个设计器日后再说。
配置好的表单点击保存按钮后,会存放到wf_form表中,以json的方式把表单数据放在content字段中
注意,后面会谈到流程模型,模型部署时会将使用到的表单另存一份存入wf_deploy_form表中,flowable的部署和activiti一样支持多版本,所以每个部署的版本都会存入一个表单。当流程部署时,WfModelServiceImpl.deployModel方法最后会把流程中包含的表单存放到wf_deploy_form中,注意是独立存放,也就是对应的content数据会完整的作为一个和流程版本一致保存到wf_deploy_form表中。这个参见后面的流程模型中的部署部分。
4.4 流程模型
部分内容直接复制自该项目的官方文档:
http://rfp-doc.konbai.work/
模型管理
模型管理是整个工作流中重要的一步,流程模型设计决定了工作流流转的各操作步骤。流程设计必须要有开始节点和结束节点。目前流程设计必须要有“申请人/发起人”节点。
新增模型
对应源码:WfModelController.add–>WfModelServiceImpl.insertModel
@Override
public void insertModel(WfModelBo modelBo) {
//基于org.flowable.engine.repository的Model
Model model = repositoryService.newModel();
model.setName(modelBo.getModelName());
model.setKey(modelBo.getModelKey());
model.setCategory(modelBo.getCategory());
//通过buildMetaInfo方法给WfMetaInfoDto添加流程描述以及创建者的用户名
String metaInfo = buildMetaInfo(new WfMetaInfoDto(), modelBo.getDescription());
model.setMetaInfo(metaInfo);
// 保存流程模型
repositoryService.saveModel(model);
}
执行的sql:
insert into ACT_RE_MODEL(ID_, REV_, NAME_, KEY_, CATEGORY_, CREATE_TIME_, LAST_UPDATE_TIME_, VERSION_, META_INFO_, DEPLOYMENT_ID_, EDITOR_SOURCE_VALUE_ID_, EDITOR_SOURCE_EXTRA_VALUE_ID_, TENANT_ID_) values('b8cdb45f-8359-11ed-bdca-b40edef8b08d', 1, '测试请假流程v1.0', 'Process_1671865594483', 'test001', '2022-12-24T15:08:09.417+0800', '2022-12-24T15:08:09.417+0800', 1, '{"createUser":null,"description":null,"formType":null,"formId":null}', NULL, NULL, NULL, '')
根据3.4对表结构的介绍,流程定义相关的act_re为前缀的表是通过RepositoryService来进行操作的。
RepositoryService
资源管理类,提供了管理和控制流程发布包和流程定义的操作。使用工作流建模工具设计的业务流程图需要使用此service将流程定义文件的内容部署到计算机。
除了部署流程定义以外还可以:查询引擎中的发布包和流程定义。
暂停或激活发布包,对应全部和特定流程定义。 暂停意味着它们不能再执行任何操作了,激活是对应的反向操作。获得多种资源,像是包含在发布包里的文件, 或引擎自动生成的流程图。
获得流程定义的pojo版本, 可以用来通过java解析流程,而不必通过xml。
WfModelServiceImpl继承了FlowServiceFactory,在FlowServiceFactory中注入了所有的flowable引擎类.
@Component
@Getter
public class FlowServiceFactory {
@Resource
protected RepositoryService repositoryService;
@Resource
protected RuntimeService runtimeService;
@Resource
protected IdentityService identityService;
@Resource
protected TaskService taskService;
@Resource
protected FormService formService;
@Resource
protected HistoryService historyService;
@Resource
protected ManagementService managementService;
@Qualifier("processEngine")
@Resource
protected ProcessEngine processEngine;
}
保存的数据将存入流程设计模型部署表( act_re_model )
保存的模型是有版本的,具体描述参见后面的流程设计部分
流程设计
bpmn-process-designer
流程设计器使用的插件是bpmn-process-designer
详细了解可参见:
https://blog.csdn.net/weixin_43550766/article/details/121283568
1. 常规
一般只需要设计名称即可,ID默认使用生成的。
2. 表单
开始节点必须配置表单,否则后续部署流程无法通过。
3. 任务(用户任务)
用户任务分为这四大类。
- 指定用户
- 角色
- 部门
- 发起人
- 多实例审批人设置“指定用户”有两人以上、角色或部门,则下方会显示多实例设置。
- 会签:所选的审批人全员通过后,才会进入到下一节点。
- 或签:所选的审批人有一名审批人同意后,即可进入到下一节点。
4. 执行监听器
执行监听器(Execution Listener)可以在流程执行中发生特定的事件时,执行外部Java代码或计算表达式。
事件类型:start(开始)、end(结束)
监听器类型:
1. Java类:填写类的全路径,如:com.ruoyi.flowable.listener.ReceiptExecutionListener 2. 表达式:使用UEL进行表达式解析。 3. 代理表达式:监听器接口,通常为@Component的value值,如:${receiptExecutionListener} 4. 脚本
5. 任务监听器
Task Listener 主要是监听用户节点事件。用法与执行监听器类似,不赘述了。
6.代码实现–保存流程
保存流程的代码在
WfModelController.save–>WfModelServiceImpl.saveModel
@Override
@Transactional(rollbackFor = Exception.class)
public void saveModel(WfModelBo modelBo) {
log.info("保存流程设计");
// 查询模型信息
Model model = repositoryService.getModel(modelBo.getModelId());
if (ObjectUtil.isNull(model)) {
throw new RuntimeException("流程模型不存在!");
}
//把接收到的流程设计器转换的xml,通过flowable自带的bpmnXMLConverter.convertToBpmnModel转换成flowable中的BpmnModel
BpmnModel bpmnModel = ModelUtils.getBpmnModel(modelBo.getBpmnXml());
if (ObjectUtil.isEmpty(bpmnModel)) {
throw new RuntimeException("获取模型设计失败!");
}
String processName = bpmnModel.getMainProcess().getName();
// 获取开始节点
StartEvent startEvent = ModelUtils.getStartEvent(bpmnModel);
if (ObjectUtil.isNull(startEvent)) {
throw new RuntimeException("开始节点不存在,请检查流程设计是否有误!");
}
// 获取开始节点配置的表单Key
if (StrUtil.isBlank(startEvent.getFormKey())) {
throw new RuntimeException("请配置流程表单");
}
Model newModel;
//如果保存时选择了使用新版本保存,则版本号+1,数据库中新增加一条记录
if (Boolean.TRUE.equals(modelBo.getNewVersion())) {
newModel = repositoryService.newModel();
newModel.setName(processName);
newModel.setKey(model.getKey());
newModel.setCategory(model.getCategory());
newModel.setMetaInfo(model.getMetaInfo());
newModel.setVersion(model.getVersion() + 1);
} else {
newModel = model;d
// 设置流程名称
newModel.setName(processName);
}
// 保存流程模型
repositoryService.saveModel(newModel);
// 保存 BPMN XML
repositoryService.addModelEditorSource(newModel.getId(), ModelUtils.getBpmnXml(bpmnModel));
}
设计好流程图保存的时候会有提示,问是否以新版本保存,如果选是,则在act_re_model表中新增一条记录,Version+1,之前的一条记录作为历史记录存在。
insert into ACT_RE_MODEL(ID_, REV_, NAME_, KEY_, CATEGORY_, CREATE_TIME_, LAST_UPDATE_TIME_, VERSION_, META_INFO_, DEPLOYMENT_ID_, EDITOR_SOURCE_VALUE_ID_, EDITOR_SOURCE_EXTRA_VALUE_ID_, TENANT_ID_) values('d09eb340-835a-11ed-bdca-b40edef8b08d', 1, '测试请假流程v1.0', 'Process_1671865594483', 'test001', '2022-12-24T15:15:58.871+0800', '2022-12-24T15:15:58.871+0800', 2, '{"createUser":null,"description":null,"formType":null,"formId":null}', NULL, NULL, NULL, '')
如果点击设为最新,则调用latestModel方法,在获取当前模型最新版本后,在版本号上再加1后保存
设计好的流程通过下面语句保存在act_ge_bytearray表中,id作为act_re_model的外键EDITOR_SOURCE_VALUE_ID存放。
insert into ACT_GE_BYTEARRAY(ID_, REV_, NAME_, BYTES_, DEPLOYMENT_ID_) values ( 'd0a0d621-835a-11ed-bdca-b40edef8b08d', 1, 'source', 'java.io.ByteArrayInputStream@19d4b9b4', NULL )
5.部署流程
代码–部署流程
WfModelController.deployModel–>WfModelServiceImpl.deployModel
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deployModel(String modelId) {
// 获取流程模型
Model model = repositoryService.getModel(modelId);
if (ObjectUtil.isNull(model)) {
throw new RuntimeException("流程模型不存在!");
}
// 获取流程图
String bpmnXml = queryBpmnXmlById(modelId);
BpmnModel bpmnModel = ModelUtils.getBpmnModel(bpmnXml);
String processName = model.getName() + ProcessConstants.SUFFIX;
// 部署流程,在act_re_deployment表新建一条记录
Deployment deployment = repositoryService.createDeployment()
.name(model.getName())
.key(model.getKey())
.addBpmnModel(processName, bpmnModel)
.category(model.getCategory())
.deploy();
// 保存部署表单
return deployFormService.saveInternalDeployForm(deployment.getId(), bpmnModel);
}
部署后执行的关键sql:
1.act_re_deployment表新建一条记录
insert into ACT_RE_DEPLOYMENT(ID_, NAME_, CATEGORY_, KEY_, TENANT_ID_, DEPLOY_TIME_, DERIVED_FROM_, DERIVED_FROM_ROOT_, PARENT_DEPLOYMENT_ID_, ENGINE_VERSION_) values('259114c2-835f-11ed-bdca-b40edef8b08d', '测试请假流程v1.0', 'test001', 'Process_1671865594483', '', '2022-12-24T15:46:59.374+0800', NULL, NULL, '259114c2-835f-11ed-bdca-b40edef8b08d', NULL)
ACT_RE_PROCDEF(流程定义表)表新建一条记录
insert into ACT_RE_PROCDEF(ID_, REV_, CATEGORY_, NAME_, KEY_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_, DGRM_RESOURCE_NAME_, DESCRIPTION_, HAS_START_FORM_KEY_, HAS_GRAPHICAL_NOTATION_ , SUSPENSION_STATE_, DERIVED_FROM_, DERIVED_FROM_ROOT_, DERIVED_VERSION_, TENANT_ID_, ENGINE_VERSION_) values ('Process_1673191270430:1:5c78488e-900b-11ed-b837-b40edef8b08d', 1, 'http://flowable.org/bpmn', '测试流程3.0', 'Process_1673191270430', 1, '5c67f4db-900b-11ed-b837-b40edef8b08d', '测试流程3.0.bpmn', '测试流程3.0.Process_1673191270430.png', NULL, true, true, 1, NULL, NULL, 0, '', NULL)
2.ACT_GE_BYTEARRAY,将该流程模型需要的.bpmn和png图片以二进制的形式,分为两行记录的方式分别存入该表中。通过DEPLOYMENT_ID_对应act_re_deployment的ID_。加上一开始流程模型创建时的source记录,一共三条记录。
INSERT INTO ACT_GE_BYTEARRAY(ID_, REV_, NAME_, BYTES_, DEPLOYMENT_ID_, GENERATED_) VALUES
('259114c3-835f-11ed-bdca-b40edef8b08d', 1, '测试请假流程v1.0.bpmn', 'java.io.ByteArrayInputStream@94beb82', '259114c2-835f-11ed-bdca-b40edef8b08d', false) ,
('25a2c804-835f-11ed-bdca-b40edef8b08d', 1, '测试请假流程v1.0.Process_1671865594483.png', 'java.io.ByteArrayInputStream@111647d5', '259114c2-835f-11ed-bdca-b40edef8b08d', true)
3.act_re_procdef新增一条流程定义记录,流程定义id=processKey:version:流程定义自己生成的uuid
流程定义被解析为一个内部可执行的对象模型,以便流程实例可以从中启动。
通过DEPLOYMENT_ID_对应act_re_deployment的ID_,和act_re_deployment是多对一的关系,因为一个部署的bar包里可能包含多个流程定义文件,每个流程定义文件都会有一条记录在 ACT_REPROCDEF 表内,每个流程定义的数据,都会对应ACT_GE_BYTEARRAY 表内的一个资源文件和 PNG 图片文件。和 ACT_GE_BYTEARRAY 的关联是通过程序用ACT_GE_BYTEARRAY.NAME 与 ACT_RE_PROCDEF.NAME 完成的,在数据库表结构中没有体现。
insert into ACT_RE_PROCDEF(ID_, REV_, CATEGORY_, NAME_, KEY_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_, DGRM_RESOURCE_NAME_, DESCRIPTION_, HAS_START_FORM_KEY_, HAS_GRAPHICAL_NOTATION_ , SUSPENSION_STATE_, DERIVED_FROM_, DERIVED_FROM_ROOT_, DERIVED_VERSION_, TENANT_ID_, ENGINE_VERSION_) values ('Process_1671865594483:1:25a31625-835f-11ed-bdca-b40edef8b08d', 1, 'http://flowable.org/bpmn', '测试请假流程v1.0', 'Process_1671865594483', 1, '259114c2-835f-11ed-bdca-b40edef8b08d', '测试请假流程v1.0.bpmn', '测试请假流程v1.0.Process_1671865594483.png', NULL, true, true, 1, NULL, NULL, 0, '', NULL)
4.wf_deploy_form
和流程捆绑的表单,也会在wf_deploy_form表中存入一份包含content的完整数据,因为后期的表单更新,并对流程重新部署后,原历史版本中的表单是不会变的。
wf_deploy_form的id和act_re_deployment的ID_一致。
6.创建新流程
流程列表
流程部署成功后,通过办公管理中的新建流程,创建新的流程。
新建流程的前端文件:ruoyi-ui/src/views/workflow/work/index.vue
通过WfProcessController.list–>WfProcessServiceImpl.processList获得已经部署的流程
通过后台打印的sql分析
SELECT RES.* from ACT_RE_PROCDEF RES WHERE RES.VERSION_ = (select max(VERSION_) from ACT_RE_PROCDEF where KEY_ = RES.KEY_ and ( (TENANT_ID_ IS NOT NULL and TENANT_ID_ = RES.TENANT_ID_) or (TENANT_ID_ IS NULL and RES.TENANT_ID_ IS NULL) ) ) and (RES.SUSPENSION_STATE_ = 1) order by RES.KEY_ asc LIMIT 10 OFFSET 0
SELECT RES.* from ACT_RE_DEPLOYMENT RES WHERE RES.ID_ = 'fb32e0be-7429-11ed-9514-b40edef8b08d' order by RES.ID_ asc
查询了两个表,因为设置了最新版本作为条件,所以先通过查询ACT_RE_PROCDEF的最大version获得最新版本的流程定义信息,在通过流程信息的delopymentId作为条件到ACT_RE_DEPLOYMENT中查询流程部署数据。
因此,在该方法中可以获得流程定义id,和部署id,分别存放在WfDefinitionVo的definitionId和deploymentId中。
public TableDataInfo<WfDefinitionVo> processList(PageQuery pageQuery) {
Page<WfDefinitionVo> page = new Page<>();
// 流程定义列表数据查询
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery()
.latestVersion()//最新版本
.active()//处于激活状态
.orderByProcessDefinitionKey()//通过流程定义key的升序排序
.asc();
long pageTotal = processDefinitionQuery.count();
if (pageTotal <= 0) {
return TableDataInfo.build();
}
int offset = pageQuery.getPageSize() * (pageQuery.getPageNum() - 1);
List<ProcessDefinition> definitionList = processDefinitionQuery.listPage(offset, pageQuery.getPageSize());
List<WfDefinitionVo> definitionVoList = new ArrayList<>();
for (ProcessDefinition processDefinition : definitionList) {
String deploymentId = processDefinition.getDeploymentId();
Deployment deployment = repositoryService.createDeploymentQuery().deploymentId(deploymentId).singleResult();
WfDefinitionVo vo = new WfDefinitionVo();
vo.setDefinitionId(processDefinition.getId());
vo.setProcessKey(processDefinition.getKey());
vo.setProcessName(processDefinition.getName());
vo.setVersion(processDefinition.getVersion());
vo.setDeploymentId(processDefinition.getDeploymentId());
vo.setSuspended(processDefinition.isSuspended());
// 流程定义时间
vo.setCategory(deployment.getCategory());
vo.setDeploymentTime(deployment.getDeploymentTime());
definitionVoList.add(vo);
}
page.setRecords(definitionVoList);
page.setTotal(pageTotal);
return TableDataInfo.build(page);
}
开始节点的表单
发起流程后,进入开始节点,开始节点必须指定一个表单进行填写:
这个表单怎么来的呢:通过发起按钮,调用handleStart方法,调用路由,并传递流程定义id和部署id,这两个id非常重要
通过路由进入了ruoyi-ui/src/views/workflow/work/start.vue
在这里通过WfProcessController.getForm–>WfProcessServiceImpl.selectFormContent获取当前流程开始节点所对应的form:
流程定义id(definitionId)用来获得bpmn,从而获得启动节点,这样就可以通过启动节点的id以及formKey来作为查询该节点所配置的表单的筛选条件,但是,不同的流程模型可能会有相同的节点id以及formKey,所以必须有对应的部署id(deployId)作为筛选条件。
public String selectFormContent(String definitionId, String deployId) {
//1.通过流程定义id获得bpmn
//1.1 通过repositoryService.getProcessModel获得输入流
InputStream inputStream = repositoryService.getProcessModel(definitionId);
//1.2 通过 IoUtil.readUtf8将输入流接收并转换为String字符串
String bpmnString;
try {
bpmnString = IoUtil.readUtf8(inputStream);
} catch (IORuntimeException exception) {
throw new RuntimeException("获取流程设计失败!");
}
//1.3 将字符串转换为BpmnMode实体类
BpmnModel bpmnModel = ModelUtils.getBpmnModel(bpmnString);
//2 获得开始节点
StartEvent startEvent = ModelUtils.getStartEvent(bpmnModel);
//3 获取开始节点所对应的表单内容
WfDeployFormVo deployFormVo = deployFormMapper.selectVoOne(new LambdaQueryWrapper<WfDeployForm>()
.eq(WfDeployForm::getDeployId, deployId) //部署id,由于下面的FormKey和节点id可能会在不同的流程设计中重复,所以必须存在这个条件
.eq(WfDeployForm::getFormKey, startEvent.getFormKey())
.eq(WfDeployForm::getNodeKey, startEvent.getId()));
return deployFormVo.getContent();
}
对应的sql:
select * from ACT_GE_BYTEARRAY where DEPLOYMENT_ID_ = '259114c2-835f-11ed-bdca-b40edef8b08d' AND NAME_ = '测试请假流程v1.0.bpmn'
SELECT deploy_id,form_key,node_key,node_name,content FROM wf_deploy_form WHERE (deploy_id = '259114c2-835f-11ed-bdca-b40edef8b08d' AND form_key = 'key_1606546838505914370' AND node_key = 'Event_0vcrsj2')
我的流程
填写好表单之后,点击确定,流程正式启动,可以通过idea的services看到日志:
界面跳转到我的流程界面:
ruoyi-ui/src/views/workflow/work/own.vue
WfProcessController.start–>WfProcessServiceImpl.startProcess
接收两个参数,procDefId为要发起的流程id,variables为表单中的内容
步骤为:1.判断要发起的流程是否为激活状态 2.设置流程发起人id到流程中 3.通过runtimeService.startProcessInstanceById启动流程 4.通过wfTaskService.startFirstTask启动第一个任务
这段源码用到了repositoryService,他是用来管理流程部署和流程定义bpmn的
runtimeService:用来管理已经启动的流程实例的
taskService: 用来管理流程实例中的每个任务的。
public void startProcess(String procDefId, Map<String, Object> variables) {
log.info("启动流程 通过流程id:" + procDefId);
try {
//先通过流程id获得当前流程,判断是否被挂起,只有激活状态才可以被激活
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(procDefId).singleResult();
if (Objects.nonNull(processDefinition) && processDefinition.isSuspended()) {
throw new ServiceException("流程已被挂起,请先激活流程");
}
// 设置流程发起人Id到流程中
this.buildProcessVariables(variables);
//runtimeService.startProcessInstanceByKey()
ProcessInstance processInstance = runtimeService.startProcessInstanceById(procDefId, variables);
// 第一个用户任务为发起人,则自动完成任务
wfTaskService.startFirstTask(processInstance, variables);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("流程启动错误");
}
}
上面的wfTaskService.startFirstTask 调用了WfTaskServiceImpl.startFirstTask,判断如果是第一个任务是发起人,则直接完成,这个我们后面会进行详细测试。
/**
* 启动第一个任务
* @param processInstance 流程实例
* @param variables 流程参数
*/
@Override
public void startFirstTask(ProcessInstance processInstance, Map<String, Object> variables) {
// 若第一个用户任务为发起人,则自动完成任务
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getProcessInstanceId()).singleResult();
if (ObjectUtil.isNotEmpty(task)) {
String userIdStr = (String) variables.get(TaskConstants.PROCESS_INITIATOR);
if (StrUtil.equals(task.getAssignee(), userIdStr)) {
taskService.addComment(task.getId(), processInstance.getProcessInstanceId(), FlowComment.NORMAL.getType(), LoginHelper.getNickName() + "发起流程申请");
// taskService.setAssignee(task.getId(), userIdStr);
taskService.complete(task.getId(), variables);
}
}
}
启动任务后影响的表数据
启动完成之后,返回了流程实例ID,运行中的流程数据会保存到_act_ru_actinst_、act_ru_execution、act_ru_identitylink、act_ru_task表中
ACT_RU_EXECUTION: 执行实例表
insert into ACT_RU_EXECUTION (ID_, REV_, PROC_INST_ID_, BUSINESS_KEY_, PROC_DEF_ID_, ACT_ID_, IS_ACTIVE_, IS_CONCURRENT_, IS_SCOPE_,IS_EVENT_SCOPE_, IS_MI_ROOT_, PARENT_ID_, SUPER_EXEC_, ROOT_PROC_INST_ID_, SUSPENSION_STATE_, TENANT_ID_, NAME_, START_ACT_ID_, START_TIME_, START_USER_ID_, IS_COUNT_ENABLED_, EVT_SUBSCR_COUNT_, TASK_COUNT_, JOB_COUNT_, TIMER_JOB_COUNT_, SUSP_JOB_COUNT_, DEADLETTER_JOB_COUNT_, EXTERNAL_WORKER_JOB_COUNT_, VAR_COUNT_, ID_LINK_COUNT_, CALLBACK_ID_, CALLBACK_TYPE_, REFERENCE_ID_, REFERENCE_TYPE_, PROPAGATED_STAGE_INST_ID_, BUSINESS_STATUS_) values ('af71bca6-8367-11ed-bdca-b40edef8b08d', 1, 'af71bca6-8367-11ed-bdca-b40edef8b08d', NULL, 'Process_1671865594483:1:25a31625-835f-11ed-bdca-b40edef8b08d', NULL, true, false, true, false, false, NULL, NULL, 'af71bca6-8367-11ed-bdca-b40edef8b08d', 1, '', NULL, 'Event_0vcrsj2', '2022-12-24T16:48:06.669+0800', '1', true, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL ) , ('af71bcac-8367-11ed-bdca-b40edef8b08d', 1, 'af71bca6-8367-11ed-bdca-b40edef8b08d', NULL, 'Process_1671865594483:1:25a31625-835f-11ed-bdca-b40edef8b08d', 'Activity_0jrw6f0', true, false, false, false, false, 'af71bca6-8367-11ed-bdca-b40edef8b08d', NULL, 'af71bca6-8367-11ed-bdca-b40edef8b08d', 1, '', NULL, NULL, '2022-12-24T16:48:06.669+0800', NULL, true, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL )
如果是类似本例中的串行流程,则只有两条记录。一条为流程实例记录(主实例记录),另外一条为分支流程记录,流程实例记录的特点是ID_和PROC_INST_ID是一致的,分支流程记录则为当前流程的这个分支已经执行到的节点记录,ID_和PROC_INST_ID不同,并且自己的PARENT_ID_也就是父id指向了流程id,和其一致。如果是并行节点,有多个分支,则这里也会有多条分支记录。
比如我新建了一个流程,需要并行路由分成了两个分支,需要由两个用户节点进行审批
则表中的数据会多出一条子流程实例,两条自实例分别指向已经到达的节点。
此时我们让经理赵审核通过
此时会发现其中一个子流程实体的act_id指向了并行网关,is_active也变成了0
之后我们让业务李也通过,则两条分支合并为一条分支,act_id指向了总经理审核:
ACT_RU_ACTINST (Active Instance)(运行中实例的活动表):存储运行中各个节点的信息
insert into ACT_RU_ACTINST ( ID_, REV_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, TRANSACTION_ORDER_, DURATION_, DELETE_REASON_, TENANT_ID_ ) values ('af71bcad-8367-11ed-bdca-b40edef8b08d', 1, 'Process_1671865594483:1:25a31625-835f-11ed-bdca-b40edef8b08d', 'af71bca6-8367-11ed-bdca-b40edef8b08d', 'af71bcac-8367-11ed-bdca-b40edef8b08d', 'Event_0vcrsj2', NULL, NULL, NULL, 'startEvent', NULL, '2022-12-24T16:48:06.669+0800', '2022-12-24T16:48:06.669+0800', 1, 0, NULL, '') , ('af71bcae-8367-11ed-bdca-b40edef8b08d', 1, 'Process_1671865594483:1:25a31625-835f-11ed-bdca-b40edef8b08d', 'af71bca6-8367-11ed-bdca-b40edef8b08d', 'af71bcac-8367-11ed-bdca-b40edef8b08d', 'Flow_0ykdvqu', NULL, NULL, NULL, 'sequenceFlow', NULL, '2022-12-24T16:48:06.669+0800', '2022-12-24T16:48:06.669+0800', 2, 0, NULL, '') , ('af71bcaf-8367-11ed-bdca-b40edef8b08d', 1, 'Process_1671865594483:1:25a31625-835f-11ed-bdca-b40edef8b08d', 'af71bca6-8367-11ed-bdca-b40edef8b08d', 'af71bcac-8367-11ed-bdca-b40edef8b08d', 'Activity_0jrw6f0', 'af71e3c0-8367-11ed-bdca-b40edef8b08d', NULL, '申请环节', 'userTask', '1', '2022-12-24T16:48:06.669+0800', NULL, 3, NULL, NULL, '')
ACT_RU_TASK:运行时任务节点表,保存当前流程运行到达的任务节点.存入了已经进入的任务,可以通过和流程设计中的任务id进行比较得出该结论。该任务完成后会删除这条信息,仅保存在历史记录相关表中。
insert into ACT_RU_TASK (ID_, REV_, NAME_, PARENT_TASK_ID_, DESCRIPTION_, PRIORITY_, CREATE_TIME_, OWNER_, ASSIGNEE_, DELEGATION_, EXECUTION_ID_, PROC_INST_ID_, PROC_DEF_ID_, TASK_DEF_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, SCOPE_DEFINITION_ID_, PROPAGATED_STAGE_INST_ID_, TASK_DEF_KEY_, DUE_DATE_, CATEGORY_, SUSPENSION_STATE_, TENANT_ID_, FORM_KEY_, CLAIM_TIME_, IS_COUNT_ENABLED_, VAR_COUNT_, ID_LINK_COUNT_, SUB_TASK_COUNT_) values ('af71e3c0-8367-11ed-bdca-b40edef8b08d', 1, '申请环节', NULL, NULL, 50, '2022-12-24T16:48:06.669+0800', NULL, '1', NULL, 'af71bcac-8367-11ed-bdca-b40edef8b08d', 'af71bca6-8367-11ed-bdca-b40edef8b08d', 'Process_1671865594483:1:25a31625-835f-11ed-bdca-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, NULL, 'Activity_0jrw6f0', NULL, NULL, 1, '', 'key_1606546838505914370', NULL, true, 0, 0, 0 )
ACT_RU_VARIABLE(运行时流程变量数据表) 有表单中的三个字段和一个发起人id,体现在NAME_字段,值存放在TEXT_字段,数据在流程启动的时候存入.
act_ru_identitylink 任务参与者数据表。主要存储当前节点参与者的信息。
ACT_HI_TASKINST 历史任务表,记录已经运行到的任务流程,如果是已完成的流程,end_time不为空
insert into ACT_HI_TASKINST ( ID_, REV_, TASK_DEF_ID_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, SCOPE_DEFINITION_ID_, PROPAGATED_STAGE_INST_ID_, NAME_, PARENT_TASK_ID_, DESCRIPTION_, OWNER_, ASSIGNEE_, START_TIME_, CLAIM_TIME_, END_TIME_, DURATION_, DELETE_REASON_, TASK_DEF_KEY_, FORM_KEY_, PRIORITY_, DUE_DATE_, CATEGORY_, TENANT_ID_, LAST_UPDATED_TIME_ ) values ('3b03f5ac-843d-11ed-bb0a-b40edef8b08d', 1, NULL, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3afc7b8d-843d-11ed-bb0a-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, '经理赵审核', NULL, NULL, NULL, '1606943771414302721', '2022-12-25T18:16:43.634+0800', NULL, NULL, NULL, NULL, 'Activity_1uu9bcv', NULL, 50, NULL, NULL, '', '2022-12-25T18:16:43.634+0800') , ('3b03f5b0-843d-11ed-bb0a-b40edef8b08d', 1, NULL, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3b03a788-843d-11ed-bb0a-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, '业务李审核', NULL, NULL, NULL, '1606943896886906882', '2022-12-25T18:16:43.634+0800', NULL, NULL, NULL, NULL, 'Activity_0q89vsf', NULL, 50, NULL, NULL, '', '2022-12-25T18:16:43.634+0800')
ACT_HI_ACTINST 历史活动信息表。这里记录流程流转过的所有节点
insert into ACT_HI_ACTINST ( ID_, REV_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, TRANSACTION_ORDER_, DURATION_, DELETE_REASON_, TENANT_ID_ ) values ('3b033256-843d-11ed-bb0a-b40edef8b08d', 1, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3afc7b8d-843d-11ed-bb0a-b40edef8b08d', 'Flow_1ebjw0d', NULL, NULL, NULL, 'sequenceFlow', NULL, '2022-12-25T18:16:43.629+0800', '2022-12-25T18:16:43.629+0800', 1, 0, NULL, '') , ('3b035967-843d-11ed-bb0a-b40edef8b08d', 1, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3afc7b8d-843d-11ed-bb0a-b40edef8b08d', 'Gateway_0tyx4ie', NULL, NULL, NULL, 'parallelGateway', NULL, '2022-12-25T18:16:43.630+0800', '2022-12-25T18:16:43.631+0800', 2, 1, NULL, '') , ('3b03ce99-843d-11ed-bb0a-b40edef8b08d', 1, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3afc7b8d-843d-11ed-bb0a-b40edef8b08d', 'Flow_0bd888t', NULL, NULL, NULL, 'sequenceFlow', NULL, '2022-12-25T18:16:43.633+0800', '2022-12-25T18:16:43.633+0800', 3, 0, NULL, '') , ('3b03ce9a-843d-11ed-bb0a-b40edef8b08d', 1, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3b03a788-843d-11ed-bb0a-b40edef8b08d', 'Flow_0ok3o5m', NULL, NULL, NULL, 'sequenceFlow', NULL, '2022-12-25T18:16:43.633+0800', '2022-12-25T18:16:43.633+0800', 4, 0, NULL, '') , ('3b03f5ab-843d-11ed-bb0a-b40edef8b08d', 1, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3afc7b8d-843d-11ed-bb0a-b40edef8b08d', 'Activity_1uu9bcv', '3b03f5ac-843d-11ed-bb0a-b40edef8b08d', NULL, '经理赵审核', 'userTask', '1606943771414302721', '2022-12-25T18:16:43.634+0800', NULL, 5, NULL, NULL, '') , ('3b03f5af-843d-11ed-bb0a-b40edef8b08d', 1, 'Process_1671960329397:1:52d255e4-8436-11ed-bb0a-b40edef8b08d', '3afc7b87-843d-11ed-bb0a-b40edef8b08d', '3b03a788-843d-11ed-bb0a-b40edef8b08d', 'Activity_0q89vsf', '3b03f5b0-843d-11ed-bb0a-b40edef8b08d', NULL, '业务李审核', 'userTask', '1606943896886906882', '2022-12-25T18:16:43.634+0800', NULL, 6, NULL, NULL, '')
ACT_HI_IDENTITYLINK 任务参与者数据表。主要存储历史节点参与者的信息
insert into ACT_HI_IDENTITYLINK (ID_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, SCOPE_DEFINITION_ID_, CREATE_TIME_) values ('3b026f05-843d-11ed-bb0a-b40edef8b08d', 'participant', '1', NULL, NULL, '3afc7b87-843d-11ed-bb0a-b40edef8b08d', NULL, NULL, NULL, NULL, '2022-12-25T18:16:43.625+0800') , ('3b03f5ad-843d-11ed-bb0a-b40edef8b08d', 'assignee', '1606943771414302721', NULL, '3b03f5ac-843d-11ed-bb0a-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, '2022-12-25T18:16:43.634+0800') , ('3b03f5ae-843d-11ed-bb0a-b40edef8b08d', 'participant', '1606943771414302721', NULL, NULL, '3afc7b87-843d-11ed-bb0a-b40edef8b08d', NULL, NULL, NULL, NULL, '2022-12-25T18:16:43.634+0800') , ('3b03f5b1-843d-11ed-bb0a-b40edef8b08d', 'assignee', '1606943896886906882', NULL, '3b03f5b0-843d-11ed-bb0a-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, '2022-12-25T18:16:43.634+0800') , ('3b03f5b2-843d-11ed-bb0a-b40edef8b08d', 'participant', '1606943896886906882', NULL, NULL, '3afc7b87-843d-11ed-bb0a-b40edef8b08d', NULL, NULL, NULL, NULL, '2022-12-25T18:16:43.634+0800')
流程详情
在我的流程(own.vue)中点击详情,会跳转到/work/detail.vue中
带着如下几个参数:
在detail.vue中的activated()方法中(当页面被激活时),获取传递来的几个参数,判断当前的任务id是否存在,如果存在,则调用详情信息(getProcessDetails方法).
getProcessDetails中又调用了detailProcess方法,
通过WfProcessController.detail–>WfProcessServiceImpl.queryProcessDetail获取详细信息的数据返还给前端界面ruoyi-ui/src/views/workflow/work/detail.vue
WfProcessServiceImpl.queryProcessDetail:
public WfDetailVo queryProcessDetail(String procInsId, String deployId, String taskId) {
HashMap m = new HashMap();
m.put("testlocal","testlocal_value");
taskService.setVariablesLocal(taskId,m);
WfDetailVo detailVo = new WfDetailVo();
//通过当前的任务id:taskId获取当前的任务实例
historyService.createHistoricTaskInstanceQuery:查询ACT_HI_TASKINST表,(历史的任务实例表) 历史的任务实例表, 存放已经办理的任务。
HistoricTaskInstance taskIns = historyService.createHistoricTaskInstanceQuery()
.taskId(taskId)
.includeIdentityLinks() //关联查询ACT_HI_IDENTITYLINK 历史流程人员表 : 任务参与者数据表。主要存储历史节点参与者的信息,目的是取出该任务的办理人
.includeProcessVariables() //流程变量,贯穿整个流程 ACT_HI_VARINST
.includeTaskLocalVariables() //Local变量,仅生存在当前节点任务中 ACT_HI_VARINST
.singleResult();
if (taskIns == null) {
throw new ServiceException("没有可办理的任务!");
}
//通过流程定义id获取当前任务的bpmn与表单以及历史流程节点信息
InputStream inputStream = repositoryService.getProcessModel(taskIns.getProcessDefinitionId());
detailVo.setBpmnXml(IoUtil.readUtf8(inputStream));//获取bpmn,用于显示流程追踪,配合setFowViewer
detailVo.setTaskFormData(currTaskFormData(deployId, taskIns));//取得当前节点的表单,在待办任务中可以看到,如果存在表单,则出现在审批意见的上面
detailVo.setHistoryProcNodeList(historyProcNodeList(procInsId));//历史流程节点信息--流转记录
detailVo.setProcessFormList(processFormList(procInsId, deployId, taskIns));//processFormList,获取历史中已经填写好的表单--表单信息
detailVo.setFlowViewer(getFlowViewer(procInsId));//获取流程追踪用到的数据
return detailVo;
}
该方法详解:
流程变量和Local变量
流程变量:存在于整个流程中,存放在act_ru_variable以及变量历史表ACT_HI_VARINST中
通过输出的语句,把includeTaskLocalVariables()方法注释掉,也就是不要Local变量,则执行的sql少了一段RES.ID=VAR.TASK_ID_ 的条件
这句执行中,除了获取审批人的ACT_HI_IDENTITYLINK表外,还有ACT_HI_TASKINST 表 和 ACT_HI_VARINST表,在ACT_HI_VARINST表中,全局流程的TASK_ID_字段为null,EXECUTION_ID_和ACT_HI_TASKINST的PROC_INST_ID_一致,也就是流程实例的id一致,而LOCAL变量的EXECUTION_ID_和流程实例id是不一致的,它只存在于这个任务中。
如下图的ACT_HI_TASKINST表的截图为证,testlocal是LOCAL变量
这是添加测试用的LOCAL变量的代码:
流程详情–流转记录
源码:
detailVo.setHistoryProcNodeList(historyProcNodeList(procInsId));
调用了historyProcNodeList并传入了流程实例id
具体实现方式参见下方的源码注释:
WfProcessServiceImpl.historyProcNodeList
private List<WfProcNodeVo> historyProcNodeList(String procInsId) {
//取得该流程流转经过的所有节点:historicActivityInstanceList
//historyService.createHistoricActivityInstanceQuery查询ACT_HI_ACTINST表,该表记录流程流转过的所有节点
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(procInsId)
//CollUtil.newHashSet,HuTool中的神器,创建一个新的hashset,只查询三种节点:开始节点,结束节点和用户任务节点
.activityTypes(CollUtil.newHashSet(BpmnXMLConstants.ELEMENT_EVENT_START, BpmnXMLConstants.ELEMENT_EVENT_END, BpmnXMLConstants.ELEMENT_TASK_USER))
.orderByHistoricActivityInstanceStartTime().desc()
.orderByHistoricActivityInstanceEndTime().desc()
.list();
//取得当前流程实例的历史数据,在本段代码中用于取得实例中的发起人。
//historyService.createHistoricProcessInstanceQuery查询ACT_HI_PROCINST表
/*流程实例的历史数据会被保存到act_hi_procinst表中,只要流程被启动,就会将流程实例的数据写入到act_hi_procinst表中,
并且一个流程只会写入一条数据。该表中会记录流程的开始id和结束id*/
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(procInsId)
.singleResult();
//taskService.getProcessInstanceComments查询ACT_HI_COMMENT历史意见表
// 详细说明: 用于保存流程审核的批注信息。一般我们审批任务时,都会填写审批意见、审批时间、审批人等信息
List<Comment> commentList = taskService.getProcessInstanceComments(procInsId);
List<WfProcNodeVo> elementVoList = new ArrayList<>();
//遍历该流程流转经过的所有节点,存放到工作流节点元素实体类WfProcNodeVo中
for (HistoricActivityInstance activityInstance : historicActivityInstanceList) {
WfProcNodeVo elementVo = new WfProcNodeVo();
elementVo.setProcDefId(activityInstance.getProcessDefinitionId());
elementVo.setActivityId(activityInstance.getActivityId());//activityId:节点id
elementVo.setActivityName(activityInstance.getActivityName());
elementVo.setActivityType(activityInstance.getActivityType());
elementVo.setCreateTime(activityInstance.getStartTime());
elementVo.setEndTime(activityInstance.getEndTime());
if (ObjectUtil.isNotNull(activityInstance.getDurationInMillis())) {//活动耗时
elementVo.setDuration(DateUtil.formatBetween(activityInstance.getDurationInMillis(), BetweenFormatter.Level.SECOND));//以秒为单位记录
}
//如果是开始节点,则存入发起人的id和昵称
if (BpmnXMLConstants.ELEMENT_EVENT_START.equals(activityInstance.getActivityType())) {
if (ObjectUtil.isNotNull(historicProcessInstance)) {
Long userId = Long.parseLong(historicProcessInstance.getStartUserId());
SysUser user = userService.selectUserById(userId);
if (user != null) {
elementVo.setAssigneeId(user.getUserId());
elementVo.setAssigneeName(user.getNickName());
}
}
//如果是用户任务节点,则存入审批人的id和昵称
} else if (BpmnXMLConstants.ELEMENT_TASK_USER.equals(activityInstance.getActivityType())) {
if (StringUtils.isNotBlank(activityInstance.getAssignee())) {
SysUser user = userService.selectUserById(Long.parseLong(activityInstance.getAssignee()));
elementVo.setAssigneeId(user.getUserId());
elementVo.setAssigneeName(user.getNickName());
}
// 展示审批人员,单独审批人员参见上面的审批人代码,如果是组则需要本段代码
List<HistoricIdentityLink> linksForTask = historyService.getHistoricIdentityLinksForTask(activityInstance.getTaskId());//act_hi_identitylink
StringBuilder stringBuilder = new StringBuilder();
for (HistoricIdentityLink identityLink : linksForTask) {
//如果是候选组的分配方式
if ("candidate".equals(identityLink.getType())) {
if (StringUtils.isNotBlank(identityLink.getUserId())) {
SysUser user = userService.selectUserById(Long.parseLong(identityLink.getUserId()));
stringBuilder.append(user.getNickName()).append(",");
}
if (StringUtils.isNotBlank(identityLink.getGroupId())) {
if (identityLink.getGroupId().startsWith(TaskConstants.ROLE_GROUP_PREFIX)) {
Long roleId = Long.parseLong(StringUtils.stripStart(identityLink.getGroupId(), TaskConstants.ROLE_GROUP_PREFIX));
SysRole role = roleService.selectRoleById(roleId);
stringBuilder.append(role.getRoleName()).append(",");
} else if (identityLink.getGroupId().startsWith(TaskConstants.DEPT_GROUP_PREFIX)) {
Long deptId = Long.parseLong(StringUtils.stripStart(identityLink.getGroupId(), TaskConstants.DEPT_GROUP_PREFIX));
SysDept dept = deptService.selectDeptById(deptId);
stringBuilder.append(dept.getDeptName()).append(",");
}
}
}
}
if (StringUtils.isNotBlank(stringBuilder)) {
elementVo.setCandidate(stringBuilder.substring(0, stringBuilder.length() - 1));
}
// 获取意见评论内容
if (CollUtil.isNotEmpty(commentList)) {
List<Comment> comments = new ArrayList<>();
for (Comment comment : commentList) {
if (comment.getTaskId().equals(activityInstance.getTaskId())) {
comments.add(comment);
}
}
elementVo.setCommentList(comments);
}
}
elementVoList.add(elementVo);
}
return elementVoList;
}
对应的前端:
流程详情–表单信息
在这里显示已经完成的表单信息,可以有多个表单。
//获取流程表单信息(不包括当前任务节点)
detailVo.setProcessFormList(processFormList(procInsId, deployId, taskIns));
获取的表单来自流程中的两个节点:开始节点(StartEvent)和用户任务节点(UserTask)
代码中的processFormList里通过buildStartFormData,buildUserTaskFormData两个方法分别取得了所有表单
/**
* 获取流程表单信息(不包括当前任务节点)
*/
private List<FormConf> processFormList(String procInsId, String deployId, HistoricTaskInstance taskIns) {
//procFormList: 最终的返回值,存入了来自开始节点的表单和已完成的用户任务节点的表单
List<FormConf> procFormList = new ArrayList<>();
//historyService.createHistoricProcessInstanceQuery查询ACT_HI_PROCINST表
/*流程实例的历史数据会被保存到act_hi_procinst表中,只要流程被启动,就会将流程实例的数据写入到act_hi_procinst表中*/
//通过流程实例id获取流程历史表流程实例以及流程实例对应的流程变量
HistoricProcessInstance historicProcIns = historyService.createHistoricProcessInstanceQuery().processInstanceId(procInsId).includeProcessVariables().singleResult();
//根据流程定义id获取流程定义
Process process = repositoryService.getBpmnModel(historicProcIns.getProcessDefinitionId()).getMainProcess();
buildStartFormData(historicProcIns, process, deployId, procFormList);//获取开始节点的表单
buildUserTaskFormData(procInsId, deployId, process, procFormList);//获取已完成的用户任务表单
return procFormList;
}
buildStartFormData:
/**
* 获得开始节点的表单
* @param historicProcIns
* @param process
* @param deployId
* @param procFormList
*/
private void buildStartFormData(HistoricProcessInstance historicProcIns, Process process, String deployId, List<FormConf> procFormList) {
procFormList = procFormList == null ? new ArrayList<>() : procFormList;
//从ACT_HI_ACTINST(该表记录流程流转过的所有节点)中查询到该流程实例的开始节点记录
HistoricActivityInstance startInstance = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(historicProcIns.getId())
.activityId(historicProcIns.getStartActivityId())
.singleResult();
//获得开始节点
StartEvent startEvent = (StartEvent) process.getFlowElement(startInstance.getActivityId());
//通过部署id:deployId,以及刚刚获得的开始节点的formKey以及节点id获得表单
WfDeployFormVo startFormInfo = deployFormMapper.selectVoOne(new LambdaQueryWrapper<WfDeployForm>()
.eq(WfDeployForm::getDeployId, deployId)
.eq(WfDeployForm::getFormKey, startEvent.getFormKey())
.eq(WfDeployForm::getNodeKey, startEvent.getId()));
if (ObjectUtil.isNotNull(startFormInfo)) {
FormConf formConf = JsonUtils.parseObject(startFormInfo.getContent(), FormConf.class);
if (null != formConf) {
formConf.setTitle(startEvent.getName());
formConf.setDisabled(true);
formConf.setFormBtns(false);
ProcessFormUtils.fillFormData(formConf, historicProcIns.getProcessVariables());
procFormList.add(formConf);
}
}
}
buildUserTaskFormData:
/**
* 获得用户任务节点的表单数据
* @param procInsId
* @param deployId
* @param process
* @param procFormList
*/
private void buildUserTaskFormData(String procInsId, String deployId, Process process, List<FormConf> procFormList) {
procFormList = procFormList == null ? new ArrayList<>() : procFormList;
//SELECT RES.* from ACT_HI_ACTINST RES WHERE RES.PROC_INST_ID_ = '89a35ffd-7efb-11ed-9a7d-b40edef8b08d' and RES.ACT_TYPE_ = 'userTask' and RES.END_TIME_ is not null order by START_TIME_ asc
List<HistoricActivityInstance> activityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(procInsId).finished() //已完成的节点,在sql中体现为endTime不为空
.activityType(BpmnXMLConstants.ELEMENT_TASK_USER)//ACT_TYPE_ = 'userTask'
.orderByHistoricActivityInstanceStartTime().asc()
.list();
for (HistoricActivityInstance instanceItem : activityInstanceList) {
UserTask userTask = (UserTask) process.getFlowElement(instanceItem.getActivityId(), true);
//取得该节点的FormKey
String formKey = userTask.getFormKey();
if (formKey == null) {
continue;
}
// 查询任务节点参数,并转换成Map
Map<String, Object> variables;
variables = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(procInsId)
.taskId(instanceItem.getTaskId())
.list()
.stream()
.collect(Collectors.toMap(HistoricVariableInstance::getVariableName, HistoricVariableInstance::getValue));
//取得表单
WfDeployFormVo deployFormVo = deployFormMapper.selectVoOne(new LambdaQueryWrapper<WfDeployForm>()
.eq(WfDeployForm::getDeployId, deployId)
.eq(WfDeployForm::getFormKey, formKey)
.eq(WfDeployForm::getNodeKey, userTask.getId()));
if (ObjectUtil.isNotNull(deployFormVo)) {
FormConf formConf = JsonUtils.parseObject(deployFormVo.getContent(), FormConf.class);
if (null != formConf) {
//填充表单值
formConf.setTitle(userTask.getName());
formConf.setDisabled(true);
formConf.setFormBtns(false);
ProcessFormUtils.fillFormData(formConf, variables);
procFormList.add(formConf);
}
}
}
}
对应的前端代码,表单信息传递给了processFormList:
流程详情–流程追踪
先来看一下在detail.vue中的流程追踪中的前端代码是如何设计的,都用到了哪些变量:
1.xmlData : 显示原始的bpmn图
2.finishdInfo : 里面包括了很多的子变量,已完成的节点,线路,以及被驳回的线路,通过finishdInfo和xmlData进行配合,从而让原始的bpmn可以以多种不同的颜色来区分节点不同的状态,比如已完成的节点为绿色,正在进行中的节点为蓝色,被驳回的节点为红色,未经过的节点为黑色等。
3.allCommentList:historyProcNodeList :在节点中显示审批人的批注.
detailVo.setFlowViewer(getFlowViewer(procInsId));//获取流程追踪用到的数据
getFlowViewer源码详解:
/**
* 获取流程执行过程
*
* @param procInsId
* @return
*/
private WfViewerVo getFlowViewer(String procInsId) {
// 构建查询条件
//根据流程id查询流程流转过的所有节点,查询数据库中的ACT_HI_ACTINST表
HistoricActivityInstanceQuery query = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(procInsId);
List<HistoricActivityInstance> allActivityInstanceList = query.list();
if (CollUtil.isEmpty(allActivityInstanceList)) {
return new WfViewerVo();
}
// 获取流程发布Id信息
String processDefinitionId = allActivityInstanceList.get(0).getProcessDefinitionId();
//通过流程定义的id获取流程的bpmn实体类
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
// 查询所有已完成的元素(已完成的元素特点为endTime不为空,反之就是未完成的节点)
List<HistoricActivityInstance> finishedElementList = allActivityInstanceList.stream()
.filter(item -> ObjectUtil.isNotNull(item.getEndTime())).collect(Collectors.toList());
// 所有已完成的连线
Set<String> finishedSequenceFlowSet = new HashSet<>();
// 所有已完成的任务节点
Set<String> finishedTaskSet = new HashSet<>();
finishedElementList.forEach(item -> {
//BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW:bpmn.xml中<sequenceFlow>标记,也就是连线,节点间的连线
//判断是连线还是任务节点,分别添加到各自的集合中。
if (BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW.equals(item.getActivityType())) {
finishedSequenceFlowSet.add(item.getActivityId());
} else {
finishedTaskSet.add(item.getActivityId());
}
});
// 查询所有未结束的节点
Set<String> unfinishedTaskSet = allActivityInstanceList.stream()
.filter(item -> ObjectUtil.isNull(item.getEndTime()))
.map(HistoricActivityInstance::getActivityId)
.collect(Collectors.toSet());
// DFS 查询未通过的元素集合,驳回节点集合。
Set<String> rejectedSet = FlowableUtils.dfsFindRejects(bpmnModel, unfinishedTaskSet, finishedSequenceFlowSet, finishedTaskSet);//已拒绝的元素
return new WfViewerVo(finishedTaskSet, finishedSequenceFlowSet, unfinishedTaskSet, rejectedSet);
}
获得驳回节点:
FlowableUtils.dfsFindRejects
/**
* 深搜递归获取流程未通过的节点
* @param bpmnModel 流程模型
* @param unfinishedTaskSet 未结束的任务节点
* @param finishedSequenceFlowSet 已经完成的连线
* @param finishedTaskSet 已完成的任务节点
* @return
*/
public static Set<String> dfsFindRejects(BpmnModel bpmnModel, Set<String> unfinishedTaskSet, Set<String> finishedSequenceFlowSet, Set<String> finishedTaskSet) {
if (ObjectUtil.isNull(bpmnModel)) {
throw new ServiceException("流程模型不存在");
}
//获取bpmn中的所有元素
Collection<FlowElement> allElements = getAllElements(bpmnModel.getMainProcess().getFlowElements(), null);
Set<String> rejectedSet = new HashSet<>();
for (FlowElement flowElement : allElements) {
// 用户节点且未结束元素
if (flowElement instanceof UserTask && unfinishedTaskSet.contains(flowElement.getId())) {
List<String> hasSequenceFlow = iteratorFindFinishes(flowElement, null);//迭代获取父级节点列表,向前找到所有已经经过的连线
List<String> rejects = iteratorFindRejects(flowElement, finishedSequenceFlowSet, finishedTaskSet, hasSequenceFlow, null);//根据正在运行的任务节点,迭代获取子级任务节点列表,向后找
rejectedSet.addAll(rejects);
}
}
return rejectedSet;
}
这个方法中用到了iteratorFindFinishes和iteratorFindRejects两个方法,通过这些代码的逻辑,我们可以得出他是如何取得被驳回的节点的,即:如果当前节点之后存在已完成的任务节点即为驳回节点。
/**
* 迭代获取父级节点列表,向前找
* @param source 起始节点
* @param hasSequenceFlow 已经经过的连线的ID,用于判断线路是否重复
* @return
*/
public static List<String> iteratorFindFinishes(FlowElement source, List<String> hasSequenceFlow) {
//已经经过的连线的ID,用于判断线路是否重复
hasSequenceFlow = hasSequenceFlow == null ? new ArrayList<>() : hasSequenceFlow;
// 根据类型,获取入口连线
List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
if (sequenceFlows != null) {
// 循环找到目标元素
for (SequenceFlow sequenceFlow: sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (hasSequenceFlow.contains(sequenceFlow.getId())) {
continue;
}
// 添加已经走过的连线
hasSequenceFlow.add(sequenceFlow.getId());
//找到连线的上一个节点
FlowElement finishedElement = sequenceFlow.getSourceFlowElement();
// 类型为子流程,则添加子流程开始节点出口处相连的节点
if (finishedElement instanceof SubProcess) {
FlowElement firstElement = (StartEvent) ((SubProcess) finishedElement).getFlowElements().toArray()[0];
// 获取子流程的连线
hasSequenceFlow.addAll(iteratorFindFinishes(firstElement, null));
}
// 继续迭代
hasSequenceFlow = iteratorFindFinishes(finishedElement, hasSequenceFlow);
}
}
return hasSequenceFlow;
}
/**
* 根据正在运行的任务节点,迭代获取子级任务节点列表,向后找
* @param source 起始节点
* @param finishedSequenceFlowSet 已经完成的连线
* @param finishedTaskSet 已经完成的任务节点
* @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复
* @param rejectedList 未通过的元素
* @return
*/
public static List<String> iteratorFindRejects(FlowElement source, Set<String> finishedSequenceFlowSet, Set<String> finishedTaskSet
, List<String> hasSequenceFlow, List<String> rejectedList) {
hasSequenceFlow = hasSequenceFlow == null ? new ArrayList<>() : hasSequenceFlow;
rejectedList = rejectedList == null ? new ArrayList<>() : rejectedList;
// 根据类型,获取出口连线,取得当前节点的所有出口连线
List<SequenceFlow> sequenceFlows = getElementOutgoingFlows(source);
if (sequenceFlows != null) {
// 循环找到目标元素
for (SequenceFlow sequenceFlow: sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (hasSequenceFlow.contains(sequenceFlow.getId())) {
continue;
}
// 添加已经走过的连线
hasSequenceFlow.add(sequenceFlow.getId());
//通过出口连线找到该节点的下一个节点
FlowElement targetElement = sequenceFlow.getTargetFlowElement();
// 添加未完成的节点
//如果当前节点的下一个节点已经有了endtime,说明已经经过该节点并且被驳回了。如果被驳回多次,就通过hasSequenceFlow判断是否是连线重复
if (finishedTaskSet.contains(targetElement.getId())) {
//添加驳回节点
rejectedList.add(targetElement.getId());
}
// 添加未完成的连线
if (finishedSequenceFlowSet.contains(sequenceFlow.getId())) {
//添加驳回连线
rejectedList.add(sequenceFlow.getId());
}
// 如果节点为子流程节点情况,则从节点中的第一个节点开始获取
if (targetElement instanceof SubProcess) {
FlowElement firstElement = (FlowElement) (((SubProcess) targetElement).getFlowElements().toArray()[0]);
List<String> childList = iteratorFindRejects(firstElement, finishedSequenceFlowSet, finishedTaskSet, hasSequenceFlow, null);
// 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续
if (childList != null && childList.size() > 0) {
rejectedList.addAll(childList);
continue;
}
}
// 继续迭代
rejectedList = iteratorFindRejects(targetElement, finishedSequenceFlowSet, finishedTaskSet, hasSequenceFlow, rejectedList);
}
}
return rejectedList;
}
7.待办任务
本例中,请假流程的审批环节指定人为 经理赵 ,通过其用户名登录后可以在 办公管理–待办任务 中看到需要办理的流程任务。对应的前端为ruoyi-ui/src/views/workflow/work/todo.vue
查询待办事务列表
后端代码WfProcessController.todoProcess–>WfProcessServiceImpl.queryPageTodoProcessList
/**
* 查询待办事务列表
* @param pageQuery 分页参数
* @return
*/
@Override
public TableDataInfo<WfTaskVo> queryPageTodoProcessList(PageQuery pageQuery) {
Page<WfTaskVo> page = new Page<>();
Long userId = LoginHelper.getUserId();
/**
SELECT RES.*, VAR.ID_ as VAR_ID_, VAR.NAME_ as VAR_NAME_, VAR.TYPE_ as VAR_TYPE_, VAR.REV_ as VAR_REV_,
VAR.PROC_INST_ID_ as VAR_PROC_INST_ID_, VAR.EXECUTION_ID_ as VAR_EXECUTION_ID_, VAR.TASK_ID_ as VAR_TASK_ID_,
VAR.BYTEARRAY_ID_ as VAR_BYTEARRAY_ID_, VAR.DOUBLE_ as VAR_DOUBLE_, VAR.TEXT_ as VAR_TEXT_, VAR.TEXT2_ as VAR_TEXT2_,
VAR.LONG_ as VAR_LONG_, VAR.SCOPE_ID_ AS VAR_SCOPE_ID_, VAR.SUB_SCOPE_ID_ AS VAR_SUB_SCOPE_ID_,VAR.SCOPE_TYPE_ AS VAR_SCOPE_TYPE_
FROM ( SELECT RES.* from ACT_RU_TASK RES WHERE RES.SUSPENSION_STATE_ = 1
and (RES.ASSIGNEE_ = '1604529718934958081' or ( RES.ASSIGNEE_ is null
and exists(select LINK.ID_ from ACT_RU_IDENTITYLINK LINK where LINK.TASK_ID_ = RES.ID_ and LINK.TYPE_ = 'candidate'
and (LINK.USER_ID_ = '1604529718934958081' or ( LINK.GROUP_ID_ IN ( 'ROLE2' , 'DEPT103' ) ) ))))
order by RES.CREATE_TIME_ desc LIMIT 10 OFFSET 0 ) RES left outer join ACT_RU_VARIABLE VAR
ON (RES.PROC_INST_ID_ = VAR.EXECUTION_ID_) order by RES.CREATE_TIME_ desc
* **/
TaskQuery taskQuery = taskService.createTaskQuery()
.active() //对应数据库中act_ru_task的 SUSPENSION_STATE_:暂停状态 1激活 2暂停)
.includeProcessVariables() //级联查询ACT_RU_VARIABLE表中的流程变量,条件为RES.PROC_INST_ID_ = VAR.EXECUTION_ID_
.taskCandidateOrAssigned(userId.toString()) //增加条件,级联查询ACT_RU_IDENTITYLINK表,查明当前登录的用户就是指派的审批用户
.taskCandidateGroupIn(TaskUtils.getCandidateGroup())//增加or条件,如果上面的条件不成立,则查明当前用户是不是在指定的组中
.orderByTaskCreateTime().desc();
//select count(distinct RES.ID_) from ACT_RU_TASK RES WHERE RES.SUSPENSION_STATE_ = 1 and (RES.ASSIGNEE_ = '1604529718934958081' or ( RES.ASSIGNEE_ is null and exists(select LINK.ID_ from ACT_RU_IDENTITYLINK LINK where LINK.TASK_ID_ = RES.ID_ and LINK.TYPE_ = 'candidate' and (LINK.USER_ID_ = '1604529718934958081' or ( LINK.GROUP_ID_ IN ( 'ROLE2' , 'DEPT103' ) ) ))))
page.setTotal(taskQuery.count());
int offset = pageQuery.getPageSize() * (pageQuery.getPageNum() - 1);
List<Task> taskList = taskQuery.listPage(offset, pageQuery.getPageSize());
List<WfTaskVo> flowList = new ArrayList<>();
for (Task task : taskList) {
WfTaskVo flowTask = new WfTaskVo();
// 当前流程信息
flowTask.setTaskId(task.getId());
flowTask.setTaskDefKey(task.getTaskDefinitionKey());
flowTask.setCreateTime(task.getCreateTime());
flowTask.setProcDefId(task.getProcessDefinitionId());
flowTask.setTaskName(task.getName());
// 流程定义信息
//act_re_procdef
// 业务流程定义数据表。此表和 ACT_RE_DEPLOYMENT 是多对一的关系,
// 即,一个部署的bar包里可能包含多个流程定义文件,每个流程定义文件都会有一条记录在 ACT_REPROCDEF 表内,
// 每个流程定义的数据,都会对于 ACT_GE_BYTEARRAY 表内的一个资源文件和 PNG 图片文件。
// 和 ACT_GE_BYTEARRAY 的关联是通过程序用ACT_GE_BYTEARRAY.NAME 与 ACT_RE_PROCDEF.NAME 完成的,在数据库表结构中没有体现。
//SELECT RES.* from ACT_RE_PROCDEF RES WHERE RES.ID_ = 'Process_1671384513972:2:7f26be5c-7efb-11ed-9a7d-b40edef8b08d' order by RES.ID_ asc
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(task.getProcessDefinitionId())
.singleResult();
flowTask.setDeployId(pd.getDeploymentId());
flowTask.setProcDefName(pd.getName());
flowTask.setProcDefVersion(pd.getVersion());
flowTask.setProcInsId(task.getProcessInstanceId());
// 流程发起人信息,可以通过通过获取流程实例历史记录中获得发起人的id
//SELECT RES.* , DEF.KEY_ as PROC_DEF_KEY_, DEF.NAME_ as PROC_DEF_NAME_, DEF.VERSION_ as PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ as DEPLOYMENT_ID_ from ACT_HI_PROCINST RES left outer join ACT_RE_PROCDEF DEF on RES.PROC_DEF_ID_ = DEF.ID_ WHERE RES.PROC_INST_ID_ = '89a35ffd-7efb-11ed-9a7d-b40edef8b08d' order by RES.ID_ asc
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.singleResult();
SysUser startUser = userService.selectUserById(Long.parseLong(historicProcessInstance.getStartUserId()));
flowTask.setStartUserId(startUser.getNickName());
flowTask.setStartUserName(startUser.getNickName());
flowTask.setStartDeptName(startUser.getDept().getDeptName());
// 流程变量
flowTask.setProcVars(this.getProcessVariables(task.getId()));
flowList.add(flowTask);
}
page.setRecords(flowList);
return TableDataInfo.build(page);
}
点击页面中的办理界面依然会进入detail.vue界面,传递的参数也是一样的:
任务办理
审批意见是必填的。
通过
如果是最后一步,流程直接结束。
前端界面依然是detail.vue
注意前端的表单有两个,一个是流程表单taskFormRef,一个是审核表单$refs[‘taskForm’],如果流程表单存在,则会把流程表单中的数据保存到taskForm.variables中一并提交到后台。
/** 通过任务 */
handleComplete() {
// 校验表单
const taskFormRef = this.$refs.taskFormParser;//取得当前任务节点表单,如果没有表单,则不需要校验
const isExistTaskForm = taskFormRef !== undefined; //表单不为空返回true
// 若无任务表单,则 taskFormPromise 为 true,即不需要校验
const taskFormPromise = !isExistTaskForm ? true : new Promise((resolve, reject) => {
taskFormRef.$refs[taskFormRef.formConfCopy.formRef].validate(valid => {
valid ? resolve() : reject()
})
});
//$refs['taskForm']是审批表单
const approvalPromise = new Promise((resolve, reject) => {
this.$refs['taskForm'].validate(valid => {
valid ? resolve() : reject()
})
});
Promise.all([taskFormPromise, approvalPromise]).then(() => {
//如果存在流程任务表单,则把流程变量存入审核表单的variables集合中。
if (isExistTaskForm) {
this.taskForm.variables = taskFormRef[taskFormRef.formConfCopy.formModel]
}
complete(this.taskForm).then(response => {
this.$modal.msgSuccess(response.msg);
this.goBack();
});
})
}
WfTaskController.complete—>WfTaskServiceImpl.complete
/**
* 完成任务
*
* @param taskBo 请求实体参数
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void complete(WfTaskBo taskBo) {
Task task = taskService.createTaskQuery().taskId(taskBo.getTaskId()).singleResult();
if (Objects.isNull(task)) {
throw new ServiceException("任务不存在");
}
//如果当前任务是委派任务,需要在addComment的第三个参数说明这是一个委派任务类型,否则就是普通任务
if (DelegationState.PENDING.equals(task.getDelegationState())) {
taskService.addComment(taskBo.getTaskId(), taskBo.getProcInsId(), FlowComment.DELEGATE.getType(), taskBo.getComment());
//如果是委派任务,则需要解决任务后返还给原来的办理人,所以用resolveTask方法
taskService.resolveTask(taskBo.getTaskId());
} else {
//1、在TaskService中addComment才是新增,saveComment是修改
//2、使用addComment时,应在操作流程之前。例如当前流程节点任务是完结操作时,应该先进行审批信息的新增addComment,再进行complete的完结操作
taskService.addComment(taskBo.getTaskId(), taskBo.getProcInsId(), FlowComment.NORMAL.getType(), taskBo.getComment());
//设置办理人
Long userId = LoginHelper.getUserId();
taskService.setAssignee(taskBo.getTaskId(), userId.toString());
//当前节点如果存在表单,则会以流程变量的方式存入complete方法中,传递给后面的节点进行处理。
if (ObjectUtil.isNotEmpty(taskBo.getVariables())) {
//taskService.complete(taskBo.getTaskId(), taskBo.getVariables(), true);
taskService.complete(taskBo.getTaskId(), taskBo.getVariables());
} else {
taskService.complete(taskBo.getTaskId());
}
}
// 设置任务节点名称
taskBo.setTaskName(task.getName());
// 处理抄送用户,makeCopy方法中会判断taskBo.getCopyUserIds()是否有需要抄送的用户,如果没有则直接结束该方法
if (!copyService.makeCopy(taskBo)) {
throw new RuntimeException("抄送任务失败");
}
}
通过后对数据库表的影响:
ACT_HI_COMMENT 历史意见表中保存审批意见
insert into ACT_HI_COMMENT (ID_, TYPE_, TIME_, USER_ID_, TASK_ID_, PROC_INST_ID_, ACTION_, MESSAGE_, FULL_MSG_) values ('21d96ae7-852e-11ed-8bd5-b40edef8b08d', '1', '2022-12-26T23:01:10.124+0800', NULL, '8e2b9465-843f-11ed-bb0a-b40edef8b08d', '08cb71cf-843f-11ed-bb0a-b40edef8b08d', 'AddComment', '同意', 'java.io.ByteArrayInputStream@79086c82')
ACT_HI_ACTINST历史流程活动节点表中新增当前节点(无论是任务节点,连线还是路由都会在这个表体现)
insert into ACT_HI_ACTINST ( ID_, REV_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, TRANSACTION_ORDER_, DURATION_, DELETE_REASON_, TENANT_ID_ ) values ('21e32ee8-852e-11ed-8bd5-b40edef8b08d', 1, 'Process_1671960329397:2:f96e2fbe-843e-11ed-bb0a-b40edef8b08d', '08cb71cf-843f-11ed-bb0a-b40edef8b08d', '08d499a0-843f-11ed-bb0a-b40edef8b08d', 'Flow_0pjrhu5', NULL, NULL, NULL, 'sequenceFlow', NULL, '2022-12-26T23:01:10.188+0800', '2022-12-26T23:01:10.188+0800', 1, 0, NULL, '') , ('21e37d09-852e-11ed-8bd5-b40edef8b08d', 1, 'Process_1671960329397:2:f96e2fbe-843e-11ed-bb0a-b40edef8b08d', '08cb71cf-843f-11ed-bb0a-b40edef8b08d', '08d499a0-843f-11ed-bb0a-b40edef8b08d', 'Event_1gd18xz', NULL, NULL, NULL, 'endEvent', NULL, '2022-12-26T23:01:10.190+0800', '2022-12-26T23:01:10.195+0800', 2, 5, NULL, '')
之后更新该节点的状态为已经完结,关键位end_time和耗时被赋值
update ACT_HI_ACTINST SET REV_ = 2, PROC_DEF_ID_ = 'Process_1671960329397:2:f96e2fbe-843e-11ed-bb0a-b40edef8b08d', END_TIME_ = '2022-12-26T23:01:10.183+0800', DURATION_ = 102468045 where ID_ = '8e2b9464-843f-11ed-bb0a-b40edef8b08d' and REV_ = 1
ACT_HI_TASKINST历史任务表,更新任务节点的end_time_,DURATION_等字段说明该节点已经完结。
update ACT_HI_TASKINST SET REV_ = 2, END_TIME_ = '2022-12-26T23:01:10.174+0800', DURATION_ = 102468036, LAST_UPDATED_TIME_ = '2022-12-26T23:01:10.174+0800' where ID_ = '8e2b9465-843f-11ed-bb0a-b40edef8b08d' and REV_ = 1
ACT_HI_PROCINST,如果如同本实例,当前节点已经是最后个任务了,历史流程实例表也会更新其end_time,表示当前流程实例已经完结
Execute SQL:update ACT_HI_PROCINST SET REV_ = 2, END_TIME_ = '2022-12-26T23:01:10.229+0800', DURATION_ = 102691858, END_ACT_ID_ = 'Event_1gd18xz' where ID_ = '08cb71cf-843f-11ed-bb0a-b40edef8b08d' and REV_ = 1
更新执行实例表ACT_RU_EXECUTION中的act_id指向,如果发现新的指向已经是end节点,则流程结束,之后会执行一系列的删除操作
update ACT_RU_EXECUTION SET REV_ = 3, ACT_ID_ = 'Event_1gd18xz', IS_ACTIVE_ = false, TASK_COUNT_ = 0 where ID_ = '08d499a0-843f-11ed-bb0a-b40edef8b08d' and REV_ = 2
如果流程结束,ru相关表会删除以下内容
ACT_RU_VARIABLE当前运行时流程中的所有变量
delete from ACT_RU_VARIABLE where EXECUTION_ID_ = '08cb71cf-843f-11ed-bb0a-b40edef8b08d'
删除运行中的任务节点
delete from ACT_RU_TASK where ID_ = '8e2b9465-843f-11ed-bb0a-b40edef8b08d' and REV_ = 1
delete from ACT_RU_TASK where EXECUTION_ID_ = '08cb71cf-843f-11ed-bb0a-b40edef8b08d'
ACT_RU_ACTINST 删除运行中实例的活动表中当前流程实例的所有数据
delete from ACT_RU_ACTINST where PROC_INST_ID_ = '08cb71cf-843f-11ed-bb0a-b40edef8b08d'
ACT_RU_EXECUTION 删除执行实例表中的主流程与分之流程信息
delete from ACT_RU_EXECUTION where ID_ = '08d499a0-843f-11ed-bb0a-b40edef8b08d' and REV_ = 3
delete from ACT_RU_EXECUTION where ID_ = '08cb71cf-843f-11ed-bb0a-b40edef8b08d' and REV_ = 5
退回
这里是从经理赵节点退回到最开始的请假单处理节点
点击退回按钮后,显示可以退回的节点:
WfTaskController.findReturnTaskList–>WfTaskServiceImpl.findReturnTaskList
/**
* 获取所有可回退的节点
*
* @param bo
* @return
*/
@Override
public List<UserTask> findReturnTaskList(WfTaskBo bo) {
// 当前任务 task
Task task = taskService.createTaskQuery().taskId(bo.getTaskId()).singleResult();
// 获取流程定义信息
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId()).singleResult();
// 获取所有节点信息,暂不考虑子流程情况
Process process = repositoryService.getBpmnModel(processDefinition.getId()).getProcesses().get(0);
Collection<FlowElement> flowElements = process.getFlowElements();
// 获取当前任务节点元素
UserTask source = null;
if (flowElements != null) {
for (FlowElement flowElement : flowElements) {
// 类型为用户节点
if (flowElement.getId().equals(task.getTaskDefinitionKey())) {
source = (UserTask) flowElement;
}
}
}
// 获取节点的所有路线
//FlowableUtils.findRoad
//source为当前节点,向前找
List<List<UserTask>> roads = FlowableUtils.findRoad(source, null, null, null);
// 可回退的节点列表
List<UserTask> userTaskList = new ArrayList<>();
for (List<UserTask> road : roads) {
if (userTaskList.size() == 0) {
// 还没有可回退节点直接添加
userTaskList = road;
} else {
// 如果已有回退节点,则比对取交集部分
userTaskList.retainAll(road);
}
}
final UserTask s = source;
userTaskList = userTaskList.stream().filter(tt->{
return FlowableUtils.iteratorCheckSequentialReferTarget(s, tt.getId(), null, null);
// if(tt.getId().equals("Activity_1walb8u")){
// FlowableUtils.iteratorCheckSequentialReferTarget(s, tt.getId(), null, null);
// }
// return true;
}).collect(Collectors.toList());
return userTaskList;
}
FlowableUtils.findRoad 从当前task节点开始,把当前task节点之前的所有usertask节点全部找到,存入集合。
/**
* 从后向前寻路,获取到达节点的所有路线
* 不存在直接回退到子流程,但是存在回退到父级流程的情况
* @param source 起始节点
* @param passRoads 已经经过的点集合
* @param roads 路线
* @return
*/
public static List<List<UserTask>> findRoad(FlowElement source, List<UserTask> passRoads, Set<String> hasSequenceFlow, List<List<UserTask>> roads) {
passRoads = passRoads == null ? new ArrayList<>() : passRoads;//其中一条线的节点
roads = roads == null ? new ArrayList<>() : roads;//所有线路的节点
hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow;//已经过的路线,用来判断重复
// 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代
if (source instanceof StartEvent && source.getSubProcess() != null) {
roads = findRoad(source.getSubProcess(), passRoads, hasSequenceFlow, roads);
}
// 根据类型,获取入口连线
List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
if (sequenceFlows != null && sequenceFlows.size() != 0) {
for (SequenceFlow sequenceFlow: sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (hasSequenceFlow.contains(sequenceFlow.getId())) {
continue;
}
// 添加已经走过的连线
hasSequenceFlow.add(sequenceFlow.getId());
// 添加经过路线
if (sequenceFlow.getSourceFlowElement() instanceof UserTask) {
passRoads.add((UserTask) sequenceFlow.getSourceFlowElement());
}
// 继续迭代
roads = findRoad(sequenceFlow.getSourceFlowElement(), passRoads, hasSequenceFlow, roads);
}
} else {
// 添加路线
roads.add(passRoads);
}
return roads;
}
FlowableUtils.iteratorCheckSequentialReferTarget 当前项目的退回模式是串行退回模式,举例,当前节点为source,要退回的节点为target,假设source有多条向前的路径,则所有路径必须都要经过target才算串行。当然,是单向串行流程,肯定也是没问题的。
/**
* 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行
* 不存在直接回退到子流程中的情况,但存在从子流程出去到父流程情况
* @param source 起始节点
* @param isSequential 是否串行
* @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复
* @param targetKsy 目标节点
* @return
*/
public static Boolean iteratorCheckSequentialReferTarget(FlowElement source, String targetKsy, Set<String> hasSequenceFlow, Boolean isSequential) {
isSequential = isSequential == null ? true : isSequential;
hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow;
// 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代
if (source instanceof StartEvent && source.getSubProcess() != null) {
isSequential = iteratorCheckSequentialReferTarget(source.getSubProcess(), targetKsy, hasSequenceFlow, isSequential);
}
// 根据类型,获取入口连线
List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
if (sequenceFlows != null) {
// 循环找到目标元素
for (SequenceFlow sequenceFlow: sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (hasSequenceFlow.contains(sequenceFlow.getId())) {
continue;
}
// 添加已经走过的连线
hasSequenceFlow.add(sequenceFlow.getId());
// 如果目标节点已被判断为并行,后面都不需要执行,直接返回
if (isSequential == false) {
break;
}
// 这条线路存在目标节点,这条线路完成,进入下个线路
if (targetKsy.equals(sequenceFlow.getSourceFlowElement().getId())) {
continue;
}
if (sequenceFlow.getSourceFlowElement() instanceof StartEvent) {
isSequential = false;
break;
}
// 否则就继续迭代
isSequential = iteratorCheckSequentialReferTarget(sequenceFlow.getSourceFlowElement(), targetKsy, hasSequenceFlow, isSequential);
}
}
return isSequential;
}
当选择节点并点击确认按钮后:
WfTaskServiceImpl.taskReturn
/**
* 退回任务
*
* @param bo 请求实体参数
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void taskReturn(WfTaskBo bo) {
// 当前任务 task
Task task = taskService.createTaskQuery().taskId(bo.getTaskId()).singleResult();
if (ObjectUtil.isNull(task)) {
throw new RuntimeException("获取任务信息异常!");
}
if (task.isSuspended()) {
throw new RuntimeException("任务处于挂起状态");
}
// 获取流程定义信息
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId()).singleResult();
// 获取所有节点信息
Process process = repositoryService.getBpmnModel(processDefinition.getId()).getProcesses().get(0);
// 获取全部节点列表,包含子节点
Collection<FlowElement> allElements = FlowableUtils.getAllElements(process.getFlowElements(), null);
// 获取当前任务节点元素
FlowElement source = null;
// 获取跳转的节点元素
FlowElement target = null;
if (allElements != null) {
for (FlowElement flowElement : allElements) {
// 当前任务节点元素
if (flowElement.getId().equals(task.getTaskDefinitionKey())) {
source = flowElement;
}
// 跳转的节点元素
if (flowElement.getId().equals(bo.getTargetKey())) {
target = flowElement;
}
}
}
// 从当前节点向前扫描
// 如果存在路线上不存在目标节点,说明目标节点是在网关上或非同一路线上,不可跳转
// 否则目标节点相对于当前节点,属于串行
Boolean isSequential = FlowableUtils.iteratorCheckSequentialReferTarget(source, bo.getTargetKey(), null, null);
if (!isSequential) {
throw new RuntimeException("当前节点相对于目标节点,不属于串行关系,无法回退");
}
// 获取所有正常进行的任务节点 Key,这些任务不能直接使用,需要找出其中需要撤回的任务
//taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list(); 获得所有的正在进行的任务节点
//SELECT RES.* from ACT_RU_TASK RES WHERE RES.PROC_INST_ID_ = '81a05c57-875a-11ed-bf30-b40edef8b08d' order by RES.ID_ asc
List<Task> runTaskList = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list();
List<String> runTaskKeyList = new ArrayList<>();
runTaskList.forEach(item -> runTaskKeyList.add(item.getTaskDefinitionKey()));
// 需退回任务列表
List<String> currentIds = new ArrayList<>();
// 通过父级网关的出口连线,结合 runTaskList 比对,获取需要撤回的任务
// target:要退回的节点,进入iteratorFindChildUserTasks方法后会顺着target节点向后找,每找到一个节点就会和runTaskKeyList里的所有节点做比较,
// 如果相同则确认runTaskKeyList中的这个任务节点就确实是要退回的节点
List<UserTask> currentUserTaskList = FlowableUtils.iteratorFindChildUserTasks(target, runTaskKeyList, null, null);
//取得要退回的任务节点的id
currentUserTaskList.forEach(item -> {
if(!currentIds.contains(item.getId())) currentIds.add(item.getId());
});
// 循环获取那些需要被撤回的节点的ID,用来设置驳回原因
// 因为currentIds是通过流程图方式取得,所以在和数据库中取得的任务节点做一次确认比对,确认id一致,则添加到currentTaskIds中
List<String> currentTaskIds = new ArrayList<>();
currentIds.forEach(currentId -> runTaskList.forEach(runTask -> {
if (currentId.equals(runTask.getTaskDefinitionKey())) {
currentTaskIds.add(runTask.getId());
}
}));
// 设置回退意见
for (String currentTaskId : currentTaskIds) {
taskService.addComment(currentTaskId, task.getProcessInstanceId(), FlowComment.REBACK.getType(), bo.getComment());
}
try {
// 1 对 1 或 多 对 1 情况,currentIds 当前要跳转的节点列表(1或多),targetKey 跳转到的节点(1)
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(task.getProcessInstanceId())
.moveActivityIdsToSingleActivityId(currentIds, bo.getTargetKey()).changeState();
} catch (FlowableObjectNotFoundException e) {
throw new RuntimeException("未找到流程实例,流程可能已发生变化");
} catch (FlowableException e) {
throw new RuntimeException("无法取消或开始活动");
}
// 设置任务节点名称
bo.setTaskName(task.getName());
// 处理抄送用户
if (!copyService.makeCopy(bo)) {
throw new RuntimeException("抄送任务失败");
}
}
影响的数据:
ACT_HI_COMMENT
添加审核意见,其中的FlowComment.REBACK.getType()值为2,对应TYPE_字段:
insert into ACT_HI_COMMENT (ID_, TYPE_, TIME_, USER_ID_, TASK_ID_, PROC_INST_ID_, ACTION_, MESSAGE_, FULL_MSG_) values ('ccaad2f3-8858-11ed-a984-b40edef8b08d', '2', '2022-12-30T23:44:09.060+0800', NULL, 'eb15f965-8781-11ed-9a28-b40edef8b08d', '81a05c57-875a-11ed-bf30-b40edef8b08d', 'AddComment', 'n', 'java.io.ByteArrayInputStream@7c1b7b29')
ACT_HI_TASKINST记录流转到的任务节点,ACT_HI_ACTINST记录流转到的节点
insert into ACT_HI_TASKINST ( ID_, REV_, TASK_DEF_ID_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, SCOPE_DEFINITION_ID_, PROPAGATED_STAGE_INST_ID_, NAME_, PARENT_TASK_ID_, DESCRIPTION_, OWNER_, ASSIGNEE_, START_TIME_, CLAIM_TIME_, END_TIME_, DURATION_, DELETE_REASON_, TASK_DEF_KEY_, FORM_KEY_, PRIORITY_, DUE_DATE_, CATEGORY_, TENANT_ID_, LAST_UPDATED_TIME_ ) values ( 'f5b23626-8858-11ed-a984-b40edef8b08d', 1, NULL, 'Process_1672237024526:4:97d6bea1-86bc-11ed-a6b8-b40edef8b08d', '81a05c57-875a-11ed-bf30-b40edef8b08d', 'f586e064-8858-11ed-a984-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, '表单提交', NULL, NULL, NULL, '1', '2022-12-30T23:45:17.620+0800', NULL, NULL, NULL, NULL, 'Activity_0uakd8d', 'key_1606944185392107522', 50, NULL, NULL, '', '2022-12-30T23:45:17.964+0800' )
insert into ACT_HI_ACTINST ( ID_, REV_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, TRANSACTION_ORDER_, DURATION_, DELETE_REASON_, TENANT_ID_ ) values ( 'f58818e5-8858-11ed-a984-b40edef8b08d', 1, 'Process_1672237024526:4:97d6bea1-86bc-11ed-a6b8-b40edef8b08d', '81a05c57-875a-11ed-bf30-b40edef8b08d', 'f586e064-8858-11ed-a984-b40edef8b08d', 'Activity_0uakd8d', 'f5b23626-8858-11ed-a984-b40edef8b08d', NULL, '表单提交', 'userTask', '1', '2022-12-30T23:45:17.619+0800', NULL, 1, NULL, NULL, '' )
ACT_HI_IDENTITYLINK 记录审核人id
insert into ACT_HI_IDENTITYLINK (ID_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, SCOPE_DEFINITION_ID_, CREATE_TIME_) values ('f5bcbd77-8858-11ed-a984-b40edef8b08d', 'assignee', '1', NULL, 'f5b23626-8858-11ed-a984-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, '2022-12-30T23:45:17.964+0800')
新增执行流程,运行时节点与任务节点,并更新之前退回节点的退回原因,endTime等信息
insert into ACT_RU_EXECUTION (ID_, REV_, PROC_INST_ID_, BUSINESS_KEY_, PROC_DEF_ID_, ACT_ID_, IS_ACTIVE_, IS_CONCURRENT_, IS_SCOPE_,IS_EVENT_SCOPE_, IS_MI_ROOT_, PARENT_ID_, SUPER_EXEC_, ROOT_PROC_INST_ID_, SUSPENSION_STATE_, TENANT_ID_, NAME_, START_ACT_ID_, START_TIME_, START_USER_ID_, IS_COUNT_ENABLED_, EVT_SUBSCR_COUNT_, TASK_COUNT_, JOB_COUNT_, TIMER_JOB_COUNT_, SUSP_JOB_COUNT_, DEADLETTER_JOB_COUNT_, EXTERNAL_WORKER_JOB_COUNT_, VAR_COUNT_, ID_LINK_COUNT_, CALLBACK_ID_, CALLBACK_TYPE_, REFERENCE_ID_, REFERENCE_TYPE_, PROPAGATED_STAGE_INST_ID_, BUSINESS_STATUS_) values ( 'f586e064-8858-11ed-a984-b40edef8b08d', 1, '81a05c57-875a-11ed-bf30-b40edef8b08d', NULL, 'Process_1672237024526:4:97d6bea1-86bc-11ed-a6b8-b40edef8b08d', 'Activity_0uakd8d', true, false, false, false, false, '81a05c57-875a-11ed-bf30-b40edef8b08d', NULL, '81a05c57-875a-11ed-bf30-b40edef8b08d', 1, '', NULL, NULL, '2022-12-30T23:45:17.593+0800', NULL, true, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL )
insert into ACT_RU_ACTINST ( ID_, REV_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, TRANSACTION_ORDER_, DURATION_, DELETE_REASON_, TENANT_ID_ ) values ( 'f58818e5-8858-11ed-a984-b40edef8b08d', 1, 'Process_1672237024526:4:97d6bea1-86bc-11ed-a6b8-b40edef8b08d', '81a05c57-875a-11ed-bf30-b40edef8b08d', 'f586e064-8858-11ed-a984-b40edef8b08d', 'Activity_0uakd8d', 'f5b23626-8858-11ed-a984-b40edef8b08d', NULL, '表单提交', 'userTask', '1', '2022-12-30T23:45:17.619+0800', NULL, 1, NULL, NULL, '' )
insert into ACT_RU_TASK (ID_, REV_, NAME_, PARENT_TASK_ID_, DESCRIPTION_, PRIORITY_, CREATE_TIME_, OWNER_, ASSIGNEE_, DELEGATION_, EXECUTION_ID_, PROC_INST_ID_, PROC_DEF_ID_, TASK_DEF_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, SCOPE_DEFINITION_ID_, PROPAGATED_STAGE_INST_ID_, TASK_DEF_KEY_, DUE_DATE_, CATEGORY_, SUSPENSION_STATE_, TENANT_ID_, FORM_KEY_, CLAIM_TIME_, IS_COUNT_ENABLED_, VAR_COUNT_, ID_LINK_COUNT_, SUB_TASK_COUNT_) values ('f5b23626-8858-11ed-a984-b40edef8b08d', 1, '表单提交', NULL, NULL, 50, '2022-12-30T23:45:17.620+0800', NULL, '1', NULL, 'f586e064-8858-11ed-a984-b40edef8b08d', '81a05c57-875a-11ed-bf30-b40edef8b08d', 'Process_1672237024526:4:97d6bea1-86bc-11ed-a6b8-b40edef8b08d', NULL, NULL, NULL, NULL, NULL, NULL, 'Activity_0uakd8d', NULL, NULL, 1, '', 'key_1606944185392107522', NULL, true, 0, 0, 0 )
update ACT_RU_ACTINST SET REV_ = 2, PROC_DEF_ID_ = 'Process_1672237024526:4:97d6bea1-86bc-11ed-a6b8-b40edef8b08d', END_TIME_ = '2022-12-30T23:45:17.487+0800', DURATION_ = 92359191, DELETE_REASON_ = 'Change activity to Activity_0uakd8d' where ID_ = 'eb15d254-8781-11ed-9a28-b40edef8b08d' and REV_ = 1
update ACT_HI_TASKINST SET REV_ = 2, END_TIME_ = '2022-12-30T23:45:17.534+0800', DURATION_ = 92359237, DELETE_REASON_ = 'Change activity to Activity_0uakd8d', LAST_UPDATED_TIME_ = '2022-12-30T23:45:17.534+0800' where ID_ = 'eb15f965-8781-11ed-9a28-b40edef8b08d' and REV_ = 1
新增与更新操作完成后,最后删除已经被退回的节点的task以及之前的执行流程
delete from ACT_RU_TASK where EXECUTION_ID_ = '2485cbcb-8768-11ed-981d-b40edef8b08d'
delete from ACT_RU_EXECUTION where ID_ = '2485cbcb-8768-11ed-981d-b40edef8b08d' and REV_ = 4
委派
委派:是将任务节点分给其他人处理,等其他人处理好之后,委派任务会自动回到委派人的任务中
发起后端请求之前,设置了userId,表示要委派或转办的用户,对应后端的WfTaskBo.userId
WfTaskController.delegate --> WfTaskServiceImpl.delegateTask
public void delegateTask(WfTaskBo bo) {
// 当前任务 task
Task task = taskService.createTaskQuery().taskId(bo.getTaskId()).singleResult();
if (ObjectUtil.isEmpty(task)) {
throw new ServiceException("获取任务失败!");
}
StringBuilder commentBuilder = new StringBuilder(LoginHelper.getNickName())
.append("->");
SysUser user = sysUserService.selectUserById(Long.parseLong(bo.getUserId()));
if (ObjectUtil.isNotNull(user)) {
commentBuilder.append(user.getNickName());
} else {
commentBuilder.append(bo.getUserId());
}
if (StringUtils.isNotBlank(bo.getComment())) {
commentBuilder.append(": ").append(bo.getComment());
}
// 添加审批意见
taskService.addComment(bo.getTaskId(), task.getProcessInstanceId(), FlowComment.DELEGATE.getType(), commentBuilder.toString());
// 设置办理人为当前登录人
taskService.setOwner(bo.getTaskId(), LoginHelper.getUserId().toString());
// 执行委派
taskService.delegateTask(bo.getTaskId(), bo.getUserId());
// 设置任务节点名称
bo.setTaskName(task.getName());
// 处理抄送用户
if (!copyService.makeCopy(bo)) {
throw new RuntimeException("抄送任务失败");
}
}
act_ru_task表中,woner是当前登录人, assignee_指派给了经理赵的id,DELEGATION_代理字段也会标注为PENDING,表示委派后正在办理中
经理赵登录同意后
委派任务会显示RESOLVED表示已经完成,指派人也恢复为原来的ID
原来的委派人“若依”登录后,又会重新看到需要办理的任务,这样就可以参考之前委派人的审批批注进行判断处理了。
转办
直接将办理人assignee 换成别人,这时任务的拥有者不再是转办人,而是为空,相当与将任务转出。
直接将assignee =” zhuanban”
taskService.setAssignee(taskId, userId);
数据库中也会发现,依然存在拥有者,指派人id变成了经理赵的id,但DELEGATION_一直为空,也会有转办人的id.
无论是委派还是转办,都需要指定至少一个办理人,所以需要传递一个userID.
WfTaskController.transfer–>WfTaskServiceImpl.transferTask
public void transferTask(WfTaskBo bo) {
// 当前任务 task
Task task = taskService.createTaskQuery().taskId(bo.getTaskId()).singleResult();
if (ObjectUtil.isEmpty(task)) {
throw new ServiceException("获取任务失败!");
}
StringBuilder commentBuilder = new StringBuilder(LoginHelper.getNickName())
.append("->");
SysUser user = sysUserService.selectUserById(Long.parseLong(bo.getUserId()));
if (ObjectUtil.isNotNull(user)) {
commentBuilder.append(user.getNickName());
} else {
commentBuilder.append(bo.getUserId());
}
if (StringUtils.isNotBlank(bo.getComment())) {
commentBuilder.append(": ").append(bo.getComment());
}
// 添加审批意见
taskService.addComment(bo.getTaskId(), task.getProcessInstanceId(), FlowComment.TRANSFER.getType(), commentBuilder.toString());
// 设置拥有者为当前登录人
taskService.setOwner(bo.getTaskId(), LoginHelper.getUserId().toString());
// 转办任务
taskService.setAssignee(bo.getTaskId(), bo.getUserId());
// 设置任务节点名称
bo.setTaskName(task.getName());
// 处理抄送用户
if (!copyService.makeCopy(bo)) {
throw new RuntimeException("抄送任务失败");
}
}
拒绝
拒绝后流程直接结束
WfTaskController.taskReject–>WfTaskServiceImpl.taskReject
代码中,先判断任务是否挂起,流程是否已经结束。之后,获取所有的执行实例,将其运行到end节点,流程结束。流程结束后,和complete类似,都会删除act_ru_**中的相关数据。
public void taskReject(WfTaskBo taskBo) {
log.info("判断任务是否存在,是否为挂起,如果不存在或已挂起,则报相应的异常");
// 当前任务 task
Task task = taskService.createTaskQuery().taskId(taskBo.getTaskId()).singleResult();//ACT_RU_TASK
if (ObjectUtil.isNull(task)) {
throw new RuntimeException("获取任务信息异常!");
}
if (task.isSuspended()) {
throw new RuntimeException("任务处于挂起状态");
}
// 获取流程实例,查询流程实例的状态,如果为空则该流程实例已经结束
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() //ACT_RU_EXECUTION
.processInstanceId(taskBo.getProcInsId())
.singleResult();
if (processInstance == null) {
throw new RuntimeException("流程实例不存在,请确认!");
}
// 获取流程定义信息
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(task.getProcessDefinitionId())
.singleResult();
// 添加审批意见
taskService.addComment(taskBo.getTaskId(), taskBo.getProcInsId(), FlowComment.REJECT.getType(), taskBo.getComment());
// 获取所有节点信息
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
//找到终止节点
EndEvent endEvent = ModelUtils.getEndEvent(bpmnModel);
// 终止流程
//获取运行时执行实例表
List<Execution> executions = runtimeService.createExecutionQuery().parentId(task.getProcessInstanceId()).list();
List<String> executionIds = executions.stream().map(Execution::getId).collect(Collectors.toList());
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(task.getProcessInstanceId())
//将执行移动到单个活动 ID 将当前的执行实例 全部移动到结束节点
.moveExecutionsToSingleActivityId(executionIds, endEvent.getId())
//改变状态
.changeState();
// 处理抄送用户
if (!copyService.makeCopy(taskBo)) {
throw new RuntimeException("抄送任务失败");
}
}
至此,flowable的基础部分结束。最后可以对当前流程存在的问题进行修正,做一个例子即可。
修复的bug
1.过滤空表单
修复了流程表单追踪出现空表单的bug,开始节点必须有表单,而退会无法重新回到开始节点,只能在第一个节点重新设置一个表单,第一个节点又是发起人审核会直接通过,所以这里会出现一个空表单,在显示历史表单的时候,需要过滤掉。
WfProcessServiceImpl—>filterProcFormList
2.显示所有部门和用户
作者直接使用了原系统中提供的方法获取部门tree和对应的用户列表,原方法由于使用了数据权限所以只能显示该用户所在部门及子部门的相关部门与用户数据,无法满足当前工作流需求,故新创建了两个方法替换了原来的方法。
更多操作
附
移植微服务版本需要的sql
insert into sys_menu values('1621060895732707328', '流程管理', '0', '4', 'process', '', '', 1, 0, 'M', '0', '0', '', 'skill', 'admin', sysdate(), '', null, '流程管理目录');
insert into sys_menu values('1621060895745290240', '流程分类', '1621060895732707328', '1', 'category', 'workflow/category/index', '', 1, 0, 'C', '0', '0', 'workflow:category:list', 'nested', 'admin', sysdate(), '', null, '流程分类菜单');
insert into sys_menu values('1621060895749484544', '分类查询', '1621060895745290240', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:category:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895757873152', '分类新增', '1621060895745290240', '2', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:category:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895762067456', '分类编辑', '1621060895745290240', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:category:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895778844672', '分类删除', '1621060895745290240', '4', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:category:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895787233280', '表单配置', '1621060895732707328', '2', 'form', 'workflow/form/index', '', 1, 0, 'C', '0', '0', 'workflow:form:list', 'form', 'admin', sysdate(), '', null, '表单配置菜单');
insert into sys_menu values('1621060895799816192', '表单查询', '1621060895787233280', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:form:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895804010496', '表单新增', '1621060895787233280', '2', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:form:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895812399104', '表单修改', '1621060895787233280', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:form:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895820787712', '表单删除', '1621060895787233280', '4', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:form:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895824982016', '表单导出', '1621060895787233280', '5', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:form:export', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895829176320', '流程模型', '1621060895732707328', '3', 'model', 'workflow/model/index', '', 1, 0, 'C', '0', '0', 'workflow:model:list', 'component', 'admin', sysdate(), '', null, '流程模型菜单');
insert into sys_menu values('1621060895837564928', '模型查询', '1621060895829176320', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895841759232', '模型新增', '1621060895829176320', '2', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895850147840', '模型修改', '1621060895829176320', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895854342144', '模型删除', '1621060895829176320', '4', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895858536448', '模型导出', '1621060895829176320', '5', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:export', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895862730752', '模型导入', '1621060895829176320', '6', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:import', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895866925056', '模型设计', '1621060895829176320', '7', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:designer', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060895871119360', '模型保存', '1621060895829176320', '8', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:save', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898433839104', '流程部署', '1621060895829176320', '9', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:model:deploy', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898438033408', '部署管理', '1621060895732707328', '4', 'deploy', 'workflow/deploy/index', '', 1, 0, 'C', '0', '0', 'workflow:deploy:list', 'example', 'admin', sysdate(), '', null, '部署管理菜单');
insert into sys_menu values('1621060898442227712', '部署查询', '1621060898438033408', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:deploy:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898450616320', '部署删除', '1621060898438033408', '2', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:deploy:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898454810624', '更新状态', '1621060898438033408', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:deploy:status', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898459004928', '办公管理', '0', '5', 'work', '', '', 1, 0, 'M', '0', '0', '', 'job', 'admin', sysdate(), '', null, '办公管理目录');
insert into sys_menu values('1621060898467393536', '新建流程', '1621060898459004928', '1', 'create', 'workflow/work/index', '', 1, 0, 'C', '0', '0', 'workflow:process:startList', 'guide', 'admin', sysdate(), '', null, '新建流程菜单');
insert into sys_menu values('1621060898475782144', '发起流程', '1621060898467393536', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:start', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898484170752', '我的流程', '1621060898459004928', '2', 'own', 'workflow/work/own', '', 1, 0, 'C', '0', '0', 'workflow:process:ownList', 'cascader', 'admin', sysdate(), '', null, '我的流程菜单');
insert into sys_menu values('1621060898488365056', '流程详情', '1621060898484170752', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898496753664', '流程删除', '1621060898484170752', '2', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898500947968', '流程取消', '1621060898484170752', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:cancel', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898505142272', '待办任务', '1621060898459004928', '3', 'todo', 'workflow/work/todo', '', 1, 0, 'C', '0', '0', 'workflow:process:todoList', 'time-range', 'admin', sysdate(), '', null, '待办任务菜单');
insert into sys_menu values('1621060898509336576', '流程办理', '1621060898505142272', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:approval', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898513530880', '待签任务', '1621060898459004928', '4', 'claim', 'workflow/work/claim', '', 1, 0, 'C', '0', '0', 'workflow:process:claimList', 'checkbox', 'admin', sysdate(), '', null, '待签任务菜单');
insert into sys_menu values('1621060898517725184', '流程签收', '1621060898513530880', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:claim', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898521919488', '已办任务', '1621060898459004928', '5', 'finished', 'workflow/work/finished', '', 1, 0, 'C', '0', '0', 'workflow:process:finishedList', 'checkbox', 'admin', sysdate(), '', null, '已办任务菜单');
insert into sys_menu values('1621060898526113792', '流程撤回', '1621060898521919488', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:revoke', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values('1621060898530308096', '抄送我的', '1621060898459004928', '6', 'copy', 'workflow/work/copy', '', 1, 0, 'C', '0', '0', 'workflow:process:copyList', 'checkbox', 'admin', sysdate(), '', null, '抄送我的菜单');
insert into sys_menu values('1621060898538696704', '流程导出', '1621060898530308096', '1', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:process:export', '#', 'admin', sysdate(), '', null, '');