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

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)) {
                // 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. 流程配置管理

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

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

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

 只要在上述表中将流程定义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. 提交申请,启动流程实例

前端界面:

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

前端界面:

后端代码:

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

前端界面:

后端代码:

    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. 任务管理

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

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

前端返回结果:

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()); // 节点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表为串接业务流程和工作流的中间表,字段如下图,大家看图自行创建就行:

基于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.mfbz.cn/a/696464.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

转让北京劳务分包地基基础施工资质条件和流程

地基基础资质转让流程是怎样的?对于企业来说&#xff0c;资质证书不仅是实力的证明&#xff0c;更是获得工程承包的前提。而在有了资质证书后&#xff0c;企业才可以安心的准备工程投标&#xff0c;进而在工程竣工后获得收益。而对于从事地基基础工程施工的企业&#xff0c;需…

10.1 Go Goroutine

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

基于STM32开发的智能农业监控系统

目录 引言环境准备智能农业监控系统基础代码实现&#xff1a;实现智能农业监控系统 4.1 土壤湿度传感器数据读取4.2 温湿度传感器数据读取4.3 水泵与风扇控制4.4 用户界面与数据可视化应用场景&#xff1a;农业环境监测与管理问题解决方案与优化收尾与总结 1. 引言 随着智能…

【文末附gpt升级秘笈】AI热潮降温与AGI场景普及的局限性

AI热潮降温与AGI场景普及的局限性 摘要&#xff1a; 随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;AI热一度席卷全球&#xff0c;引发了广泛的关注和讨论。然而&#xff0c;近期一些学者和行业专家对AI的发展前景提出了质疑&#xff0c;认为AI热潮将逐渐…

BIOPLUSS引领膳食行业创新、整合与再造

2024年NHNE如期而至&#xff0c;同时今年也是中挪建交70年周年&#xff0c;BIOPLUSS作为挪威品牌代表也参加了此次NHNE国际健康营养博览会&#xff0c;此次NHNE展会吸收了来自30多个国家及地区的1200多家品牌参与&#xff0c;BIOPLUSS同时受挪威领事馆、挪威创新署邀请&#xf…

Chapter 6 Frequency Response of Amplifiers

Chapter 6 Frequency Response of Amplifiers 这一节我们学习单极和差分运放的频率响应. 6.1 General Considerations 我们关心magnitude vs 频率, 因此有低通, 带通, 高通滤波器 6.1.1 Miller Effect Miller’s Theorem 考虑impedance Z1和Z2, X和Y之间增益为Av. Z1 Z/(…

C语言 图形化界面方式连接MySQL【C/C++】【图形化界面组件分享】

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;MySQL之旅_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 一.配置开发环境 二…

为什么选择海外服务器?

如何选择跨境电商服务器&#xff1a;详细指南 选择合适的服务器是跨境电商企业成功的基础。服务器的性能和稳定性直接影响着网站的访问速度、用户体验和安全性&#xff0c;进而影响着企业的销量和利润。那么&#xff0c;跨境电商企业该如何选择服务器呢&#xff1f; ​​​​​…

【微信小程序】事件传参的两种方式

文章目录 1.什么是事件传参2.data-*方式传参3.mark自定义数据 1.什么是事件传参 事件传参:在触发事件时&#xff0c;将一些数据作为参数传递给事件处理函数的过程&#xff0c;就是事件传参 在微信小程序中&#xff0c;我们经常会在组件上添加一些自定义数据&#xff0c;然后在…

0元白嫖阿里云4G内存云服务器——感谢伟大的CSDN和阿里云

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信您对博主首页也很感兴趣o (ˉ▽ˉ&#xff1b;) 学生邮箱白嫖/免费安装JetBrains全家桶(IDEA/pycharm等) —— 保姆级教程-CSDN博客 目录 1、学生认证领取300元优惠券 ​2、购买云服务器 1、学生认证领取…

车载电子电气架构 - 智能座舱基础技术

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

STM32CubeIDE使用过程记录

最近在做一款机器人的开发&#xff0c;使用到了STM32CubeIDE&#xff0c;这里记录一些使用技巧方便后续查阅。 STM32CubeIDE使用过程记录 快捷键开启代码自动补全功能看门狗设置CRC设置IO口取反定时器设置 及 定时器中断外部中断GPIO配置STC15单片机GPIO模式配置片内闪存&#…

【Python教程】3-控制流、循环结构与简单字符串操作

在整理自己的笔记的时候发现了当年学习python时候整理的笔记&#xff0c;稍微整理一下&#xff0c;分享出来&#xff0c;方便记录和查看吧。个人觉得如果想简单了解一名语言或者技术&#xff0c;最简单的方式就是通过菜鸟教程去学习一下。今后会从python开始重新更新&#xff0…

frida hook微信防撤回(PC端)

PC端&#xff1a; 微信的主要功能都是在WeChat\[3.9.10.27]\WeChatWin.dll动态链接库中实现的 直接进IDA分析 都没有符号表 我们需要找一下实现撤回功能的函数&#xff0c;尝试在字符串里搜索revokeMsg 也是有非常多的字符串 我们需要用frida来hook这些字符串来找出撤回实际…

初识volatile

volatile&#xff1a;可见性、不能保证原子性(数据不安全)、禁止指令重排 可见性&#xff1a;多线程修改共享内存的变量的时候&#xff0c;修改后会通知其他线程修改后的值&#xff0c;此时其他线程可以读取到修改后变量的值。 指令重排&#xff1a;源代码的代码顺序与编译后字…

【学习】DCMM认证提升企业竞争优势的表现

DCMM认证是企业提升数据管理能力的重要途径。它不仅可以帮助企业评估自身的数据管理水平&#xff0c;还可以为企业提供改进的方向和目标。在数字化时代&#xff0c;拥有强大的数据管理能力是企业成功的关键。因此&#xff0c;通过DCMM认证&#xff0c;企业可以更好地适应数字化…

ARM交叉编译

目录 一、介绍 1、本地编译 2、交叉编译 二、交叉工具链 1、概念 2、工具 3、获取方法 三、交叉编译运行程序 1、pc机操作&#xff08;x86_64&#xff09; ​2、开发板操作&#xff08;ARM&#xff09; 一、介绍 1、本地编译 本地编译是在与目标运行环境相同的机器上…

大模型训练的艺术:从预训练到增强学习的四阶段之旅

文章目录 大模型训练的艺术&#xff1a;从预训练到增强学习的四阶段之旅1. 预训练阶段&#xff08;Pretraining&#xff09;2. 监督微调阶段&#xff08;Supervised Finetuning, SFT&#xff09;3. 奖励模型训练阶段&#xff08;Reward Modeling&#xff09;4. 增强学习微调阶段…

FreeRTOS消息队列

消息队列简介 队列是为了任务与任务、 任务与中断之间的通信而准备的&#xff0c; 可以在任务与任务、 任务与中断之间传递消息&#xff0c; 队列中可以存储有限的、 大小固定的数据项目。任务与任务、 任务与中断之间要交流的数据保存在队列中&#xff0c; 叫做队列项目。 队…

【博士每天一篇文献-综述】Modularity in Deep Learning A Survey

阅读时间&#xff1a;2023-12-8 1 介绍 年份&#xff1a;2023 作者&#xff1a;孙浩哲&#xff0c;布朗克斯医疗卫生系统 会议&#xff1a; Science and Information Conference 引用量&#xff1a;4 论文主要探讨了深度学习中的模块化&#xff08;modularity&#xff09;概念…