camunDa
camunDa 是2013年从Activiti5 中分离出来的一个新的工作流引擎。Camunda 官方提供了 Camunda Platform、Camunda Modeler,其中 Camunda Platform 以 Camunda engine 为基础为用户提供可视化界面,Camunda Modeler 是流程文件建模平台,在 Camunda Modeler 创建的流程文件可以 deploy 到 Camunda Platform 并进行管理。另外三方服务可通过 Camunda 官方提供的 rest 或者 java api 来访问 Camunda engine,操作的结果也可以在 Camunda Platform 查看和管理。
camunDa环境搭建
准备条件:JDK17,Maven版本3.8.6
具体代码可以通过CamunDa官方提供的框架生成器自动生成基础环境代码
CamunDa基础项目生成链接:https://start.camunda.com/
生成代码之后,可以得到具体的使用DEMO了,但是不一定能适合在实际工作中,所以部分代码是需要进行调整的
博主这边也贴上自己搭建的具体步骤(使用官方搭建的目的是快速使用,具体使用那种搭建方式,自行把握吧!)
POM依赖(最外层POM文件依赖数据)
<?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">
<!--<parent>
<artifactId>base-platform</artifactId>
<groupId>com.secondcloud</groupId>
<version>1.0.0</version>
</parent>-->
<parent>
<groupId>com.secondcloud.dependency</groupId>
<artifactId>dependency-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>com.secondcloud</groupId>
<artifactId>new-workflow-engine</artifactId>
<version>1.0.0</version>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<description>工作流服务</description>
<modules>
<module>new-workflow-engine-client</module>
<module>new-workflow-engine-server</module>
</modules>
<profiles>
<profile>
<id>dev</id>
<properties>
<!--当前环境-->
<profile.name>dev</profile.name>
<!--私有镜像仓库-->
<docker.registry>******:5000</docker.registry>
<!--Nacos配置中心地址;服务发现地址-->
<nacos.server-addr>******:8848</nacos.server-addr>
<!--Nacos配置中心命名空间,用于支持多环境.这里必须使用ID,不能使用名称,默认为空-->
<nacos.namespace>dev</nacos.namespace>
<nacos.username>nacos</nacos.username>
<nacos.password>nacos</nacos.password>
</properties>
</profile>
<!-- 测试 -->
<!--<profile>
<id>test</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profile.name>test</profile.name>
<docker.registry>******:5000</docker.registry>
<nacos.server-addr>******:8848</nacos.server-addr>
<nacos.namespace>test</nacos.namespace>
<nacos.username>nacos</nacos.username>
<nacos.password>nacos</nacos.password>
</properties>
</profile>-->
</profiles>
</project>
server服务pom文件依赖
<?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">
<parent>
<artifactId>new-workflow-engine</artifactId>
<groupId>com.secondcloud</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.secondcloud</groupId>
<artifactId>new-workflow-engine-server</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!-- 引入基础依赖 server通用依赖引入-,这些依赖可以不是CamunDa的主要依赖,是实际项目中用到的->
<dependency>
<groupId>com.secondcloud.dependency</groupId>
<artifactId>dependency-server</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baidu.contact-center</groupId>
<artifactId>cf-auth-api-token</artifactId>
<version>5.1.5</version>
</dependency>
<dependency>
<groupId>com.secondcloud.client</groupId>
<artifactId>new-workflow-engine-client</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- camunda 依赖->
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter</artifactId>
<version>7.19.0</version>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Rest服务接口 -->
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
<version>7.19.0</version>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Spin (XML & JSON) -->
<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-engine-plugin-spin</artifactId>
<version>7.19.0</version>
</dependency>
<dependency>
<groupId>org.camunda.spin</groupId>
<artifactId>camunda-spin-dataformat-all</artifactId>
<version>1.19.5</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
<version>7.19.0</version>
</dependency>
</dependencies>
<build>
<!-- 打包名称:默认带版本号 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 打包后,将jar复制到指定目录 -->
<!--<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>-->
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/**.*</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
client依赖POM
<?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">
<parent>
<groupId>com.secondcloud.dependency</groupId>
<artifactId>dependency-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath></relativePath>
</parent>
<groupId>com.secondcloud.client</groupId>
<artifactId>new-workflow-engine-client</artifactId>
<version>1.0.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<dependencies>
<!-- 引入基础依赖 dependency-client模块中已引入通用jar-->
<dependency>
<groupId>com.secondcloud.dependency</groupId>
<artifactId>dependency-client</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
这依赖几乎是空的,导入的唯一一个依赖是博主工作中具体使用到的依赖项目,大家是没法导入的,可以去除
项目目录结构也贴一下
然后就需要去更改一下我们的数据库链接设置了
数据链接默认使用的是H2,我们需要改为我们自己对应的数据库和驱动,连接上数据库之后,启动项目就会自动生成工作流需要使用的表结构了
# 数据源
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
username: root
password: 密码
url: jdbc:postgresql://pg10:5432/workflow-manage
main:
allow-circular-references: true
urlHost: "https://test.jxvrgvtech.cn"
其次就是配置CamunDa的账户和密码
camunda.bpm.admin-user:
id: dem
password: demo
相同的都是写在我们的yml文件里面即可
这样我们的项目环境就算是搭建完成了
CamunDa流程图建立
项目启动完成之后就会生成对应的表结构,我们可以在数据库中看到,如果没有生成,可能是数据库什么的配置有问题,可以通过官方提供的快速搭建方法再试一下,博主的项目因为涉及隐私较多,部分代码可能缺失了。
这些ACT开头的就是自动生成的表结构了
我们这个时候就要进行画流程图了,Camunda他是不自带流程图工具,是需要我们另外下载的,下载:https://camunda.com/download/modeler/
下载完成之后,打开页面是这样的
博主这边是使用bpmn文件的,不是挂载到云端的,所以选择第二个本地的形式画图
这是博主画的一个比较符合现实逻辑的一个请假流程示意图,下方也有相对应的说明,其中,
这个带人头的方框是用户任务,也就是需要我们人为去手动触发,审核的,不不是系统自动处理的,这个博主就不再多说这个具体使用方法了
但是这个里面有一些配置,需要注意一下,不然我们的流程图是没法对应上我们的业务数据的
图上已经标明了,具体的参数大家可以自行定义,其中的${starter}是从我们第一个启动流程里面获取到的,至于启动流程里面这个具体指,后面我们会再代码中具体讲到
这是流程发起之后,通过图1,的请假天数进行判断需要走到那个节点的,中间这个带X的图形是我们的一个排他网关,相当于IF语句,通过我们的EL表达式,进行流程流转
这个里面的leaders是我们从代码中进行添加的处理人的数据,可进行动态变更
这里是处理人处理的意见数据
最后就是流程结束了,我这边也附上流程图,大家可以复制下来,看具体数据
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1rrprgw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.24.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.21.0">
<bpmn:process id="Process_exclusive_gateway" name="排他网关" isExecutable="true" camunda:historyTimeToLive="180">
<bpmn:startEvent id="StartEvent_1" name="开始" camunda:initiator="starter">
<bpmn:outgoing>Flow_1pgd9ua</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1pgd9ua" sourceRef="StartEvent_1" targetRef="Activity_1kmi54i" />
<bpmn:userTask id="Activity_1kmi54i" name="员工请假" camunda:assignee="${starter}">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="reason" label="请假理由" type="string" />
<camunda:formField id="leaveDays" label="请假天数" type="long" defaultValue="1" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1pgd9ua</bpmn:incoming>
<bpmn:outgoing>Flow_05ygtrp</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_1tbcq0a">
<bpmn:incoming>Flow_05ygtrp</bpmn:incoming>
<bpmn:outgoing>Flow_09sdkiu</bpmn:outgoing>
<bpmn:outgoing>Flow_138ijpi</bpmn:outgoing>
<bpmn:outgoing>Flow_0gdtha1</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_05ygtrp" sourceRef="Activity_1kmi54i" targetRef="Gateway_1tbcq0a" />
<bpmn:sequenceFlow id="Flow_09sdkiu" name="小于等于三天" sourceRef="Gateway_1tbcq0a" targetRef="Activity_1hvlj71">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${leaveDays<=3}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_1hvlj71" name="直接领导审批" camunda:assignee="zhangsan">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="comment" label="评论" type="string" defaultValue="同意" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_09sdkiu</bpmn:incoming>
<bpmn:outgoing>Flow_0yqeod6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:intermediateThrowEvent id="Event_1pl4d8u">
<bpmn:incoming>Flow_0yqeod6</bpmn:incoming>
<bpmn:incoming>Flow_08juds0</bpmn:incoming>
<bpmn:incoming>Flow_1ouaja5</bpmn:incoming>
</bpmn:intermediateThrowEvent>
<bpmn:sequenceFlow id="Flow_0yqeod6" sourceRef="Activity_1hvlj71" targetRef="Event_1pl4d8u" />
<bpmn:sequenceFlow id="Flow_138ijpi" name="大于三天小于等于五天" sourceRef="Gateway_1tbcq0a" targetRef="Activity_0jv1xqx">
<bpmn:extensionElements>
<camunda:executionListener delegateExpression="${addLeaders}" event="take" />
</bpmn:extensionElements>
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${leaveDays>3 && leaveDays<=5}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_0jv1xqx" name="直接领导和经理审批" camunda:assignee="${leader}">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="comment" label="评论" type="string" defaultValue="同意" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_138ijpi</bpmn:incoming>
<bpmn:outgoing>Flow_08juds0</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics isSequential="true" camunda:collection="${leaders}" camunda:elementVariable="leader" />
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_08juds0" sourceRef="Activity_0jv1xqx" targetRef="Event_1pl4d8u" />
<bpmn:sequenceFlow id="Flow_0gdtha1" name="大于五天" sourceRef="Gateway_1tbcq0a" targetRef="Activity_19axysr">
<bpmn:extensionElements>
<camunda:executionListener delegateExpression="${addLeaders}" event="take" />
</bpmn:extensionElements>
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${leaveDays>5}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_19axysr" name="直接领导、经理和董事长审批" camunda:assignee="${leader}">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="comment" label="评论" type="string" defaultValue="同意" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0gdtha1</bpmn:incoming>
<bpmn:outgoing>Flow_1ouaja5</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics isSequential="true" camunda:collection="${leaders}" camunda:elementVariable="leader" />
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1ouaja5" sourceRef="Activity_19axysr" targetRef="Event_1pl4d8u" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_exclusive_gateway">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="232" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="159" y="275" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0e8erbr_di" bpmnElement="Activity_1kmi54i">
<dc:Bounds x="340" y="210" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1tbcq0a_di" bpmnElement="Gateway_1tbcq0a" isMarkerVisible="true">
<dc:Bounds x="515" y="225" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0p55xr1_di" bpmnElement="Activity_1hvlj71">
<dc:Bounds x="1010" y="57" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1pl4d8u_di" bpmnElement="Event_1pl4d8u">
<dc:Bounds x="1042" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1tadrud_di" bpmnElement="Activity_0jv1xqx">
<dc:Bounds x="790" y="210" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ourst1_di" bpmnElement="Activity_19axysr">
<dc:Bounds x="1010" y="390" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1pgd9ua_di" bpmnElement="Flow_1pgd9ua">
<di:waypoint x="188" y="250" />
<di:waypoint x="340" y="250" />
<bpmndi:BPMNLabel>
<dc:Bounds x="493" y="191" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_05ygtrp_di" bpmnElement="Flow_05ygtrp">
<di:waypoint x="440" y="250" />
<di:waypoint x="515" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_09sdkiu_di" bpmnElement="Flow_09sdkiu">
<di:waypoint x="540" y="225" />
<di:waypoint x="540" y="97" />
<di:waypoint x="1010" y="97" />
<bpmndi:BPMNLabel>
<dc:Bounds x="522" y="161" width="66" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0yqeod6_di" bpmnElement="Flow_0yqeod6">
<di:waypoint x="1060" y="137" />
<di:waypoint x="1060" y="232" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_138ijpi_di" bpmnElement="Flow_138ijpi">
<di:waypoint x="565" y="250" />
<di:waypoint x="790" y="250" />
<bpmndi:BPMNLabel>
<dc:Bounds x="645" y="232" width="77" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_08juds0_di" bpmnElement="Flow_08juds0">
<di:waypoint x="890" y="250" />
<di:waypoint x="1042" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0gdtha1_di" bpmnElement="Flow_0gdtha1">
<di:waypoint x="540" y="275" />
<di:waypoint x="540" y="430" />
<di:waypoint x="1010" y="430" />
<bpmndi:BPMNLabel>
<dc:Bounds x="533" y="350" width="44" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ouaja5_di" bpmnElement="Flow_1ouaja5">
<di:waypoint x="1060" y="390" />
<di:waypoint x="1060" y="268" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
这个代码就是流程图生成出来的bpmn文件了,其中注意一下,使用官方生成的项目框架生成的bpmn文件中会缺失一个数据,我这边进行截图标识一下,需要我们手动添加
如果不添加这个,启动时会报错的,到这了,流程图就算是画好了,我们再项目中建立一个文件加,专门存放流程图即可
启动成功之后,我们可以通过访问项目进去查看项目流程数量和在执行的数量等信息
访问地址的端口是我们再YML文件中配置的,大家自行变更
http://127.0.0.1:5003/camunda/app/cockpit/default/#/login
代码调用
到这里之后,就可以在项目中通过具体案例去使用工作流了
案例如下
package com.secondcloud.industry.server.controller;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.camunda.bpm.engine.HistoryService;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.history.HistoricActivityInstance;
import org.camunda.bpm.engine.history.HistoricProcessInstance;
import org.camunda.bpm.engine.history.HistoricTaskInstance;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Comment;
import org.camunda.bpm.engine.task.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
@RestController
public class Demo {
@Autowired
private IdentityService identityService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
/**
* 创建流程实例
* processDefKey:这里首先需要传入的是流程定义模板的key,前提是我们之前已经部署了相应的流程模板。(Process_0bhiqm1)
* businessKey:这是流程实例的业务标识Key,需要用该字段与我们的业务单据进行绑定。
* initiator:启动流程实例时的操作人,这里可以理解为制单人,或者是送审人,但是需要注意,在实际应用场景中,我们的制单人不一定就是单据送审人。
*/
@GetMapping("/startProcessInstanceByDefKey")
public String startProcessInstanceByDefKey(@RequestParam(value = "processDefKey") String processDefKey,
@RequestParam(value = "business") String business,
@RequestParam(value = "applicantName") String applicantName) {
HashMap<String, Object> variable = new HashMap<>();
//流程启动初始化数据
variable.put("initiator", applicantName);
variable.put("isFree", true);
identityService.setAuthenticatedUserId(applicantName); //ACT_HI_PROCINST.START_USER_ID字段的赋值-开始节点人
ProcessInstance instance = runtimeService.startProcessInstanceByKey(processDefKey, business, variable);
// PROCINST procinst = procinst = new PROCINST(instance.getProcessDefinitionId(), instance.getProcessInstanceId(), instance.getBusinessKey(), instance.isSuspended(), instance.isEnded());
return "流程创建成功";
}
/**
* 提交申请
* leaveDays:模板中定义的数据变量。
* business:这是流程实例的业务标识Key,需要用该字段与我们的业务单据进行绑定。
* applicantName:单据送审人,也就是申请人
*/
@GetMapping("/submitApplication")
public String submitApplication(@RequestParam(value = "leaveDays") Long leaveDays,
@RequestParam(value = "business") String business,
@RequestParam(value = "applicantName") String applicantName) {
String resultString = "";
Task task = queryTaskByBusinessKey(business, applicantName);
if (ObjectUtils.isNull(task)) {
resultString = "没有查询到对应的单据流程";
} else if (!task.getAssignee().equalsIgnoreCase(applicantName)) {
resultString = "没有审核权限!";
} else {
//设置审核人
HashMap<String, Object> map = new HashMap<>();
map.put("reason", "请假");
map.put("leaveDays", leaveDays);
List<String> leaders = new ArrayList<>();
if (leaveDays > 3 && leaveDays <= 5) {
leaders.add("zhangsan");
leaders.add("lisi");
} else if (leaveDays > 5) {
leaders.add("zhangsan");
leaders.add("lisi");
leaders.add("wangwu");
}
map.put("leader", leaders);
String taskId = task.getId();
taskService.setVariables(taskId, map);
taskService.complete(taskId);
resultString = "请假成功!";
}
return resultString;
}
/**
* 审核操作
*
* @param businessKey 业务id
* @param initiator 流程处理人
* @param comment 处理意见
*/
@GetMapping("/submitProcessInstance")
public String submitProcessInstance(@RequestParam(value = "businessKey") String businessKey,
@RequestParam(value = "initiator") String initiator,
@RequestParam(value = "comment") String comment) {
String resultString = "";
Task task = queryTaskByBusinessKey(businessKey, initiator);
if (ObjectUtils.isNull(task)) {
resultString = "没有查询到对应的单据流程";
} else if (!task.getAssignee().equalsIgnoreCase(initiator)) {
resultString = "没有审核权限!";
} else {
if (task.getAssignee().equals("zhangsan")) {
Map<String, Object> variables = taskService.getVariables(task.getId());
System.out.println("variables" + variables.toString());
String flag = "true";
HashMap<String, Object> map = new HashMap<>();
map.put("flag", flag);
map.put("comment", comment);
taskService.createComment(task.getId(), task.getProcessInstanceId(), "审核原因:" + comment);
taskService.complete(task.getId(), map);
return "zhangsan" + (flag.equals("true") ? "同意" : "不同意");
} else if (task.getAssignee().equals("lisi")) {
String flag = "true";
HashMap<String, Object> map = new HashMap<>();
map.put("flag", flag);
map.put("comment", comment);
taskService.createComment(task.getId(), task.getProcessInstanceId(), "审核原因:" + comment);
taskService.complete(task.getId(), map);
return "lisi" + (flag.equals("true") ? "同意" : "不同意");
} else if (task.getAssignee().equals("wangwu")) {
String flag = "true";
HashMap<String, Object> map = new HashMap<>();
map.put("flag", flag);
map.put("comment", comment);
taskService.createComment(task.getId(), task.getProcessInstanceId(), "审核原因:" + comment);
taskService.complete(task.getId(), map);
return "wangwu" + (flag.equals("true") ? "同意" : "不同意");
}
}
return resultString;
}
/**
* 根据业务标识代码获取当前节点
*
* @param businessKey 业务id
* @param initiator 流程处理人或者流程制作人
*/
private Task queryTaskByBusinessKey(String businessKey, String initiator) {
Task task = null;
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processInstanceBusinessKey(businessKey)
.singleResult();
if (ObjectUtils.isNull(instance)) return null;
List<Task> list = taskService.createTaskQuery()
.processInstanceId(instance.getProcessInstanceId())
.active() //正在运行时的节点?
.list();
if (list.size() == 1) task = list.get(0);
else {
task = taskService.createTaskQuery()
.processInstanceId(instance.getProcessInstanceId())
.active()
.taskAssignee(initiator)
.singleResult();
}
return task;
}
@Autowired
private HistoryService historyService;
/**
* 查询我创建的流程
*
* @param userId 创建流程人的用户ID
* @return
*/
@GetMapping("/queryMySalaryProcess/{userId}")
public List<String> queryMySalaryProcess(@PathVariable(value = "userId") String userId) {
/* 迭代【可添加更多种条件查询】:时间范围、内置分页查询等! */
List<HistoricProcessInstance> list = list = historyService.createHistoricProcessInstanceQuery()
//通过制单人来查询流程中的数据
.startedBy(userId)
.list();
if (CollectionUtils.isEmpty(list)) {
return null;
}
ArrayList<String> businessKeyList = new ArrayList<>();
list.forEach(procInst -> {
businessKeyList.add(procInst.getBusinessKey());
});
return businessKeyList;
}
/**
* 查询已办/未办 单据
*
* @param userId 用户id
* @param type 数据类型
*/
@GetMapping("/queryMyTodoTask")
public List queryMyTodoTask(@RequestParam(value = "userId") String userId,
@RequestParam(value = "type") String type) {
ArrayList<Object> businessList = new ArrayList<>();
//查询代办
if (type.equalsIgnoreCase("0")) {
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(userId)
.active()
// .listPage() 【可分页查询】
.list();
if (CollectionUtils.isEmpty(tasks)) {
return null;
}
tasks.forEach(task -> {
//为查询到相关单据
String businessKey = historyService.createHistoricProcessInstanceQuery()
// .orderByProcessInstanceStartTime() 【可添加时间查询范围】
// .orderByProcessInstanceEndTime()
.processInstanceId(task.getProcessInstanceId())
.singleResult()
.getBusinessKey();
businessList.add(businessKey);
});
}
//查询已办
else if (type.equalsIgnoreCase("1")) {
List<HistoricTaskInstance> completedTaskList = historyService.createHistoricTaskInstanceQuery()
.taskAssignee(userId)
.finished()
.taskDeleteReason("completed")
.list();
if (CollectionUtils.isEmpty(completedTaskList)) {
//为查询到相关单据
return null;
}
completedTaskList.forEach(taskOld -> {
String businessKey = historyService.createHistoricProcessInstanceQuery()
// .orderByProcessInstanceStartTime() 【可添加时间查询范围】
// .orderByProcessInstanceEndTime()
.processInstanceId(taskOld.getProcessInstanceId())
.singleResult()
.getBusinessKey();
businessList.add(businessKey);
});
}
return businessList;
}
/**
* 驳回操作
*/
@GetMapping("/turnTask")
public String turnTask(@RequestParam(value = "userId") String userId,
@RequestParam(value = "businessKey") String businessKey,
@RequestParam(value = "type") String type,
@RequestParam(value = "comment") String comment) {
Task task = queryTaskByBusinessKey(businessKey, userId);
if (ObjectUtils.isNull(task)) {
String result = new String("未查询到对应的审核单据!");
return new String("未查询到对应的审核单据!");
}
if (!task.getAssignee().equalsIgnoreCase(userId)) {
return new String("没用审核权限!");
}
//获取所有已办节点
List<HistoricActivityInstance> userTaskList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
//用户类型节点
.activityType("userTask")
.finished() //已经完成的节点
.orderByHistoricActivityInstanceEndTime()
.asc()
.list();
//流程实例的活动实例树
/* ActivityInstance tree = runtimeService.getActivityInstance(task.getProcessInstanceId()); */
if (userTaskList == null || CollectionUtils.isEmpty(userTaskList)) {
return new String("当前任务无法驳回!");
}
switch (type) {
//驳回第一任制单人
case "1": {
// 1.驳回提交已经无 ok2.驳回第一任还是驳回的上一任 3.会签节点上的驳回操作出现只结束了当前任务的驳回!
if (userTaskList.size() < 2) {
return new String("第一个用户节点无法驳回!");
}
HistoricActivityInstance historicActivityInstance = userTaskList.get(0);
String toActId = historicActivityInstance.getActivityId();
String assignee = historicActivityInstance.getAssignee();
//设置流程可变参数
HashMap<String, Object> taskVariable = new HashMap<>();
taskVariable.put("assignee", assignee);
//流程审核+驳回
//任务流程创建了提交模板Comment 但是没有提交 taskService.complete(taskId)。--所有节点也不会提交到后台去
taskService.createComment(task.getId(), task.getProcessInstanceId(), "驳回原因:" + comment);
//任务流程实例修改位置
runtimeService.createProcessInstanceModification(task.getProcessInstanceId())
// .cancelActivityInstance(getInstanceIdForActivity(tree,task.getTaskDefinitionKey())) //关闭当前节点相关的任务
.cancelAllForActivity(task.getTaskDefinitionKey())
//暂时不清楚该内容提交到何处!
.setAnnotation("进行了驳回到第一任务节点操作!")
//启动目标活动节点
.startBeforeActivity(toActId)
.setVariables(taskVariable)
.execute();
return new String("驳回到制单人成功!");
}
//驳回上一任
case "2": {
//判断当前节点是否为第一个节点
HistoricActivityInstance historicActivityInstance = userTaskList.get(0);
String activityId = historicActivityInstance.getActivityId();
if (activityId.equals(task.getTaskDefinitionKey())) {
return new String("第一节点无法驳回!");
}
//获取上一个节点
Map<String, String> lastNode = getLastNode(userTaskList, task.getTaskDefinitionKey());
if (ObjectUtils.isNull(lastNode)) {
return new String("退回节点异常!");
}
String toActId = lastNode.get("toActId");
String assignee = lastNode.get("assignee");
//设置流程中的可变参数
HashMap<String, Object> taskVariable = new HashMap<>(2);
taskVariable.put("leader", assignee);
//进行驳回操作
taskService.createComment(task.getId(), task.getProcessInstanceId(), "驳回:" + comment);
runtimeService.createProcessInstanceModification(task.getProcessInstanceId())
//.cancelActivityInstance(getInstanceIdForActivity(tree,task.getTaskDefinitionKey())) //关闭相关任务!(当前节点就会被删除。但是之前审核过的节点还是会存在!)
//该方式关闭所有activityId相同的activity活动都会被取消暂停(会签节点)
.cancelAllForActivity(task.getTaskDefinitionKey())
.setAnnotation("进行驳回到上一任务节点操作!")
//启动目标活动节点
.startBeforeActivity(toActId)
//流程可变参数赋值
.setVariables(taskVariable)
.execute();
return new String("驳回上一任成功!");
}
//驳回任一任
case "3": {
//
}
default: {
}
}
return null;
}
private Map<String, String> getLastNode(List<HistoricActivityInstance> resultList, String currentActivityId) {
HashMap<String, String> backNode = new HashMap<>();
//新建一个有序不重复集合
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
for (HistoricActivityInstance his : resultList) {
linkedHashMap.put(his.getActivityId(), his.getAssignee());
}
int originSize = resultList.size();
//判断历史节点中是否已经存在过当前节点
boolean flag = false;
for (Map.Entry entry : linkedHashMap.entrySet()) {
if (currentActivityId.equalsIgnoreCase((String) entry.getKey())) {
flag = true;
break;
}
}
//当前节点不在历史节点里面,最后一个节点是完成节点
if (!flag) {
HistoricActivityInstance historicActivityInstance = resultList.get(originSize - 1);
backNode.put("toActId", historicActivityInstance.getActivityId());
backNode.put("assignee", historicActivityInstance.getAssignee());
return backNode;
//当前节点在历史节点中(已经退回过)
} else {
ListIterator<Map.Entry<String, String>> li = new ArrayList<>(linkedHashMap.entrySet()).listIterator();
while (li.hasNext()) {
Map.Entry<String, String> entry = li.next();
if (currentActivityId.equalsIgnoreCase(entry.getKey())) {
//光标上一到当前节点
li.previous();
//当前相同节点的上一节点
Map.Entry<String, String> previous = li.previous();
backNode.put("toActId", previous.getKey());
backNode.put("assignee", previous.getValue());
return backNode;
}
}
}
return null;
}
//审核日志查询
/**
* [注:日志顺序 1开始时间 相同顺延 2排列结束时间]
* activityType:节点类型 null就不显示
* taskId:taskId相同的为会签节点
* state:completed审核完成 deleted驳回 null待审核
* */
@GetMapping("/queryProcessLog/{businessKey}")
public List queryProcessLog(@PathVariable(value = "businessKey") String businessKey) {
String processInstanceId = historyService.createHistoricProcessInstanceQuery()
.processInstanceBusinessKey(businessKey)
.singleResult()
.getRootProcessInstanceId();
List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricActivityInstanceStartTime() //这里开始时间相同的可以,只能前端在根据结束时间继续排序
.asc()
.list();
List<Map<String,Object>> result=new ArrayList<>(list.size());
System.out.println(list.size());
for (HistoricActivityInstance historicActivityInstance : list) {
Map<String,Object> map=new HashMap<>();
String taskId = historicActivityInstance.getTaskId();
List<Comment> taskComments = taskService.getTaskComments(taskId);
System.out.println("taskId = " + taskId);
System.out.println(taskComments.size());
map.put("activityName",historicActivityInstance.getActivityName());
System.out.println("historicActivityInstance.getActivityType() = " + historicActivityInstance.getActivityType());
map.put("activityType",matching(historicActivityInstance.getActivityType()));
map.put("assignee",historicActivityInstance.getAssignee()==null?"无":historicActivityInstance.getAssignee());
map.put("taskId",historicActivityInstance.getTaskId());
map.put("act_Id",historicActivityInstance.getActivityId());
/*加入activity状态字段*/
if (taskId != null) {
map.put("state",historyService.createHistoricTaskInstanceQuery().taskId(taskId).singleResult().getDeleteReason());
}
Date startTime = historicActivityInstance.getStartTime();
if (ObjectUtils.isNull(startTime)){
map.put("starTime","");
}
else{
map.put("startTime", DateFormatUtils.format(historicActivityInstance.getStartTime(),"yyyy-MM-dd HH:mm:ss") );
}
Date endTime = historicActivityInstance.getEndTime();
if (ObjectUtils.isNull(endTime)){
map.put("endTime","");
map.put("costTime","");
}else {
map.put("endTime",DateFormatUtils.format(historicActivityInstance.getEndTime(),"yyyy-MM-dd HH:mm:ss"));
map.put("costTime",getDatePoor(historicActivityInstance.getEndTime(),historicActivityInstance.getStartTime()));
}
if (taskComments.size()>0){
map.put("message",taskComments.get(0).getFullMessage());
}else {
map.put("message","无");
}
result.add(map);
}
System.out.println("result = " + result);
return result;
}
/** 时间差计算 */
public String getDatePoor(Date endDate, Date nowDate) {
long nd = 1000 * 24 * 60 * 60;
long nh = 1000 * 60 * 60;
long nm = 1000 * 60;
long ns = 1000;
// 获得两个时间的毫秒时间差异
long diff = endDate.getTime() - nowDate.getTime();
// 计算差多少天
long day = diff / nd;
// 计算差多少小时
long hour = diff % nd / nh;
// 计算差多少分钟
long min = diff % nd % nh / nm;
// 计算差多少秒//输出结果
long sec = diff % nd % nh % nm / ns;
return day + "天" + hour + "小时" + min + "分钟"+ sec + "秒";
}
/** 日志log类型替换 */
private String matching(String ActivityType){
String value="";
switch (ActivityType){
case "startEvent":
value="流程开始";
break;
case "userTask":
value="用户处理";
break;
case "noneEndEvent":
value="流程结束";
break;
default:
value="未知节点";
break;
}
return value;
}
}
上方代码是一个完整的流程,从流程发起到流程结束,审核通过、驳回,查询我创建的流程和我的代办已办和审核日志功能,
其中我们要注意的是,流程的创建者并不一定是流程的申请者(大多数情况下是一个,但是存在不是一个的情况),代码中也有较多的代码注释,具体的使用,大家仔细阅读即可理解掌握。
项目源码已经附上,有问题可评论留言!