Giới thiệu về Camunda BPM
Camunda là một trong những đóng góp chính cho dự án Activiti (bên cạnh Alfresco), đồng thời là đối tác tư vấn hàng đầu của dự án này. Theo đánh giá của đội ngũ phát triển Camunda, Activiti có thể đã bị giới hạn bởi nhu cầu workflow tập trung vào tài liệu của Alfresco - điều này một phần do đặc thù của BPMN - và đã bỏ qua tiềm năng của một nền tảng BPM linh hoạt hơn. Chính vì vậy, Camunda đã tách ra từ Activiti để phát triển một engine workflow mới mã nguồn mở độc lập.
Tài liệu và cài đặt
- Trang chủ: https://camunda.com/
- Tải về: https://camunda.com/download/
Phạm vi triển khai thực tế
Trong bài viết này, chúng ta sẽ tìm hiểu cách triển khai Camunda BPM với các tính năng cơ bản của một hệ thống OA thông dụng:
- Tạo mới và phê duyệt workflow
- Từ chối và quay lại node trước đó
- Chuyển giao công việc cho người khác
- Thêm người phê duyệt trước/sau node hiện tại
- Thu hồi và khởi động lại quy trình
- Hủy quy trình và chuyển tiếp để xem
- Nhắc nhở người phê duyệt
Lưu ý: Bài viết tập trung vào việc tạo model BPMN từ phía backend thông qua API, không sử dụng form engine hay thư viện BPMN.js vì lý do thẩm mỹ và độ phức tạp của business logic.
Cấu hình Maven
Để tích hợp Camunda vào dự án Spring Boot, cần thêm các dependency sau:
implementation "org.camunda.bpm.springboot:camunda-bpm-spring-boot-starter:7.16.0"
implementation "org.camunda.bpm.springboot:camunda-bpm-spring-boot-starter-webapp:7.16.0"
implementation "org.camunda.bpm:camunda-engine-plugin-spin:7.16.0"
implementation "org.camunda.spin:camunda-spin-dataformat-all:1.16.0"
Utility class tạo BPMN Model
Class này chịu trách nhiệm xây dựng cấu trúc BPMN theo chương trình, bao gồm các node, sequence flow và gateway:
package com.oa.workflow.utils;
import org.camunda.bpm.model.bpmn.Bpmn;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;
import org.camunda.bpm.model.bpmn.instance.*;
import org.camunda.bpm.model.bpmn.instance.bpmndi.*;
import org.camunda.bpm.model.bpmn.instance.camunda.*;
import org.camunda.bpm.model.bpmn.instance.dc.Bounds;
import org.camunda.bpm.model.bpmn.instance.di.Waypoint;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@Component
public class BpmnModelBuilder {
private static final String DEFAULT_NAMESPACE = "http://workflow.example.org";
public String buildProcessModel(Map<String, Object> params, Long userId) {
ProcessConfig processConfig = extractProcessConfig(params);
// Khởi tạo model BPMN rỗng
BpmnModelInstance modelInstance = Bpmn.createEmptyModel();
// Tạo Definitions với namespace
Definitions definitions = modelInstance.newInstance(Definitions.class);
definitions.setTargetNamespace(DEFAULT_NAMESPACE);
modelInstance.setDefinitions(definitions);
// Tạo Diagram và Plane cho việc hiển thị
BpmnDiagram diagram = modelInstance.newInstance(BpmnDiagram.class);
BpmnPlane plane = modelInstance.newInstance(BpmnPlane.class);
diagram.setBpmnPlane(plane);
definitions.addChildElement(diagram);
// Tạo Process chính
String processKey = "Process_" + generateUniqueId();
Process process = createElement(definitions, processKey, Process.class);
process.setName(processConfig.getProcessName());
process.setExecutable(true);
// Gán process vào plane
plane.setBpmnElement(process);
// Tạo Start Event
StartEvent startEvent = createElement(process, "Event_" + generateUniqueId(), StartEvent.class);
startEvent.setCamundaInitiator("initiator");
drawShape(modelInstance, plane, startEvent.getId());
// Tạo User Task đầu tiên (người khởi tạo)
UserTask initiatorTask = createElement(process, "Activity_" + generateUniqueId(), UserTask.class);
initiatorTask.setName("Người khởi tạo");
initiatorTask.setCamundaAssignee("${initiator}");
drawShape(modelInstance, plane, initiatorTask.getId());
// Kết nối Start Event với Initiator Task
SequenceFlow flow1 = createFlow(process, startEvent, initiatorTask);
drawEdge(modelInstance, plane, flow1.getId());
// Tạo End Event
EndEvent endEvent = createElement(process, "Event_" + generateUniqueId(), EndEvent.class);
// Thêm listener cho End Event
ExtensionElements extElements = createElement(endEvent, "", ExtensionElements.class);
addExecutionListener(extElements, "#{ProcessEndListener}", "start");
addExecutionListener(extElements, "#{ProcessEndListener}", "end");
endEvent.setExtensionElements(extElements);
drawShape(modelInstance, plane, endEvent.getId());
// Xử lý các node tiếp theo một cách đệ quy
processNodes(modelInstance, plane, process, initiatorTask, endEvent,
null, serializeJson(params.get("nodeData")), userId);
// Validate và chuyển đổi thành XML
Bpmn.validateModel(modelInstance);
return Bpmn.convertToString(modelInstance);
}
private void processNodes(BpmnModelInstance modelInstance, BpmnPlane plane,
Process process, FlowNode previousNode, FlowNode endEvent,
Map<String, String> conditionMap, String jsonData, Long userId) {
// Xử lý node từ JSON data
// Hỗ trợ các loại node: UserTask, CopyTask, ExclusiveGateway
}
private <T extends BpmnModelElementInstance> T createElement(
BpmnModelElementInstance parent, String id, Class<T> clazz) {
T element = parent.getModelInstance().newInstance(clazz);
if (id != null && !id.isEmpty()) {
element.setAttributeValue("id", id, true);
}
parent.addChildElement(element);
return element;
}
private SequenceFlow createFlow(Process process, FlowNode from, FlowNode to) {
String flowId = from.getId() + "-" + to.getId();
SequenceFlow flow = createElement(process, flowId, SequenceFlow.class);
process.addChildElement(flow);
flow.setSource(from);
from.getOutgoing().add(flow);
flow.setTarget(to);
to.getIncoming().add(flow);
return flow;
}
private void drawShape(BpmnModelInstance model, BpmnPlane plane, String elementId) {
BpmnShape shape = model.newInstance(BpmnShape.class);
BaseElement element = model.getModelElementById(elementId);
shape.setBpmnElement(element);
shape.setId(elementId + "_di");
Bounds bounds = model.newInstance(Bounds.class);
bounds.setX(150);
bounds.setY(80);
bounds.setHeight(80);
bounds.setWidth(100);
shape.setBounds(bounds);
plane.addChildElement(shape);
}
private void drawEdge(BpmnModelInstance model, BpmnPlane plane, String elementId) {
BpmnEdge edge = model.newInstance(BpmnEdge.class);
BaseElement element = model.getModelElementById(elementId);
edge.setId(elementId + "_di");
edge.setBpmnElement(element);
Waypoint wp1 = model.newInstance(Waypoint.class);
wp1.setX(300);
wp1.setY(100);
edge.addChildElement(wp1);
Waypoint wp2 = model.newInstance(Waypoint.class);
wp2.setX(300);
wp2.setY(100);
edge.addChildElement(wp2);
plane.addChildElement(edge);
}
private String generateUniqueId() {
return String.valueOf(System.currentTimeMillis()) +
(int)(Math.random() * 10000);
}
}
Utility class thao tác với Node
Class này cung cấp các phương thức để lấy thông tin về node tiếp theo trong workflow dựa trên các điều kiện:
package com.oa.workflow.utils;
import org.camunda.bpm.engine.*;
import org.camunda.bpm.engine.history.HistoricVariableInstance;
import org.camunda.bpm.engine.impl.RepositoryServiceImpl;
import org.camunda.bpm.engine.impl.bpmn.behavior.UserTaskActivityBehavior;
import org.camunda.bpm.engine.impl.el.ExpressionManager;
import org.camunda.bpm.engine.impl.javax.el.ExpressionFactory;
import org.camunda.bpm.engine.impl.javax.el.ValueExpression;
import org.camunda.bpm.engine.impl.juel.ExpressionFactoryImpl;
import org.camunda.bpm.engine.impl.juel.SimpleContext;
import org.camunda.bpm.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.camunda.bpm.engine.impl.pvm.PvmActivity;
import org.camunda.bpm.engine.impl.pvm.PvmTransition;
import org.camunda.bpm.engine.impl.pvm.process.ActivityImpl;
import org.camunda.bpm.engine.impl.task.TaskDefinition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
@Component
public class WorkflowNodeResolver {
@Autowired private RepositoryService repositoryService;
@Autowired private RuntimeService runtimeService;
@Autowired private HistoryService historyService;
public List<Map<String, Object>> resolveNextNodes(String processInstanceId, boolean fromHistory) {
Map<String, Object> variables = collectVariables(processInstanceId, fromHistory);
List<TaskDefinition> nextTasks = null;
try {
nextTasks = calculateNextTasks(processInstanceId, variables, fromHistory);
} catch (Exception e) {
// xử lý exception
}
return convertToResult(nextTasks);
}
private Map<String, Object> collectVariables(String processInstanceId, boolean fromHistory) {
Map<String, Object> variables = new HashMap<>();
if (fromHistory) {
List<HistoricVariableInstance> varList = historyService
.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.list();
for (HistoricVariableInstance var : varList) {
variables.put(var.getName(), var.getValue());
}
} else {
variables = runtimeService.getVariables(processInstanceId);
}
return variables;
}
private List<TaskDefinition> calculateNextTasks(String processInstanceId,
Map<String, Object> condition,
boolean fromHistory) {
ProcessDefinitionEntity processDef = getProcessDefinition(processInstanceId, fromHistory);
List<ActivityImpl> activities = processDef.getActivities();
// Lọc start event
activities = activities.stream()
.filter(a -> "startEvent".equals(a.getProperty("type")))
.collect(Collectors.toList());
List<TaskDefinition> tasks = new CopyOnWriteArrayList<>();
analyzeNextNodes(tasks, activities, activities.get(0).getId(), processInstanceId, condition);
return tasks;
}
private void analyzeNextNodes(List<TaskDefinition> tasks,
List<ActivityImpl> activityList,
String currentActivityId,
String processInstanceId,
Map<String, Object> condition) {
for (ActivityImpl activity : activityList) {
if ("userTask".equals(activity.getProperty("type")) &&
!currentActivityId.equals(activity.getId())) {
TaskDefinition taskDef = ((UserTaskActivityBehavior)
activity.getActivityBehavior()).getTaskDefinition();
tasks.add(taskDef);
// Tiếp tục phân tích node tiếp theo
List<PvmTransition> outgoing = activity.getOutgoingTransitions();
List<ActivityImpl> nextList = new ArrayList<>();
nextList.add((ActivityImpl) outgoing.get(0).getDestination());
analyzeNextNodes(tasks, nextList, nextList.get(0).getId(),
processInstanceId, condition);
} else if (activity.getProperty("type").toString().contains("EndEvent")) {
// Xử lý end event
} else if ("multiInstanceBody".equals(activity.getProperty("type"))) {
// Xử lý multi-instance
List<ActivityImpl> subActivities = ((ActivityImpl) activity).getActivities();
for (ActivityImpl subActivity : subActivities) {
TaskDefinition taskDef = ((UserTaskActivityBehavior)
subActivity.getActivityBehavior()).getTaskDefinition();
tasks.add(taskDef);
}
} else if ("exclusiveGateway".equals(activity.getProperty("type")) ||
"inclusiveGateway".equals(activity.getProperty("type"))) {
List<PvmTransition> outgoing = activity.getOutgoingTransitions();
if (outgoing.size() == 1) {
List<ActivityImpl> nextList = new ArrayList<>();
nextList.add((ActivityImpl) outgoing.get(0).getDestination());
analyzeNextNodes(tasks, nextList, nextList.get(0).getId(),
processInstanceId, condition);
} else if (outgoing.size() > 1) {
for (PvmTransition transition : outgoing) {
ActivityImpl dest = (ActivityImpl) transition.getDestination();
Object conditionText = transition.getProperty("conditionText");
if (conditionText != null &&
evaluateCondition(condition, conditionText.toString().trim())) {
List<ActivityImpl> nextList = new ArrayList<>();
nextList.add(dest);
analyzeNextNodes(tasks, nextList, "", processInstanceId, condition);
}
}
}
}
}
}
private boolean evaluateCondition(Map<String, Object> condition, String expression) {
try {
ExpressionFactory factory = new ExpressionFactoryImpl();
SimpleContext context = new SimpleContext();
if (condition != null) {
for (Map.Entry<String, Object> entry : condition.entrySet()) {
context.setVariable(entry.getKey(),
factory.createValueExpression(entry.getValue(), String.class));
}
}
ValueExpression ve = factory.createValueExpression(context, expression, boolean.class);
return (Boolean) ve.getValue(context);
} catch (Exception e) {
return false;
}
}
private ProcessDefinitionEntity getProcessDefinition(String processInstanceId, boolean fromHistory) {
String definitionId;
if (fromHistory) {
definitionId = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult()
.getProcessDefinitionId();
} else {
definitionId = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult()
.getProcessDefinitionId();
}
return (ProcessDefinitionEntity) ((RepositoryServiceImpl) repositoryService)
.getDeployedProcessDefinition(definitionId);
}
}
Utility class quản lý Workflow Service
Class chính xử lý các nghiệp vụ workflow như deploy, khởi tạo, phê duyệt, từ chối, chuyển giao:
package com.oa.workflow.service;
import org.camunda.bpm.engine.*;
import org.camunda.bpm.engine.history.HistoricActivityInstance;
import org.camunda.bpm.engine.history.HistoricTaskInstance;
import org.camunda.bpm.engine.impl.RepositoryServiceImpl;
import org.camunda.bpm.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.camunda.bpm.engine.impl.pvm.process.ActivityImpl;
import org.camunda.bpm.engine.repository.Deployment;
import org.camunda.bpm.engine.runtime.ActivityInstance;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.runtime.ProcessInstanceModificationBuilder;
import org.camunda.bpm.engine.task.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
@Component
public class WorkflowService {
@Autowired private RepositoryService repositoryService;
@Autowired private RuntimeService runtimeService;
@Autowired private IdentityService identityService;
@Autowired private TaskService taskService;
@Autowired private HistoryService historyService;
// Deploy process definition
public Map<String, Object> deployProcess(String processName, String bpmnXml) {
Deployment deployment = repositoryService.createDeployment()
.name(processName)
.addString(processName + ".bpmn20.xml", bpmnXml)
.deploy();
Map<String, Object> result = new HashMap<>();
result.put("deploymentId", deployment.getId());
result.put("deploymentName", deployment.getName());
return result;
}
// Start process instance
public Map<String, Object> startProcess(String initiator, String processDefKey,
String businessKey, Map<String, Object> variables) {
identityService.setAuthenticatedUserId(initiator);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
processDefKey, businessKey, variables);
Map<String, Object> result = new HashMap<>();
result.put("processInstanceId", processInstance.getId());
result.put("initiator", initiator);
result.put("processDefinitionId", processInstance.getProcessDefinitionId());
return result;
}
// Approve task
public Map<String, Object> approveTask(String taskId, String userId,
String processInstanceId, String comment,
Map<String, Object> variables) {
identityService.setAuthenticatedUserId(userId);
if (comment != null && !comment.isEmpty()) {
taskService.createComment(taskId, processInstanceId, comment);
}
taskService.complete(taskId, variables);
return new HashMap<>();
}
// Reject task - quay lại người khởi tạo
public Map<String, Object> rejectTask(String taskId, String userId,
String processInstanceId, String comment) {
identityService.setAuthenticatedUserId(userId);
HistoricTaskInstance currentTask = historyService
.createHistoricTaskInstanceQuery()
.taskId(taskId)
.singleResult();
if (currentTask == null) {
return Map.of("error", "Không tìm thấy task");
}
taskService.createComment(taskId, currentTask.getProcessInstanceId(), comment);
ProcessDefinitionEntity processDef = (ProcessDefinitionEntity)
((RepositoryServiceImpl) repositoryService)
.getDeployedProcessDefinition(currentTask.getProcessDefinitionId());
ActivityImpl currentActivity = processDef.findActivity(currentTask.getTaskDefinitionKey());
// Lấy activity đầu tiên
List<HistoricActivityInstance> firstActivities = historyService
.createHistoricActivityInstanceQuery()
.activityType("userTask")
.processInstanceId(processInstanceId)
.finished()
.orderByHistoricActivityInstanceEndTime()
.asc()
.list();
if (firstActivities.isEmpty()) {
return new HashMap<>();
}
ActivityImpl startActivity = processDef.findActivity(
firstActivities.get(0).getActivityId());
// Thực hiện reject
runtimeService.createProcessInstanceModification(processInstanceId)
.cancelAllForActivity(currentActivity.getActivityId())
.setAnnotation("Từ chối")
.startBeforeActivity(startActivity.getActivityId())
.execute();
return new HashMap<>();
}
// Transfer task
public Map<String, Object> transferTask(String taskId, String userId) {
identityService.setAuthenticatedUserId(userId);
taskService.delegateTask(taskId, userId);
return new HashMap<>();
}
// Return to previous node
public Map<String, Object> returnToNode(String taskId, String processInstanceId,
String targetActivityId, String comment) {
taskService.createComment(taskId, processInstanceId, comment);
ProcessInstanceModificationBuilder modifier =
runtimeService.createProcessInstanceModification(processInstanceId);
Set<String> activityIds = new HashSet<>();
taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.active()
.list()
.forEach(task -> {
String activityId = task.getTaskDefinitionKey();
if (activityIds.add(activityId)) {
modifier.cancelAllForActivity(activityId);
}
});
modifier.startBeforeActivity(targetActivityId + "#multiInstanceBody")
.execute();
return new HashMap<>();
}
// Add signature (trước hoặc sau node hiện tại)
public Map<String, Object> addSignature(String taskId, String userId,
String processInstanceId,
Integer signatureType,
String addUserId, Integer approvalType) {
HistoricTaskInstance currentTask = historyService
.createHistoricTaskInstanceQuery()
.taskId(taskId)
.singleResult();
if (currentTask == null) {
return Map.of("error", "Không tìm thấy task");
}
taskService.createComment(taskId, currentTask.getProcessInstanceId(), "Thêm người ký");
ActivityInstance activity = runtimeService.getActivityInstance(processInstanceId);
String activityId = activity.getChildActivityInstances()[0]
.getActivityId().split("#")[0];
// Lấy danh sách người phê duyệt hiện tại
List<String> currentList = (List<String>) runtimeService
.getVariableLocal(currentTask.getProcessInstanceId(),
"assigneeList_" + activityId);
LinkedList<String> linkedList = currentList.stream()
.collect(Collectors.toCollection(LinkedList::new));
int currentIndex = linkedList.indexOf(userId);
if (signatureType == 1) { // Thêm trước
if (currentIndex == 0) {
linkedList.addFirst(addUserId);
} else {
linkedList.add(currentIndex, addUserId);
}
} else { // Thêm sau
if (linkedList.size() - 1 == currentIndex) {
linkedList.addLast(addUserId);
} else {
linkedList.add(currentIndex + 1, addUserId);
}
}
// Xử lý theo loại phê duyệt
Map<String, Object> variables = new HashMap<>();
if (approvalType == 3) { // Sequential
// Xử lý đặc biệt cho sequential
}
variables.put("assigneeList_" + activityId, linkedList);
// Thực hiện thêm signature
ProcessInstanceModificationBuilder modifier =
runtimeService.createProcessInstanceModification(processInstanceId);
Set<String> activityIdSet = new HashSet<>();
taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.active()
.list()
.forEach(task -> {
String actId = task.getTaskDefinitionKey();
if (activityIdSet.add(actId)) {
modifier.cancelAllForActivity(actId);
}
});
modifier.startBeforeActivity(
activity.getChildActivityInstances()[0].getActivityId())
.setVariablesLocal(variables)
.execute();
return Map.of("userIds", linkedList);
}
// Get pending tasks
public Map<String, Object> getPendingTasks(String userId, int pageIndex, int pageSize) {
int firstResult = (pageIndex - 1) * pageSize;
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(userId)
.listPage(firstResult, pageSize);
long total = taskService.createTaskQuery()
.taskAssignee(userId)
.count();
Map<String, Object> result = new HashMap<>();
result.put("list", tasks);
result.put("pageCount", total);
return result;
}
// Get completed tasks
public Map<String, Object> getCompletedTasks(String userId, int pageIndex, int pageSize) {
int firstResult = (pageIndex - 1) * pageSize;
List<HistoricTaskInstance> tasks = historyService
.createHistoricTaskInstanceQuery()
.taskAssignee(userId)
.finished()
.listPage(firstResult, pageSize);
long total = historyService.createHistoricTaskInstanceQuery()
.taskAssignee(userId)
.finished()
.count();
Map<String, Object> result = new HashMap<>();
result.put("list", tasks);
result.put("pageCount", total);
return result;
}
// Withdraw process
public Map<String, Object> withdrawProcess(String taskId, String processInstanceId, String userId) {
HistoricTaskInstance currentTask = historyService
.createHistoricTaskInstanceQuery()
.taskId(taskId)
.singleResult();
if (currentTask == null) {
return Map.of("error", "Không tìm thấy task");
}
taskService.createComment(taskId, processInstanceId, "Thu hồi");
ProcessInstanceModificationBuilder modifier =
runtimeService.createProcessInstanceModification(processInstanceId);
Set<String> activityIds = new HashSet<>();
taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.active()
.list()
.forEach(task -> {
String activityId = task.getTaskDefinitionKey();
if (activityIds.add(activityId)) {
modifier.cancelAllForActivity(activityId);
}
});
modifier.setAnnotation("Thu hồi")
.startBeforeActivity(currentTask.getTaskDefinitionKey())
.execute();
return new HashMap<>();
}
// Restart process
public Map<String, Object> restartProcess(String initiator, String processDefinitionId,
String processInstanceId, String businessKey,
Map<String, Object> variables) {
runtimeService.setVariables(processInstanceId, variables);
// Skip initiator task
List<Task> initiatorTasks = taskService.createTaskQuery()
.taskAssignee(initiator)
.taskName("Người khởi tạo")
.processInstanceId(processInstanceId)
.list();
if (!initiatorTasks.isEmpty()) {
taskService.complete(initiatorTasks.get(0).getId());
}
return new HashMap<>();
}
}
Enumeration class
package com.oa.workflow.constant;
public enum WorkflowElementType {
PROCESS("Process_"),
EVENT("Event_"),
ACTIVITY("Activity_"),
GATEWAY("Gateway_"),
TASK("Task_");
private final String prefix;
private WorkflowElementType(String prefix) {
this.prefix = prefix;
}
public String getPrefix() {
return prefix;
}
}
Listener Implementations
Copy Task Listener
package com.oa.workflow.listener;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.delegate.DelegateTask;
import org.camunda.bpm.engine.delegate.TaskListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component("CopyTaskListener")
public class CopyTaskListener implements TaskListener {
@Autowired private TaskService taskService;
@Override
public void notify(DelegateTask delegateTask) {
String eventName = delegateTask.getEventName();
String taskId = delegateTask.getId();
String processInstanceId = delegateTask.getProcessInstanceId();
String taskDefinitionKey = delegateTask.getTaskDefinitionKey();
List<String> userList = (List<String>) delegateTask.getVariable(
"assigneeList_" + taskDefinitionKey);
if ("create".equals(eventName)) {
// Xử lý nghiệp vụ copy
// Gửi notification cho người được copy
if (userList != null && !userList.isEmpty()) {
// Lưu vào bảng待阅
// Gửi tin nhắn
}
// Complete task tự động
taskService.complete(delegateTask.getId());
} else if ("complete".equals(eventName)) {
// Hoàn thành
}
}
}
Process End Listener
package com.oa.workflow.listener;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
@Component("ProcessEndListener")
public class ProcessEndListener implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) throws Exception {
String eventName = execution.getEventName();
String businessKey = execution.getProcessBusinessKey();
String processInstanceId = execution.getProcessInstanceId();
if ("start".equals(eventName)) {
// Process started
String initiator = (String) execution.getVariable("initiator");
// Gửi notification cho người khởi tạo
} else if ("end".equals(eventName)) {
// Process completed
// Cập nhật trạng thái hoàn thành
}
}
}
Task Notification Listener
package com.oa.workflow.listener;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.delegate.DelegateTask;
import org.camunda.bpm.engine.delegate.TaskListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("TaskNotificationListener")
public class TaskNotificationListener implements TaskListener {
@Autowired private TaskService taskService;
@Override
public void notify(DelegateTask delegateTask) {
String eventName = delegateTask.getEventName();
String taskId = delegateTask.getId();
String taskDefinitionKey = delegateTask.getTaskDefinitionKey();
String processInstanceId = delegateTask.getProcessInstanceId();
if (TaskListener.EVENTNAME_CREATE.equals(eventName)) {
// Cập nhật trạng thái
// Gửi notification cho người phê duyệt
} else if ("complete".equals(eventName)) {
// Cập nhật trạng thái hoàn thành
}
}
}
Cấu trúc Database (49 bảng)
Camunda sử dụng 49 bảng trong database để lưu trữ thông tin workflow. Dưới đây là cấu trúc chính:
General Data (act_ge_*)
- act_ge_bytearray - Lưu trữ dữ liệu nhị phân (BPMN XML, form data)
- act_ge_property - Cấu hình engine (sequence, schema)
- act_ge_schema_log - Log thực thi database script
History (act_hi_*)
- act_hi_procinst - Lịch sử process instances
- act_hi_taskinst - Lịch sử tasks
- act_hi_actinst - Lịch sử các activity
- act_hi_varinst - Lịch sử biến
- act_hi_detail - Chi tiết biến
- act_hi_comment - Lịch sử comments
- act_hi_identitylink - Quan hệ user/group
Identity (act_id_*)
- act_id_user - Thông tin user
- act_id_group - Thông tin group
- act_id_membership - Quan hệ user-group
Repository (act_re_*)
- act_re_procdef - Định nghĩa process
- act_re_deployment - Thông tin deployment
Runtime (act_ru_*)
- act_ru_execution - Process đang chạy
- act_ru_task - Task đang chờ
- act_ru_variable - Biến runtime
- act_ru_identitylink - Quan hệ runtime
Kết luận
Bài viết đã trình bày chi tiết cách triển khai Camunda BPM trong hệ thống OA, bao gồm:
- Cấu hình và tích hợp với Spring Boot
- Xây dựng BPMN model từ backend
- Các nghiệp vụ workflow cơ bản
- Listener xử lý sự kiện
- Cấu trúc database của Camunda
Với những utility classes và cách tổ chức code như trên, việc triển khai một hệ thống workflow hoàn chỉnh trở nên dễ dàng và có thể mở rộng theo nhu cầu business.