PHP项目实现用户自定义审批记录:从零构建灵活工作流引擎
目录导读
- 核心需求与设计挑战
- 数据库模型设计:动态审批记录的基石
- 核心代码实现:三步构建自定义审批逻辑
- 用户交互界面:配置审批流程的友好入口
- 常见问题与性能优化
- 问答环节:开发者最关心的5个问题
- 总结与扩展建议
核心需求与设计挑战
在众多企业级PHP项目中(如OA系统、合同管理系统、报销平台),用户自定义审批记录是一项高频需求,传统硬编码审批流的方式维护成本极高——业务部门每调整一个审批节点,就需要开发人员修改代码、更新数据库。

核心挑战在于:
- 动态性:审批层级、审批人、审批条件必须可配置,并且能实时生效
- 兼容性:已发起的审批记录不受后续规则变更影响
- 可靠性:支持驳回、转审、加签等复杂操作时的状态一致性
本文将基于PHP(Laravel/Symfony框架为例),结合关系型数据库,提供一套完整的、经过生产验证的用户自定义审批记录实现方案。
数据库模型设计:动态审批记录的基石
核心数据表结构(MySQL)
-- 1. 审批流程模板表:定义不同业务类型的审批规则
CREATE TABLE `approval_flows` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL COMMENT '流程名称(如:采购审批)',
`business_type` varchar(50) DEFAULT NULL COMMENT '业务类型标识(如:purchase)',
`is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 审批节点表:每个流程下的审批步骤
CREATE TABLE `approval_nodes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`flow_id` int(11) NOT NULL COMMENT '关联流程ID',
`node_order` tinyint(3) DEFAULT 0 COMMENT '节点顺序(从1开始)',
`node_type` varchar(20) DEFAULT 'user' COMMENT '审批类型(user用户/role角色/department部门)',
`approver_id` int(11) DEFAULT NULL COMMENT '审批人ID(当node_type=user时)',
`approver_role_id` int(11) DEFAULT NULL COMMENT '角色ID(当node_type=role时)',
`condition_expression` text COMMENT '条件表达式(可选:部门ID>100)',
`is_required` tinyint(1) DEFAULT 1 COMMENT '是否必须审批',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_flow_order` (`flow_id`, `node_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 审批实例表:每次发起审批生成的记录
CREATE TABLE `approval_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`flow_id` int(11) NOT NULL COMMENT '使用的流程模板ID',
`business_id` int(11) DEFAULT NULL COMMENT '业务单据ID(如采购单ID)',
`submitter_id` int(11) DEFAULT NULL COMMENT '发起人ID',
`status` enum('pending','approved','rejected','cancelled') DEFAULT 'pending',
`current_node_order` tinyint(3) DEFAULT 0 COMMENT '当前待审批节点序号',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`finished_at` timestamp NULL DEFAULT NULL COMMENT '审批完成时间',
PRIMARY KEY (`id`),
KEY `idx_business` (`business_type`, `business_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 审批日志表:记录每个节点的审批动作
CREATE TABLE `approval_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`record_id` int(11) NOT NULL COMMENT '关联审批记录ID',
`node_id` int(11) NOT NULL COMMENT '节点ID',
`node_order` tinyint(3) DEFAULT 0 COMMENT '节点序号',
`operator_id` int(11) DEFAULT NULL COMMENT '操作人ID',
`action` enum('approve','reject','transfer') NOT NULL COMMENT '操作类型',
`remark` text COMMENT '审批意见',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计关键点:
- 使用
flow_id+node_order组合解决多版本流程问题 - 引入
condition_expression字段支持条件分支(如:金额>5000时需总经理审批) approval_records中的current_node_order字段直接存储当前节点序号,避免每步都查节点表
核心代码实现:三步构建自定义审批逻辑
第一步:提交审批时动态生成实例
// 示例代码:Laravel框架
public function submitApproval(SubmitApprovalRequest $request)
{
$flow = ApprovalFlow::with('nodes')->findOrFail($request->flow_id);
// 检查流程模板是否启用
if (!$flow->is_active) {
throw new \Exception('该审批流程已停用');
}
// 验证条件表达式(动态计算是否跳过某些节点)
$firstNode = $flow->nodes->first(function ($node) {
return $this->evaluateCondition($node->condition_expression);
});
if (!$firstNode) {
throw new \Exception('未找到符合条件的审批节点');
}
// 创建审批实例
$record = ApprovalRecord::create([
'flow_id' => $flow->id,
'business_id' => $request->business_id,
'business_type' => $flow->business_type,
'submitter_id' => Auth::id(),
'status' => 'pending',
'current_node_order' => $firstNode->node_order,
]);
// 写入首节点审批日志(待审批状态)
ApprovalLog::create([
'record_id' => $record->id,
'node_id' => $firstNode->id,
'node_order' => $firstNode->node_order,
'operator_id' => $firstNode->approver_id ?? null,
'action' => 'pending',
'remark' => '待审批'
]);
return response()->json(['message' => '提交成功', 'record_id' => $record->id]);
}
// 条件表达式解析器(简化版)
private function evaluateCondition($expression, $context = [])
{
if (empty($expression)) return true;
// 实际项目建议使用表达式解析库,如 symfony/expression-language
// 示例:简单解析 "$amount > 5000"
preg_match('/^\$([a-z_]+)\s*(>|<|>=|<=|==)\s*(\d+)$/i', $expression, $matches);
if ($matches) {
$var = $context[$matches[1]] ?? 0;
return eval("return $var $matches[2] $matches[3];");
}
return true;
}
第二步:审批操作逻辑(处理、驳回、转审)
public function processApproval(ProcessApprovalRequest $request, $recordId)
{
$record = ApprovalRecord::with('logs')->findOrFail($recordId);
$node = ApprovalNode::where('flow_id', $record->flow_id)
->where('node_order', $record->current_node_order)
->first();
// 权限检查:当前登录用户必须是指定的审批人
$allowApproverIds = $this->getActualApprovers($node);
if (!in_array(Auth::id(), $allowApproverIds)) {
throw new \Exception('您不是当前审批人');
}
// 记录操作日志
ApprovalLog::create([
'record_id' => $record->id,
'node_id' => $node->id,
'node_order' => $node->node_order,
'operator_id' => Auth::id(),
'action' => $request->action, // approve 或 reject
'remark' => $request->remark
]);
if ($request->action === 'reject') {
// 驳回:直接结束流程
$record->update(['status' => 'rejected', 'finished_at' => now()]);
// 触发业务回调:通知发起人
return response()->json(['message' => '已驳回']);
}
// 批准:查找下一节点
$nextNode = ApprovalNode::where('flow_id', $record->flow_id)
->where('node_order', '>', $record->current_node_order)
->where('is_required', true)
->orderBy('node_order')
->first();
if ($nextNode && $this->evaluateCondition($nextNode->condition_expression)) {
// 有下一节点,更新当前节点
$record->update(['current_node_order' => $nextNode->node_order]);
// 通知下一审批人
return response()->json(['message' => '已转至下一审批']);
} else {
// 所有节点完成
$record->update(['status' => 'approved', 'finished_at' => now()]);
// 触发业务状态变更
return response()->json(['message' => '审批通过']);
}
}
// 获取实际可审批人员(支持用户/角色/部门三种模式)
private function getActualApprovers($node)
{
switch ($node->node_type) {
case 'user':
return [$node->approver_id];
case 'role':
return RoleUser::where('role_id', $node->approver_role_id)->pluck('user_id')->toArray();
case 'department':
return User::where('department_id', $node->approver_id)->pluck('id')->toArray();
default:
return [];
}
}
第三步:审批记录展示与时间线
public function getApprovalTimeline($recordId)
{
$record = ApprovalRecord::with(['logs.operator:id,name,avatar', 'flow.nodes'])->findOrFail($recordId);
$timeline = [];
$flow = $record->flow;
// 按照节点顺序组织时间线
foreach ($flow->nodes as $node) {
$log = $record->logs->firstWhere('node_id', $node->id);
$timeline[] = [
'node_order' => $node->node_order,
'node_name' => $node->name,
'approver' => $log ? $log->operator->name : '待审批',
'action' => $log ? $log->action : 'pending',
'remark' => $log ? $log->remark : '',
'time' => $log ? $log->created_at->format('Y-m-d H:i:s') : null,
];
}
return response()->json(['timeline' => $timeline]);
}
用户交互界面:配置审批流程的友好入口
前端配置界面建议(基于Vue.js)
// 流程配置组件核心数据结构
const flowConfig = {
name: '采购审批',
business_type: 'purchase',
nodes: [
{
id: 1,
node_order: 1,
node_type: 'user',
approver_id: 1001, // 用户选择器
condition_expression: '', // 可选条件
is_required: true
},
{
id: 2,
node_order: 2,
node_type: 'role',
approver_role_id: 5, // 角色选择器
condition_expression: '$amount > 5000', // 仅金额>5000时触发
is_required: false
}
]
};
// 拖拽排序(使用 vuedraggable)
// 条件编辑器(支持变量提示:$amount, $department_id等)
关键交互细节:
- 节点类型提供
用户选择、角色选择、部门选择三种模式 - 可设置
跳过条件(当条件不满足时自动跳过该节点) - 提供
测试流程按钮,模拟提交流程验证节点逻辑
常见问题与性能优化
Q1:企业微信/钉钉集成时,审批人选择如何动态同步?
A:建议在管理员配置审批节点时,通过API同步通讯录并缓存到本地users表,在getActualApprovers()方法中,对于node_type=role的场景,实时查询角色-人员关联表。
Q2:已发起的审批记录还能修改流程吗?
A:不能,已发起的审批记录会clone当时流程模板的快照(可在approval_record表中增加snapshot字段存储JSON),修改流程模板不影响已有记录。
Q3:如何避免高并发下的审批状态错误?
A:使用数据库行级锁(SELECT ... FOR UPDATE)或者Redis分布式锁,审批操作前锁定approval_record行。
优化建议:
- 数据库层面:对
approval_logs按月分表 - 缓存层面:常用流程模板缓存到Redis(
flow:template:{flow_id}) - 异步通知:使用消息队列(如RabbitMQ)处理审批通知
问答环节:开发者最关心的5个问题
问题1:审批节点支持多同意人(会签)吗?
答:支持,可以在approval_nodes表中增加approval_mode字段(any(或签)/ all(会签)),当all时,在approval_logs中统计当前节点通过人数,直到所有指定审批人同意后才进入下一节点,关键逻辑在processApproval方法中增加计数判断。
问题2:用户自定义审批条件表达式的安全性怎么保证?
答:禁止直接使用eval(),生产中推荐使用symfony/expression-language(支持沙箱模式)或编写自己的AST解析器,在管理后台对表达式做白名单校验,只允许访问预定义的变量(如$amount、$department)。
问题3:如何在审批记录中展示驳回后重新提交的版本控制?
答:在approval_records表中增加version字段,驳回后生成新的version(+1),新version的current_node_order重置为1,前端展示时按版本分组显示时间线。
问题4:如果审批人离职,如何处理卡在ta节点上的审批?
答:管理后台提供节点转交功能,使用转交(transfer)操作,在approval_logs记录原审批人,并修改当前节点approver_id为新的审批人,建议配合定时任务,定期扫描超过7天未处理的审批,发送提醒。
问题5:支持子父级审批流程嵌套吗?
答:可以,设计approval_nodes的parent_node_id自引用字段,形成树形结构,父节点审批通过后,触发子节点审批,这种场景多用于二级审批(如金额超过10万需集团副总裁审批)。
总结与扩展建议
本文从数据库设计、代码实现、交互界面到性能优化,系统性地讲解了在PHP项目中如何实现用户自定义审批记录,核心思路是:将审批流程抽象为“流程模板”的节点序列,通过动态解析条件表达式控制节点跳转,用审批日志记录完整流转历史。
对于更高要求的场景,推荐扩展方向:
- 引入并行审批节点(多人同时审批)
- 增加超时自动审批机制
- 结合消息队列实现异步通知
- 提供RESTful API给第三方系统调用
你的下一个项目是否考虑将这个模型集成到现有的OA或ERP系统中?通过灵活的自定义审批引擎,可以为企业节省大量的重复开发成本。