掘金 后端 ( ) • 2024-06-11 09:34

theme: smartblue

作者:后端小肥肠

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

1.前言

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

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

2. 核心代码

本章只做代码简介及核心代码讲解,文末会提供源代码链接。

2.1. 流程定义模型管理

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

image.png

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);
        // 标识key
        ObjectNode 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后缀的文件来进行部署流程定义

image.png

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

前端界面:

image.png

后端代码:

    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)

    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)) {
                // zip
                deployment.addZipInputStream(new ZipInputStream(input));
            }else {
                // xml 或 bpmn
                deployment.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. 流程配置管理

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

image.png

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

image.png

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

image.png

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

   @PutMapping
    public 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. 提交申请,启动流程实例

前端界面:

image.png

在本工作流管理系统中,需要在流程启动时动态指定一级审批用户,我这里指定的是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. 更新业务状态为:办理中, 和流程实例id
        return 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. 挂起或激活流程实例

前端界面:

image.png

后端代码:

@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获取历史流程图

前端界面:

image.png

后端代码:

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();
        // 获取高亮连线id
        List<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获取任务办理历史记录

前端界面:

image.png

后端代码:

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()); // 任务ID
        result.put("taskName", hti.getName()); // 任务名称
        result.put("processInstanceId", hti.getProcessInstanceId()); //流程实例ID
        result.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. 任务管理

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

image.png

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. 获取目标节点(下一个节点)

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

image.png

后端代码:

@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()); // 节点id
            node.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获取当前节点信息,并递归遍历当前节点的连线信息,获取所有下一个节点的信息,将其封装成列表并返回给调用方。

前端返回结果:

image.png

2.5.3. 完成任务

前端传入参数:

image.png

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. 获取历史任务节点,用于驳回功能

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

前端界面:

image.png

后端代码:

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()); // 节点id
                node.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. 获取流程模型实例 BpmnModel
            BpmnModel 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_execution
            List<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表为串接业务流程和工作流的中间表,字段如下图,大家看图自行创建就行:

image.png

基于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表。

image.png

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

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

3. 源码地址

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

4. 结语

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

结语3.jpg