Springboot整合Camunda工作流引擎實(shí)現(xiàn)審批流程實(shí)例
環(huán)境:Spingboot2.6.14 +
camunda-spring-boot-starter7.18.0
環(huán)境配置
依賴配置
<camunda.version>7.18.0</camunda.version>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
<version>${camunda.version}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
<version>${camunda.version}</version>
</dependency>
應(yīng)用程序配置
camunda.bpm:
webapp:
# 設(shè)置管理控制臺的訪問上下文
application-path: /workflow
auto-deployment-enabled: true
admin-user:
# 配置登錄管理控制臺的用戶
id: admin
password: admin
firstName: admin
filter:
create: All tasks
database:
#數(shù)據(jù)庫類型
type: mysql
#是否自動(dòng)更新表信息
schema-update: true
logging:
level:
#配置日志,這樣在開發(fā)過程中就能看到每步執(zhí)行的SQL語句了
'[org.camunda.bpm.engine.impl.persistence.entity]': debug
---
spring:
jersey:
application-path: /api-flow
type: servlet
servlet:
load-on-startup: 0
通過上面的配置后訪問控制臺:http://localhost:8100/workflow/
默認(rèn)是沒有上面的tasks中的內(nèi)容,這里是我之前測試數(shù)據(jù)
環(huán)境準(zhǔn)備好后,接下來就可以設(shè)計(jì)工作流程。
上面的camunda-bpm-spring-boot-starter-rest依賴中定義了一系列操作camunda的 rest api 這api的實(shí)現(xiàn)是通過jersey實(shí)現(xiàn),我們可以通過/api-flow前綴來訪問這些接口,具體有哪些接口,我們可以通過官方提供的camunda-bpm-run-7.18.0.zip
設(shè)計(jì)流程
這里設(shè)計(jì)兩個(gè)節(jié)點(diǎn)的審批流程,經(jīng)理審批---》人事審批 流程。
經(jīng)理審批節(jié)點(diǎn)
人事審批節(jié)點(diǎn)
上面配置了2個(gè)用戶任務(wù)節(jié)點(diǎn),并且為每個(gè)任務(wù)節(jié)點(diǎn)都設(shè)置了表達(dá)式,指定節(jié)點(diǎn)的審批人。
最終生成的流程XML內(nèi)容如下:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="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:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="sample-diagram" targetNamespace="http://pack.org/bpmn" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
<bpmn2:process id="Process_1" isExecutable="true">
<bpmn2:startEvent id="StartEvent_1">
<bpmn2:outgoing>Flow_18pxcpx</bpmn2:outgoing>
</bpmn2:startEvent>
<bpmn2:sequenceFlow id="Flow_18pxcpx" sourceRef="StartEvent_1" targetRef="Activity_0vs8hu4" />
<bpmn2:userTask id="Activity_0vs8hu4" camunda:assignee="${uid}">
<bpmn2:incoming>Flow_18pxcpx</bpmn2:incoming>
<bpmn2:outgoing>Flow_0n014x3</bpmn2:outgoing>
</bpmn2:userTask>
<bpmn2:sequenceFlow id="Flow_0n014x3" sourceRef="Activity_0vs8hu4" targetRef="Activity_0bcruuz" />
<bpmn2:userTask id="Activity_0bcruuz" camunda:assignee="${mid}">
<bpmn2:incoming>Flow_0n014x3</bpmn2:incoming>
<bpmn2:outgoing>Flow_0dsfy6s</bpmn2:outgoing>
</bpmn2:userTask>
<bpmn2:endEvent id="Event_1xosttx">
<bpmn2:incoming>Flow_0dsfy6s</bpmn2:incoming>
</bpmn2:endEvent>
<bpmn2:sequenceFlow id="Flow_0dsfy6s" sourceRef="Activity_0bcruuz" targetRef="Event_1xosttx" />
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="252" y="252" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1py8b5e_di" bpmnElement="Activity_0vs8hu4">
<dc:Bounds x="340" y="230" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0arbs87_di" bpmnElement="Activity_0bcruuz">
<dc:Bounds x="500" y="230" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1xosttx_di" bpmnElement="Event_1xosttx">
<dc:Bounds x="662" y="252" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_18pxcpx_di" bpmnElement="Flow_18pxcpx">
<di:waypoint x="288" y="270" />
<di:waypoint x="340" y="270" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0n014x3_di" bpmnElement="Flow_0n014x3">
<di:waypoint x="440" y="270" />
<di:waypoint x="500" y="270" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0dsfy6s_di" bpmnElement="Flow_0dsfy6s">
<di:waypoint x="600" y="270" />
<di:waypoint x="662" y="270" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>
部署流程
這里我不通過上面的rest api 進(jìn)行部署,而是通過自定義的接口然后調(diào)用camunda的相關(guān)api來實(shí)現(xiàn)流程部署。
上面的流程設(shè)計(jì)我是通過vue整合的camunda進(jìn)行設(shè)計(jì),并沒有使用官方提供的設(shè)計(jì)器。設(shè)計(jì)完成后直接上傳到服務(wù)端。
接口
@RestController
@RequestMapping("/camunda")
public class BpmnController {
// 上傳路徑
@Value("${gx.camunda.upload}")
private String path ;
// 通用的工作流操作api服務(wù)類
@Resource
private ProcessService processService ;
@PostMapping("/bpmn/upload")
public AjaxResult uploadFile(MultipartFile file, String fileName, String name) throws Exception {
try {
// 上傳并返回新文件名稱
InputStream is = file.getInputStream() ;
File storageFile = new File(path + File.separator + fileName) ;
FileOutputStream fos = new FileOutputStream(new File(path + File.separator + fileName)) ;
byte[] buf = new byte[10 * 1024] ;
int len = -1 ;
while((len = is.read(buf)) > -1) {
fos.write(buf, 0, len) ;
}
fos.close() ;
is.close() ;
// 創(chuàng)建部署流程
processService.createDeploy(fileName, name, new FileSystemResource(storageFile)) ;
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
}
部署流程Service
// 這個(gè)是camunda spring boot starter 自動(dòng)配置
@Resource
private RepositoryService repositoryService ;
public void createDeploy(String resourceName, String name, org.springframework.core.io.Resource resource) {
try {
Deployment deployment = repositoryService.createDeployment()
.addInputStream(resourceName, resource.getInputStream())
.name(name)
.deploy();
logger.info("流程部署id: {}", deployment.getId());
logger.info("流程部署名稱: {}", deployment.getName());
} catch (IOException e) {
throw new RuntimeException(e) ;
}
}
執(zhí)行上面的接口就能將上面設(shè)計(jì)的流程部署到camunda中(其實(shí)就是將流程文件保存到了數(shù)據(jù)庫中,對應(yīng)的數(shù)據(jù)表是:act_ge_bytearray)。
啟動(dòng)流程
啟動(dòng)流程還是一樣,通過我們自己的接口來實(shí)現(xiàn)。
接口
@RestController
@RequestMapping("/process")
public class ProcessController {
@Resource
private ProcessService processService ;
// 根據(jù)流程定義id,啟動(dòng)流程;整個(gè)流程需要?jiǎng)討B(tài)傳2個(gè)參數(shù)(審批人),如果不傳將會報(bào)錯(cuò)
@GetMapping("/start/{processDefinitionId}")
public AjaxResult startProcess(@PathVariable("processDefinitionId") String processDefinitionId) {
Map<String, Object> variables = new HashMap<>() ;
variables.put("uid", "1") ;
variables.put("mid", "1000") ;
processService.startProcessInstanceAssignVariables(processDefinitionId, "AKF", variables) ;
return AjaxResult.success("流程啟動(dòng)成功") ;
}
}
服務(wù)Service接口
@Resource
private RuntimeService runtimeService ;
public ProcessInstance startProcessInstanceAssignVariables(String processDefinitionId, String businessKey, Map<String, Object> variables) {
ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinitionId, businessKey, variables);
logger.info("流程定義ID: {}", processInstance.getProcessDefinitionId());
logger.info("流程實(shí)例ID: {}", processInstance.getId());
logger.info("BussinessKey: {}", processInstance.getBusinessKey()) ;
return processInstance ;
}
流程啟動(dòng)后就可以查看當(dāng)前需要自己審批的所有審批單
接口實(shí)現(xiàn)
@Resource
private TaskService taskService ;
@Resource
private ManagementService managementService ;
// 根據(jù)時(shí)間段查詢
public List<Task> queryTasksByBusinessAndCreateTime(String assignee, String businessKey, String startTime, String endTime) {
NativeTaskQuery nativeQuery = taskService.createNativeTaskQuery() ;
nativeQuery.sql("select distinct RES.* from " + managementService.getTableName(TaskEntity.class) + " RES "
+ " left join " + managementService.getTableName(IdentityLinkEntity.class) + " I on I.TASK_ID_ = RES.ID_ "
+ " WHERE (RES.ASSIGNEE_ = #{assignee} or "
+ " (RES.ASSIGNEE_ is null and I.TYPE_ = 'candidate' "
+ " and (I.USER_ID_ = #{assignee} or I.GROUP_ID_ IN ( #{assignee} ) ))) "
+ " and RES.CREATE_TIME_ between #{startTime} and #{endTime} "
+ " order by RES.CREATE_TIME_ asc LIMIT #{size} OFFSET 0") ;
nativeQuery.parameter("assignee", assignee) ;
nativeQuery.parameter("startTime", startTime) ;
nativeQuery.parameter("endTime", endTime) ;
nativeQuery.parameter("size", Integer.MAX_VALUE) ;
return nativeQuery.list() ;
}
審批流程
流程啟動(dòng)后,接下來就是各個(gè)用戶任務(wù)節(jié)點(diǎn)配置的用戶進(jìn)行審批
接口
@GetMapping("/approve/{id}")
public AjaxResult approve(@PathVariable("id") String instanceId) {
if (StringUtils.isEmpty(instanceId)) {
return AjaxResult.error("未知審批任務(wù)") ;
}
// 下面的參數(shù)信息應(yīng)該自行保存管理(與發(fā)起審批設(shè)置的指派人要一致)
Map<String, Object> variables = new HashMap<>() ;
// 第一個(gè)節(jié)點(diǎn)所要提供的遍歷信息(這里就是依次類推,mid等)
variables.put("uid", "1") ;
processService.executionTask(variables, instanceId, task -> {}, null) ;
return AjaxResult.success() ;
}
服務(wù)Service接口
@Resource
private TaskService taskService ;
@Resource
private RuntimeService runtimeService ;
@Transactional
public void executionTask(Map<String, Object> variables, String instanceId, Consumer<Task> consumer, String type) {
Task task = taskService.createTaskQuery().processInstanceId(instanceId).singleResult() ;
if (task == null) {
logger.error("任務(wù)【{}】不存在", instanceId) ;
throw new RuntimeException("任務(wù)【" + instanceId + "】不存在") ;
}
taskService.setVariables(task.getId(), variables);
taskService.complete(task.getId(), variables) ;
long count = runtimeService.createExecutionQuery().processInstanceId(instanceId).count();
if (count == 0) {
consumer.accept(task) ;
}
}
以上就完成了從整個(gè)流程的生命周期:
設(shè)計(jì)流程 ---》部署流程 ---》啟動(dòng)流程 ---》審批流程
完畢?。。?/p>