【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现(附源码)(下篇)

作者:后端小肥肠

上篇:【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现(上篇)_spring security activiti7-CSDN博客

目录

1.前言

2. 核心代码

2.1. 流程定义模型管理

2.1.1. 新增流程定义模型数据

2.1.2. 通过流程定义模型id部署流程定义

2.1.3. 导出流程定义模型zip压缩包

2.2. 流程定义管理

2.2.1.  更新流程状态:激活(启动)或者挂起(暂停)

2.2.2.  导出流程定义文件(xml,png)

2.2.3. 上传zip、bpmn、xml后缀的文件来进行部署流程定义

2.3. 流程配置管理

2.4. 流程实例管理

2.4.1. 提交申请,启动流程实例

2.4.2. 撤回申请

2.4.3. 挂起或激活流程实例

2.4.4. 通过流程实例id获取历史流程图

2.4.5. 通过流程实例id获取任务办理历史记录

2.5. 任务管理

2.5.1.  查询当前用户的待办任务

 2.5.2. 获取目标节点(下一个节点)

2.5.3. 完成任务

2.5.4. 获取历史任务节点,用于驳回功能

2.5.5. 驳回历史节点

2.6. 请假申请管理

3. 源码地址

4. 结语


1.前言

在《基于Spring Security的Activiti7工作流管理系统简介及实现(上篇)》中,向大家展示了工作流管理系统的功能界面及模块,具体应用场景,在本文中将会讲解该工作流管理系统实现的具体技术细节及核心代码。

本文面向人群为有工作流基础的后端人员,如对您有帮助请三连支持一下小肥肠~

2. 核心代码

本章只做代码简介(部分代码,简单的crud不介绍)及核心代码讲解,文末会提供源代码链接(仅后端)。

2.1. 流程定义模型管理

流程定义模型管理对应前端的模型管理界面,相关接口包括新增流程定义模型数据条件分页查询流程定义模型数据通过流程定义模型id部署流程定义导出流程定义模型zip压缩包删除流程定义模型

2.1.1. 新增流程定义模型数据
    public Result add(ModelAddREQ req) throws Exception {/*String name = "请假流程模型";String key = "leaveProcess";String desc = "请输入描述信息……";*/int version = 0;// 1. 初始空的模型Model model = repositoryService.newModel();model.setName(req.getName());model.setKey(req.getKey());model.setVersion(version);// 封装模型json对象ObjectNode objectNode  = objectMapper.createObjectNode();objectNode.put(ModelDataJsonConstants.MODEL_NAME, req.getName());objectNode.put(ModelDataJsonConstants.MODEL_REVISION, version);objectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, req.getDescription());model.setMetaInfo(objectNode.toString());// 保存初始化的模型基本信息数据repositoryService.saveModel(model);// 封装模型对象基础数据json串// {"id":"canvas","resourceId":"canvas","stencilset":{"namespace":"http://b3mn.org/stencilset/bpmn2.0#"},"properties":{"process_id":"未定义"}}ObjectNode editorNode = objectMapper.createObjectNode();ObjectNode stencilSetNode = objectMapper.createObjectNode();stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");editorNode.replace("stencilset", stencilSetNode);// 标识keyObjectNode propertiesNode = objectMapper.createObjectNode();propertiesNode.put("process_id", req.getKey());editorNode.replace("properties", propertiesNode);repositoryService.addModelEditorSource(model.getId(), editorNode.toString().getBytes("utf-8"));return Result.ok(model.getId());}

上述代码实现了创建一个基于 Activiti 7 的工作流模型的功能。关键步骤包括初始化模型对象,封装模型的元信息和基础数据为 JSON 字符串,以及将该字符串保存到模型编辑器中。最终返回新创建模型的ID作为结果。

新增流程定义模型数据主要涉及到了 Activiti 7 中的模型管理相关的表,包括:

  1. ACT_RE_MODEL:用于存储模型的基本信息,如模型名称、键、版本等。
  2. ACT_GE_BYTEARRAY:存储模型编辑器的源数据,即模型对象的基础数据 JSON 字符串。

这些表存储了创建的工作流模型的信息,包括其名称、键、版本、元信息和基础数据,以便后续的流程定义和流程实例化。

2.1.2. 通过流程定义模型id部署流程定义
    public Result deploy(String modelId) throws Exception {// 1. 查询流程定义模型json字节码byte[] jsonBytes = repositoryService.getModelEditorSource(modelId);if(jsonBytes == null) {return Result.error("模型数据为空,请先设计流程定义模型,再进行部署");}// 将json字节码转为 xml 字节码,因为bpmn2.0规范中关于流程模型的描述是xml格式的,而activiti遵守了这个规范byte[] xmlBytes = bpmnJsonXmlBytes(jsonBytes);if(xmlBytes == null) {return Result.error("数据模型不符合要求,请至少设计一条主线流程");}// 2. 查询流程定义模型的图片byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);// 查询模型的基本信息Model model = repositoryService.getModel(modelId);// xml资源的名称 ,对应act_ge_bytearray表中的name_字段String processName = model.getName() + ".bpmn20.xml";// 图片资源名称,对应act_ge_bytearray表中的name_字段String pngName = model.getName() + "." + model.getKey() + ".png";// 3. 调用部署相关的api方法进行部署流程定义Deployment deployment = repositoryService.createDeployment().name(model.getName()) // 部署名称.addString(processName, new String(xmlBytes, "UTF-8")) // bpmn20.xml资源.addBytes(pngName, pngBytes) // png资源.deploy();// 更新 部署id 到流程定义模型数据表中model.setDeploymentId(deployment.getId());repositoryService.saveModel(model);return Result.ok();}

上述代码实现了根据给定的模型ID部署流程定义的功能。它首先查询模型的 JSON 字节码,并将其转换为符合 BPMN 2.0 规范的 XML 字节码,然后查询模型的图片字节码。接着,通过创建部署对象并添加相应的资源文件进行流程定义的部署,最后更新模型的部署ID,并返回部署成功的结果。 

 通过流程定义模型id部署流程定义涉及了 Activiti 7 中的以下几张表:

  1. ACT_RE_MODEL:用于存储模型的基本信息,如模型名称、键、版本等。
  2. ACT_GE_BYTEARRAY:存储模型的编辑器源数据、XML 格式的流程定义文件以及流程图片等资源数据。
  3. ACT_RE_DEPLOYMENT:存储流程部署的相关信息,如部署名称、部署时间等。
2.1.3. 导出流程定义模型zip压缩包
    public void exportZip(String modelId, HttpServletResponse response) {ZipOutputStream zipos = null;try {// 实例化zip输出流zipos = new ZipOutputStream(response.getOutputStream());// 压缩包文件名String zipName = "模型不存在";// 1. 查询模型基本信息Model model = repositoryService.getModel(modelId);if(model != null) {// 2. 查询流程定义模型的json字节码byte[] bpmnJsonBytes = repositoryService.getModelEditorSource(modelId);// 2.1 将json字节码转换为xml字节码byte[] xmlBytes = bpmnJsonXmlBytes(bpmnJsonBytes);if(xmlBytes == null) {zipName = "模型数据为空-请先设计流程定义模型,再导出";}else {// 压缩包文件名zipName = model.getName() + "." + model.getKey() + ".zip";// 将xml添加到压缩包中(指定xml文件名:请假流程.bpmn20.xml )zipos.putNextEntry(new ZipEntry(model.getName() + ".bpmn20.xml"));zipos.write(xmlBytes);// 3. 查询流程定义模型的图片字节码byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);if(pngBytes != null) {// 图片文件名(请假流程.leaveProcess.png)zipos.putNextEntry(new ZipEntry(model.getName() + "." + model.getKey() + ".png"));zipos.write(pngBytes);}}}response.setContentType("application/octet-stream");response.setHeader("Content-Disposition","attachment; filename=" + URLEncoder.encode(zipName, "UTF-8") + ".zip");// 刷出响应流response.flushBuffer();} catch (Exception e) {e.printStackTrace();} finally {if(zipos != null) {try {zipos.closeEntry();zipos.close();} catch (IOException e) {e.printStackTrace();}}}}

这段代码实现了根据给定的模型ID导出流程定义及相关图片的功能。它首先查询模型的基本信息,包括模型名称和键,然后查询模型的 JSON 字节码,并将其转换为符合 BPMN 2.0 规范的 XML 字节码。接着,将 XML 文件和模型的图片字节码压缩成一个 ZIP 文件,通过 HttpServletResponse 输出给用户进行下载。 

2.2. 流程定义管理

流程定义管理对应前端的流程管理界面,相关接口包括条件分页查询相同key的最新版本的流程定义列表数据更新流程状态:激活(启动)或者挂起(暂停)、删除流程定义导出流程定义文件(xml,png)上传zip、bpmn、xml后缀的文件来进行部署流程定义

2.2.1.  更新流程状态:激活(启动)或者挂起(暂停)

前端界面:

后端代码: 

    public Result updateProcDefState(String ProcDefiId) {ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(ProcDefiId).singleResult();// 判断是否挂起,true则挂起,false则激活if(processDefinition.isSuspended()) {// 将当前为挂起状态更新为激活状态// 参数说明:参数1:流程定义id,参数2:是否激活(true是否级联对应流程实例,激活了则对应流程实例都可以审批),参数3:什么时候激活,如果为null则立即激活,如果为具体时间则到达此时间后激活repositoryService.activateProcessDefinitionById(ProcDefiId, true, null);}else {// 将当前为激活状态更新为挂起状态// 参数说明:参数1:流程定义id,参数2:是否挂起(true是否级联对应流程实例,挂起了则对应流程实例都不可以审批),参数3:什么时候挂起,如果为null则立即挂起,如果为具体时间则到达此时间后挂起repositoryService.suspendProcessDefinitionById(ProcDefiId, true, null);}return Result.ok();}
2.2.2.  导出流程定义文件(xml,png)
@GetMapping("/export/{type}/{definitionId}")
public void exportFile(@PathVariable String type,@PathVariable String definitionId,HttpServletResponse response) {try {ProcessDefinition processDefinition = repositoryService.getProcessDefinition(definitionId);String resourceName = "文件不存在";if("xml".equals(type)) {// 获取的是 xml 资源名resourceName = processDefinition.getResourceName();}else if("png".equals(type)) {// 获取 png 图片资源名resourceName = processDefinition.getDiagramResourceName();}// 查询到相关的资源输入流 (deploymentId, resourceName)InputStream input =repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), resourceName);// 创建输出流response.setHeader("Content-Disposition","attachment; filename=" + URLEncoder.encode(resourceName, "UTF-8"));// 流的拷贝放到设置请求头下面,不然文件大于10k可能无法导出IOUtils.copy(input, response.getOutputStream());response.flushBuffer();} catch (Exception e) {e.printStackTrace();log.error("导出文件失败:{}", e.getMessage());}}

这段代码实现了根据流程定义ID导出流程定义文件(XML 或 PNG 格式)的功能。它首先根据流程定义ID查询相关的流程定义信息,然后根据用户请求的类型(XML 或 PNG)获取对应的资源名。接着,通过 repositoryService.getResourceAsStream() 方法获取资源的输入流,并将其写入 HttpServletResponse 的输出流中,实现文件的下载。 

2.2.3. 上传zip、bpmn、xml后缀的文件来进行部署流程定义
@PostMapping("/file/deploy")    
public Result deployByFile(@RequestParam("file") MultipartFile file) {try {// 文件名+后缀名String filename = file.getOriginalFilename();// 文件后缀名String suffix = filename.substring(filename.lastIndexOf(".") + 1).toUpperCase();InputStream input = file.getInputStream();DeploymentBuilder deployment = repositoryService.createDeployment();if("ZIP".equals(suffix)) {// zipdeployment.addZipInputStream(new ZipInputStream(input));}else {// xml 或 bpmndeployment.addInputStream(filename, input);}// 部署名称deployment.name(filename.substring(0, filename.lastIndexOf(".")));// 开始部署deployment.deploy();return Result.ok();} catch (IOException e) {e.printStackTrace();log.error("部署失败:" + e.getMessage());return Result.error("部署失败");}}

这段代码实现了通过上传文件部署流程定义的功能。它接受一个 MultipartFile 对象作为参数,获取上传文件的文件名和后缀名,并根据后缀名判断文件类型(ZIP 或 XML/BPMN)。然后根据文件类型,使用相应的方法将文件内容添加到部署构建器中,设置部署名称,并最终调用 deploy() 方法进行部署。 

2.3. 流程配置管理

流程配置主要是将流程定义与具体的业务(如请假,借款)进行绑定。在实际项目中建议在表中配置死即可。

在上图中,关联路由名对应前端路由名称,关联路由组件名对应前端表单名称:

流程配置绑定表如下图所示:

 只要在上述表中将流程定义KEY和前端参数(路由名,表单名)进行绑定即可。后台代码如下:

   @PutMappingpublic Result saveOrUpdate(@RequestBody ProcessConfig processConfig) {boolean b = processConfigService.saveOrUpdate(processConfig);if(b) {return Result.ok();}else {return Result.error("操作失败");}}

2.4. 流程实例管理

流程实例管理对应前端的业务办理界面(请假申请、借款申请),相关接口包括提交申请,启动流程实例撤回申请挂起或激活流程实例通过流程实例id获取申请表单组件名等。

2.4.1. 提交申请,启动流程实例

前端界面:

在本工作流管理系统中,需要在流程启动时动态指定一级审批用户,我这里指定的是username,为了更好的用户体验可以改为指定用户的真实姓名,通过下拉框来选择审批人。

后端代码:

    public Result startProcess(StartREQ req) {// 1. 通过业务路由名获取流程配置信息:流程定义key和表单组件名(查询历史审批记录需要)ProcessConfig processConfig =processConfigService.getByBusinessRoute(req.getBusinessRoute());// 2. 表单组件名设置到流程变量中,后面查询历史审批记录需要Map<String, Object> variables = req.getVariables(); // 前端已经传递了当前申请信息{entity: {业务申请数据}}variables.put("formName", processConfig.getFormName());// 判断办理人为空,则直接结束List<String> assignees = req.getAssignees();if(CollectionUtils.isEmpty(assignees)) {return Result.error("请指定审批人");}// 3. 启动流程实例(提交申请)Authentication.setAuthenticatedUserId(UserUtils.getUsername());ProcessInstance pi =runtimeService.startProcessInstanceByKey(processConfig.getProcessKey(),req.getBusinessKey(), variables);// 将流程定义名称 作为 流程实例名称runtimeService.setProcessInstanceName(pi.getProcessInstanceId(), pi.getProcessDefinitionName());// 4. 设置任务办理人List<Task> taskList = taskService.createTaskQuery().processInstanceId(pi.getId()).list();for (Task task : taskList) {if(assignees.size() == 1) {// 如果只能一个办理人,则直接设置为办理人taskService.setAssignee(task.getId(), assignees.get(0));}else {// 多个办理人,则设置为候选人for(String assignee: assignees) {taskService.addCandidateUser(task.getId(), assignee);}}}// 5. 更新业务状态为:办理中, 和流程实例idreturn businessStatusService.updateState(req.getBusinessKey(),BusinessStatusEnum.PROCESS,pi.getProcessInstanceId());}

这段代码实现了启动流程实例的功能。首先根据业务路由名获取流程配置信息,设置表单组件名到流程变量中。然后判断办理人是否为空,若为空则返回错误信息。接着通过设置认证用户为当前用户启动流程实例,将流程定义名称作为流程实例名称,并设置任务办理人。最后更新业务状态为办理中,并返回更新结果。 

启动流程实例涉及了 Activiti 7 中的以下几张表:

  1. ACT_RU_TASK:用于存储流程任务的运行时信息,包括任务的唯一标识、流程实例ID、任务名称等。
  2. ACT_RU_PROCESS_INSTANCE:存储流程实例的运行时信息,包括流程实例的唯一标识、流程定义ID、当前活动节点等。
  3. ACT_RU_VARIABLE:用于存储流程实例的运行时变量信息,包括流程实例ID、变量名称、变量值等。
  4. ACT_HI_TASKINST:存储历史流程任务的信息,包括任务的执行过程、持续时间等。
  5. ACT_HI_PROCINST:存储历史流程实例的信息,包括流程实例的启动时间、结束时间等。
  6. ACT_HI_ACTINST:存储历史流程执行的信息,包括每个流程实例的执行路径、执行活动的持续时间等。
2.4.2. 撤回申请
    public Result cancel(String businessKey, String procInstId, String message) {// 1. 删除当前流程实例runtimeService.deleteProcessInstance(procInstId,UserUtils.getUsername() + " 主动撤回了当前申请:" + message);// 2. 删除历史记录historyService.deleteHistoricProcessInstance(procInstId);historyService.deleteHistoricTaskInstance(procInstId);// 3. 更新业务状态return businessStatusService.updateState(businessKey, BusinessStatusEnum.CANCEL, "");}

这段代码实现了取消流程实例的功能。它首先通过流程实例ID删除当前运行中的流程实例,并添加一条撤回消息作为删除原因。然后删除相关的历史记录,包括历史流程实例和历史任务实例。最后更新业务状态为取消,并返回更新结果。

撤回申请涉及了 Activiti 7 中的以下几张表:

  1. ACT_RU_PROCESS_INSTANCE:用于存储流程实例的运行时信息,包括流程实例的唯一标识、当前活动节点等。
  2. ACT_HI_PROCINST:存储历史流程实例的信息,包括流程实例的启动时间、结束时间等。
  3. ACT_HI_TASKINST:存储历史任务实例的信息,包括任务的执行过程、持续时间等。
2.4.3. 挂起或激活流程实例

前端界面:

后端代码:

  @PutMapping("/state/{procInstId}")public Result updateProcInstState(@PathVariable String procInstId) {// 1. 查询指定流程实例的数据ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(procInstId).singleResult();// 2. 判断当前流程实例的状态if(processInstance.isSuspended()) {// 如果是已挂起,则更新为激活状态runtimeService.activateProcessInstanceById(procInstId);}else {// 如果是已激活,则更新为挂起状态runtimeService.suspendProcessInstanceById(procInstId);}return Result.ok();}

这段代码实现了更新流程实例状态的功能。它首先查询指定流程实例的数据,然后判断当前流程实例的状态,若是已挂起则更新为激活状态,若是已激活则更新为挂起状态。最后返回更新结果。 

2.4.4. 通过流程实例id获取历史流程图

前端界面:

后端代码:

    public void getHistoryProcessImage(String prodInstId, HttpServletResponse response) {InputStream inputStream = null;try {// 1.查询流程实例历史数据HistoricProcessInstance instance = historyService.createHistoricProcessInstanceQuery().processInstanceId(prodInstId).singleResult();// 2. 查询流程中已执行的节点,按时开始时间降序排列List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery().processInstanceId(prodInstId).orderByHistoricActivityInstanceStartTime().desc().list();// 3. 单独的提取高亮节点id ( 绿色)List<String> highLightedActivityIdList =historicActivityInstanceList.stream().map(HistoricActivityInstance::getActivityId).collect(Collectors.toList());// 4. 正在执行的节点 (红色)List<Execution> runningActivityInstanceList = runtimeService.createExecutionQuery().processInstanceId(prodInstId).list();List<String> runningActivityIdList = new ArrayList<>();for (Execution execution : runningActivityInstanceList) {if(StringUtils.isNotEmpty(execution.getActivityId())) {runningActivityIdList.add(execution.getActivityId());}}// 获取流程定义Model对象BpmnModel bpmnModel = repositoryService.getBpmnModel(instance.getProcessDefinitionId());// 实例化流程图生成器CustomProcessDiagramGenerator generator = new CustomProcessDiagramGenerator();// 获取高亮连线idList<String> highLightedFlows = generator.getHighLightedFlows(bpmnModel, historicActivityInstanceList);// 生成历史流程图inputStream = generator.generateDiagramCustom(bpmnModel, highLightedActivityIdList,runningActivityIdList, highLightedFlows,"宋体", "微软雅黑", "黑体");// 响应相关图片response.setContentType("image/svg+xml");byte[] bytes = IOUtils.toByteArray(inputStream);ServletOutputStream outputStream = response.getOutputStream();outputStream.write(bytes);outputStream.flush();outputStream.close();}catch (Exception e) {e.printStackTrace();}finally {if( inputStream != null){try {inputStream.close();} catch (IOException e) {e.printStackTrace();}}}}

这段代码实现了根据流程实例ID获取历史流程图的功能。它首先查询指定流程实例的历史数据和已执行的节点信息,并提取出高亮节点和正在执行的节点的ID列表。然后根据流程定义的模型对象和节点信息,使用自定义的流程图生成器生成历史流程图,并将流程图以 SVG 格式返回给前端。 

2.4.5. 通过流程实例id获取任务办理历史记录

前端界面:

后端代码:

    public Result getHistoryInfoList(String procInstId) {// 查询每任务节点历史办理情况List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().processInstanceId(procInstId).orderByHistoricTaskInstanceStartTime().asc().list();List<Map<String, Object>> records = new ArrayList<>();for (HistoricTaskInstance hti : list) {Map<String, Object> result = new HashMap<>();result.put("taskId", hti.getId()); // 任务IDresult.put("taskName", hti.getName()); // 任务名称result.put("processInstanceId", hti.getProcessInstanceId()); //流程实例IDresult.put("startTime", DateUtils.format(hti.getStartTime())); // 开始时间result.put("endTime", DateUtils.format(hti.getEndTime())); // 结束时间result.put("status", hti.getEndTime() == null ? "待处理": "已处理"); // 状态result.put("assignee", hti.getAssignee()); // 办理人// 撤回原因String message = hti.getDeleteReason();if(StringUtils.isEmpty(message)) {List<Comment> taskComments = taskService.getTaskComments(hti.getId());message = taskComments.stream().map(m -> m.getFullMessage()).collect(Collectors.joining("。"));}result.put("message", message);records.add(result);}return Result.ok(records);}

这段代码实现了查询指定流程实例的历史任务信息列表的功能。它首先通过历史任务实例查询服务查询指定流程实例的历史任务信息,并按照任务开始时间升序排序。然后遍历历史任务列表,将每个历史任务的相关信息封装到一个 Map 中,并将所有的 Map 组成一个列表返回给调用方,包括任务ID任务名称流程实例ID任务开始时间任务结束时间任务状态办理人以及撤回原因等。 

2.5. 任务管理

任务管理对应前端待办任务和已办任务界面,包含查询当前用户的待办任务获取目标节点(下一个节点)完成任务获取历史任务节点用于驳回功能驳回历史节点等接口。

2.5.1.  查询当前用户的待办任务
 @PostMapping("/list/wait")public Result findWaitTask(@RequestBody TaskREQ req) {String assignee = UserUtils.getUsername();TaskQuery query = taskService.createTaskQuery().taskCandidateOrAssigned(assignee) // 候选人或者办理人.orderByTaskCreateTime().asc();if(StringUtils.isNotEmpty(req.getTaskName())) {query.taskNameLikeIgnoreCase("%" + req.getTaskName() + "%");}// 分页查询List<Task> taskList = query.listPage(req.getFirstResult(), req.getSize());long total = query.count();List<Map<String, Object>> records = new ArrayList<>();for (Task task : taskList) {Map<String, Object> result = new HashMap<>();result.put("taskId", task.getId());result.put("taskName", task.getName());result.put("processStatus", task.isSuspended() ? "已暂停": "已启动");result.put("taskCreateTime", DateUtils.format(task.getCreateTime()) );result.put("processInstanceId", task.getProcessInstanceId());result.put("executionId", task.getExecutionId());result.put("processDefinitionId", task.getProcessDefinitionId());// 任务办理人: 如果是候选人则没有值,办理人才有result.put("taskAssignee", task.getAssignee());// 查询流程实例ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult();result.put("processName", pi.getProcessDefinitionName());result.put("version", pi.getProcessDefinitionVersion());result.put("proposer", pi.getStartUserId());result.put("businessKey", pi.getBusinessKey());records.add(result);}Map<String, Object> result = new HashMap<>();result.put("total", total);result.put("records", records);return Result.ok(result);}

这段代码实现了查询待办任务列表的功能。它首先获取当前用户的用户名作为任务的候选人或办理人,然后根据任务查询条件构建任务查询对象,并按任务创建时间升序排列。接着根据分页参数查询待办任务列表,并统计总数。最后,将待办任务的相关信息(如任务ID任务名称流程状态任务创建时间流程实例ID等)封装到一个列表中,并返回给调用方。 

 2.5.2. 获取目标节点(下一个节点)

本工作流框架支持动态指定审批人,故完成本节点审批时,需要动态获取下一任务节点,方便在本节点通过审批后动态指定下一个节点审批人。

后端代码: 

 @GetMapping("/next/node")public Result getNextNodeInfo(@RequestParam String taskId) {Task task = taskService.createTaskQuery().taskId(taskId).singleResult();// 2. 从当前任务信息中获取此流程定义id,String processDefinitionId = task.getProcessDefinitionId();// 3. 拿到流程定义id后可获取此bpmnModel对象BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);// 4. 通过任务节点id,来获取当前节点信息FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());// 封装下一个用户任务节点信息List<Map<String, Object>> nextNodes = new ArrayList<>();getNextNodes(flowElement, nextNodes);return Result.ok(nextNodes);}public void getNextNodes(FlowElement flowElement, List<Map<String, Object>> nextNodes) {// 获取当前节点的连线信息List<SequenceFlow> outgoingFlows = ((FlowNode) flowElement).getOutgoingFlows();// 当前节点的所有下一节点出口for (SequenceFlow outgoingFlow : outgoingFlows) {// 下一节点的目标元素FlowElement nextFlowElement = outgoingFlow.getTargetFlowElement();if(nextFlowElement instanceof UserTask) {// 用户任务,则获取响应给前端设置办理人或者候选人Map<String, Object> node = new HashMap<>();node.put("id", nextFlowElement.getId()); // 节点idnode.put("name", nextFlowElement.getName()); // 节点名称nextNodes.add(node);}else if(nextFlowElement instanceof EndEvent) {break;}else if(nextFlowElement instanceof ParallelGateway // 并行网关|| nextFlowElement instanceof ExclusiveGateway) { // 排他网关getNextNodes(nextFlowElement, nextNodes);}}}

 这段代码实现了获取指定任务的下一个节点信息的功能。它首先根据任务ID查询任务信息,然后根据任务信息获取流程定义ID,并通过流程定义ID获取相应的 BPMN 模型对象。接着根据任务节点ID获取当前节点信息,并递归遍历当前节点的连线信息,获取所有下一个节点的信息,将其封装成列表并返回给调用方。

前端返回结果:

2.5.3. 完成任务

前端传入参数:

TaskCompleteREQ 编写:

public class TaskCompleteREQ implements Serializable {@ApiModelProperty("任务ID")private String taskId;@ApiModelProperty("审批意见")private String message;@ApiModelProperty("下一个节点审批,key: 节点id, vallue:审批人集合,多个人使用英文逗号分隔")private Map<String, String> assigneeMap;public String getMessage() {return StringUtils.isEmpty(message) ? "审批通过": message;}/*** 通过节点id获取审批人集合* @param key* @return*/public String[] getAssignees(String key) {if(assigneeMap == null) {return null;}return assigneeMap.get(key).split(",");}}

完成任务代码:

    @PostMapping("/complete")public Result completeTask(@RequestBody TaskCompleteREQ req) {String taskId = req.getTaskId();//1. 查询任务信息org.activiti.api.task.model.Task task = taskRuntime.task(taskId);if(task == null) {return Result.error("任务不存在或不是您办理的任务");}String procInstId = task.getProcessInstanceId();// 2. 指定任务审批意见taskService.addComment(taskId, procInstId, req.getMessage());// 3. 完成任务taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());// 4. 查询下一个任务List<Task> taskList = taskService.createTaskQuery().processInstanceId(procInstId).list();// 5. 指定办理人if(CollectionUtils.isEmpty(taskList)) {// task.getBusinessKey() m5版本中没有 值HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery().processInstanceId(procInstId).singleResult();// 更新业务状态已完成return businessStatusService.updateState(hpi.getBusinessKey(), BusinessStatusEnum.FINISH);}else {Map<String, String> assigneeMap = req.getAssigneeMap();if(assigneeMap == null) {// 如果没有办理人,直接将流程实例删除(非法操作)return deleteProcessInstance(procInstId);}// 有办理人for (Task t: taskList) {if(StringUtils.isNotEmpty(t.getAssignee())) {// 如果当前任务有办理人,则直接忽略,不用指定办理人continue;}// 根据当前任务节点id获取办理人String[] assignees = req.getAssignees(t.getTaskDefinitionKey());if(ArrayUtils.isEmpty(assignees)) {// 没有办理人return deleteProcessInstance(procInstId);}if(assignees.length == 1) {taskService.setAssignee(t.getId(), assignees[0]);}else {// 多个作为候选人for(String assignee: assignees) {taskService.addCandidateUser(t.getId(), assignee);}}}}return Result.ok();}

这段代码实现了完成任务的操作,并根据任务完成情况进行下一步的流程处理。它首先根据任务ID查询任务信息,然后添加任务审批意见并完成任务。接着查询流程实例的下一个任务,如果没有下一个任务则更新业务状态为已完成;如果有下一个任务,则根据指定的办理人信息指派任务给相应的用户或候选人。

2.5.4. 获取历史任务节点,用于驳回功能

本工作流框架支持在审批过程中驳回至之前的任意节点,需要完成这个功能首先我们应该获取运行流程中的历史任务节点。

前端界面:

后端代码:

ps:源代码获取历史任务节点代码有bug,这是我修改以后的,源代码我没改(因为我懒 = =)

    public ResponseStructure getBackNodes(String taskId) {try {Task task = taskService.createTaskQuery().taskId(taskId).singleResult();// 2. 从当前任务信息中获取此流程定义id,String processDefinitionId = task.getProcessDefinitionId();// 3. 拿到流程定义id后可获取此bpmnModel对象BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);// 4. 通过任务节点id,来获取当前节点信息FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());List<Map<String,Object>>parentNodes=new ArrayList<>();getParentNodes(flowElement,parentNodes);return ResponseStructure.success(parentNodes);} catch (Exception e) {e.printStackTrace();return ResponseStructure.failed("查询驳回节点失败:" + e.getMessage());}}public void getParentNodes(FlowElement flowElement, List<Map<String, Object>> parentNodes) {List<SequenceFlow>incommingFlows=((FlowNode)flowElement).getIncomingFlows();for (SequenceFlow incommingFlow : incommingFlows) {FlowNode source = (FlowNode)incommingFlow.getSourceFlowElement();if(source instanceof ParallelGateway||source instanceof ExclusiveGateway){getParentNodes(source,parentNodes);}else if(source instanceof StartEvent){break;}else if(source instanceof UserTask){Map<String, Object> node = new HashMap<>();node.put("activityId", source.getId()); // 节点idnode.put("activityName", source.getName()); // 节点名称parentNodes.add(node);getParentNodes(source,parentNodes);}}}

这段代码实现了获取指定任务可驳回的节点信息的功能。它首先根据任务ID查询当前任务信息,然后根据当前任务的流程定义ID获取BpmnModel对象,通过任务节点ID递归查询父节点信息,将可驳回的节点信息封装成列表返回给调用方。

2.5.5. 驳回历史节点
    @PostMapping("/back")public Result backProcess(@RequestParam String taskId,@RequestParam String targetActivityId) {try {// 1. 查询当前任务信息Task task = taskService.createTaskQuery().taskId(taskId).taskAssignee(UserUtils.getUsername()).singleResult();if(task == null) {return Result.error("当前任务不存在或你不是任务办理人");}String procInstId = task.getProcessInstanceId();// 2. 获取流程模型实例 BpmnModelBpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());// 3. 当前节点信息FlowNode curFlowNode = (FlowNode)bpmnModel.getMainProcess().getFlowElement(task.getTaskDefinitionKey());// 4. 获取当前节点的原出口连线List<SequenceFlow> sequenceFlowList = curFlowNode.getOutgoingFlows();// 5. 临时存储当前节点的原出口连线List<SequenceFlow> oriSequenceFlows = new ArrayList<>();oriSequenceFlows.addAll(sequenceFlowList);// 6. 将当前节点的原出口清空sequenceFlowList.clear();// 7. 获取目标节点信息FlowNode targetFlowNode = (FlowNode)bpmnModel.getFlowElement(targetActivityId);// 8. 获取驳回的新节点// 获取目标节点的入口连线List<SequenceFlow> incomingFlows = targetFlowNode.getIncomingFlows();// 存储所有目标出口List<SequenceFlow> allSequenceFlow = new ArrayList<>();for (SequenceFlow incomingFlow : incomingFlows) {// 找到入口连线的源头(获取目标节点的父节点)FlowNode source = (FlowNode)incomingFlow.getSourceFlowElement();List<SequenceFlow> sequenceFlows;if(source instanceof ParallelGateway) {// 并行网关: 获取目标节点的父节点(并行网关)的所有出口,sequenceFlows = source.getOutgoingFlows();} else {// 其他类型父节点, 则获取目标节点的入口连续sequenceFlows = targetFlowNode.getIncomingFlows();}allSequenceFlow.addAll(sequenceFlows);}// 9. 将当前节点的出口设置为新节点curFlowNode.setOutgoingFlows(allSequenceFlow);// 10. 完成当前任务,流程就会流向目标节点创建新目标任务//      删除已完成任务,删除已完成并行任务的执行数据 act_ru_executionList<Task> list = taskService.createTaskQuery().processInstanceId(procInstId).list();for (Task t : list) {if(taskId.equals(t.getId())) {// 当前任务,完成当前任务String message = String.format("【%s 驳回任务 %s => %s】",UserUtils.getUsername(), task.getName(), targetFlowNode.getName());taskService.addComment(t.getId(), procInstId, message);// 完成任务,就会进行驳回到目标节点,产生目标节点的任务数据taskService.complete(taskId);// 删除执行表中 is_active_ = 0的执行数据, 使用command自定义模型DelelteExecutionCommand deleteExecutionCMD = new DelelteExecutionCommand(task.getExecutionId());managementService.executeCommand(deleteExecutionCMD);}else {// 删除其他未完成的并行任务// taskService.deleteTask(taskId); // 注意这种方式删除不掉,会报错:流程正在运行中无法删除。// 使用command自定义命令模型来删除,直接操作底层的删除表对应的方法,对应的自定义是否删除DeleteTaskCommand deleteTaskCMD = new DeleteTaskCommand(t.getId());managementService.executeCommand(deleteTaskCMD);}}// 13. 完成驳回功能后,将当前节点的原出口方向进行恢复curFlowNode.setOutgoingFlows(oriSequenceFlows);// 12. 查询目标任务节点历史办理人List<Task> newTaskList = taskService.createTaskQuery().processInstanceId(procInstId).list();for (Task newTask : newTaskList) {// 取之前的历史办理人HistoricTaskInstance oldTargerTask = historyService.createHistoricTaskInstanceQuery().taskDefinitionKey(newTask.getTaskDefinitionKey()) // 节点id.processInstanceId(procInstId).finished() // 已经完成才是历史.orderByTaskCreateTime().desc() // 最新办理的在最前面.list().get(0);taskService.setAssignee(newTask.getId(), oldTargerTask.getAssignee());}return Result.ok();} catch (Exception e) {e.printStackTrace();return Result.error("驳回失败:"+ e.getMessage());}}

这段代码实现了流程任务的驳回功能。它首先查询当前任务信息,然后获取流程模型实例,通过修改当前节点的出口连线为目标节点的入口连线,完成当前任务并删除已完成的其他任务(并行网关),恢复当前节点的原出口方向,最后设置目标任务节点的办理人为之前的历史办理人。 

2.6. 请假申请管理

请假申请管理对应前端请假申请页面,包含新增请假申请、条件分页查询请假申请列表数据、查询请假详情信息、更新请假详情信息接口。接口都很简单,我在这里讲一下业务流程和工作流怎么串接起来。

创建BusinessStatus表:

BusinessStatus表为串接业务流程和工作流的中间表,字段如下图,大家看图自行创建就行:

基于status字段,在代码中创建BusinessStatusEnum枚举:

@Getter
@AllArgsConstructor
public enum BusinessStatusEnum {CANCEL(0, "已撤回"), WAIT(1, "待提交"), PROCESS(2, "处理中"),FINISH(3, "已完成"), INVALID(4, "已作废"), DELETE(5, "已删除");private Integer code;private String desc;public static BusinessStatusEnum getEumByCode(Integer code){if(code == null) return null;for(BusinessStatusEnum statusEnum: BusinessStatusEnum.values()) {if(statusEnum.getCode() == code) {return statusEnum;}}return null;}}

 新增申请,流程审批通过,驳回,需要顺带操作BusinessStatus表。

由上图即可看出哪些申请新增了,哪些还没有绑定流程,哪些流程正在运行,哪些流程已经执行完毕。

到此,源码已经讲解完啦,还有一些比较简单的可以异步源码地址去看。

3. 源码地址

关注gzh:后端小肥肠  免费领取源码资源

4. 结语

本文作为《基于Spring Security的Activiti7工作流管理系统简介及实现》的下半部分,以实例代码及代码讲解展示了工作流管理系统的实现,文末还粘贴了源码地址,如本文对你有帮助,请动动发财的小手点点关注哦~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/25621.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阻塞队列和线程池

一、什么是阻塞队列 1.1 什么是队列 队列是先进先出。 队列是一种特殊的线性表&#xff0c;特殊之处在于它只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&#xff08;rear&#xff09;进行插入操作&#xff0c;和栈一样&#xff0c;队…

Redis 双写一致原理篇

前言 我们都知道,redis一般的作用是顶在mysql前面做一个"带刀侍卫"的角色,可以缓解mysql的服务压力,但是我们如何保证数据库的数据和redis缓存中的数据的双写一致呢,我们这里先说一遍流程,然后以流程为切入点来谈谈redis和mysql的双写一致性是如何保证的吧 流程 首先…

10.1 Go Goroutine

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

transformer 位置编码源码解读

import torch import mathdef get_positional_encoding(max_len, d_model):"""计算位置编码参数&#xff1a;max_len -- 序列的最大长度d_model -- 位置编码的维度返回&#xff1a;一个形状为 (max_len, d_model) 的位置编码张量"""positional_e…

【机器学习】GPT-4中的机器学习如何塑造人类与AI的新对话

&#x1f680;时空传送门 &#x1f50d;引言&#x1f4d5;GPT-4概述&#x1f339;机器学习在GPT-4中的应用&#x1f686;文本生成与摘要&#x1f388;文献综述与知识图谱构建&#x1f6b2;情感分析与文本分类&#x1f680;搜索引擎优化&#x1f4b4;智能客服与虚拟助手&#x1…

27-LINUX--I/O复用-poll

一.poll概述 poll是一个多路复用的I/O模型&#xff0c;一个进程监视多个文件描述符&#xff0c;当文件描述符就绪时&#xff0c;poll返回可读并做相应处理。 1.poll的模型 #include <poll.h>struct pollfd {int fd; //文件描述符short events; //事件类型 s…

OpenAI新研究破解GPT-4大脑,分解1600万个特征打开“黑匣子”,Ilya 、Jan Leike也参与了!

6月7日凌晨&#xff0c;OpenAI在官网发布了一个新的研究成果&#xff0c;首次破解GPT-4的神经网络活动。通过改进大规模训练稀疏自动编码器将GPT-4的内部表示分解为 1600 万个特征。而且&#xff0c;前段时间离职的Ilya Sutskever、Jan Leike也是作者之一&#xff01; 这不是破…

将AIRNet集成到yolov8中,实现端到端训练与推理

AIRNet是一个图像修复网络,支持对图像进行去雾、去雨、去噪声的修复。其基于对比的退化编码器(CBDE),将各种退化类型统一到同一嵌入空间;然后,基于退化引导恢复网络(DGRN)将嵌入空间修复为目标图像。可以将AIRNet的输出与yolov8进行端到端集成,实现部署上的简化。 本博…

关于 Redis 中集群

哨兵机制中总结到&#xff0c;它并不能解决存储容量不够的问题&#xff0c;但是集群能。 广义的集群&#xff1a;只要有多个机器&#xff0c;构成了分布式系统&#xff0c;都可以称之为一个“集群”&#xff0c;例如主从结构中的哨兵模式。 狭义的集群&#xff1a;redis 提供的…

Django学习二:配置mysql,创建model实例,自动创建数据库表,对mysql数据库表已经创建好的进行直接操作和实验。

文章目录 前言一、项目初始化搭建1、创建项目&#xff1a;test_models_django2、创建应用app01 二、配置mysql三、创建model实例&#xff0c;自动创建数据库表1、创建对象User类2、执行命令 四、思考问题&#xff08;****&#xff09;1、是否会生成新表呢&#xff08;答案报错&…

React保姆级教学

React保姆级教学 一、创建第一个react项目二、JSX基本语法与react基础知识1、 插值语法&#xff1a;2、 循环一个简单列表3、 实现简单条件渲染4、 实现复杂的条件渲染5、 事件绑定6、 基础组件&#xff08;函数组件&#xff09;7、 使用useState8、 基础样式控制9、 动态类名1…

好书推荐之《生成式 AI 入门与亚马逊云科技AWS实战》

最近小李哥在亚马逊云科技峰会领到了一本关于如何在云计算平台上设计、开发GenAI应用的书&#xff0c;名字叫&#xff1a;《生成式 AI 入门与亚马逊云科技AWS实战》&#xff0c;今天仔细看了下&#xff0c;发现这本书讲的真的很好&#xff01;他涵盖了当下AI领域所有热门的技术…

《软件定义安全》之四:什么是软件定义安全

第4章 什么是软件定义安全 1.软件定义安全的含义 1.1 软件定义安全的提出 虚拟化、云计算、软件定义架构的出现&#xff0c;对安全体系提出了新的挑战。如果要跟上网络演进的步伐和业务快速创新的速度&#xff0c;安全体系应该朝以下方向演变。 &#x1d7ed; 安全机制软件…

设计软件有哪些?照明工具篇,渲染100邀请码1a12

阴影和照明涉及到图片的真实感和氛围&#xff0c;所以熟练使用照明工具是设计师的必备能力&#xff0c;这次我们介绍一些照明工具。 1、VRaySun VRaySun是VRay渲染器中的一个功能&#xff0c;用于模拟太阳光源。它是一种方便易用的光源类型&#xff0c;能够产生逼真的日光效果…

解密Spring Boot:深入理解条件装配与条件注解

文章目录 一、条件装配概述1.1 条件装配的基本原理1.2 条件装配的作用 二、常用注解2.1 ConditionalOnClass2.2 ConditionalOnBean2.3 ConditionalOnProperty2.4 ConditionalOnExpression2.5 ConditionalOnMissingBean 三、条件装配的实现原理四、实际案例 一、条件装配概述 1…

Wireshark TS | 应用传输丢包问题

问题背景 仍然是来自于朋友分享的一个案例&#xff0c;实际案例不难&#xff0c;原因也就是互联网线路丢包产生的重传问题。但从一开始只看到数据包截图的判断结果&#xff0c;和最后拿到实际数据包的分析结果&#xff0c;却不是一个结论&#xff0c;方向有点跑偏&#xff0c;…

MySQL: 索引与事务

文章目录 1. 索引 (Index)1.1 概念1.2 作用1.3 使用场景1.4 索引的使用1.5 索引的使用案例 (不要轻易尝试)1.6 索引背后的数据结构1.7 重点总结 2.事务2.1 为什么要使用事务2.2 事务的概念2.3 事务的使用2.4 对事务的理解2.5 事务的基本特性 1. 索引 (Index) 1.1 概念 索引是…

深入STL之 栈与队列:数据结构探索之旅

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;模拟实现list与迭代器 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀stack和queue &#x1f4…

探索软件工程师在新能源汽车研发中的角色与贡献

随着全球对可持续发展的关注不断增加&#xff0c;新能源汽车的研发与应用成为了汽车行业的一个重要方向。作为软件工程师&#xff0c;参与新能源汽车研发不仅能够推动科技创新&#xff0c;还能为环保事业贡献力量。本文将深入探讨软件工程师在新能源汽车研发中的具体贡献、所需…

C#操作MySQL从入门到精通(20)——更新数据

前言: 谈到数据库,大家最容易脱口而出的就是增删改查,本文所说的更新数据就是增删改查的改,改变数据的意思。 本文测试使用的数据库如下: 1、更新一列 所谓更新一列的意思就是只更改一列数据,并且通常要使用where条件,因为不加这个条件的话会导致将所有行的数据进行…