本文目录导读:

在 PHP 中实现流程审批(工作流)没有单一的“标准答案”,因为需求差异很大,但核心架构和思路是相通的。
今天的最佳实践是:不要在数据库里硬编码状态机,而是用“节点 + 流转记录”的通用表结构。
以下是详细的实现方案,从数据库设计到代码逻辑,再到进阶优化。
第一阶段:数据库设计(核心)
不需要复杂的 BPMN 引擎,用这三张表就能覆盖 90% 的业务场景。
审批模板表 (approval_template)
定义这条流程有几个环节,以及每个环节是谁来审批。
CREATE TABLE `approval_template` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '流程名称,如:请假审批', `node_order` int(11) NOT NULL COMMENT '节点顺序,1,2,3...', `node_name` varchar(50) NOT NULL COMMENT '节点名称,如:部门经理审批', `approver_type` varchar(20) NOT NULL COMMENT '审批人类型:ROLE(角色) / USER(指定人) / DEPT_LEADER(直属上级)', `approver_value` varchar(255) DEFAULT NULL COMMENT '对应ID,如角色ID或用户ID', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
示例数据: | id | name | node_order | node_name | approver_type | approver_value | |----|------|------------|-----------|---------------|----------------| | 1 | 请假审批 | 1 | 直属领导审批 | DEPT_LEADER | null | | 2 | 请假审批 | 2 | 人事经理审批 | ROLE | 1 | | 3 | 请假审批 | 3 | 总经理审批 | USER | 5 |
审批单据表 (每发起一条流程,就插入一条记录)
CREATE TABLE `approval_bill` ( `id` int(11) NOT NULL AUTO_INCREMENT, `template_name` varchar(50) DEFAULT NULL, `bill_no` varchar(32) NOT NULL COMMENT '业务单号,如请假单号', `current_node` int(11) DEFAULT '1' COMMENT '当前进行到第几个节点', `status` tinyint(4) DEFAULT '0' COMMENT '状态:0-审批中,1-通过,2-驳回,3-撤回', `submitter_id` int(11) NOT NULL COMMENT '发起人ID', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
审批记录表 (谁在什么时候做了什么操作)
CREATE TABLE `approval_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `bill_id` int(11) NOT NULL COMMENT '单据ID', `node_order` int(11) DEFAULT NULL COMMENT '操作的是第几节点', `action` varchar(10) DEFAULT NULL COMMENT '操作:AGREE(同意) / REJECT(驳回) / RETURN(退回上一步)', `comment` varchar(500) DEFAULT NULL COMMENT '审批意见', `operator_id` int(11) NOT NULL COMMENT '操作人ID', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第二阶段:核心业务逻辑(PHP代码)
发起审批(写入流程)
public function startApproval($userId, $billNo, $templateId) {
// 1. 查询模板所有节点
$nodes = $this->db->query("SELECT * FROM approval_template
WHERE name = (SELECT name FROM approval_template WHERE id = ?)
ORDER BY node_order", [$templateId]);
// 2. 创建审批单,当前节点为第一个节点
$billId = $this->db->insert('approval_bill', [
'bill_no' => $billNo,
'current_node' => 1,
'status' => 0,
'submitter_id' => $userId
]);
// 3. 初始化日志
$this->db->insert('approval_log', [
'bill_id' => $billId,
'node_order' => 1,
'action' => 'START',
'operator_id' => $userId
]);
// 4. 发送待办通知给第一个节点的审批人(详见下方“找审批人”)
return $billId;
}
寻找下一个审批人(关键)
根据模板的 approver_type 动态匹配对应的人员。
public function getNextApprover($templateNode, $bill) {
$type = $templateNode['approver_type'];
if ($type == 'ROLE') {
// 根据角色ID查用户(多对多,通常取一个)
return $this->db->query("SELECT user_id FROM role_user WHERE role_id = ?", [$templateNode['approver_value']]);
} elseif ($type == 'USER') {
// 直接指定人
return [$templateNode['approver_value']];
} elseif ($type == 'DEPT_LEADER') {
// 查询发起人的直属上级(假设用户表有 manager_id)
$submitter = $this->db->query("SELECT manager_id FROM users WHERE id = ?", [$bill['submitter_id']]);
return [$submitter['manager_id']];
}
}
执行审批操作(同意/驳回/退回)
public function doApprove($billId, $operatorId, $action, $comment) {
$bill = $this->db->query("SELECT * FROM approval_bill WHERE id = ?", [$billId]);
$currentNode = $bill['current_node'];
// 权限校验:确认该操作员是否在“当前节点”的审批人列表中(略)
// 记录日志
$this->db->insert('approval_log', [
'bill_id' => $billId,
'node_order' => $currentNode,
'action' => $action,
'comment' => $comment,
'operator_id' => $operatorId
]);
if ($action == 'REJECT') {
// 驳回:流程直接终止,状态变为 驳回
$this->db->update('approval_bill', ['status' => 2], $billId);
return '驳回完成';
}
if ($action == 'RETURN') {
// 退回上一级(如果上一级存在)
if ($currentNode > 1) {
$this->db->update('approval_bill', ['current_node' => $currentNode - 1], $billId);
// 通知退回的审批人
}
return '已退回上一步';
}
// 同意操作
// 检查是否还有下一节点
$nextNodeOrder = $currentNode + 1;
$nextNode = $this->db->query("SELECT * FROM approval_template WHERE node_order = ?", [$nextNodeOrder]);
if (empty($nextNode)) {
// 没有下一节点了,流程全部通过
$this->db->update('approval_bill', ['status' => 1], $billId);
// 通知发起人审批通过
return '审批通过';
} else {
// 有下一节点,推进流程
$this->db->update('approval_bill', ['current_node' => $nextNodeOrder], $billId);
// 通知下一个审批人
return '已流转到下一节点';
}
}
第三阶段:进阶优化与注意事项
并发控制(防止重复审批)
使用乐观锁或悲观锁防止两个管理员同时操作同一个单据。
// 使用数据库的悲观锁(事务中)
$this->db->query("SELECT * FROM approval_bill WHERE id = ? FOR UPDATE", [$billId]);
// 再执行后续更新逻辑
网关(条件分支)
金额大于5000需要总经理审批,否则只要经理审批。
可以在模板表中加一个 condition 字段(JSON格式),或者在代码中判断:
if ($bill['amount'] > 5000) {
// 跳到第3节点
$this->db->update('approval_bill', ['current_node' => 3], $billId);
} else {
// 跳到第2节点
$this->db->update('approval_bill', ['current_node' => 2], $billId);
}
会签(需要多个人同时审批才算通过)
这里涉及到队列和票数,可以加一张 approval_assignee 表:
-- 记录某个节点的具体处理人 CREATE TABLE `approval_task` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `bill_id` INT NOT NULL, `node_order` INT NOT NULL, `assignee_id` INT NOT NULL COMMENT '待办人ID', `status` TINYINT DEFAULT 0 COMMENT '0待办 1已完成', UNIQUE KEY `unique_bill_node_user` (`bill_id`, `node_order`, `assignee_id`) );
- 某个节点的待办人全部点击“同意”后,才推进到下一个节点。
- 只要有一个点击“驳回”,流程即终止。
通知机制(实时推送)
如果使用 WebSocket(如 Workerman 或 Swoole),审批人可以通过接口拉取自己的待办列表,如果是简单的邮件/短信通知,在 doApprove 的下一步循环里调用发送逻辑即可。
第四阶段:使用现成组件(可选)
如果你不想自己造轮子,且项目技术栈兼容,可以考虑以下方案:
- Symfony Workflow Component:这是 Symfony 的核心组件,可以单独使用,它基于
StateMachine(状态机)的概念,用 YAML 定义转换逻辑,适合后端逻辑层使用,配合 Doctrine ORM 处理状态持久化。// workflow.yaml blog_publishing: type: 'state_machine' marking_store: type: 'method' property: 'currentPlace' supports: App\Entity\BlogPost places: - draft - reviewed - rejected transitions: submit_review: from: draft to: reviewed - PHP 微服务 + Buildup(自建):适合大型企业,将流程引擎独立成微服务,通过 HTTP/API 调用。
不要试图用 PHP 去实现复杂的 BPMN 画布,采用“模板 + 单据 + 日志”的轻量级设计,配合 Laravel(推荐)或 ThinkPHP 的队列和事务机制,完全能够支撑中小型项目的审批需求。
重点代码:
- 绑定下一个审批人逻辑(
getNextApprover)。 - 处理并行节点时的“会签”集合。
- 加锁防止并发重复提交。