SpringBoot版本: 2.0.1.RELEASE
Flowable版本: 6.3.1
Activiti版本: 6.0.0
一.添加pom依赖
因为之前我整合的时候有报错关于sqlsession的错误,后面查询文章才发现flowable要排除掉mybatis,又没说具体排除哪一个,所以我这干脆全部排除了
<!-- Flowable dependencies --><dependency><groupId>org.flowable</groupId><artifactId>flowable-spring-boot-starter</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-engine</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-spring</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-rest</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-cmmn-spring</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-dmn-spring</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency>
<!--activiti modeler start--><dependency><groupId>org.activiti</groupId><artifactId>activiti-json-converter</artifactId><version>6.0.0</version><exclusions><exclusion><groupId>org.activiti</groupId><artifactId>activiti-bpmn-model</artifactId></exclusion></exclusions></dependency>
二.yml配置flowable/Activiti相关配置项
至于层级关系,都是和spring同级的,我一般放在mybatis配置项下面,类似下面这样
flowable:#关闭定时任务JOBasync-executor-activate: false# 将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。database-schema-update: true# activiti 模块activiti:# 解决启动报错:class path resource [processes/] cannot be resolved to URL because it does not existcheck-process-definitions: false# 检测身份信息表是否存在db-identity-used: true# 默认为false# false:项目启动时,如果数据库中没有表将抛出异常(上线后使用)# true:项目启动时,如果数据库中没有表将自动生成(开发和测试时使用)database-schema-update: true
三.数据库配置项中url添加&nullCatalogMeansCurrent=true配置
因为我之前用过activiti工作流,如果不加这个配置工作流会默认查询整个数据库连接下所有的库,只要发现有相关的act表就会认为已经自动生成了. 导致你在A库有了工作流的表之后想在B库再去自动生成就会发现生成不了.加上这个配置就可以了,不知道flowable影不影响,我都习惯给加上了
# nullCatalogMeansCurrent=true # 因为mysql使用schema标识库名而不是catalog,因此mysql会扫描所有的库来找表, # 如果其他库中有相同名称的表,activiti就以为找到了,本质上这个表在当前数据库中并不存在。 # 解决办法就是在数据库链接的配置后面加上 nullCatalogMeansCurrent=true,标识之搜索当前链接的库。
url: jdbc:mysql://192.168.*.*:3306/flowable?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
四.启动项目 自动生成数据表
这个会根据flowable的版本而来 实际可能不一致 我的flowable版本是6.3.1,生成了66张表
Activiti版本是6.0.0,生成了28张表
一些常用的需要关注的表的含义(注明了注释)
五.IDEA中安装Flowable/Activiti插件
安装插件: Flowable BPMN visualizer / Activiti BPMN visualizer
然后在resources目录下新建processes目录
右键新建一个基于BOMN 2.0协议的文件
绘制流程图
注意: flowable/Activiti都会将处于processes目录下的符合后缀名要求的文件自动部署
不想要这个功能你可以修改目录名或者查询网站搜索下相关的关闭命令
六.工作流提供的几个Service
Flowable和Activiti都给我们提供了几个Service
因为本身Flowable的团队就是从Activiti团队中过来的,所以Service的命名及使用基本都是一致的,会一个就会另一个
// 运行时执行管理Service@Autowiredprivate RuntimeService runtimeService;// 工作流任务管理Service@Autowiredprivate TaskService taskService;// 管理流程定义Service@Autowiredprivate RepositoryService repositoryService;// 核心类 可以理解为流程引擎对象@Autowiredprivate ProcessEngine processEngine;// 历史管理Service@Autowiredprivate HistoryService historyService;
七.常用方法
因为几乎所有业务都需要用到"PROC_INST_ID_" 也就是 "流程实例Id"
以及当前任务id-TASK_ID
所以我建议把这两个id都放在业务表中和你的业务表一对一关联(一般都是一个业务开一个流程实例)
1.动态部署
(提供一个文件,通过repositoryService创建部署对象去读取xml文件的内容来自动部署)
部署完之后ACT_RE_DEPLOYMENT,ACT_RE_PROCDEF两个表中会有数据
MultipartFile file; // 提供一个二进制文件对象
Deployment deployment = null;try{DeploymentBuilder deploymentBuilder =repositoryService.createDeployment()
.name(Objects.requireNonNull(file.getOriginalFilename()).substring(0, file.getOriginalFilename().indexOf(".bpmn20.xml"))).addInputStream(file.getOriginalFilename(),file.getInputStream());deployment = deploymentBuilder.deploy();}catch (Exception e){e.printStackTrace();throw new CustomException("文件部署失败");}// 部署完后可以获取部署ID
// 根据部署ID查询流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();
System.out.println("流程【"+processDefinition.getName()+"】部署成功, 部署ID为: "+processDefinition.getDeploymentId()+"流程定义Key为: "+processDefinition.getKey());
2.流程启动
Map<String, Object> variables = new HashMap<>();variables.put("startUser", SecurityUtils.getLoginUser().getUser().getUserId().toString());variables.put("data", processInit.getData());variables.put("remark", processInit.getRemark());ProcessInstance processInstance = null;try{// 根据部署ID查询流程定义ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(processInit.getDeployId()).singleResult();//启动流程实例,同时还要指定业务标识businessKey 项目Id//第一个参数:是指流程定义key//第二个参数:业务标识businessKeyprocessInstance = runtimeService.startProcessInstanceByKey(processDefinition.getKey(),processInit.getWorkId().toString(),variables);runtimeService.setProcessInstanceName(processInstance.getId(),processDefinition.getName());
启动后ACT_RU_EXECUTION会看到流程实例当前执行的节点概况,包括历史节点和当前时节点
PROC_INST_ID_是流程实例Id
这两个表也可以看到实例相关节点信息
3.执行下一任务节点并添加审核备注
核心方法为:
taskService.complete(task.getId());
需要注意的是:如果没有执行人,需要先认领任务而后才去审核任务
SysUser user = SecurityUtils.getLoginUser().getUser();String taskId; // 当前任务IdString processInstanceId; // 流程实例IdProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();String businessKey = processInstance.getBusinessKey(); // 流程实例的业务Id-businessKey// 获取当前待办任务Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).taskId(taskId).singleResult();if(task == null){throw new CustomException(CustomExceptionEnums.DATA_NOT_FOUND);}String assignee = task.getAssignee();if(StringUtils.isEmpty(assignee)){// 如果没有执行人 需要先认领任务taskService.claim(task.getId(),user.getUserId().toString());}if(StringUtils.isNotEmpty(processApproved.getRemark())){// 添加审批备注taskService.addComment(taskId, processInstanceId, ActivityCommentType.APPROVED_REMARK.getType(), processApproved.getRemark());}// 完成当前任务taskService.complete(task.getId());
通过后可通过taskService获取下一个任务节点Id
// 获取下一个任务ID List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list();
4.获取待办/已办
因为Activiti和Flowable中并没有获取待办任务和已办任务的直接方法可以调用
所以需要我们自己写sql语句来获取
需要用到以下的表
待办任务所需
"ACT_RU_TASK"运行时任务节点表
"ACT_RU_IDENTITYLINK"运行时流程人员表
"ACT_HI_PROCINST"历史流程实例表
已办任务所需
"ACT_HI_TASKINST"历史任务节点表
"ACT_HI_PROCINST"历史流程实例表
然后我自己通过union将结果合并,并通过task_status来区分待办和已办
-- 待办和已办union all查询
select * from
(
select 0 as task_status,
RES.ID_ as task_id,RES.NAME_ as task_name,RES.CREATE_TIME_ as start_time,null as end_time,RES.PROC_INST_ID_ as process_instance_id,RES.ASSIGNEE_ as assignee,P.BUSINESS_KEY_ as business_key ,P.NAME_ as process_instance_name ,I.TYPE_ as type,I.USER_ID_ as user_id, I.GROUP_ID_ as group_id ,RES.TASK_DEF_KEY_ as task_def_key
from ACT_RU_TASK RES
left join ACT_RU_IDENTITYLINK I on I.TASK_ID_ = RES.ID_
left join ACT_HI_PROCINST P on P.PROC_INST_ID_ = RES.PROC_INST_ID_ UNION ALLselect 1 as task_status,
RES.ID_ as task_id,RES.NAME_ as task_name,RES.START_TIME_ as start_time,RES.END_TIME_ as end_time,RES.PROC_INST_ID_ as process_instance_id,RES.ASSIGNEE_ as assignee,P.BUSINESS_KEY_ as business_key ,P.NAME_ as process_instance_name ,null as type,null as user_id, null as group_id,RES.TASK_DEF_KEY_ as task_def_key
from ACT_HI_TASKINST RES
left join ACT_HI_PROCINST P on P.PROC_INST_ID_ = RES.PROC_INST_ID_ and RES.END_TIME_ is not null
)v
然后可以通过条件查询,比如"执行人","执行部门","任务节点定义key"等来进行查询
(task_status : 待办/已办)
(assignee : 执行人Id)
(task_def_key : 任务节点定义key)
(type : 表示候选用户
candidate
: 表示候选用户或用户组。在任务尚未被领取时,这些用户或用户组可以领取任务。participant
: 表示任务的参与者。通常情况下,当任务被领取后,参与者就是具体执行任务的用户。owner
: 表示任务的所有者。在某些情况下,任务可能会指定一个所有者,通常是在任务创建时指定,这个所有者有特殊权限或责任来处理任务。)
(group_id : 部门Id)
(start_time : 任务节点生成时间)
(end_time : 任务节点完成时间)
比如以下的where条件可以用作待办任务的筛选查询
WHERE v.task_status = 0 and v.assignee = '1' and v.task_def_key= 'sid-5db27d85-b516-4784-8b58-0f5c9e1c3235' or (v.assignee is null and v.type = 'candidate' and (v.user_id = '1' or v.group_id IN ( '土整中心','123' ) )) order by v.start_time desc
比如以下的where条件可以用作已办任务的筛选查询
WHERE v.task_status = 1 and v.assignee = '1' and v.task_def_key= 'sid-8abc3887-40c5-49c2-b8c0-b4d3fdefb7a0' and v.end_time is not null order by v.end_time desc
5. 通过taskId获取该任务的备注
/*** 通过taskId获取该任务的备注* @param taskId 任务Id* @param commentType 备注类型* @return 备注详情*/@Overridepublic List<Comment> getCommentByTaskId(String taskId,String commentType){if(StringUtils.isNotEmpty(commentType)){// 带有type筛选return taskService.getTaskComments(taskId, commentType);}return taskService.getTaskComments(taskId);}
6.查询截止目前可驳回的节点列表
我的逻辑是查询当前已完成的用户任务,不包括"开始"节点(因为"开始"节点不是用户任务)
然后有一些特殊情况需要处理:
比如多次驳回,因为每一次驳回实际上都是将当前任务节点完成然后再加一条"目标驳回任务节点"用作下一个节点
比如多次驳回之后取最新的节点,不可能出现可驳回列表中出现两个key相同的节点,这不符合逻辑
/*** 查询截止目前可驳回的节点列表* @param processInstanceId 流程ID* @return 结果*/@Overridepublic List<ActHistoricActivityInstanceVo> getAllActInstByInstanceIdToSimple(String processInstanceId,String taskDefKey){// 只要用户任务// 目前已完成的用户任务List<HistoricActivityInstance> userTask = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().activityType("userTask").finished().list();// 流程定义时的用户任务ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();String processDefinitionId = processInstance.getProcessDefinitionId();ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(processDefinitionId).singleResult();// 定义时的所有节点Collection<FlowElement> flowElements = repositoryService.getBpmnModel(processDefinitionId).getMainProcess().getFlowElements();List<ActHistoricActivityInstanceVo> userTasks = new ArrayList<>();// 循环for (HistoricActivityInstance historicActivityInstance : userTask) {for (FlowElement flowElement : flowElements) {if (flowElement instanceof UserTask) {UserTask definitionUserTask = (UserTask) flowElement;String activityName = definitionUserTask.getName();String activityId = definitionUserTask.getId();String activityTaskDefKey = definitionUserTask.getId();// 可以查看其他用户任务属性,如Assignee、Candidate Users、Candidate Groups等// 获取任务定义对象信息// 入参taskDefKey对应的对象SysProcessTaskDef processTaskDef = sysProcessTaskDefMapper.selectSysProcessTaskDefById(taskDefKey);// 当前循环体中taskDefKey对应的对象SysProcessTaskDef currentTaskDef = sysProcessTaskDefMapper.selectSysProcessTaskDefById(activityTaskDefKey);if(currentTaskDef.getSort() >= processTaskDef.getSort()){// 不能驳回到后面的任务去(防止二次提交后可驳回的任务列表乱套)continue;}// 如果Id匹配 就加入自定义集合返回if(activityId.equals(historicActivityInstance.getActivityId()) && !activityTaskDefKey.equals(taskDefKey)){ActHistoricActivityInstanceVo activityInstanceVo = new ActHistoricActivityInstanceVo();activityInstanceVo.setActivityId(activityId);activityInstanceVo.setActivityName(activityName);// 对重复的跳过 (针对多次驳回导致存在多个相同节点的情况)
// if(userTasks.contains(activityInstanceVo)) continue;ActHistoricActivityInstanceVo actHistoricActivityInstanceVo = userTasks.stream().filter(u -> u.getActivityId().equals(activityInstanceVo.getActivityId())).findAny().orElse(null);if(actHistoricActivityInstanceVo != null && actHistoricActivityInstanceVo.getStartTime().compareTo(historicActivityInstance.getStartTime()) < 0){// 存在并且时间早于这一次就删除 (针对多次驳回导致存在多个相同节点的情况,取最新的节点审核情况)userTasks.remove(actHistoricActivityInstanceVo);}// 转换赋值BeanUtils.copyProperties(historicActivityInstance, activityInstanceVo);// 获取type为isFillSteps的comment(是否为填报步骤:提交填报时的那个步骤)List<Comment> comments = taskService.getTaskComments(activityInstanceVo.getTaskId(), "isFillSteps");if(comments != null && comments.size() > 0){activityInstanceVo.setFillSteps(true);}userTasks.add(activityInstanceVo);}}}}if(userTasks.size() > 0){userTasks = userTasks.stream().sorted(Comparator.comparing(ActHistoricActivityInstanceVo::getStartTime)).collect(Collectors.toList());}return userTasks;}
7.驳回到指定节点
提供流程实例Id,当前任务Id,驳回目标任务Id
/*** 驳回到指定节点* @param actRollbackIo 驳回对象*/@Override@Transactional(rollbackFor = Throwable.class,propagation = Propagation.REQUIRES_NEW)public void rejectToNode(ActRollbackIo actRollbackIo){String processInstanceId = actRollbackIo.getProcessInstanceId();String currentTaskId = actRollbackIo.getTaskId(); // 当前任务String targetTaskId = actRollbackIo.getTargetTaskId(); // 驳回目标任务// 获取该流程所有的活动实例List<HistoricActivityInstance> hisActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).list();ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();String processDefinitionId = processInstance.getProcessDefinitionId();// 当前任务Task currentTask = taskService.createTaskQuery().taskId(currentTaskId).singleResult();// 驳回目标任务HistoricTaskInstance targetTask = historyService.createHistoricTaskInstanceQuery().finished().taskId(targetTaskId).singleResult();if(currentTask == null || targetTask == null){throw new CustomException(CustomExceptionEnums.MISSING_DATA_ERROR);}try{// 当前活动HistoricActivityInstance currentActivity = hisActivityList.stream().filter(e -> currentTask.getId().equals(e.getTaskId())).collect(Collectors.toList()).get(0);// 驳回目标活动HistoricActivityInstance targetActivity = hisActivityList.stream().filter(e -> targetTask.getId().equals(e.getTaskId())).collect(Collectors.toList()).get(0);// 获取xml文件对象BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);// 获取驳回目标活动节点FlowNode targetFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(targetActivity.getActivityId());// 获取当前活动节点FlowNode currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currentActivity.getActivityId());// 临时保存当前活动的原始方向List<SequenceFlow> originalSequenceFlowList = new ArrayList<>();originalSequenceFlowList.addAll(currentFlowNode.getOutgoingFlows());// 清理活动方向currentFlowNode.getOutgoingFlows().clear();// 建立新方向SequenceFlow newSequenceFlow = new SequenceFlow();newSequenceFlow.setId("newSequenceFlowId");newSequenceFlow.setSourceFlowElement(currentFlowNode);newSequenceFlow.setTargetFlowElement(targetFlowNode);List<SequenceFlow> newSequenceFlowList = new ArrayList<>();newSequenceFlowList.add(newSequenceFlow);// 当前节点指向新的方向currentFlowNode.setOutgoingFlows(newSequenceFlowList);// 完成当前任务taskService.claim(currentTaskId,SecurityUtils.getLoginUser().getUser().getUserId().toString());taskService.complete(currentTaskId);// 重新查询当前任务Task nextTask = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();String assignee = (StringUtils.isNull(actRollbackIo.getAssignee())) ? targetTask.getAssignee() : actRollbackIo.getAssignee().toString();if (null != nextTask) {System.out.println("最新任务=="+nextTask);taskService.setAssignee(nextTask.getId(), assignee);}else{throw new CustomException(CustomExceptionEnums.OPERATION_FAILED);}// 恢复原始方向currentFlowNode.setOutgoingFlows(originalSequenceFlowList);
}
8.撤回 删除流程
/*** 撤回 删除流程* @param processInstanceId 流程实例Id* @param fillId 任务填报Id* @param message 撤回备注信息*/@Override@Transactional(rollbackFor = Throwable.class)public int revoke(String processInstanceId,Long fillId,String message){runtimeService.deleteProcessInstance(processInstanceId, message);historyService.deleteHistoricProcessInstance(processInstanceId);return landProjectFillService.cleanProcessInstance(fillId);}
后续有新的方法再来补充