ERP里的审批流、业务流程自动化,都离不开工作流引擎。
很多ERP系统自带工作流,但灵活性有限。如果要做定制化审批、多级审批、条件分支、并行审批,就需要一个可配置的工作流引擎。
一、工作流引擎的核心概念
- 流程定义(Process Definition)
描述一个完整的业务流程。比如"采购审批流程":提交→部门经理审批→财务审批→总经理审批→执行。
- 流程实例(Process Instance)
流程定义的一次运行。一个采购单走一次审批,就是一个实例。
- 任务(Task)
流程中的每个步骤。当前需要人处理的待办事项。
- 流转(Transition)
任务之间的连线。定义从A节点到B节点的条件。
二、流程定义模型
- 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"}
]
}
- 节点类型
public enum NodeType
{
Start, // 开始节点
End, // 结束节点
UserTask, // 用户任务(需要人处理)
ServiceTask, // 系统任务(自动执行)
ExclusiveGW, // 排他网关(条件分支)
ParallelGW, // 并行网关(同时走多条线)
SubProcess // 子流程
}
ERP里最常用的是UserTask和ExclusiveGW。ServiceTask用于自动触发业务逻辑,比如审批通过后自动生成凭证。
三、流程实例运行时
- 实例表
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)
);
- 任务表
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)
);
- 历史记录表
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)
);
四、引擎核心逻辑
- 流程启动
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;
}
}
- 任务完成
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);
}
}
- 条件求值
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
};
}
- 处理人解析
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}")
};
}
五、高级特性
- 并行审批
多个部门同时审批,全部通过后才进入下一步。
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);
}
}
}
- 加签和转办
实际业务中,审批人经常需要加签(拉其他人一起审)或转办(让别人代审)。
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);
}
}
- 催办
超时未处理的任务自动催办。
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的基础设施。审批流、业务流程自动化、流程监控都依赖它。设计得好,业务灵活度会大大提升。
------云策数链