ERP工作流引擎设计与实现

ERP里的审批流、业务流程自动化,都离不开工作流引擎。

很多ERP系统自带工作流,但灵活性有限。如果要做定制化审批、多级审批、条件分支、并行审批,就需要一个可配置的工作流引擎。

一、工作流引擎的核心概念

  1. 流程定义(Process Definition)

描述一个完整的业务流程。比如"采购审批流程":提交→部门经理审批→财务审批→总经理审批→执行。

  1. 流程实例(Process Instance)

流程定义的一次运行。一个采购单走一次审批,就是一个实例。

  1. 任务(Task)

流程中的每个步骤。当前需要人处理的待办事项。

  1. 流转(Transition)

任务之间的连线。定义从A节点到B节点的条件。

二、流程定义模型

  1. BPMN简化的流程模型

不需要完整的BPMN,ERP里常用的节点类型就几种:

复制代码
CREATE TABLE wf_process_def (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    process_code VARCHAR(30) NOT NULL UNIQUE,
    process_name VARCHAR(100) NOT NULL,
    
    -- 绑定的业务模块
    module_code VARCHAR(30),          -- PURCHASE/APPROVAL/EXPENSE...
    business_type VARCHAR(30),        -- PURCHASE_ORDER/EXPENSE_CLAIM...
    
    -- 版本控制
    version INT NOT NULL DEFAULT 1,
    is_active TINYINT DEFAULT 1,
    
    -- 流程描述(JSON)
    process_json JSON NOT NULL,
    
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_module (module_code, business_type)
);

process_json存储流程结构:

复制代码
{
  "nodes": [
    {
      "id": "start",
      "type": "start",
      "name": "开始"
    },
    {
      "id": "dept_manager",
      "type": "user_task",
      "name": "部门经理审批",
      "assignee": {
        "type": "role",
        "value": "DEPT_MANAGER"
      },
      "formKey": "approval_form"
    },
    {
      "id": "finance",
      "type": "user_task",
      "name": "财务审批",
      "assignee": {
        "type": "role",
        "value": "FINANCE_MANAGER"
      },
      "condition": {
        "field": "amount",
        "operator": ">",
        "value": 10000
      }
    },
    {
      "id": "gm",
      "type": "user_task",
      "name": "总经理审批",
      "assignee": {
        "type": "user",
        "value": "${initiator.dept_gm_id}"
      },
      "condition": {
        "field": "amount",
        "operator": ">",
        "value": 50000
      }
    },
    {
      "id": "end",
      "type": "end",
      "name": "结束"
    }
  ],
  "edges": [
    {"from": "start", "to": "dept_manager"},
    {"from": "dept_manager", "to": "finance"},
    {"from": "finance", "to": "gm"},
    {"from": "finance", "to": "end"},
    {"from": "gm", "to": "end"}
  ]
}
  1. 节点类型
复制代码
public enum NodeType
{
    Start,       // 开始节点
    End,         // 结束节点
    UserTask,    // 用户任务(需要人处理)
    ServiceTask, // 系统任务(自动执行)
    ExclusiveGW, // 排他网关(条件分支)
    ParallelGW,  // 并行网关(同时走多条线)
    SubProcess   // 子流程
}

ERP里最常用的是UserTask和ExclusiveGW。ServiceTask用于自动触发业务逻辑,比如审批通过后自动生成凭证。

三、流程实例运行时

  1. 实例表
复制代码
CREATE TABLE wf_process_instance (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    
    -- 关联定义
    process_def_id BIGINT NOT NULL,
    process_code VARCHAR(30) NOT NULL,
    version INT NOT NULL,
    
    -- 关联业务
    business_id BIGINT NOT NULL,
    business_no VARCHAR(30),
    
    -- 发起人
    initiator_id BIGINT NOT NULL,
    initiator_name VARCHAR(50),
    
    -- 状态
    status VARCHAR(20) NOT NULL DEFAULT 'RUNNING',
    -- RUNNING / COMPLETED / REJECTED / CANCELLED / SUSPENDED
    
    -- 当前节点
    current_node_id VARCHAR(30),
    
    -- 时间
    start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    end_time DATETIME,
    
    INDEX idx_business (process_code, business_id),
    INDEX idx_status (status),
    INDEX idx_initiator (initiator_id)
);
  1. 任务表
复制代码
CREATE TABLE wf_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    
    -- 关联实例
    instance_id BIGINT NOT NULL,
    node_id VARCHAR(30) NOT NULL,
    node_name VARCHAR(100),
    
    -- 处理人
    assignee_id BIGINT,
    assignee_name VARCHAR(50),
    
    -- 状态
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    -- PENDING / COMPLETED / REJECTED / DELEGATED
    
    -- 动作
    action VARCHAR(20),
    opinion VARCHAR(500),
    
    -- 时间
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    claim_time DATETIME,              -- 认领时间
    complete_time DATETIME,           -- 完成时间
    
    -- 催办
    reminder_count INT DEFAULT 0,
    last_reminder_time DATETIME,
    
    -- 截止时间
    due_time DATETIME,
    
    INDEX idx_instance (instance_id),
    INDEX idx_assignee (assignee_id, status),
    INDEX idx_status (status)
);
  1. 历史记录表
复制代码
CREATE TABLE wf_task_history (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    instance_id BIGINT NOT NULL,
    task_id BIGINT,
    node_id VARCHAR(30),
    node_name VARCHAR(100),
    
    assignee_id BIGINT,
    assignee_name VARCHAR(50),
    
    action VARCHAR(20),
    opinion VARCHAR(500),
    
    action_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    duration_minutes INT,
    
    -- 流转数据快照
    variables_snapshot JSON,
    
    INDEX idx_instance (instance_id),
    INDEX idx_time (action_time)
);

四、引擎核心逻辑

  1. 流程启动
复制代码
public class WorkflowEngine
{
    public ProcessInstance StartProcess(
        string processCode, long businessId, long initiatorId)
    {
        // 1. 加载流程定义
        var def = _processDefRepo.GetActive(processCode);
        
        // 2. 创建实例
        var instance = new ProcessInstance
        {
            ProcessDefId = def.Id,
            ProcessCode = processCode,
            Version = def.Version,
            BusinessId = businessId,
            InitiatorId = initiatorId,
            Status = "RUNNING",
            CurrentNodeId = "start"
        };
        _instanceRepo.Insert(instance);
        
        // 3. 加载流程变量
        var variables = LoadBusinessVariables(businessId);
        
        // 4. 从开始节点出发,找下一个节点
        var startNode = def.GetNode("start");
        var nextNodes = def.GetNextNodes(startNode);
        
        // 5. 创建任务
        foreach (var node in nextNodes)
        {
            if (node.Type == NodeType.UserTask)
            {
                // 检查条件
                if (node.Condition != null && !EvaluateCondition(node.Condition, variables))
                    continue;
                
                var assignee = ResolveAssignee(node.Assignee, initiatorId, variables);
                CreateTask(instance.Id, node, assignee);
            }
            else if (node.Type == NodeType.ServiceTask)
            {
                // 自动执行
                ExecuteServiceTask(node, variables);
            }
        }
        
        return instance;
    }
}
  1. 任务完成
复制代码
public void CompleteTask(long taskId, string action, string opinion, long userId)
{
    var task = _taskRepo.GetById(taskId);
    
    if (task.Status != "PENDING")
        throw new Exception("任务已完成或已处理");
    
    if (task.AssigneeId != userId)
        throw new Exception("无权处理此任务");
    
    var instance = _instanceRepo.GetById(task.InstanceId);
    var def = _processDefRepo.GetById(instance.ProcessDefId);
    
    // 记录历史
    _historyRepo.Insert(new TaskHistory
    {
        InstanceId = instance.Id,
        TaskId = taskId,
        NodeId = task.NodeId,
        NodeName = task.NodeName,
        AssigneeId = userId,
        Action = action,
        Opinion = opinion,
        ActionTime = DateTime.Now,
        DurationMinutes = (int)(DateTime.Now - task.CreateTime).TotalMinutes
    });
    
    // 更新任务状态
    task.Status = action == "APPROVE" ? "COMPLETED" : "REJECTED";
    task.Action = action;
    task.Opinion = opinion;
    task.CompleteTime = DateTime.Now;
    _taskRepo.Update(task);
    
    if (action == "REJECT")
    {
        // 驳回:流程结束
        instance.Status = "REJECTED";
        instance.EndTime = DateTime.Now;
        _instanceRepo.Update(instance);
        
        // 触发驳回回调
        OnProcessRejected(instance);
        return;
    }
    
    // 查找下一个节点
    var currentNode = def.GetNode(task.NodeId);
    var nextNodes = def.GetNextNodes(currentNode);
    var variables = LoadBusinessVariables(instance.BusinessId);
    
    var hasNext = false;
    foreach (var node in nextNodes)
    {
        if (node.Type == NodeType.End)
        {
            // 流程结束
            instance.Status = "COMPLETED";
            instance.EndTime = DateTime.Now;
            _instanceRepo.Update(instance);
            OnProcessCompleted(instance);
            continue;
        }
        
        if (node.Type == NodeType.UserTask)
        {
            if (node.Condition != null && !EvaluateCondition(node.Condition, variables))
                continue;
            
            var assignee = ResolveAssignee(node.Assignee, instance.InitiatorId, variables);
            CreateTask(instance.Id, node, assignee);
            hasNext = true;
        }
        else if (node.Type == NodeType.ServiceTask)
        {
            ExecuteServiceTask(node, variables);
            hasNext = true;
        }
    }
    
    if (!hasNext && instance.Status == "RUNNING")
    {
        instance.Status = "COMPLETED";
        instance.EndTime = DateTime.Now;
        _instanceRepo.Update(instance);
    }
}
  1. 条件求值
复制代码
public bool EvaluateCondition(Condition condition, Dictionary<string, object> variables)
{
    var fieldValue = variables.TryGetValue(condition.Field, out var val) ? val : null;
    
    return condition.Operator switch
    {
        ">" => Convert.ToDecimal(fieldValue) > Convert.ToDecimal(condition.Value),
        ">=" => Convert.ToDecimal(fieldValue) >= Convert.ToDecimal(condition.Value),
        "<" => Convert.ToDecimal(fieldValue) < Convert.ToDecimal(condition.Value),
        "<=" => Convert.ToDecimal(fieldValue) <= Convert.ToDecimal(condition.Value),
        "==" => fieldValue?.ToString() == condition.Value.ToString(),
        "!=" => fieldValue?.ToString() != condition.Value.ToString(),
        "in" => condition.Values.Contains(fieldValue?.ToString()),
        _ => true
    };
}
  1. 处理人解析
复制代码
public List<long> ResolveAssignee(Assignee assignee, long initiatorId, Dictionary<string, object> variables)
{
    return assignee.Type switch
    {
        "user" => new List<long> { ResolveUser(assignee.Value, initiatorId, variables) },
        "role" => _userRepo.GetUsersByRole(assignee.Value),
        "department_head" => _userRepo.GetDeptHead(initiatorId),
        "expression" => EvaluateExpression(assignee.Value, variables),
        _ => throw new Exception($"不支持的审批人类型: {assignee.Type}")
    };
}

五、高级特性

  1. 并行审批

多个部门同时审批,全部通过后才进入下一步。

复制代码
public void HandleParallelGateway(long instanceId, string gatewayId)
{
    var def = _processDefRepo.GetByInstanceId(instanceId);
    var instance = _instanceRepo.GetById(instanceId);
    
    // 查询该网关下所有任务是否完成
    var pendingTasks = _taskRepo.GetByInstanceAndNodePrefix(instanceId, gatewayId, "PENDING");
    
    if (pendingTasks.Count > 0)
        return; // 还有任务没完成,等待
    
    // 全部完成,走下一步
    var gateway = def.GetNode(gatewayId);
    var nextNodes = def.GetNextNodes(gateway);
    
    foreach (var node in nextNodes)
    {
        if (node.Type == NodeType.UserTask)
        {
            var variables = LoadBusinessVariables(instance.BusinessId);
            var assignee = ResolveAssignee(node.Assignee, instance.InitiatorId, variables);
            CreateTask(instance.Id, node, assignee);
        }
    }
}
  1. 加签和转办

实际业务中,审批人经常需要加签(拉其他人一起审)或转办(让别人代审)。

复制代码
public void DelegateTask(long taskId, long toUserId, string reason)
{
    var task = _taskRepo.GetById(taskId);
    
    // 记录转办历史
    _historyRepo.Insert(new TaskHistory
    {
        InstanceId = task.InstanceId,
        NodeId = task.NodeId,
        AssigneeId = task.AssigneeId,
        Action = "DELEGATE",
        Opinion = $"转办给 {toUserId},原因:{reason}"
    });
    
    // 更新任务处理人
    task.AssigneeId = toUserId;
    _taskRepo.Update(task);
}

public void AddSign(long taskId, long addUserId, AddSignType type)
{
    // type: BEFORE(前加签)或 AFTER(后加签)
    
    if (type == AddSignType.Before)
    {
        // 创建一个新任务,新任务完成后再回到当前任务
        var task = _taskRepo.GetById(taskId);
        task.Status = "SUSPENDED";
        _taskRepo.Update(task);
        
        CreateTask(task.InstanceId, task.NodeId, addUserId, "加签", taskId);
    }
    else
    {
        // 当前任务完成后,再创建一个新任务给加签人
        var task = _taskRepo.GetById(taskId);
        task.AfterSignUserId = addUserId;
        _taskRepo.Update(task);
    }
}
  1. 催办

超时未处理的任务自动催办。

复制代码
public void CheckOverdueTasks()
{
    // 查找超时未处理的任务
    var overdueTasks = _taskRepo.GetOverdue();
    
    foreach (var task in overdueTasks)
    {
        // 发送催办通知
        _notificationService.Send(new Notification
        {
            UserId = task.AssigneeId,
            Type = "TASK_OVERDUE",
            Title = $"待办超时提醒:{task.NodeName}",
            Content = $"您有一个待办任务已超时{GetOverdueDays(task)}天,请尽快处理。",
            Link = $"/workflow/task/{task.Id}"
        });
        
        // 更新催办计数
        task.ReminderCount++;
        task.LastReminderTime = DateTime.Now;
        _taskRepo.Update(task);
        
        // 超过3次催办,通知上级
        if (task.ReminderCount >= 3)
        {
            var managerId = _userRepo.GetManager(task.AssigneeId);
            _notificationService.Send(new Notification
            {
                UserId = managerId,
                Type = "TASK_ESCALATION",
                Title = $"任务升级通知:{task.NodeName}",
                Content = $"{task.AssigneeName}的待办任务已超时多次未处理。"
            });
        }
    }
}

六、流程变量

流程中需要传递数据,比如金额、部门、申请人信息。

复制代码
CREATE TABLE wf_variable (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    instance_id BIGINT NOT NULL,
    variable_name VARCHAR(50) NOT NULL,
    variable_value TEXT,
    variable_type VARCHAR(20),         -- STRING/NUMBER/BOOLEAN/JSON
    scope VARCHAR(20) DEFAULT 'INSTANCE', -- INSTANCE/TASK
    
    INDEX idx_instance (instance_id),
    UNIQUE KEY uk_instance_name (instance_id, variable_name, scope)
);

业务数据不需要全部存到流程变量里,只需要存流程判断用到的关键值。

比如金额,用于判断走哪条审批线。部门,用于找审批人。

七、流程监控

复制代码
-- 流程运行统计
SELECT 
    process_name,
    COUNT(*) AS total_count,
    SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS completed,
    SUM(CASE WHEN status = 'REJECTED' THEN 1 ELSE 0 END) AS rejected,
    SUM(CASE WHEN status = 'RUNNING' THEN 1 ELSE 0 END) AS running,
    ROUND(AVG(TIMESTAMPDIFF(HOUR, start_time, COALESCE(end_time, NOW()))), 1) AS avg_hours
FROM wf_process_instance
WHERE start_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY process_name;

-- 审批效率分析
SELECT 
    node_name,
    COUNT(*) AS task_count,
    ROUND(AVG(duration_minutes), 1) AS avg_minutes,
    ROUND(MAX(duration_minutes / 60), 1) AS max_hours,
    SUM(CASE WHEN duration_minutes > 2880 THEN 1 ELSE 0 END) AS over_2days_count
FROM wf_task_history
WHERE action_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY node_id, node_name
ORDER BY avg_minutes DESC;

工作流引擎是ERP的基础设施。审批流、业务流程自动化、流程监控都依赖它。设计得好,业务灵活度会大大提升。

------云策数链

相关推荐
赤龙ERP4 天前
制造企业数字化转型:赤龙ERP完整解决方案(2026版)
制造·erp
Cilsoft 秦汉信息科技4 天前
Microsoft Dynamics 365 Finance Operations 企业级财务与运营管理平台
microsoft·erp·dynamics 365·财务管理·企业管理软件·dynamics 365 fo·microsoftd365fo
元拓数智5 天前
AI 自动化工作流,正在重塑企业数据工程的效率边界
大数据·人工智能·ai·自动化·工作流·数据工程
切糕师学AI6 天前
一推即发:基于 Git 与 Markdown 的多平台自动发布流水线
自动化·工作流
熊文豪9 天前
打造智能写作工作流:n8n + 蓝耘MaaS平台完整实战指南
ai写作·工作流·n8n·蓝耘maas
赤龙ERP10 天前
中小企业如何用开源ERP实现数字化转型
erp
Cilsoft 秦汉信息科技11 天前
通用销售订单管理软件
vue·管理系统·erp·客户管理·销售订单
Cilsoft 秦汉信息科技11 天前
VUE服务行业ERP系统
vue·管理系统·erp·售后服务·维修管理
Cilsoft 秦汉信息科技11 天前
VUE制造业ERP系统
vue·管理系统·erp·制造业·生产管理