本文目录导读:

在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 | 小型项目快速集成 |
| 自研引擎 | 完全定制 | 复杂业务、多分支、动态审批人 |
实现用户自定义审批优化的核心:
- 数据驱动:把流程结构存入数据库。
- 引擎解耦:写一个纯逻辑的审批引擎,不依赖任何业务数据。
- 表达式安全解析:用表达式库处理条件分支。
- 缓存+队列:保证高并发下性能稳定。
如果你能提供更具体的业务需求(如审批节点数上限、是否需要会签/或签、是否需要并行审批等),我可以进一步细化方案。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播