本 demo 使用 activiti 框架实现了一个二级审批流程示例。包含前端和后端,后端用 springboot+activity 实现,前端用 vue+iview 实现。前后端是分离的。如果你只关注后端,那么你可以不实现前端,用 Postman 来测试后端即可。

1. 流程图

流程图的 xml:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
  <process id="LeaveRequestProcess" name="请假流程" isExecutable="true">
    <extensionElements>
      <activiti:executionListener event="start" class="com.test.activiti.MyTaskListener"></activiti:executionListener>
    </extensionElements>
    <startEvent id="startevent1" name="Start"></startEvent>
    <exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway"></exclusiveGateway>
    <endEvent id="endevent1" name="End"></endEvent>
    <userTask id="usertask1" name="新的请假申请">
      <extensionElements>
        <activiti:formProperty id="startDate" name="请假日期" type="date" datePattern="yyyy-MM-dd" required="true"></activiti:formProperty>
        <activiti:formProperty id="reason" name="请假理由" type="string" required="true"></activiti:formProperty>
        <activiti:formProperty id="days" name="请假天数" type="long" required="true"></activiti:formProperty>
        <activiti:taskListener event="create" class="com.test.activiti.MyTaskListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
    <userTask id="usertask2" name="等待Leader审批">
      <extensionElements>
        <activiti:formProperty id="comment" name="备注" type="string"></activiti:formProperty>
        <activiti:formProperty id="leaderApproved" name="主管同意" type="boolean" required="true"></activiti:formProperty>
        <activiti:taskListener event="create" class="com.test.activiti.MyTaskListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
    <sequenceFlow id="flow3" sourceRef="usertask2" targetRef="exclusivegateway1"></sequenceFlow>
    <userTask id="usertask3" name="等待总监审批">
      <extensionElements>
        <activiti:formProperty id="managerApproved" name="是否同意" type="boolean" required="true"></activiti:formProperty>
        <activiti:formProperty id="managerComment" name="备注" type="string"></activiti:formProperty>
        <activiti:taskListener event="create" class="com.test.activiti.MyTaskListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
    <sequenceFlow id="flow4" name="Leader同意且&gt;3天" sourceRef="exclusivegateway1" targetRef="usertask3">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days>3 && leaderAgree==true}]]></conditionExpression>
    </sequenceFlow>
    <userTask id="usertask4" name="等待人资备案">
      <extensionElements>
        <activiti:taskListener event="create" class="com.test.activiti.MyTaskListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
    <sequenceFlow id="flow7" sourceRef="usertask4" targetRef="endevent1"></sequenceFlow>
    <sequenceFlow id="flow10" name="Leader驳回" sourceRef="exclusivegateway1" targetRef="usertask1">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${leaderAgree==false}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow11" name="Leader同意且&lt;=3天" sourceRef="exclusivegateway1" targetRef="usertask4">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days<=3 && leaderAgree==true}]]></conditionExpression>
    </sequenceFlow>
    <exclusiveGateway id="exclusivegateway2" name="Exclusive Gateway"></exclusiveGateway>
    <sequenceFlow id="flow12" sourceRef="usertask3" targetRef="exclusivegateway2"></sequenceFlow>
    <sequenceFlow id="flow13" name="总监同意" sourceRef="exclusivegateway2" targetRef="usertask4">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${directorAgree==true}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow14" name="总监驳回" sourceRef="exclusivegateway2" targetRef="usertask1">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${directorAgree==false}]]></conditionExpression>
    </sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_LeaveRequestProcess">
    <bpmndi:BPMNPlane bpmnElement="LeaveRequestProcess" id="BPMNPlane_LeaveRequestProcess">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="25.0" y="160.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="exclusivegateway1" id="BPMNShape_exclusivegateway1">
        <omgdc:Bounds height="40.0" width="40.0" x="385.0" y="157.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="740.0" y="370.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="100.0" y="150.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
        <omgdc:Bounds height="55.0" width="105.0" x="235.0" y="150.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
        <omgdc:Bounds height="55.0" width="105.0" x="516.0" y="151.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask4" id="BPMNShape_usertask4">
        <omgdc:Bounds height="55.0" width="105.0" x="705.0" y="151.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="exclusivegateway2" id="BPMNShape_exclusivegateway2">
        <omgdc:Bounds height="40.0" width="40.0" x="620.0" y="100.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="60.0" y="177.0"></omgdi:waypoint>
        <omgdi:waypoint x="100.0" y="177.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
        <omgdi:waypoint x="205.0" y="177.0"></omgdi:waypoint>
        <omgdi:waypoint x="235.0" y="177.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="340.0" y="177.0"></omgdi:waypoint>
        <omgdi:waypoint x="385.0" y="177.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="425.0" y="177.0"></omgdi:waypoint>
        <omgdi:waypoint x="516.0" y="178.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="16.0" width="91.0" x="404.0" y="150.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow7" id="BPMNEdge_flow7">
        <omgdi:waypoint x="757.0" y="206.0"></omgdi:waypoint>
        <omgdi:waypoint x="757.0" y="370.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow10" id="BPMNEdge_flow10">
        <omgdi:waypoint x="405.0" y="157.0"></omgdi:waypoint>
        <omgdi:waypoint x="404.0" y="95.0"></omgdi:waypoint>
        <omgdi:waypoint x="274.0" y="95.0"></omgdi:waypoint>
        <omgdi:waypoint x="137.0" y="95.0"></omgdi:waypoint>
        <omgdi:waypoint x="137.0" y="118.0"></omgdi:waypoint>
        <omgdi:waypoint x="152.0" y="150.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="16.0" width="57.0" x="239.0" y="101.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow11" id="BPMNEdge_flow11">
        <omgdi:waypoint x="405.0" y="197.0"></omgdi:waypoint>
        <omgdi:waypoint x="405.0" y="292.0"></omgdi:waypoint>
        <omgdi:waypoint x="543.0" y="292.0"></omgdi:waypoint>
        <omgdi:waypoint x="662.0" y="292.0"></omgdi:waypoint>
        <omgdi:waypoint x="662.0" y="179.0"></omgdi:waypoint>
        <omgdi:waypoint x="705.0" y="178.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="48.0" width="100.0" x="504.0" y="269.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow12" id="BPMNEdge_flow12">
        <omgdi:waypoint x="621.0" y="178.0"></omgdi:waypoint>
        <omgdi:waypoint x="640.0" y="177.0"></omgdi:waypoint>
        <omgdi:waypoint x="640.0" y="140.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow13" id="BPMNEdge_flow13">
        <omgdi:waypoint x="660.0" y="120.0"></omgdi:waypoint>
        <omgdi:waypoint x="757.0" y="119.0"></omgdi:waypoint>
        <omgdi:waypoint x="757.0" y="151.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="16.0" width="44.0" x="660.0" y="120.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow14" id="BPMNEdge_flow14">
        <omgdi:waypoint x="640.0" y="100.0"></omgdi:waypoint>
        <omgdi:waypoint x="639.0" y="72.0"></omgdi:waypoint>
        <omgdi:waypoint x="137.0" y="72.0"></omgdi:waypoint>
        <omgdi:waypoint x="152.0" y="150.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="16.0" width="44.0" x="391.0" y="61.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

2. 任务监听器

从流程定义文件中可以看到,在每个用户节点中均使用了自定义的任务监听器,即类 MyTaskListener:

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.ExecutionListener;
import org.activiti.engine.delegate.TaskListener;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * Created by qq on 2018/10/4.
 */
public class MyTaskListener implements ExecutionListener,TaskListener {

    public static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(MyTaskListener.class);

    public void notify(DelegateExecution execution) {}
    public void notify(DelegateTask delegateTask) {

        String processInstanceId = delegateTask.getProcessInstanceId();


        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();

        String taskName = delegateTask.getName();

        Map<String, Object> map = delegateTask.getVariables();

        if ("新的请假申请".equals(taskName)) {

            delegateTask.setAssignee(map.get("applicant").toString());
        }else if ("等待Leader审批".equals(taskName)) {
            delegateTask.setAssignee(map.get("leader").toString());
        }else if("等待总监审批".equals(taskName)){
            delegateTask.setAssignee(map.get("director").toString());
        }else if("等待人资备案".equals(taskName)){
            delegateTask.setAssignee(map.get("hr").toString());
        }
    }
}

在这个监听器中,根据节点名自动设置了任务的经办人,从而实现了任务的自动流转。

3. 新建控制器 LeaveProcessController

作为一个系统,首先需要实现的是登录功能:

 	// 登录
    @RequestMapping("/login")
    @ResponseBody
    public BaseResponse login(@RequestBody JSONObject json, HttpServletResponse res) {
        BaseResponse result = new BaseResponse();

        String username = json.getAsString("username");
        Assert.hasText(username, "用户名不能为空");
        result.data = username;

        try {
            username = URLEncoder.encode(username, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        Cookie cookie = new Cookie("username", username);
        cookie.setMaxAge(3600); //设置cookie的过期时间是3600s
        res.addCookie(cookie);

        result.success = true;

        return result;
    }

伪代码,请重新实现登录逻辑。重点不在这里,略过。

4. 发起流程 —— 新建请假申请

首先是 3 个私有方法,分别用于创建流程引擎、流程定义和流程实例:

```
private ProcessDefinition getProcessDefinition(ProcessEngine processEngine) {
    RepositoryService repositoryService = processEngine.getRepositoryService();
    DeploymentBuilder deployment = repositoryService.createDeployment();
    deployment.addClasspathResource("LeaveRequestProcess.bpmn");
    Deployment deploy = deployment.deploy();
    String id = deploy.getId();
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
            .deploymentId(id).singleResult();

    return processDefinition;
}

private ProcessEngine getProcessEngine() {

    ProcessEngineConfiguration cfg = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration();
    ProcessEngine processEngine = cfg.buildProcessEngine();
    String name = processEngine.getName();
    String version = ProcessEngine.VERSION;

    return processEngine;
}

private ProcessInstance getProcessInstance(ProcessEngine processEngine, ProcessDefinition processDefinition, Map<String, Object> leave) {

    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId(), leave);
    LOGGER.info("启动流程:{}", processInstance.getProcessDefinitionKey());
    return processInstance;
}
```

这些都是 activiti 的套路。值得注意的是 getProcessInstance 方法的这一句:

 ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId(), leave);

这样会把 leave (请假流程的业务数据,比如天数、日期,事由)以流程实例变量的形式保存起来,这样流程中的每个节点都可以访问到这个变量。很方便。

然后是新建请假条。这个 api 特殊一点,因为这个任务会在启动流程实例时自动生成,只需要实例化一个流程实例就可以了,这个任务节点会自动进入待办。

```
@RequestMapping("/newLeave")
@ResponseBody
public BaseResponse newLeave(@RequestBody JSONObject json, HttpServletRequest req) {
    BaseResponse result = new BaseResponse();

    String username = _getUser(req,json);
    Assert.hasText(username, "请重新登录");

    json.put("applicant", username);

    // 启动新的流程
    ProcessInstance pi = getProcessInstance(processEngine, processDefinition, json);


    Task task = this.processEngine.getTaskService().createTaskQuery().processInstanceId(pi.getId()).active().singleResult();

    json.put("taskId",task.getId());
    return _submitApplication(json, req);

}
```

getProcessInstance 方法有 3 个参数,一个流程引擎实例,一个流程定义实例,一个 json 对象。前 2 者是控制器的私有变量,自动实例化了:

```
// 创建流程引擎
private ProcessEngine processEngine = getProcessEngine();
// 部署流程
private ProcessDefinition processDefinition = getProcessDefinition(processEngine);
```

第 3 个参数是一个 JSONObject 对象,表示了请假申请表单,包含请假日期,请假天数、请假人等。

注意,启动新流程会自动执行第一个节点,即流程图中的“开始”节点,执行完开始节点行后,自动流转到第二个节点,即”新的请假申请”任务就自动生成了。因此,当流程实例生成后,我们可以直接查找该流程实例的 active 任务,即可得到流程图中的第二个任务“新的请假申请”。然后再执行该任务。

_submitApplication() 是用来真正办理第二个节点任务的:

```
private BaseResponse _submitApplication(JSONObject json, HttpServletRequest req) {

    BaseResponse result = new BaseResponse();
    String username = _getUser(req,json);
    String taskId = json.getAsString("taskId");

    Assert.hasText(taskId, "待办不存在");

    Assert.hasText(username, "请重新登录");

    Assert.hasText(json.getAsString("leader"), "上级不能为空");
    Assert.hasText(json.getAsString("director"), "总监不能为空");
    Assert.hasText(json.getAsString("hr"), "HR不能为空");

    Assert.hasText(json.getAsString("date"), "请假日期不能为空");
    Assert.isTrue(json.getAsNumber("days").intValue() > 0, "请假天数不能小于等于 0");

    TaskService taskService = processEngine.getTaskService();

    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

    if (task != null) {

        task.setAssignee(json.getAsString("applicant"));

        taskService.setVariables(taskId, json);

        taskService.complete(taskId);

        result.success = true;
        result.message = "请假申请提交成功";
    } else {
        result.success = false;
        result.message = "任务不存在";
    }
    return result;
}
```

注意这个地方:

	Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

	if (task != null) {
		task.setAssignee(json.getAsString("applicant"));

		taskService.setVariables(taskId, json);

		taskService.complete(taskId);
	}

首先通过 taskId 查找到待办任务,找到之后首先设置 assignee,因为一般情况下请假都是自己提申请的,所以任务的经办人就是请假人。

然后是把完整的业务表单(json 对象中包含了请假条的具体信息)保存到流程实例全局变量中,这样该流程实例的所有节点(任务)都能访问业务表单中的信息了,方便后续人员审批。

最后是办结该任务,因为数据都在全局变量中,所以 complete 方法也不需要传递什么参数了。
_getUser 方法从 cookie 中获取登录用户的用户名,以此来判断用户有没有登录(这里仅仅出于演示目的,读者千万不要这样做):

private String _getUser(HttpServletRequest req,JSONObject json) {
        String username = json.getAsString("username");
        if (username != null) {
            return username;
        }

        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("username")) {
                    username = cookie.getValue();
                    try {
                        username = URLDecoder.decode(username, "utf-8");
                        return username;
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return null;
    }
Logo

前往低代码交流专区

更多推荐