星云erp整合activiti--阶段一

137 阅读7分钟

前言

鉴于交流学习时候很多同学吐槽 开源的星云erp很多功能不完整,特别重要的审批流程管理没有等问题。现在决定基于开源的星云erp 最新版整合activiti。

星云erp整合activiti 源码

配套星云erp移动版源码

开发目标

在最新版 星云erp中整合activiti ,并完成 创建流程、部署流程、启动任务、查询代办、处理任务、查询历史任务的相关测试。

准备工作

1、  基于版本

目前 gitee上master分支下的 星云erp,同学们自行下载,也可以下载我已经整合了的版本。

(注意: 这里只是做技术交流分享,同学们自觉遵守原作的相关协议)

2、  相关技术栈

Springboot 2.2.2.RELEASE

MyBatis-plus 3.4.2

Spring-session-data-redis 2.2.0.RELEASE

HuTool 5.7.17

Lombok 1.18.10

EasyExcel 2.2.10

JDK 1.8

Mysql 5.7.18

Redis 4.0.8

Activiti 7.1.0.M4 (用7.1.0.M6 会强制需要SpringSecurity认证)

(以上技术栈要求版本进攻参考,大家可以根据自己开发环境调整)

3、  项目架构分析

(为了方便直奔主题,决定直接在 xingyun-api下整合)

整合Activiti

1、引入Activiti相关依赖

在后端工程 xingyun-api模块的 pom.xml文件中添加依赖

    <!--activity工作流依赖-->
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-spring-boot-starter</artifactId>
            <version>7.1.0.M4</version>
            <exclusions>
                <exclusion>
                    <artifactId>mybatis</artifactId>
                    <groupId>org.mybatis</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-image-generator</artifactId>
            <version>7.1.0.M4</version>
        </dependency>
        <!--结束activity工作流依赖-->

2、  配置application.yml文件

在spring标签下添加activiti的相关配置

spring:
  activiti:
    db-history-used: true  #进行相关activiti操作时,要生成history相关的表
    history-level: full  #history相关表中的历史记录都要被记录

至此,如没有问题则可以启动 项目,会自动生成 activiti的 25(为什么不是28张,请自行百度)张数据表。

运行结果如下:

通过Navicat查看到数据库中新增了以下相关表,证明 activiti整合成功。

image.png

创建测试流程

1、idea中安装 BPMN-Activiti-Diagram 插件

● 在file---setting --plugins 中搜索组件 BPMN-Activiti-Diagram

image.png ● 安装完成后

image.png

2、  创建流程

(阶段一、流程的设计直接使用插件来创建,后期会整合到PC版)

● 在 resources文件夹中 创建审批流程的xml

image.png

● 这里我已经创建了一个 借支流程测试的 工作流程(自行到gitee下下载)

${sp==1}

${sp1==1&&money>3000}

${sp1==1&&money==3000}

${sp==0}

${sp1==0}

${sp2==1}

${sp2==0}

<bpmndi:BPMNDiagram id="BPMNDiagram_借支申请">

<bpmndi:BPMNPlane bpmnElement="jz" id="BPMNPlane_借支申请">

<bpmndi:BPMNShape id="shape-e58741b4-6e31-4074-935f-374c6b28948d" bpmnElement="sid-2f65ca73-6ae5-411f-80d6-61b87da2868c">

<omgdc:Bounds x="-185.0" y="-65.0" width="30.0" height="30.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNShape id="shape-4e82b3f5-9b68-4e6d-9979-e1797046a8dc" bpmnElement="sid-bac0db30-99bf-4c15-b563-f8d90229359d">

<omgdc:Bounds x="-105.0" y="-90.0" width="100.0" height="80.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNShape id="shape-8738aadc-c9b0-4425-8aa6-72bbc112810f" bpmnElement="sid-ae0b0e78-1fd6-4864-8fa6-a9cfdac4eaaa">

<omgdc:Bounds x="40.0" y="-90.0" width="100.0" height="80.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNShape id="shape-5c376243-ed67-4880-9409-7bf2a5b8dae7" bpmnElement="sid-7a04db13-b607-4b56-a008-82bf49e87b5b">

<omgdc:Bounds x="280.0" y="-90.0" width="100.0" height="80.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNShape id="shape-f4d77dfe-e759-4a2c-b999-4d38e883c3f3" bpmnElement="sid-9a50a142-d5c4-49d8-91b5-fdd6823b7e0b">

<omgdc:Bounds x="589.99994" y="-200.0" width="100.0" height="80.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNShape id="shape-95f6c6a9-cf28-410a-9a3b-793f64127a58" bpmnElement="sid-05c6d003-a2b1-416c-920a-7a1e5dfb3112">

<omgdc:Bounds x="625.0" y="-65.0" width="30.0" height="30.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNEdge id="edge-76f787ac-1d25-41c9-bbe9-b764552a3efb" bpmnElement="sid-ceda75ac-fedb-4509-96c1-22b4b39d1bdf">

<omgdi:waypoint x="-154.99998" y="-50.000004"/>

<omgdi:waypoint x="-105.0" y="-50.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNEdge id="edge-f1dfb186-d63f-4ab1-9919-0916d2a30398" bpmnElement="sid-315bb768-b704-4d6a-9b58-e22afecfe575">

<omgdi:waypoint x="-5.0" y="-50.0"/>

<omgdi:waypoint x="40.0" y="-50.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNShape id="shape-19cca33f-c048-4e9d-a1f0-97f8f8ec8ca4" bpmnElement="sid-b8aef085-b4ee-434d-ab5e-b989336d6d75">

<omgdc:Bounds x="175.0" y="-70.0" width="40.0" height="40.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNEdge id="edge-149b8ca9-3916-44d3-ad2f-474eef6c9f94" bpmnElement="sid-ed8a6b21-c4b2-48f9-902a-b6724070c6f5">

<omgdi:waypoint x="140.0" y="-50.0"/>

<omgdi:waypoint x="175.0" y="-50.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNEdge id="edge-28443c4d-dc3b-4da7-8d62-5a54400f301f" bpmnElement="sid-db35498f-3561-418c-a181-804aa14ecbc9">

<omgdi:waypoint x="215.0" y="-50.0"/>

<omgdi:waypoint x="280.0" y="-50.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNShape id="shape-f7b93b8f-6ce5-4350-a070-08fe7e6fecd3" bpmnElement="sid-5e5ead45-e3c8-41dc-96ef-c9d853a48c35">

<omgdc:Bounds x="400.0" y="-70.0" width="40.0" height="40.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNEdge id="edge-60e6cdc2-cec6-4d15-b526-d89ccd6a93c7" bpmnElement="sid-40d5bb77-d68d-42bd-a8e2-e437566c1c76">

<omgdi:waypoint x="380.0" y="-50.0"/>

<omgdi:waypoint x="400.0" y="-50.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNEdge id="edge-abb25af8-44c2-4235-b38a-e30bcc433ae4" bpmnElement="sid-2c8957f3-4ed2-45a6-bf5d-f4ef07c3ce5e">

<omgdi:waypoint x="420.0" y="-70.0"/>

<omgdi:waypoint x="589.99994" y="-160.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNEdge id="edge-754f9d94-a77b-4eef-a481-88a754263399" bpmnElement="sid-f42a08d5-2f62-4079-89d6-d9a031727976">

<omgdi:waypoint x="440.0" y="-50.0"/>

<omgdi:waypoint x="625.0" y="-50.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNShape id="shape-54161de0-db42-4b4e-81b0-ff591997ba36" bpmnElement="sid-de1dac9b-ac4f-44af-9431-e8c41724040d">

<omgdc:Bounds x="180.0" y="-180.0" width="30.0" height="30.0"/>

</bpmndi:BPMNShape>

<bpmndi:BPMNEdge id="edge-9a7697b0-b324-4edd-b1ac-632a35774be7" bpmnElement="sid-91a7bfa6-8e6c-4040-ba3c-3b4f0b2936f9">

<omgdi:waypoint x="195.0" y="-70.0"/>

<omgdi:waypoint x="195.0" y="-150.00002"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNEdge id="edge-f51508e0-aa41-42c3-abb6-fe0c0fd190fc" bpmnElement="sid-e52aacd6-dfd8-4dd0-8b2d-55005519fc18">

<omgdi:waypoint x="420.0" y="-65.0"/>

<omgdi:waypoint x="420.00003" y="-167.5"/>

<omgdi:waypoint x="210.00002" y="-165.00002"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNEdge id="edge-0da791b8-acbc-4ff0-acb2-4226aa9a42b5" bpmnElement="sid-457e06a6-1bb9-48f1-93a6-8905c36daaa7">

<omgdi:waypoint x="639.99994" y="-120.0"/>

<omgdi:waypoint x="640.0" y="-65.0"/>

</bpmndi:BPMNEdge>

<bpmndi:BPMNEdge id="edge-778941f6-726b-464c-b439-c3d3695e6428" bpmnElement="sid-d44d1aa5-63a2-4091-9854-dec3d31648e5">

<omgdi:waypoint x="639.99994" y="-199.99998"/>

<omgdi:waypoint x="639.9999" y="-215.0"/>

<omgdi:waypoint x="420.00003" y="-215.0"/>

<omgdi:waypoint x="195.0" y="-215.0"/>

<omgdi:waypoint x="195.0" y="-200.00002"/>

<omgdi:waypoint x="195.0" y="-172.50002"/>

</bpmndi:BPMNEdge>

</bpmndi:BPMNPlane>

</bpmndi:BPMNDiagram>

测试流程

这里为了测试和理解activiti的使用,我暂时使用 Test单元测试

1、  创建测试类

(这里为了更直观理解,测试类 违反 命名规则 中文命名 可忽略)

image.png



import com.lframework.xingyun.api.XingYunApiApplication;
import lombok.extern.slf4j.Slf4j;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.history.HistoricVariableInstance;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Comment;
import org.activiti.engine.task.Task;
import org.activiti.engine.task.TaskQuery;
import org.activiti.image.ProcessDiagramGenerator;
import org.activiti.image.impl.DefaultProcessDiagramGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.CollectionUtils;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 借支流程测试
 *
 */
@SpringBootTest(classes = XingYunApiApplication.class)
@Slf4j
public class 借支测试 {
	
	@Autowired
	RepositoryService ps;
	
	@Test
	public void 部署流程() {
		Deployment deploy = ps.createDeployment().addClasspathResource("借支申请.bpmn20.xml").deploy();
		System.out.println(deploy.getId());
	}
	
	
	@Autowired
	RuntimeService runtimeService;
	@Test
	public void 申请借支() {
		Map<String,Object> map = new HashMap<>();
		map.put("name","刚哥");
		// 根据流程定义的ID启动流程
		ProcessInstance instance = runtimeService.startProcessInstanceByKey("jz","jz1",map);
		// 查询此任务并继续
		Task task = taskService.createTaskQuery().processInstanceBusinessKey("jz1").singleResult();
		taskService.complete(task.getId());
		
	}
	
	
	@Autowired
	TaskService taskService;
	
	@Test
	public void 查询待审核的任务() {
		TaskQuery query = taskService.createTaskQuery();
		List<Task> taskList = query.
			processInstanceBusinessKey("jz1"). // 以业务id作为条件
//                processDefinitionKey("qj"). // 以流程id作为条件
//                taskAssignee("张三"). // 以任务办理人作为条件
	list();
		// 3.输出任务信息
		for (Task task : taskList) {
			System.out.println("任务ID:"+task.getId());
			System.out.println("任务名称:"+task.getName());
			System.out.println("任务的创建时间:"+task.getCreateTime());
			System.out.println("任务的办理人:"+task.getAssignee());
			System.out.println("流程实例ID:"+task.getProcessInstanceId());
			System.out.println("执行对象ID:"+task.getExecutionId());
			System.out.println("流程定义ID:"+task.getProcessDefinitionId());
		}
	}
	
	
	//办理任务(主要操作ACT_RU_EXECUTION、ACT_RU_TASK表)
	@Test
	public void 办理任务() {
		// processInstanceBusinessKey 业务id
		// taskAssignee 任务处理人
		Task task = taskService.createTaskQuery().
			processInstanceBusinessKey("jz1")
//                .taskAssignee("刚哥")
			.singleResult();
		// 设置审批意见
		 taskService.addComment(task.getId(),task.getProcessInstanceId(),"同意");
		// 任务id 完成任务进入下一步
		Map<String, Object> map = new HashMap<>();
		map.put("sp", 1);
		map.put("money", 3500);
		taskService.complete(task.getId(), map);

		System.out.println("任务ID:"+task.getId());
		System.out.println("任务名称:"+task.getName());
		System.out.println("任务审批完成");

	}
	
	
	
	@Autowired
	HistoryService historyService;
	/**
	 * 历史活动实例查询
	 */
	@Test
	public void queryHistoryTask() {
		List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery() // 创建历史活动实例查询
//                .processInstanceId("b6d83887-ad4b-11ee-b76f-005056c00008") // 执行流程实例id
			.processInstanceBusinessKey("jz1")  //根据业务id,查询当前业务下创建的流程实例
			.orderByTaskCreateTime()
			.asc()
			.list();
		for (HistoricTaskInstance hai : list) {
			System.out.println("活动ID:" + hai.getId());
			System.out.println("流程实例ID:" + hai.getProcessInstanceId());
			System.out.println("活动名称:" + hai.getName());
			System.out.println("办理人:" + hai.getAssignee());
			System.out.println("开始时间:" + hai.getStartTime());
			System.out.println("结束时间:" + hai.getEndTime());
			
			
			List<Comment> taskComments = taskService.getTaskComments(hai.getId());
			for (Comment taskComment : taskComments) {
				System.out.println("---"+taskComment.toString());
				System.out.println("审批意见:" + taskComment.getFullMessage());
			}
		}
	}
	
	
	@Test
	public void 根据业务id判断流程是否结束(){
		ProcessInstance pi = runtimeService.createProcessInstanceQuery().
			processInstanceBusinessKey("qj:10086").singleResult();
		System.out.println(pi==null?"结束":"未结束");
	}
	
	
	/**
	 * 查询历史流程变量
	 */
	@Test
	public void queryHisProVariable(){
		// 先根据流程id查询出历史任务
		HistoricTaskInstance task = historyService.createHistoricTaskInstanceQuery().processInstanceBusinessKey("jz5")
			.list().get(0);
		
		// 查询出该任务执行中所有的变量
		List<HistoricVariableInstance> historicVariableInstanceList = historyService
			.createHistoricVariableInstanceQuery().
			processInstanceId(task.getProcessInstanceId()).list();
		
		boolean b = true;
		
		for(HistoricVariableInstance v: historicVariableInstanceList){
			if(v.getVariableName().endsWith("sp")){
				int sp = (int) v.getValue();
				if (sp == 0) {
					b = false;
					break;
				}
			}
		}
		
		System.out.println("(b?:"") = " + (b?"流程审批通过":"流程审批不通过"));
	}
	
	
	
	@Autowired
	RepositoryService repositoryService;
	
	@Test
	public void 流程图(){
		try {
			// 获取历史流程实例
			HistoricProcessInstance historicProcessInstance = historyService
				.createHistoricProcessInstanceQuery()
				.processInstanceBusinessKey("jz1").singleResult();
			// 获取流程中已经执行的节点,按照执行先后顺序排序
			List<HistoricActivityInstance> historicActivityInstances = historyService
				.createHistoricActivityInstanceQuery()
				.processInstanceId(historicProcessInstance.getId())
				.orderByHistoricActivityInstanceId()
				.asc().list();
			// 高亮已经执行流程节点ID集合
			List<String> highLightedActivitiIds = new ArrayList<>();
			int index = 1;
			for (HistoricActivityInstance historicActivityInstance : historicActivityInstances) {
				// 用默认颜色
				highLightedActivitiIds.add(historicActivityInstance.getActivityId());
				index++;
			}
			ProcessDiagramGenerator processDiagramGenerator = null;
			// 使用默认的程序图片生成器
			processDiagramGenerator = new DefaultProcessDiagramGenerator();
			BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
			// 高亮流程已发生流转的线id集合
			List<String> highLightedFlowIds = getHighLightedFlows(bpmnModel, historicActivityInstances);
			// 使用默认配置获得流程图表生成器,并生成追踪图片字符流
			InputStream imageStream = processDiagramGenerator.generateDiagram(bpmnModel,
				highLightedActivitiIds, highLightedFlowIds, "宋体","微软雅黑", "黑体");
			// 输出图片内容
			int byteSize = 1024;
			byte[] b = new byte[byteSize];
			OutputStream os = Files.newOutputStream(Paths.get("E:\ATTPD\activity\src\main\resources\a.svg"));
			int len;
			while ((len = imageStream.read(b, 0, byteSize)) != -1) {
				os.write(b, 0, len);
			}
			os.close();
		} catch (Exception e) {
		
		}
		
	}
	
	/**
	 * 获取已经流转的线
	 *
	 * @param bpmnModel
	 * @param historicActivityInstances
	 * @return
	 */
	private static List<String> getHighLightedFlows(BpmnModel bpmnModel, List<HistoricActivityInstance> historicActivityInstances) {
		// 高亮流程已发生流转的线id集合
		List<String> highLightedFlowIds = new ArrayList<>();
		// 全部活动节点
		List<FlowNode> historicActivityNodes = new ArrayList<>();
		// 已完成的历史活动节点
		List<HistoricActivityInstance> finishedActivityInstances = new ArrayList<>();
		
		for (HistoricActivityInstance historicActivityInstance : historicActivityInstances) {
			FlowNode flowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstance.getActivityId(), true);
			historicActivityNodes.add(flowNode);
			if (historicActivityInstance.getEndTime() != null) {
				finishedActivityInstances.add(historicActivityInstance);
			}
		}
		
		FlowNode currentFlowNode = null;
		FlowNode targetFlowNode = null;
		// 遍历已完成的活动实例,从每个实例的outgoingFlows中找到已执行的
		for (HistoricActivityInstance currentActivityInstance : finishedActivityInstances) {
			// 获得当前活动对应的节点信息及outgoingFlows信息
			currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currentActivityInstance.getActivityId(), true);
			List<SequenceFlow> sequenceFlows = currentFlowNode.getOutgoingFlows();
			
			/**
			 * 遍历outgoingFlows并找到已已流转的 满足如下条件认为已已流转: 1.当前节点是并行网关或兼容网关,则通过outgoingFlows能够在历史活动中找到的全部节点均为已流转 2.当前节点是以上两种类型之外的,通过outgoingFlows查找到的时间最早的流转节点视为有效流转
			 */
			if ("parallelGateway".equals(currentActivityInstance.getActivityType()) || "inclusiveGateway".equals(currentActivityInstance.getActivityType())) {
				// 遍历历史活动节点,找到匹配流程目标节点的
				for (SequenceFlow sequenceFlow : sequenceFlows) {
					targetFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(sequenceFlow.getTargetRef(), true);
					if (historicActivityNodes.contains(targetFlowNode)) {
						highLightedFlowIds.add(targetFlowNode.getId());
					}
				}
			} else {
				List<Map<String, Object>> tempMapList = new ArrayList<>();
				for (SequenceFlow sequenceFlow : sequenceFlows) {
					for (HistoricActivityInstance historicActivityInstance : historicActivityInstances) {
						if (historicActivityInstance.getActivityId().equals(sequenceFlow.getTargetRef())) {
							Map<String, Object> map = new HashMap<>();
							map.put("highLightedFlowId", sequenceFlow.getId());
							map.put("highLightedFlowStartTime", historicActivityInstance.getStartTime().getTime());
							tempMapList.add(map);
						}
					}
				}
				
				if (!CollectionUtils.isEmpty(tempMapList)) {
					// 遍历匹配的集合,取得开始时间最早的一个
					long earliestStamp = 0L;
					String highLightedFlowId = null;
					for (Map<String, Object> map : tempMapList) {
						long highLightedFlowStartTime = Long.valueOf(map.get("highLightedFlowStartTime").toString());
						if (earliestStamp == 0 || earliestStamp >= highLightedFlowStartTime) {
							highLightedFlowId = map.get("highLightedFlowId").toString();
							earliestStamp = highLightedFlowStartTime;
						}
					}
					
					highLightedFlowIds.add(highLightedFlowId);
				}
				
			}
			
		}
		return highLightedFlowIds;
	}
	

}

测试结果

1、  部署流程

image.png

结果

image.png

也可以在数据库中看到

image.png

2、  启动流程,创建申请任务

image.png

结果

image.png

也可在数据库中查看

image.png

image.png

3、  查看待审批任务

image.png

测试结果

image.png

4、  处理任务

image.png

处理结果

image.png

5、  查看历史任务

image.png

结果

image.png

至此阶段一的开发目标已经达到

下阶段目标

1、  目标

把任务的相关管理整合到PC端,并能通过界面管理

2、  效果图

image.png