一个二级审批流程的具体实现(1)
本 demo 使用 activiti 框架实现了一个二级审批流程示例。包含前端和后端,后端用 springboot+activity 实现,前端用 vue+iview 实现。前后端是分离的。如果你只关注后端,那么你可以不实现前端,用 Postman 来测试后端即可。1. 流程图流程图的 xml:<?xml version="1.0" encoding="
本 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同意且>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同意且<=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;
}
更多推荐
所有评论(0)