本文目录导读:

这是一个很有价值的问题,用PHP实现用户自定义审批自动化,核心在于将审批流程的定义(元数据)与执行逻辑分离,用户可以动态配置流程,而PHP引擎负责解析和执行。
下面我将提供一个分层、可扩展的设计方案,包含核心原理、数据表设计和关键代码逻辑。
核心设计思想:配置驱动 + 状态机
- 配置驱动:审批流程不是写死在代码里的,而是存储在数据库或配置文件中。
- 状态机:每个审批节点(步骤)都是一个状态,用户的操作(同意/驳回)触发状态的流转。
数据库设计(核心三张表)
用户自定义审批需要动态的、非硬编码的流程结构,通常使用 单表不循环(Adjacency List) 或 嵌套集 来存储流程节点。
approval_flows (审批流程定义表)
存储用户创建的每一个不同的审批流程模板。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
INT (PK) | 主键 |
name |
VARCHAR | 流程名称,如“报销审批V2” |
description |
TEXT | 流程描述 |
created_by |
INT | 创建者用户ID |
status |
TINYINT | 1=启用, 0=停用 |
created_at |
DATETIME |
flow_nodes (流程节点表)
定义流程中的每一步。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
INT (PK) | 主键 |
flow_id |
INT (FK) | 关联 approval_flows.id |
node_name |
VARCHAR | 节点名称,如“部门主管审批” |
node_type |
TINYINT | 1=单人审批, 2=会签(所有审批人同意), 3=或签(任一审批人同意) |
approver_type |
VARCHAR | 审批人类型:user(指定用户), role(指定角色), manager(申请人的上级) |
approver_value |
VARCHAR | 审批人值:用户ID、角色ID、或 manager |
order_index |
INT | 节点排序,数字越小越靠前 |
created_at |
DATETIME |
approval_tickets (审批工单表)
记录一个具体的审批请求实例。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
INT (PK) | 主键 |
flow_id |
INT (FK) | 使用的流程模板ID |
applicant_id |
INT | 申请人ID |
content |
TEXT | (JSON格式存储表单数据) |
current_node_id |
INT | 当前待审批的节点ID (空表示流程结束) |
status |
TINYINT | 0=待审批, 1=审批中, 2=已通过, 3=已驳回, 4=已撤销 |
created_at |
DATETIME |
approval_logs (审批日志表)
记录每一步的操作历史。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
INT (PK) | 主键 |
ticket_id |
INT (FK) | 关联 approval_tickets.id |
node_id |
INT | 操作的节点ID |
operator_id |
INT | 操作人ID |
action |
TINYINT | 1=同意, 2=驳回 |
comment |
TEXT | 审批意见 |
created_at |
DATETIME |
PHP核心逻辑实现
假设我们使用Laravel或ThinkPHP等主流框架,关键服务类如下:
创建审批工单(发起审批)
// ApprovalService.php
public function submitTicket($flowId, $applicantId, $title, $content) {
// 1. 获取流程定义
$flow = ApprovalFlow::findOrFail($flowId);
// 2. 获取第一个节点
$firstNode = FlowNode::where('flow_id', $flowId)
->orderBy('order_index', 'asc')
->first();
if (!$firstNode) {
throw new \Exception("流程未定义任何节点");
}
// 3. 创建工单,状态为“待审批”,current_node_id设为第一个节点
$ticket = ApprovalTicket::create([
'flow_id' => $flowId,
'applicant_id' => $applicantId,
'title' => $title,
'content' => json_encode($content),
'status' => 0, // 待审批
'current_node_id' => $firstNode->id,
]);
// 4. 创建日志
ApprovalLog::create([
'ticket_id' => $ticket->id,
'node_id' => null,
'operator_id' => $applicantId,
'action' => 0, // 提交
'comment' => '提交申请',
]);
// 5. 发送通知(通知第一个节点的审批人)
$this->notifyApprovers($ticket, $firstNode);
return $ticket;
}
核心:审批操作(同意/驳回)
这是最关键的方法,实现了流程的流转。
public function processApproval($ticketId, $currentUserId, $action, $comment = '') {
// 1. 获取工单和当前节点
$ticket = ApprovalTicket::findOrFail($ticketId);
$currentNode = FlowNode::findOrFail($ticket->current_node_id);
// 2. 验证权限:当前用户是否是该节点的审批人?
if (!$this->isApprover($currentUserId, $currentNode, $ticket)) {
throw new \Exception("您不是当前节点的审批人");
}
// 3. 记录日志
ApprovalLog::create([
'ticket_id' => $ticket->id,
'node_id' => $currentNode->id,
'operator_id' => $currentUserId,
'action' => $action, // 1=同意, 2=驳回
'comment' => $comment,
]);
// 4. 根据操作类型处理
if ($action == 2) { // 驳回
// 状态改为已驳回
$ticket->status = 3;
$ticket->current_node_id = null;
$ticket->save();
// 通知申请人
$this->notifyApplicant($ticket);
return ['status' => 'rejected'];
}
// 5. 同意:逻辑处理
// 5.1 如果是“会签”节点,检查是否所有审批人都已批准
if ($currentNode->node_type == 2) { // 会签
// 获取该节点的所有审批人
$allApprovers = $this->getNodeApprovers($currentNode, $ticket);
// 获取已经同意的审批人
$approvedUsers = ApprovalLog::where('ticket_id', $ticket->id)
->where('node_id', $currentNode->id)
->where('action', 1)
->pluck('operator_id')
->toArray();
// 如果还有审批人未同意,则等待,不流转到下一节点
if (count($approvedUsers) < count($allApprovers)) {
return ['status' => 'waiting_for_other_approvers'];
}
// 否则,会签完成,继续向下流转
}
// 5.2 获取下一个节点
$nextNode = FlowNode::where('flow_id', $ticket->flow_id)
->where('order_index', '>', $currentNode->order_index)
->orderBy('order_index', 'asc')
->first();
if ($nextNode) {
// 有下一节点:更新工单状态,继续流转
$ticket->current_node_id = $nextNode->id;
$ticket->status = 1; // 审批中
$ticket->save();
// 通知下一节点的审批人
$this->notifyApprovers($ticket, $nextNode);
return ['status' => 'approved', 'next_node' => $nextNode->node_name];
} else {
// 没有下一节点:流程结束,最终通过
$ticket->status = 2; // 已通过
$ticket->current_node_id = null;
$ticket->save();
// 通知申请人流程结束
$this->notifyApplicant($ticket, 'approved');
return ['status' => 'final_approved'];
}
}
辅助方法:获取审批人
private function getNodeApprovers($node, $ticket) {
switch ($node->approver_type) {
case 'user':
// 直接指定用户列表(用逗号分隔)
return explode(',', $node->approver_value);
case 'role':
// 根据角色ID查询用户
return User::where('role_id', $node->approver_value)->pluck('id')->toArray();
case 'manager':
// 获取申请人的上级(假设 User 表有 manager_id 字段)
$applicant = User::find($ticket->applicant_id);
return [$applicant->manager_id];
default:
return [];
}
}
private function isApprover($userId, $node, $ticket) {
$approvers = $this->getNodeApprovers($node, $ticket);
return in_array($userId, $approvers);
}
更灵活的方式:基于工作流引擎
对于复杂场景(条件分支、并行网关、循环),建议引入轻量级状态机库或工作流引擎(PHP有 Symfony Workflow、ProcessMaker 的开源组件)。
使用状态机库(symfony/workflow)的优势:
- 状态转移图可视化
- 自动实现Guard(条件检查)
- 事件监听(进入节点、离开节点等)
// 使用 Symfony Workflow 定义
$definition = new Definition(
['draft', 'dept_approve', 'hr_approve', 'final_approve', 'approved', 'rejected'],
[
new Transition('submit', 'draft', 'dept_approve'),
new Transition('dept_approve', 'dept_approve', 'hr_approve', ['guard' => 'isDeptApproved']),
new Transition('hr_reject', 'dept_approve', 'rejected'),
// ...
]
);
$workflow = new Workflow($definition);
$workflow->apply($ticket, 'submit'); // 触发转换
前端配置界面设计思路
提供一个可视化拖拽或表单配置界面,让用户无需写代码就能搭建流程。
配置界面元素:
- 节点类型:审批节点、抄送节点、条件分支(满足条件走A,否则走B)。
- 审批人设置:
- 指定用户(单选/多选)
- 按角色(部门经理、HR)
- 按组织架构(申请人上级、直属主管)
- 动态公式(
{applicant.department.manager})
- 审批策略:
- 单人审批(一人同意即通过)
- 会签(所有人同意)
- 或签(任意一人同意)
- 节点属性:超时自动转交、是否允许加签、驳回方式(退回上一级/退回发起人)。
前端存储:配置完成后,将节点JSON结构保存到 approval_flows 表的 nodes_json 字段中,避免每次解析多表关联。
扩展与最佳实践
-
条件分支:
- 在
flow_nodes中添加condition_expression字段,如{amount} > 5000。 - 在
processApproval中,获取下一节点前,解析条件表达式,决定走哪条分支。
- 在
-
超时自动处理:
- 使用Cron定时任务(如Laravel的Schedule):每分钟扫描
approval_tickets,查找current_node_id不为空且超过24小时未更新的工单,执行预定义动作(自动同意/转交/驳回)。
- 使用Cron定时任务(如Laravel的Schedule):每分钟扫描
-
性能优化:
- 如果流程节点非常多(如超过20步),避免每次审批都重新查询整个流程树,可以将流程的“下个节点”信息缓存到Redis。
- 使用队列(Queue)处理通知和日志写入,避免阻塞用户操作。
-
安全性:
- 严格校验
action参数(只允许1或2)。 - 使用事务(
DB::transaction)包裹提交和审批逻辑,防止数据不一致。 - 不要直接暴露
node_id给前端,使用加密后的令牌。
- 严格校验
实现用户自定义审批自动化,PHP项目建议采用 “配置(数据库/JSON)+ 状态机引擎” 的架构,重点在于:
- 存储层:设计支持动态节点的数据表结构(
flows+nodes)。 - 逻辑层:实现
submit和process两个核心方法,关键点是解析approver_type动态获取审批人,并计算下一个节点。 - 展示层:提供一个可视化配置界面,将流程定义转化为用户可理解的节点图。
这种方案灵活、可扩展,既能处理简单的“单人顺序审批”,也能支持会签、条件分支等复杂业务场景。