Hướng dẫn triển khai Camunda BPM trong hệ thống OA

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.

Thẻ: camunda bpmn workflow Java spring-boot

Đăng vào ngày 17 tháng 05 lúc 02:06