关于activiti的使用(如何高亮显示已经执行过的节点,修改高亮的颜色,节点的宽度,中文乱码的问题)

 

ActivitiController 类 cotrolelr包下面

package com.woniu.controller;


import com.woniu.utils.LakerProcessDiagramGenerator;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.engine.*;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.image.ProcessDiagramGenerator;
import org.activiti.image.impl.DefaultProcessDiagramGenerator;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;


@RestController
public class ActivitiController {
@Autowired
private ProcessEngineConfiguration processEngineConfiguration;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private HistoryService historyService;

/**
* 流程部署
*/
@RequestMapping("t1")
public void deploy(){
Deployment deploy = repositoryService.createDeployment().name("网关").
addClasspathResource("processes/demo.bpmn").deploy();
System.out.println("流程部署id:"+deploy.getId());
System.out.println("流程部署名字:"+deploy.getName());
}

/**
* 启动流程实例
*/
@RequestMapping("t2")
public void start(){
ProcessInstance myDemo = runtimeService.startProcessInstanceByKey("demo");
System.out.println("流程定义id:"+myDemo.getProcessDefinitionId());
System.out.println("当前实例id"+myDemo.getId());
System.out.println("当前活动id"+myDemo.getActivityId());
System.out.println("==================================");

}

/**
* 完成个人任务
*/
@RequestMapping("t3")
public void compleTask(String name){
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("demo")
.taskAssignee(name)
.list();
System.out.println("流程实例id:"+list.get(0).getProcessInstanceId());
System.out.println("任务id:"+list.get(0).getId());
System.out.println("任务负责人:"+list.get(0).getAssignee());
System.out.println("任务名称:"+list.get(0).getName());
// 获取任务id
String id = list.get(0).getId();
System.out.println(id);
taskService.complete(id);
System.out.println("==================================");

}


@RequestMapping("t4")
public void test1(HttpServletResponse response,String id) throws Exception {
// 获取历史流程实例
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(id).singleResult();

if (historicProcessInstance == null) {
throw new Exception();
} else {
// 获取流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(historicProcessInstance.getProcessDefinitionId()).singleResult();

// 获取流程历史中已执行节点,并按照节点在流程中执行先后顺序排序
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(id).orderByHistoricActivityInstanceId().asc().list();

// 已执行的节点ID集合
List<String> executedActivityIdList = new ArrayList<String>();

for (HistoricActivityInstance activityInstance : historicActivityInstanceList) {
executedActivityIdList.add(activityInstance.getActivityId());
}

// 获取流程图图像字符流
BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());

DefaultProcessDiagramGenerator diagramGenerator=new DefaultProcessDiagramGenerator();
//绘制bpmnModel代表的流程的流程图
InputStream inputStream = diagramGenerator.generateDiagram(bpmnModel, "png", executedActivityIdList,Collections.EMPTY_LIST, "宋体", "宋体", "宋体", null, 1.0);
OutputStream outputStream = response.getOutputStream();
IOUtils.copy(inputStream, outputStream);
inputStream.close();
outputStream.close();
}
}

/**
* 生成图片
* @param response
* @throws IOException
*/
@RequestMapping("/t5")
public void t5(HttpServletResponse response) throws IOException {
List<ProcessDefinition> demo = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("demo")
.list();
String diagramResourceName = demo.get(0).getDiagramResourceName();
InputStream imageStream = repositoryService.getResourceAsStream(
demo.get(0).getDeploymentId(), diagramResourceName);
OutputStream outputStream = response.getOutputStream();
IOUtils.copy(imageStream, outputStream);
imageStream.close();
outputStream.close();
}

/**
* 生成进行中当前的高亮节点图片
* @param response
* @throws IOException
*/
@RequestMapping("/t6")
public void t6(HttpServletResponse response) throws IOException {
List<ProcessDefinition> demo = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("demo")
.list();
BpmnModel bpmnModel = repositoryService.getBpmnModel(demo.get(0).getId());// 模型
List<String> highLightedActivities = runtimeService.getActiveActivityIds("10");// 高亮节点
List<String> highLightedFlows = new ArrayList<>(); // 高亮连接线
ProcessDiagramGenerator processDiagramGenerator = new DefaultProcessDiagramGenerator();
InputStream png = processDiagramGenerator.generateDiagram
(bpmnModel, "png",highLightedActivities,
highLightedFlows, "宋体", "微软雅黑", "黑体", null, 2.0);
OutputStream outputStream = response.getOutputStream();
IOUtils.copy(png, outputStream);
png.close();
outputStream.close();
}

/**
* 高亮执行的所有节点
* @param response
* @throws IOException
*/
@RequestMapping("/t7")
public void t7(HttpServletResponse response,String id) throws IOException {
// 获取历史流程实例
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(id).singleResult();
// 获取流程历史中已执行节点,并按照节点在流程中执行先后顺序排序
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(id).orderByHistoricActivityInstanceId().asc().list();

// 已执行的节点ID集合
List<String> executedActivityIdList = new ArrayList<String>();
for (HistoricActivityInstance activityInstance : historicActivityInstanceList) {
executedActivityIdList.add(activityInstance.getActivityId());
}
LakerProcessDiagramGenerator processDiagramGenerator = new LakerProcessDiagramGenerator();
// 获取流程图图像字符流
BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
//获取流程图的输入流
InputStream inputStream = processDiagramGenerator.generateDiagram(bpmnModel, "png", executedActivityIdList,Collections.EMPTY_LIST, "宋体", "宋体", "宋体", null, 1.0);
// //输出图片到指定路径
// IOUtils.copy(inputStream, new FileOutputStream(new File("d:/test2.png")));

ServletOutputStream outputStream = response.getOutputStream();
// 输出到控制台
IOUtils.copy(inputStream,outputStream);
System.out.println("输出成功");
}



}




// 以下三个类放在utils包

ActivitiConfig 类 解决乱码
package com.woniu.utils;

import org.activiti.spring.SpringProcessEngineConfiguration;
import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ActivitiConfig implements ProcessEngineConfigurationConfigurer {

/**
* 解決工作流生成图片乱码问题
*
* @param processEngineConfiguration processEngineConfiguration
*/
@Override
public void configure(SpringProcessEngineConfiguration processEngineConfiguration) {
processEngineConfiguration.setActivityFontName("宋体");
processEngineConfiguration.setAnnotationFontName("宋体");
processEngineConfiguration.setLabelFontName("宋体");
}
}



LakerProcessDiagramCanvas 类 配置高亮流程的颜色和宽度
package com.woniu.utils;

import org.activiti.image.impl.DefaultProcessDiagramCanvas;

import java.awt.*;
import java.awt.geom.RoundRectangle2D;

public class LakerProcessDiagramCanvas extends DefaultProcessDiagramCanvas {
public LakerProcessDiagramCanvas(int width, int height, int minX, int minY, String imageType, String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader) {
super(width, height, minX, minY, imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);
HIGHLIGHT_COLOR = Color.green; // 修改高亮颜色
THICK_TASK_BORDER_STROKE = new BasicStroke(10.0f);
}

public LakerProcessDiagramCanvas(int width, int height, int minX, int minY, String imageType) {
super(width, height, minX, minY, imageType);
HIGHLIGHT_COLOR = Color.green;
THICK_TASK_BORDER_STROKE = new BasicStroke(10.0f);
}

public void drawHighLightColor(int x, int y, int width, int height, Color color) {
Paint originalPaint = g.getPaint();
Stroke originalStroke = g.getStroke();

g.setPaint(color);
g.setStroke(THICK_TASK_BORDER_STROKE);

RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);
g.draw(rect);

g.setPaint(originalPaint);
g.setStroke(originalStroke);
}

}





LakerProcessDiagramGenerator 类 画出高亮的节点


package com.woniu.utils;

import org.activiti.bpmn.model.Process;
import org.activiti.bpmn.model.*;
import org.activiti.image.impl.DefaultProcessDiagramCanvas;
import org.activiti.image.impl.DefaultProcessDiagramGenerator;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;

public class LakerProcessDiagramGenerator extends DefaultProcessDiagramGenerator {
private BufferedImage processDiagram;

@Override
protected DefaultProcessDiagramCanvas generateProcessDiagram(BpmnModel bpmnModel, String imageType,
List<String> highLightedActivities, List<String> highLightedFlows,
String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader, double scaleFactor) {

{

prepareBpmnModel(bpmnModel);

DefaultProcessDiagramCanvas processDiagramCanvas = initProcessDiagramCanvas(bpmnModel, imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);

// Draw pool shape, if process is participant in collaboration
for (Pool pool : bpmnModel.getPools()) {
GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(pool.getId());
processDiagramCanvas.drawPoolOrLane(pool.getName(), graphicInfo);
}

// Draw lanes
for (Process process : bpmnModel.getProcesses()) {
for (Lane lane : process.getLanes()) {
GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(lane.getId());
processDiagramCanvas.drawPoolOrLane(lane.getName(), graphicInfo);
}
}

// Draw activities and their sequence-flows
for (FlowNode flowNode : bpmnModel.getProcesses().get(0).findFlowElementsOfType(FlowNode.class)) {
drawActivity(processDiagramCanvas, bpmnModel, flowNode, highLightedActivities, highLightedFlows, scaleFactor);
}

for (Process process : bpmnModel.getProcesses()) {
for (FlowNode flowNode : process.findFlowElementsOfType(FlowNode.class)) {
drawActivity(processDiagramCanvas, bpmnModel, flowNode, highLightedActivities, highLightedFlows, scaleFactor);
}
}

// Draw artifacts
for (Process process : bpmnModel.getProcesses()) {

for (Artifact artifact : process.getArtifacts()) {
drawArtifact(processDiagramCanvas, bpmnModel, artifact);
}

List<SubProcess> subProcesses = process.findFlowElementsOfType(SubProcess.class, true);
if (subProcesses != null) {
for (SubProcess subProcess : subProcesses) {
for (Artifact subProcessArtifact : subProcess.getArtifacts()) {
drawArtifact(processDiagramCanvas, bpmnModel, subProcessArtifact);
}
}
}
}

return processDiagramCanvas;
}
}

@Override
protected void drawActivity(DefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel,
FlowNode flowNode, List<String> highLightedActivities, List<String> highLightedFlows, double scaleFactor) {

{

ActivityDrawInstruction drawInstruction = activityDrawInstructions.get(flowNode.getClass());
if (drawInstruction != null) {

drawInstruction.draw(processDiagramCanvas, bpmnModel, flowNode);

// Gather info on the multi instance marker
boolean multiInstanceSequential = false, multiInstanceParallel = false, collapsed = false;
if (flowNode instanceof Activity) {
Activity activity = (Activity) flowNode;
MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = activity.getLoopCharacteristics();
if (multiInstanceLoopCharacteristics != null) {
multiInstanceSequential = multiInstanceLoopCharacteristics.isSequential();
multiInstanceParallel = !multiInstanceSequential;
}
}

// Gather info on the collapsed marker
GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
if (flowNode instanceof SubProcess) {
collapsed = graphicInfo.getExpanded() != null && !graphicInfo.getExpanded();
} else if (flowNode instanceof CallActivity) {
collapsed = true;
}

if (scaleFactor == 1.0) {
// Actually draw the markers
processDiagramCanvas.drawActivityMarkers((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight(),
multiInstanceSequential, multiInstanceParallel, collapsed);
}

// 画高亮的节点 TODO
if (highLightedActivities.contains(flowNode.getId())) {
if (highLightedActivities.get(highLightedActivities.size() - 1).equalsIgnoreCase(flowNode.getId())) {

LakerProcessDiagramCanvas lakerProcessDiagramCanvas = ((LakerProcessDiagramCanvas) processDiagramCanvas);
lakerProcessDiagramCanvas.drawHighLightColor((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight(), Color.green);
} else {
processDiagramCanvas.drawHighLight((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight());
}


}

}

// Outgoing transitions of activity
for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
boolean highLighted = (highLightedFlows.contains(sequenceFlow.getId()));
String defaultFlow = null;
if (flowNode instanceof Activity) {
defaultFlow = ((Activity) flowNode).getDefaultFlow();
} else if (flowNode instanceof Gateway) {
defaultFlow = ((Gateway) flowNode).getDefaultFlow();
}

boolean isDefault = false;
if (defaultFlow != null && defaultFlow.equalsIgnoreCase(sequenceFlow.getId())) {
isDefault = true;
}
boolean drawConditionalIndicator = sequenceFlow.getConditionExpression() != null && !(flowNode instanceof Gateway);

String sourceRef = sequenceFlow.getSourceRef();
String targetRef = sequenceFlow.getTargetRef();
FlowElement sourceElement = bpmnModel.getFlowElement(sourceRef);
FlowElement targetElement = bpmnModel.getFlowElement(targetRef);
List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());
if (graphicInfoList != null && graphicInfoList.size() > 0) {
graphicInfoList = connectionPerfectionizer(processDiagramCanvas, bpmnModel, sourceElement, targetElement, graphicInfoList);
int xPoints[] = new int[graphicInfoList.size()];
int yPoints[] = new int[graphicInfoList.size()];

for (int i = 1; i < graphicInfoList.size(); i++) {
GraphicInfo graphicInfo = graphicInfoList.get(i);
GraphicInfo previousGraphicInfo = graphicInfoList.get(i - 1);

if (i == 1) {
xPoints[0] = (int) previousGraphicInfo.getX();
yPoints[0] = (int) previousGraphicInfo.getY();
}
xPoints[i] = (int) graphicInfo.getX();
yPoints[i] = (int) graphicInfo.getY();

}

processDiagramCanvas.drawSequenceflow(xPoints, yPoints, drawConditionalIndicator, isDefault, highLighted, scaleFactor);

// Draw sequenceflow label
GraphicInfo labelGraphicInfo = bpmnModel.getLabelGraphicInfo(sequenceFlow.getId());
if (labelGraphicInfo != null) {
processDiagramCanvas.drawLabel(sequenceFlow.getName(), labelGraphicInfo, false);
}
}
}

// Nested elements
if (flowNode instanceof FlowElementsContainer) {
for (FlowElement nestedFlowElement : ((FlowElementsContainer) flowNode).getFlowElements()) {
if (nestedFlowElement instanceof FlowNode) {
drawActivity(processDiagramCanvas, bpmnModel, (FlowNode) nestedFlowElement,
highLightedActivities, highLightedFlows, scaleFactor);
}
}
}
}
}

protected static DefaultProcessDiagramCanvas initProcessDiagramCanvas(BpmnModel bpmnModel, String imageType,
String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader) {

// We need to calculate maximum values to know how big the image will be in its entirety
double minX = Double.MAX_VALUE;
double maxX = 0;
double minY = Double.MAX_VALUE;
double maxY = 0;

for (Pool pool : bpmnModel.getPools()) {
GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(pool.getId());
minX = graphicInfo.getX();
maxX = graphicInfo.getX() + graphicInfo.getWidth();
minY = graphicInfo.getY();
maxY = graphicInfo.getY() + graphicInfo.getHeight();
}

List<FlowNode> flowNodes = gatherAllFlowNodes(bpmnModel);
for (FlowNode flowNode : flowNodes) {

GraphicInfo flowNodeGraphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());

// width
if (flowNodeGraphicInfo.getX() + flowNodeGraphicInfo.getWidth() > maxX) {
maxX = flowNodeGraphicInfo.getX() + flowNodeGraphicInfo.getWidth();
}
if (flowNodeGraphicInfo.getX() < minX) {
minX = flowNodeGraphicInfo.getX();
}
// height
if (flowNodeGraphicInfo.getY() + flowNodeGraphicInfo.getHeight() > maxY) {
maxY = flowNodeGraphicInfo.getY() + flowNodeGraphicInfo.getHeight();
}
if (flowNodeGraphicInfo.getY() < minY) {
minY = flowNodeGraphicInfo.getY();
}

for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());
if (graphicInfoList != null) {
for (GraphicInfo graphicInfo : graphicInfoList) {
// width
if (graphicInfo.getX() > maxX) {
maxX = graphicInfo.getX();
}
if (graphicInfo.getX() < minX) {
minX = graphicInfo.getX();
}
// height
if (graphicInfo.getY() > maxY) {
maxY = graphicInfo.getY();
}
if (graphicInfo.getY() < minY) {
minY = graphicInfo.getY();
}
}
}
}
}

List<Artifact> artifacts = gatherAllArtifacts(bpmnModel);
for (Artifact artifact : artifacts) {

GraphicInfo artifactGraphicInfo = bpmnModel.getGraphicInfo(artifact.getId());

if (artifactGraphicInfo != null) {
// width
if (artifactGraphicInfo.getX() + artifactGraphicInfo.getWidth() > maxX) {
maxX = artifactGraphicInfo.getX() + artifactGraphicInfo.getWidth();
}
if (artifactGraphicInfo.getX() < minX) {
minX = artifactGraphicInfo.getX();
}
// height
if (artifactGraphicInfo.getY() + artifactGraphicInfo.getHeight() > maxY) {
maxY = artifactGraphicInfo.getY() + artifactGraphicInfo.getHeight();
}
if (artifactGraphicInfo.getY() < minY) {
minY = artifactGraphicInfo.getY();
}
}

List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(artifact.getId());
if (graphicInfoList != null) {
for (GraphicInfo graphicInfo : graphicInfoList) {
// width
if (graphicInfo.getX() > maxX) {
maxX = graphicInfo.getX();
}
if (graphicInfo.getX() < minX) {
minX = graphicInfo.getX();
}
// height
if (graphicInfo.getY() > maxY) {
maxY = graphicInfo.getY();
}
if (graphicInfo.getY() < minY) {
minY = graphicInfo.getY();
}
}
}
}

int nrOfLanes = 0;
for (Process process : bpmnModel.getProcesses()) {
for (Lane l : process.getLanes()) {

nrOfLanes++;

GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(l.getId());
// // width
if (graphicInfo.getX() + graphicInfo.getWidth() > maxX) {
maxX = graphicInfo.getX() + graphicInfo.getWidth();
}
if (graphicInfo.getX() < minX) {
minX = graphicInfo.getX();
}
// height
if (graphicInfo.getY() + graphicInfo.getHeight() > maxY) {
maxY = graphicInfo.getY() + graphicInfo.getHeight();
}
if (graphicInfo.getY() < minY) {
minY = graphicInfo.getY();
}
}
}

// Special case, see https://activiti.atlassian.net/browse/ACT-1431
if (flowNodes.isEmpty() && bpmnModel.getPools().isEmpty() && nrOfLanes == 0) {
// Nothing to show
minX = 0;
minY = 0;
}

return new LakerProcessDiagramCanvas((int) maxX + 10, (int) maxY + 10, (int) minX, (int) minY,
imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);
}

}



直接粘贴即可看见效果,另外附上mamven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.woniu</groupId>
<artifactId>activiti-demo-02</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.5.RELEASE</version>
</parent>


<dependencies>
<!--引入spring的web支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-basic</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

</dependencies>

</project>




以上所有代码均是参考工作代码和下面的大佬整合而来,带链接,希望可以给需要的小伙伴带来帮助
https://www.jianshu.com/p/3f976a47114c
https://www.cnblogs.com/chengxuxiaoyuan/p/12026834.html
https://juejin.cn/post/6906322920855830542









posted @ 2021-07-30 16:10  qt1234qwer  阅读(1237)  评论(0)    收藏  举报