当前位置: 首页 > news >正文

【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工作流管理系统简介及实现》的下半部分,以实例代码及代码讲解展示了工作流管理系统的实现,文末还粘贴了源码地址,如本文对你有帮助,请动动发财的小手点点关注哦~~

相关文章:

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

作者&#xff1a;后端小肥肠 上篇&#xff1a;【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现&#xff08;上篇&#xff09;_spring security activiti7-CSDN博客 目录 1.前言 2. 核心代码 2.1. 流程定义模型管理 2.1.1. 新增流程定义模型数据 …...

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

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

【数据结构与算法】使用数组实现栈:原理、步骤与应用

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《数据结构与算法》 期待您的关注 ​ 目录 一、引言 &#x1f384;栈&#xff08;Stack&#xff09;是什么&#xff1f; &#x1…...

cell的复用机制和自定义cell

cell的复用机制和自定义cell UITableView 在学习cell之前&#xff0c;我们需要先了解UITableView。UITableView继承于UIScrollView&#xff0c;拥有两个两个相关协议 UITableViewDelegate和UITableViewDataSource&#xff0c;前者用于显示单元格&#xff0c;设置行高以及对单…...

Redis 双写一致原理篇

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

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

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

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

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

hcache缓存查看工具

1、hcache概述 hcache是基于pcstat的&#xff0c;pcstat可以查看某个文件是否被缓存和根据进程pid来查看都缓存了哪些文件。hcache在其基础上增加了查看整个操作系统Cache和根据使用Cache大小排序的特性。官网:https://github.com/silenceshell/hcache 2、hcache安装 2.1下载…...

Java 数据类型 -- Java 语言的 8 种基本数据类型、字符串与数组

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 004 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…...

kafka-生产者事务-数据传递语义事务介绍事务消息发送(SpringBoot整合Kafka)

文章目录 1、kafka数据传递语义2、kafka生产者事务3、事务消息发送3.1、application.yml配置3.2、创建生产者监听器3.3、创建生产者拦截器3.4、发送消息测试3.5、使用Java代码创建主题分区副本3.6、屏蔽 kafka debug 日志 logback.xml3.7、引入spring-kafka依赖3.8、控制台日志…...

免费!GPT-4o发布,实时语音视频丝滑交互

We’re announcing GPT-4o, our new flagship model that can reason across audio, vision, and text in real time. 5月14日凌晨&#xff0c;OpenAI召开了春季发布会&#xff0c;发布会上公布了新一代旗舰型生成式人工智能大模型【GPT-4o】&#xff0c;并表示该模型对所有免费…...

DevOps的原理及应用详解(四)

本系列文章简介: 在当今快速变化的商业环境中,企业对于软件交付的速度、质量和安全性要求日益提高。传统的软件开发和运维模式已经难以满足这些需求,因此,DevOps(Development和Operations的组合)应运而生,成为了解决这些问题的有效方法。 DevOps是一种强调软件开发人员(…...

关于选择,关于处事

一个人选择应该选择的是勇敢&#xff0c;选择不应该选择的是无奈。放弃&#xff0c;不该放弃的是懦夫&#xff0c;不放弃应该放弃的是睿智。所以&#xff0c;碰到事的时候要先静&#xff0c;先不管什么事&#xff0c;先静下来&#xff0c;先淡定&#xff0c;先从容。在生活里要…...

大话设计模式解读02-策略模式

本篇文章&#xff0c;来解读《大话设计模式》的第2章——策略模式。并通过Qt和C代码实现实例代码的功能。 1 策略模式 策略模式作为一种软件设计模式&#xff0c;指对象有某个行为&#xff0c;但是在不同的场景中&#xff0c;该行为有不同的实现算法。 策略模式的特点&#…...

展会邀请 | 龙智即将亮相2024上海国际嵌入式展,带来安全合规、单一可信数据源、可追溯、高效协同的嵌入式开发解决方案

2024年6月12日至14日&#xff0c;备受全球嵌入式系统产业和社群瞩目的2024上海国际嵌入式展&#xff08;embedded world china 2024&#xff09;即将盛大开幕&#xff0c;龙智将携行业领先的嵌入式开发解决方案亮相 640展位 。 此次参展&#xff0c;龙智将全面展示专为嵌入式行…...

codeforce round951 div2

A guess the maximum 问题&#xff1a; 翻译一下就是求所有相邻元素中max - 1的最小值 代码&#xff1a; #include <iostream> #include <algorithm>using namespace std;const int N 5e4;int a[N]; int n;void solve() {cin >> n;int ans 0x3f3f3f3f;…...

arcgis开发记录

目录 文章目录 [toc]**arcgis JavaScript API安装**1. arcgisAPI下载地址&#xff1a;https://developers.arcgis.com/downloads/2. 4.4版本API&#xff1a;本地配置3. 3.18版本修改方法 **angular2中加载arcgis JS API**** arcgis加载图层 并显示图层上点的信息****使用图层上…...

RPA-UiBot6.0数据整理机器人—杂乱数据秒变报表

前言 友友们是否常常因为杂乱的数据而烦恼?数据分类、排序、筛选这些繁琐的任务是否占据了友友们的大部分时间?这篇博客将为友友们带来一个新的解决方案,让我们共同学习如何运用RPA数据整理机器人,实现杂乱数据的快速整理,为你的工作减负增效! 在这里,友友们将了…...

Application UI

本节包含关于如何用DevExpress控件模拟许多流行的应用程序ui的教程。 Windows 11 UI Windows 11和最新一代微软Office产品启发的UI。 Office Inspired UI Word、Excel、PowerPoint和Visio等微软Office应用程序启发的UI。 如何&#xff1a;手动构建Office风格的UI 本教程演示…...

关于 Redis 中集群

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

C++必修:探索C++的内存管理

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C学习 贝蒂的主页&#xff1a;Betty’s blog 1. C/C的内存分布 我们首先来看一段代码及其相关问题 int globalVar 1; static…...

python列表---基本语法(浅拷贝,深拷贝等)

文章目录 引言:列表的注意事项1 list中的浅拷贝与深拷贝1.1浅拷贝(Shallow Copy)浅拷贝的方法浅拷贝的效果1.2深拷贝(Deep Copy)深拷贝的方法深拷贝的效果1.3 总结:浅拷贝 vs 深拷贝1.4 为什么浅拷贝顶层元素如果是不可变数据就不能共享,不是传的是引用就相当于传的是地…...

go语言接口之sort.Interface接口

排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要15 行就可以搞定&#xff0c;但是一个健壮的实现需要更多的代码&#xff0c;并且我们不希望每次我们需要的时候 都重写或者拷贝这些代码。 幸运的是&#xff0c;sort包内置的提供了根据一些排序…...

android:text 总为大写字母的原因

当设置某个 Button 的 text 为英文时&#xff0c;界面上显示的是该英文的大写形式&#xff08;uppercase&#xff09;。例如&#xff1a; <Buttonandroid:id"id/btn"android:layout_width"wrap_content"android:layout_height"wrap_content"…...

CISCN2024 初赛 wp 部分复现(Re)

Misc 1. 火锅链观光打卡 答题即可 Re 1. asm_re 感谢智谱清言&#xff0c;可以读出大致加密算法 这是输入 这是加密部分 这里判断 找到疑似密文的部分&#xff0c;手动改一下端序 #asm_wp def dec(char):return (((char - 0x1E) ^ 0x4D) - 0x14) // 0x50 #return (ord(cha…...

YOLOv10、YOLOv9 和 YOLOv8 在实际视频中的对比

引言 目标检测技术是计算机视觉领域的核心任务之一&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;系列模型凭借其高效的检测速度和准确率成为了业界的宠儿。本文将详细对比YOLOv10、YOLOv9和YOLOv8在实际视频中的表现&#xff0c;探讨它们在性能、速度和实际应用…...

热题系列章节5

169. 多数元素 给定一个大小为 n 的数组&#xff0c;找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1: 输入: [3,2,3] 输出: 3 示例 2: 输入: [2,2,1,1,1,2,2] 输出:…...

ArcGIS for js 4.x 加载图层

二维&#xff1a; 1、创建vue项目 npm create vitelatest 2、安装ArcGIS JS API依赖包 npm install arcgis/core 3、引入ArcGIS API for JavaScript模块 <script setup> import "arcgis/core/assets/esri/themes/light/main.css"; import Map from arcgis…...

Three.js和Babylon.js,webGL中的对比效果分析!

hello&#xff0c;今天分享一些three.js和babylon.js常识&#xff0c;为大家选择three.js还是babylon.js做个分析&#xff0c;欢迎点赞评论转发。 一、Babylon.js是什么 Babylon.js是一个基于WebGL技术的开源3D游戏引擎和渲染引擎。它提供了一套简单易用的API&#xff0c;使开发…...

flask实现抽奖程序(一)

后端代码E:\LearningProject\lottery\app.py from flask import Flask, render_template import randomapp Flask(__name__)employees [赵一, 钱二, 孙三, 李四, 周五, 吴六, 郑七, 王八]app.route(/) def hello_world():return render_template(index.html, employeesemplo…...

wordpress防攻击代码/seo优化检测

转自&#xff1a;https://wenku.baidu.com/view/84fa86ae360cba1aa911da02.html 建立struts2wildcard项目&#xff0c;此实例基本仿照前面前面第7点的实例改写而成。为了使用通配符&#xff0c;只需要改写配置文件即可。此实例未使用通配时的配置文件如下&#xff1a; <acti…...

brophp框架如何做网站/郑州高端网站建设哪家好

一、request.Request方法的使用上一章节中介绍了request.urlopen()的使用,仅仅的很简单的使用,不能设置请求头及cookie的东西,request.Request()方法就是进一步的包装请求.1、源码查看参数class Request:def __init__(self, url, dataNone, headers{}, origin_req_hostNone, un…...

做我男朋友好不好网站/二级域名分发平台

几天前头儿要我实现程序能开机自动启动&#xff0c;搞好了&#xff0c;整理起来写下来。 private void checkBox1_CheckedChanged(object sender, EventArgs e){string path Directory.GetCurrentDirectory() "\LEDCOM.exe";//程序名RunWhenStart(checkBox1.Checke…...

湖南网站建设企业/关键词优化师

const readline require(readline-sync);//引入用户输入功能console.log(请输入数组的长度&#xff1a;);//提示用户输入let length readline.question();//获取用户输入let arr [];//创建数组for (let i 0; i < length; i) {console.log(请输入数组的第${i 1}个数&…...

宜兴做网站的公司/爱站小工具圣经

文末扫码免费领【SQL学习路径导图】唐亦六安 | 作者知乎 | 来源https://zhuanlan.zhihu.com/p/113239595刚接触sql那会&#xff0c;我总是遇到很多问题&#xff0c;写的sql太过于冗杂或无从下手&#xff1b;连接逻辑不太清晰&#xff1b;解读需求时间过长等等。一个SQL能够解决…...

wordpress 小米商城主题/什么是seo是什么意思

目录 A、阶乘求和 Ⅰ、题目解读 Ⅱ、代码 B、幸运数字 Ⅰ、题目解读 Ⅱ、代码 C: 数组分割&#xff08;时间限制: 1.0s 内存限制: 512.0MB&#xff09; D、矩形总面积&#xff08;时间限制: 1.0s 内存限制: 512.0MB&#xff09; Ⅰ、题目解读 Ⅱ、代码 E、蜗牛&#xff…...