本文以一个简答的 Demo 为案例,按节点讲解,目的是为了让刚接触流程引擎的人能更快的熟悉流程引擎开发,了解业务系统与流程引擎结合的思路。
由于是 Demo,接口存在不完善之处,需要自己补充添加。
文章:Flowable 快速入门教程:SpringBoot 集成 Flowable + Flowable Modeler 流程配置可视化(超详细)
这里的分配人为固定的人,用的使用户的编号
任务完成时,直接 complete 即可
/**
* 任务处理
*
* @param taskId 任务 Id,来自 ACT_RU_TASK
* @param assignee 设置审核人,替换
* @param map 完成任务需要的条件参数
* @return
*/
@RequestMapping(value = "/task", method = RequestMethod.POST)
public void taskByAssignee(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "assignee") String assignee, @RequestBody Map<String, Object> map) {
// 设置审核人
taskService.setAssignee(taskId, assignee);
// 设置任务参数,也可不设置:key value,只是示例
// 带 Local 为局部参数,只适用于本任务,不带 Local 为全局任务,可在其他任务调用参数
taskService.setVariableLocal(taskId, "status", true);
// 完成任务
taskService.complete(taskId, map);
logger.info("任务完成:" + taskId + " " + new Date());
}
变量:reviewer 节点审核人
说明:通过参数的形式设定分配人,因此在需要在进入任务节点之前把对应参数注入。否则会提示表达式错误。
疑问说明:如果我在审核时候才设置审核人,那我表单展示时如何知道审核人是谁?
监听器
设置进变量或者直接审核动作之前再设置都可以。业务系统代码
// 设置审核人
feignClientService.setVariable(taskId, "reviewer", "bbb");
// 完成任务,taskId 任务节点 ID
feignClientService.taskByAssignee(taskId, user.getUsername(), map);
流程引擎系统代码,同上
/**
* 任务处理
*
* @param taskId 任务 Id,来自 ACT_RU_TASK
* @param assignee 设置审核人,替换
* @param map 完成任务需要的条件参数
* @return
*/
@RequestMapping(value = "/task", method = RequestMethod.POST)
public void taskByAssignee(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "assignee") String assignee, @RequestBody Map<String, Object> map) {
// 设置审核人
taskService.setAssignee(taskId, assignee);
// 设置任务参数,也可不设置:key value,只是示例
// 带 Local 为局部参数,只适用于本任务,不带 Local 为全局任务,可在其他任务调用参数
taskService.setVariableLocal(taskId, "status", true);
// 完成任务
taskService.complete(taskId, map);
logger.info("任务完成:" + taskId + " " + new Date());
}
变量:assignee 节点审核人
说明:分支判断,如果是 admin 审核的进入 分支审核第四审核人2号
节点,其他情况走 分支审核第四审核人1号
节点,默认走 分支审核第四审核人1号
。
组的配置不管是实际还是参数形式其实差不多,因为都无法让流程直接拿到审核人。区别在于实际值
方便那些不懂流程的人配置以及我们可以直接获取组然后查找,而参数我们可以通过变量注入。还是建议自己建表存关系,降低开发难度。
业务系统代码
// 流程完成所需的条件参数
Map<String, Object> map = new HashMap<>();
map.put("assignee", user.getUsername());
// 完成任务,taskId 任务节点 ID
feignClientService.taskByAssignee(taskId, user.getUsername(), map);
流程引擎系统代码,同上。之后流程引擎 complete
后会自动根据你的参数进行分支判断
/**
* 任务处理
*
* @param taskId 任务 Id,来自 ACT_RU_TASK
* @param assignee 设置审核人,替换
* @param map 完成任务需要的条件参数
* @return
*/
@RequestMapping(value = "/task", method = RequestMethod.POST)
public void taskByAssignee(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "assignee") String assignee, @RequestBody Map<String, Object> map) {
// 设置审核人
taskService.setAssignee(taskId, assignee);
// 设置任务参数,也可不设置:key value,只是示例
// 带 Local 为局部参数,只适用于本任务,不带 Local 为全局任务,可在其他任务调用参数
taskService.setVariableLocal(taskId, "status", true);
// 完成任务
taskService.complete(taskId, map);
logger.info("任务完成:" + taskId + " " + new Date());
}
变量:
多实例类型:
说明:
assigneeList 这个参数必须在进入会签任务之前注入,原因是任务在进入会签任务时就需要根据 assigneeList 来生成 task 任务,因此这个参数必须存在。这里可以利用监听器
在任务生成之前注入,或者在之前节点就注入。
会签判断条件:完成率达到 50% 进入下个节点
业务系统代码,会签节点之前
PS:注意集合参数在请求后,流程引擎那不能用 Object 类型来接收,会导致解析时无法转化为集合,因此我这里集合单独写了个接口来设置参数
// 设置 assigneeList 参数
// 注意,这个参数是在进入会签节点之前就设置了
feignClientService.setListVariable(taskId, "assigneeList", assigneeList);
业务系统代码,会签节点
// 完成任务,taskId 任务节点 ID
feignClientService.taskByAssignee(taskId, user.getUsername(), map);
流程引擎系统代码,同上。之后流程引擎 complete
后会自动根据配置的表达式判断节点是否完成。
/**
* 任务处理
*
* @param taskId 任务 Id,来自 ACT_RU_TASK
* @param assignee 设置审核人,替换
* @param map 完成任务需要的条件参数
* @return
*/
@RequestMapping(value = "/task", method = RequestMethod.POST)
public void taskByAssignee(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "assignee") String assignee, @RequestBody Map<String, Object> map) {
// 设置审核人
taskService.setAssignee(taskId, assignee);
// 设置任务参数,也可不设置:key value,只是示例
// 带 Local 为局部参数,只适用于本任务,不带 Local 为全局任务,可在其他任务调用参数
taskService.setVariableLocal(taskId, "status", true);
// 完成任务
taskService.complete(taskId, map);
logger.info("任务完成:" + taskId + " " + new Date());
}
变量:okNum 已审核的次数
说明:ExecutionListener
任务监听器,每次有人审核执行任务就把数字 +1,达到 3 个完成节点任务,
监听器会在 complete
之前执行。
流程引擎系统代码
/**
* 会签监听器
* @author: linjinp
* @create: 2019-11-18 11:43
**/
@Component
public class MyListener implements ExecutionListener {
// 页面配置参数注入
private FixedValue num;
@Override
public void notify(DelegateExecution delegateExecution) {
// 获取页面配置参数的值
System.out.println(num.getExpressionText());
// 校验 okNum 是否已经存在
if (!delegateExecution.hasVariable("okNum")) {
delegateExecution.setVariable("okNum", 0);
}
// 已审核次数,审核一次 +1
int okNum = (int) delegateExecution.getVariable("okNum") + 1;
delegateExecution.setVariable("okNum", okNum);
}
}
我这里业务系统与流程引擎系统是分开的,不处于一个项目,有各自的数据库
业务系统:用户数据,表单,权限等,这里业务系统通过 feign
调用流程引擎的接口
流程引擎系统:流程引擎根据功能对接口进行封装,通过 Restful 形式的接口对外开放
Demo 一共 5 个功能:业务数据列表查询(这个不涉及流程引擎),审核列表数据查询,审核功能,审核历史查询,流程图查看流程进度
代码仅用作入门学习与逻辑参考
/**
* 流程部署
*
* @param modelId 流程ID,来自 ACT_DE_MODEL
* @return
*/
@RequestMapping(value = "/deploy/{modelId}", method = RequestMethod.GET)
public void deploy(@PathVariable(value = "modelId") String modelId) {
// 根据模型 ID 获取模型
Model modelData = modelService.getModel(modelId);
byte[] bytes = modelService.getBpmnXML(modelData);
if (bytes == null) {
logger.error("模型数据为空,请先设计流程并成功保存,再进行发布");
}
BpmnModel model = modelService.getBpmnModel(modelData);
if (model.getProcesses().size() == 0) {
logger.error("数据模型不符要求,请至少设计一条主线流程");
}
byte[] bpmnBytes = new BpmnXMLConverter().convertToXML(model);
String processName = modelData.getName() + ".bpmn20.xml";
// 部署流程
repositoryService.createDeployment()
.name(modelData.getName())
.addBytes(processName, bpmnBytes)
.deploy();
logger.info("流程部署成功:" + modelId + " " + new Date());
}
ACT_RE_PROCDEF
:流程定义相关信息,流程多次部署后的历史信息也可以在这张表查看
/**
* 启动流程
*
* @param deployId 部署的流程 Id,来自 ACT_RE_PROCDEF
* @param userId 用户 Id
* @param dataKey 数据 Key,业务键,一般为表单数据的 ID,仅作为表单数据与流程实例关联的依据
* @return
*/
@RequestMapping(value = "/start/{deployId}/{userId}/{dataKey}", method = RequestMethod.GET)
public void start(@PathVariable(value = "deployId") String deployId, @PathVariable(value = "userId") String userId, @PathVariable(value = "dataKey") String dataKey) {
// 设置发起人
identityService.setAuthenticatedUserId(userId);
// 根据流程 ID 启动流程
runtimeService.startProcessInstanceById(deployId, dataKey);
logger.info("流程启动成功:" + deployId + " " + new Date());
}
由于不涉及流程引擎,因此就不上代码了。
由于表单与流程引擎本身是不存在关系的,因此这里的状态储存是直接在业务数据表里储存状态,查询起来也简单。
如果需要用到流程引擎中的参数,那就自己查询就可以了。
查询当前用户需要审核的数据列表
整体逻辑:获取流程中所有审核人为当前用户的流程数据
,返回保存在流程中的业务键
以及其他需要的数据,然后对数据进行组合
。业务键也就是数据 ID
是在流程启动
时候设置到流程中的,详细看启动接口。
/**
* 获取审核列表
* @return
*/
@GetMapping(value = "/getApproveList")
public ErrorMsg getApproveList(HttpServletRequest request) {
ErrorMsg errorMsg = new ErrorMsg();
String token = request.getHeader("X-Token");
User user = (User) redisTemplate.opsForValue().get(token);
// 获取用户拥有的用户组,admin,aaa,test
List<Group> hasGroup = userPermissionDao.getUserGroup(user.getUsername());
// 取出组 ID
List<String> groups = new ArrayList<>();
hasGroup.forEach(group -> {
groups.add(group.getId());
});
// 调用流程引擎封装的接口
// 根据用户组获取需要审核的数据对应的流程信息
// 主要为了满足我设置的是实际组的情况
List<Map<String, Object>> idListByGroupMapList = feignClientService.getRuntimeBusinessKeyByGroup(groups);
// 调用流程引擎封装的接口
// 获取自己发起的正在进行的审核数据对应的流程信息
List<Map<String, Object>> idListByUserMapList = feignClientService.getRuntimeBusinessKeyByUser(user.getUsername());
// 整合两个 list
idListByUserMapList.addAll(idListByGroupMapList);
// 这里开始对数据进行组合,方便前端展示
List<String> idList = new ArrayList<>();
idListByUserMapList.forEach(idListByUserMap -> {
// businessKey 为业务键,我用来存数据的 ID
idList.add((String) idListByUserMap.get("businessKey"));
});
// 获取正在审核的数据
List<Map<String, Object>> roles = demoService.getRoleListByIdsDemo(idList);
// 数据整合,将需要在前端展示的流程信息与业务数据信息组合到一起
roles.forEach(role -> {
idListByUserMapList.forEach(idListByUserMap -> {
if (role.get("id").toString().equals(idListByUserMap.get("businessKey").toString())) {
role.put("taskId", idListByUserMap.get("taskId"));
role.put("processInstanceName", idListByUserMap.get("processInstanceName"));
role.put("startTime", idListByUserMap.get("startTime"));
}
});
});
// 统一返回
errorMsg.setErrorCode(ErrorCode.SUCCESS);
errorMsg.setErrorMsg("SUCCESS");
errorMsg.setRetData(roles);
return errorMsg;
}
根据组获取任务数据
/**
* 获取组,获取需要审核的业务键 business_key 列表
*
* @param groupIds 组 Id
* @return
*/
@RequestMapping(value = "/getRuntimeBusinessKeyByGroup", method = RequestMethod.POST)
public List<Map<String, Object>> getRuntimeBusinessKeyByGroup(@RequestBody List<String> groupIds) {
List<Map<String, Object>> idList = new ArrayList<>();
// 判断是否有组信息
if (groupIds != null && groupIds.size() > 0) {
// 根据发起人获取正在执行的任务列表
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroupIn(groupIds).list();
tasks.forEach(task -> {
Map<String, Object> data = new HashMap<>();
// 根据任务获取流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult();
// 获取流程实例中的业务键
data.put("businessKey", processInstance.getBusinessKey());
// 获取任务 Id
data.put("taskId", task.getId());
// 流程定义名称
data.put("processInstanceName", processInstance.getProcessDefinitionName());
// 流程开始时间
data.put("startTime", processInstance.getStartTime());
idList.add(data);
});
}
return idList;
}
根据审核人获取任务数据
/**
* 根据用户,获取需要审核的业务键 business_key 列表
*
* @param userId 用户 Id
* @return
*/
@RequestMapping(value = "/getRuntimeBusinessKeyByUser/{userId}", method = RequestMethod.GET)
public List<Map<String, Object>> getRuntimeBusinessKeyByUser(@PathVariable(value = "userId") String userId) {
List<Map<String, Object>> idList = new ArrayList<>();
// 根据用户获取正在进行的任务
List<Task> tasks = taskService.createTaskQuery().taskAssignee(userId).list();
tasks.forEach(task -> {
Map<String, Object> data = new HashMap<>();
// 根据任务获取流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult();
// 获取流程实例中的业务键
data.put("businessKey", processInstance.getBusinessKey());
// 获取任务 Id
data.put("taskId", task.getId());
// 流程定义名称
data.put("processInstanceName", processInstance.getProcessDefinitionName());
// 流程开始时间
data.put("startTime", processInstance.getStartTime());
idList.add(data);
});
return idList;
}
点击按钮,进行审核
整体逻辑:获取任务 Id,完成任务,对需要的参数进行设置
PS:关于用户组,审核人的参数设置建议用监听器之类的来实现,这里为了方便直接在审核里设置了
/**
* 进行审核
* @param request
* @param taskId 任务节点 Id
* @return
*/
@GetMapping(value = "/task/{taskId}/{dataId}")
public ErrorMsg taskByAssignee(HttpServletRequest request, @PathVariable(value = "taskId") String taskId, @PathVariable(value = "dataId") String dataId) {
ErrorMsg errorMsg = new ErrorMsg();
String token = request.getHeader("X-Token");
User user = (User) redisTemplate.opsForValue().get(token);
// 会签情况需要用户列表数据
List<String> assigneeList = userGroupPermissionDao.getIdByGroup("ce537a73-dbc2-4d2f-ac5f-2cdc208a20e0");
// 设置会签所需的用户列表数据
// 会签与监听器会签节点用户组参数
// 注意这里单独封装了个方法,集合不能用 Object 接收,否则流程引擎解析会失败
feignClientService.setListVariable(taskId, "assigneeList", assigneeList);
// 多实例基数,没用到
feignClientService.setVariable(taskId, "cycleNum", assigneeList.size());
// 第二审核人节点,审核人参数
feignClientService.setVariable(taskId, "reviewer", "bbb");
// 流程完成所需的条件参数
// 主要用于第三审核人节点,根据审核人进行分支判断依据
Map<String, Object> map = new HashMap<>();
map.put("assignee", user.getUsername());
// 根据任务节点 Id,获取流程实例 Id
String processInstanceId = feignClientService.getTaskInfo(taskId);
// 完成任务,taskId 任务节点 ID
feignClientService.taskByAssignee(taskId, user.getUsername(), map);
// 通过流程实例 Id,判断流程是否结束
boolean isFinish = feignClientService.checkProcessInstanceFinish(processInstanceId);
if (isFinish) {
// 更新审核状态
Role role = new Role();
role.setId(Integer.valueOf(dataId));
role.setStatus(2);
demoService.updateRoleDemo(role);
}
errorMsg.setErrorCode(ErrorCode.SUCCESS);
errorMsg.setErrorMsg("SUCCESS");
return errorMsg;
}
参数设置接口
/**
* 设置任务参数
*
* @param taskId 任务ID
* @param key 键
* @param value 值
* @return
*/
@RequestMapping(value = "/setVariable", method = RequestMethod.POST)
public void setVariable(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "key") String key, @RequestParam(value = "value") Object value) {
String processInstanceId = taskService.createTaskQuery().taskId(taskId).singleResult().getProcessInstanceId();
runtimeService.setVariable(processInstanceId, key, value);
}
/**
* 设置任务参数,List 使用
*
* @param taskId 任务ID
* @param key 键
* @param value 值
* @return
*/
@RequestMapping(value = "/setListVariable", method = RequestMethod.POST)
public void setListVariable(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "key") String key, @RequestParam(value = "value") List<String> value) {
String processInstanceId = taskService.createTaskQuery().taskId(taskId).singleResult().getProcessInstanceId();
runtimeService.setVariable(processInstanceId, key, value);
}
根据任务节点获取流程实例 Id
/**
* 根据任务节点获取流程实例 Id
*
* @param taskId 任务节点 Id
* @return
*/
@RequestMapping(value = "/getTaskInfo/{taskId}", method = RequestMethod.GET)
public String getTaskInfo(@PathVariable(value = "taskId") String taskId) {
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
return task.getProcessInstanceId();
}
通过流程实例 Id,判断流程是否结束
/**
* 通过流程实例 Id,判断流程是否结束
*
* @param processInstanceId 流程实例 Id
* @return true 结束,false 未结束
*/
@RequestMapping(value = "/checkProcessInstanceFinish/{processInstanceId}", method = RequestMethod.GET)
public boolean checkProcessInstanceFinish(@PathVariable(value = "processInstanceId") String processInstanceId) {
boolean isFinish = false;
// 根据流程 ID 获取未完成的流程中是否存在此流程
long count = historyService.createHistoricProcessInstanceQuery().unfinished().processInstanceId(processInstanceId).count();
// 不存在说明没有结束
if (count == 0) {
isFinish = true;
}
return isFinish;
}
根据审核人字段,获取已完成的任务中,审核人为当前用户的数据
PS:组审核情况,在审核时,都直接设置了审核人,因此可以直接获取到
/**
* 获取审核历史记录
* @return
*/
@GetMapping(value = "/getApproveHistory")
public ErrorMsg getApproveHistory(HttpServletRequest request) {
ErrorMsg errorMsg = new ErrorMsg();
String token = request.getHeader("X-Token");
User user = (User) redisTemplate.opsForValue().get(token);
// 调用流程引擎接口,获取审核人为当前用户的已完成的任务
List<Map<String, Object>> historys = feignClientService.getHistoryByUser(user.getUsername());
errorMsg.setErrorCode(ErrorCode.SUCCESS);
errorMsg.setErrorMsg("SUCCESS");
errorMsg.setRetData(historys);
return errorMsg;
}
/**
* 获取用户审核历史
*
* @param userId 发起人 Id
* @return
*/
@RequestMapping(value = "/getHistoryByUser/{userId}", method = RequestMethod.GET)
public List<Map<String, Object>> getHistoryByUser(@PathVariable(value = "userId") String userId) {
List<Map<String, Object>> historyList = new ArrayList<>();
// 根据用户,查询任务实例历史
List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().taskAssignee(userId).finished().orderByHistoricTaskInstanceEndTime().desc().list();
list.forEach(historicTaskInstance -> {
// 历史流程实例
HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery().processInstanceId(historicTaskInstance.getProcessInstanceId()).singleResult();
// 获取需要的历史数据
Map<String, Object> historyInfo = new HashMap<>();
historyInfo.put("assignee", historicTaskInstance.getAssignee());
// 节点名称
historyInfo.put("nodeName", historicTaskInstance.getName());
// 流程开始时间
historyInfo.put("startTime", historicTaskInstance.getCreateTime());
// 节点操作时间(本流程节点结束时间)
historyInfo.put("endTime", historicTaskInstance.getEndTime());
// 流程定义名称
historyInfo.put("processName", hpi.getProcessDefinitionName());
// 流程实例 ID
historyInfo.put("processInstanceId", historicTaskInstance.getProcessInstanceId());
// 业务键
historyInfo.put("businessKey", hpi.getBusinessKey());
historyList.add(historyInfo);
});
return historyList;
}
这里直接采用流输出直接在页面显示,因此只能前端直接调用流程引擎的接口
如果想要更完善的显示流程,建议自己解析 Json,自己前端用组件生成流程图
根据任务 ID 获取任务进度流程图
/**
* 根据任务 ID 获取任务进度流程图
*
* @param taskId 任务节点 Id
* @return
*/
@RequestMapping(value = "/getTaskProcessDiagram/{taskId}", method = RequestMethod.GET)
public void getTaskProcessDiagram(@PathVariable(value = "taskId") String taskId, HttpServletResponse httpServletResponse) {
// 根据任务 ID 获取流程实例 ID
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
String processInstanceId = task.getProcessInstanceId();
// 根据流程实例获取流程图
// 流程定义 ID
String processDefinitionId;
// 查看完成的进程中是否存在此进程
long count = historyService.createHistoricProcessInstanceQuery().finished().processInstanceId(processInstanceId).count();
if (count > 0) {
// 如果流程已经结束,则得到结束节点
HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
processDefinitionId = pi.getProcessDefinitionId();
} else {// 如果流程没有结束,则取当前活动节点
// 根据流程实例ID获得当前处于活动状态的ActivityId合集
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
processDefinitionId = pi.getProcessDefinitionId();
}
List<String> highLightedActivitis = new ArrayList<>();
// 获得活动的节点
List<HistoricActivityInstance> highLightedActivitList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();
for (HistoricActivityInstance tempActivity : highLightedActivitList) {
String activityId = tempActivity.getActivityId();
highLightedActivitis.add(activityId);
}
List<String> flows = new ArrayList<>();
//获取流程图
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
ProcessEngineConfiguration processEngineConfig = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator diagramGenerator = processEngineConfig.getProcessDiagramGenerator();
InputStream in = diagramGenerator.generateDiagram(bpmnModel, "bmp", highLightedActivitis, flows, processEngineConfig.getActivityFontName(),
processEngineConfig.getLabelFontName(), processEngineConfig.getAnnotationFontName(), processEngineConfig.getClassLoader(), 1.0, true);
OutputStream out = null;
byte[] buf = new byte[1024];
int legth;
try {
out = httpServletResponse.getOutputStream();
while ((legth = in.read(buf)) != -1) {
out.write(buf, 0, legth);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(in);
}
}
Flowable 测试用接口部分的代码
Flowable-Demo 模块, API 包下