本文目录导读:

在PHP项目中实现用户自定义审批流程(工作流)的优化,核心在于将审批逻辑从硬编码中抽离,转变为可配置、可动态解析的数据结构。
以下是实现和优化的完整思路,包含架构设计、数据库设计、代码实现及性能优化方案。
核心设计理念:从“代码逻辑”到“数据驱动”
- 规则配置化:不修改PHP代码,仅通过后台界面配置审批节点、条件、处理人。
- 引擎化:编写一个独立的“审批引擎”,负责解析配置、推进流程、处理分支。
- 事件驱动:使用钩子或事件系统触发审批,解耦业务代码与审批代码。
数据库表结构设计(核心)
需要几张核心表来承载自定义流程:
approval_flow (流程模版表)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | 主键 |
name |
varchar | 流程名称(如:请假审批) |
trigger_event |
varchar | 触发事件(如:leave.create) |
status |
tinyint | 启用/禁用 |
approval_node (节点配置表)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | 主键 |
flow_id |
int | 关联流程ID |
node_name |
varchar | 节点名称(如:直属上级审批) |
node_type |
enum | approver, condition, cc |
assignee_type |
enum | user, role, department_head, variable |
assignee_value |
varchar | 具体值(如用户ID、角色ID或变量名{creator.dept_leader}) |
order |
int | 排序 |
timeout_hours |
int | 超时时间(可空) |
approval_node_condition (条件分支表 - 用于node_type=condition)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | |
node_id |
int | 关联父节点(条件节点) |
condition_expression |
text | 条件表达式,如 {amount} > 5000 |
next_node_id |
int | 满足条件时跳转到哪个节点 |
default_next |
tinyint | 是否默认分支(无满足条件时) |
approval_instance (审批实例表 - 每一次申请)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | |
flow_id |
int | |
business_type |
varchar | 业务类型(如 leave) |
business_id |
int | 业务数据ID |
creator_id |
int | 发起人 |
current_node_id |
int | 当前等待的节点ID |
status |
enum | pending, approved, rejected, cancelled |
approval_record (审批记录表)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | |
instance_id |
int | |
node_id |
int | |
operator_id |
int | 操作人 |
action |
enum | approve, reject, transfer |
comment |
text | |
operated_at |
datetime |
PHP审批引擎核心实现
流程解析器
<?php
class ApprovalEngine
{
// 获取当前节点
public function getCurrentNode($instance) {
$node = ApprovalNode::find($instance->current_node_id);
return $node;
}
// 执行审批动作
public function processAction($instance, $action, $user, $comment) {
$this->addRecord($instance, $instance->current_node_id, $user, $action, $comment);
if ($action == 'reject') {
$instance->status = 'rejected';
$instance->save();
// 触发驳回事件
Event::dispatch('approval.rejected', $instance);
return;
}
// 查找下一个节点
$nextNode = $this->resolveNextNode($instance);
if ($nextNode == null) {
// 流程结束
$instance->status = 'approved';
$instance->current_node_id = null;
$instance->save();
// 触发完成事件
Event::dispatch('approval.completed', $instance);
} else {
// 推进到下一个节点
$instance->current_node_id = $nextNode->id;
$instance->save();
// 通知下一节点审批人
$this->notifyApprovers($nextNode, $instance);
}
}
// 解析下一个节点(核心)
private function resolveNextNode($instance) {
$currentNode = ApprovalNode::find($instance->current_node_id);
$nextNodes = ApprovalNode::where('flow_id', $instance->flow_id)
->where('order', '>', $currentNode->order)
->orderBy('order')
->get();
foreach ($nextNodes as $node) {
if ($node->node_type == 'condition') {
// 检查条件分支
$matched = $this->evaluateConditions($node, $instance);
if ($matched) {
return $matched; // 返回条件跳转的节点
}
// 没有匹配则继续下一个节点
} else {
return $node; // 普通审批节点
}
}
return null; // 流程结束
}
// 条件评估
private function evaluateConditions($conditionNode, $instance) {
// 获取业务数据(如请假金额)
$businessData = $this->getBusinessData($instance->business_type, $instance->business_id);
foreach ($conditionNode->branches as $branch) {
// 使用表达式解析,建议用Symfony ExpressionLanguage
$expression = $this->parseExpression($branch->condition_expression, $businessData);
if ($expression) {
return $branch->nextNode;
}
}
// 没有匹配,如果有默认分支则走默认
foreach ($conditionNode->branches as $branch) {
if ($branch->default_next) {
return $branch->nextNode;
}
}
return null;
}
}
条件表达式解析(安全方案)
避免使用eval(),推荐使用Symfony ExpressionLanguage组件:
composer require symfony/expression-language
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
$language = new ExpressionLanguage();
$data = ['amount' => 8000, 'department' => '研发部'];
$result = $language->evaluate('amount > 5000 and department == "研发部"', $data);
// $result = true
审批人动态解析
处理assignee_type为variable的情况:
public function getApprovers($node, $instance) {
switch ($node->assignee_type) {
case 'user':
return User::whereIn('id', explode(',', $node->assignee_value))->get();
case 'role':
return User::where('role_id', $node->assignee_value)->get();
case 'department_head':
$creator = User::find($instance->creator_id);
return User::where('department_id', $creator->department_id)
->where('is_head', 1)
->get();
case 'variable':
// 从业务数据中动态获取,如 {project.manager_id}
$businessData = $this->getBusinessData($instance->business_type, $instance->business_id);
$uid = $this->resolveVariable($node->assignee_value, $businessData);
return User::find($uid);
default:
return collect();
}
}
性能优化方案
缓存策略
- 缓存流程配置:将
approval_flow和approval_node按flow_id缓存到Redis,减少数据库查询。// 获取流程配置 public function getFlowConfig($flowId) { $cacheKey = "approval_flow:{$flowId}"; return Cache::remember($cacheKey, 3600, function() use ($flowId) { return ApprovalFlow::with('nodes.branches')->find($flowId); }); }
消息队列处理
- 审批提交后,将
processAction放入队列,避免同步阻塞。 - 使用Laravel队列或RabbitMQ:
dispatch(new ProcessApprovalJob($instanceId, $action, $userId));
索引优化
- 为
approval_record表的instance_id、operator_id添加复合索引。 - 为
approval_node表的flow_id+order添加唯一索引。
懒加载与预加载
- 加载审批实例时,使用
with('records', 'records.operator')避免N+1查询。
条件表达式预编译
- 对于高频使用的流程,可以将表达式预编译(
ExpressionLanguage->compile()),减少每次运行时的解释开销。
前端配置界面思路
提供一个可视化拖拽式流程设计器:
- 节点类型面板:审核节点、条件节点、抄送节点。
- 审批人设置UI:支持选择人员、角色、部门负责人、表单变量。
- 条件编辑器:自动根据业务字段生成下拉选择,然后拼接成表达式。
- 实时保存:每次拖拽或修改后自动保存至数据库(通过API)。
安全与最佳实践
- 权限隔离:不要让用户自由写PHP表达式,使用
ExpressionLanguage的allowed functions白名单限制。 - SQL注入防护:表达式中的值应从业务数据中安全提取,不要直接拼接。
- 超时与死锁:审批流程出现循环(A->B->A)时,要在引擎中设置最大深度检测。
- 审计日志:所有配置修改、审批操作记录到日志表。
- 并发控制:审批操作使用乐观锁(
version字段)防止重复提交。
可选的第三方库(PHP生态)
| 库 | 特点 | 适用场景 |
|---|---|---|
| symfony/workflow | Symfony官方工作流组件 | 有状态机,适合简单固定流程 |
| petru/workflow | 轻量级,支持DSL | 小型项目快速集成 |
| 自研引擎 | 完全定制 | 复杂业务、多分支、动态审批人 |
实现用户自定义审批优化的核心:
- 数据驱动:把流程结构存入数据库。
- 引擎解耦:写一个纯逻辑的审批引擎,不依赖任何业务数据。
- 表达式安全解析:用表达式库处理条件分支。
- 缓存+队列:保证高并发下性能稳定。
如果你能提供更具体的业务需求(如审批节点数上限、是否需要会签/或签、是否需要并行审批等),我可以进一步细化方案。