本文目录导读:

这是一个很有价值的问题,在PHP项目中实现用户自定义审批历史,核心挑战在于:审批流程的节点和流转路径是由用户在运行时动态定义的,而不是在代码中写死的。
为了实现这一点,通常需要采用 工作流引擎(Workflow Engine) 的设计思想,下面我将提供一个从数据库设计到代码实现的完整方案,重点围绕“可配置性”和“历史追溯”展开。
核心设计思路
摒弃硬编码的 status 字段,转而使用 “状态机 + 节点 + 动作” 的模型,用户通过后台配置审批流(流程模板),业务数据在流转时产生一份不可变更的审批历史记录。
第一阶段:数据库设计(基石)
这是整个方案的核心,需要几张关键表来支撑动态流程。
approval_flow (审批流程定义表)
定义了一个审批流叫什么名字,请假审批流”、“报销审批流”。
CREATE TABLE `approval_flow` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '流程名称', `description` text COMMENT '流程描述', `status` tinyint(4) DEFAULT '1' COMMENT '状态 1:启用 0:停用', `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
approval_flow_node (审批节点定义表)
定义了每个审批流的节点,部门经理审批”、“总监审批”、“HR归档”。用户可以自由增删改查这些节点。
CREATE TABLE `approval_flow_node` ( `id` int(11) NOT NULL AUTO_INCREMENT, `flow_id` int(11) NOT NULL COMMENT '关联流程ID', `node_name` varchar(100) NOT NULL COMMENT '节点名称,如:部门经理审批', `node_type` tinyint(4) DEFAULT '1' COMMENT '节点类型 1:审批人 2:抄送人 3:条件分支', `approver_type` varchar(50) DEFAULT NULL COMMENT '审批人类型: user/department/role/auto', `approver_id` int(11) DEFAULT NULL COMMENT '审批人ID(或角色ID)', `sort_order` int(11) DEFAULT '0' COMMENT '节点排序序号', `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `flow_id` (`flow_id`), KEY `approver_id` (`approver_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
approval_instance (审批实例表)
当用户提交一个申请时,就创建一个实例,张三-2025年5月请假”。
CREATE TABLE `approval_instance` ( `id` int(11) NOT NULL AUTO_INCREMENT, `flow_id` int(11) NOT NULL COMMENT '关联的流程ID', `business_id` int(11) DEFAULT NULL COMMENT '关联的业务数据ID(如请假单ID)', `business_type` varchar(50) DEFAULT NULL COMMENT '业务类型,如'leave', 'expense'', `submitter_id` int(11) NOT NULL COMMENT '提交人ID', `status` varchar(20) DEFAULT 'pending' COMMENT '实例状态: pending, approved, rejected, cancelled', `current_node_id` int(11) DEFAULT NULL COMMENT '当前待处理的节点ID', `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `business_type_id` (`business_type`, `business_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
approval_log (审批日志表 - 核心历史表)
这是实现“自定义审批历史”的关键,每一次操作(提交、同意、驳回、转交、撤回)都记录在此,用户通过查询这张表来获得可追溯、不可篡改的历史。
CREATE TABLE `approval_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `instance_id` int(11) NOT NULL COMMENT '关联的审批实例ID', `node_id` int(11) DEFAULT NULL COMMENT '操作的节点ID', `operator_id` int(11) NOT NULL COMMENT '操作人ID', `action` varchar(50) NOT NULL COMMENT '操作类型: submit, approve, reject, transfer, recall, comment', `comment` text COMMENT '审批意见', `result` varchar(20) DEFAULT NULL COMMENT '节点处理结果: agree, disagree, pending, transfer', `extra_data` json DEFAULT NULL COMMENT '额外数据JSON,比如转交的对象、条件分支的选择等', `created_at` datetime NOT NULL COMMENT '操作时间', PRIMARY KEY (`id`), KEY `instance_id` (`instance_id`), KEY `operator_id` (`operator_id`), KEY `node_id` (`node_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第二阶段:PHP核心逻辑实现
配置审批流 (管理后台)
用户通过管理后台(如Laravel/ThinkPHP/Tp6等框架的后台)对 approval_flow 和 approval_flow_node 进行CRUD。
// 伪代码:保存审批流节点
// 用户提交的表单:['flow_id' => 1, 'nodes' => [['name' => '经理审批', 'sort' => 1], ['name' => '总监审批', 'sort' => 2]]]
public function saveFlowNodes($flowId, $nodes) {
// 1. 删除该流程下所有旧节点
ApprovalFlowNode::where('flow_id', $flowId)->delete();
// 2. 插入新节点
foreach ($nodes as $sort => $node) {
ApprovalFlowNode::create([
'flow_id' => $flowId,
'node_name' => $node['name'],
'sort_order' => $sort + 1,
// ... 其他字段
]);
}
return true;
}
发起审批
当用户提交一个请假单时,代码需要关联一个审批流,并创建审批实例。
// 发起审批申请
public function submitApproval($userId, $businessType, $businessId, $flowId) {
// 1. 获取审批流的第一个节点
$firstNode = ApprovalFlowNode::where('flow_id', $flowId)
->orderBy('sort_order', 'asc')
->first();
if (!$firstNode) {
throw new \Exception('该审批流没有配置节点');
}
// 2. 创建审批实例
$instance = ApprovalInstance::create([
'flow_id' => $flowId,
'business_id' => $businessId,
'business_type' => $businessType,
'submitter_id' => $userId,
'status' => 'pending',
'current_node_id' => $firstNode->id,
]);
// 3. 写入审批日志(关键:记录历史)
ApprovalLog::create([
'instance_id' => $instance->id,
'node_id' => $firstNode->id,
'operator_id' => $userId,
'action' => 'submit',
'result' => 'pending',
'comment' => '发起审批申请',
'created_at' => date('Y-m-d H:i:s'),
]);
// 4. 通知第一个节点的审批人(通过事件/队列)
// $this->notifyApprover($firstNode, $instance);
return $instance;
}
处理审批(同意/驳回)
这是最核心的流转逻辑,需要先校验当前用户是否有权审批,然后更新状态并流转到下一节点。
// 处理审批动作(简化版本)
public function processApproval($instanceId, $userId, $action, $comment = '') {
$instance = ApprovalInstance::findOrFail($instanceId);
$currentNode = ApprovalFlowNode::find($instance->current_node_id);
// 1. 校验权限:当前用户是否是该节点的审批人
if (!$this->isApprover($userId, $currentNode)) {
throw new \Exception('您不是当前节点的审批人');
}
// 2. 记录日志(这是历史记录的核心数据)
$logEntry = ApprovalLog::create([
'instance_id' => $instance->id,
'node_id' => $instance->current_node_id,
'operator_id' => $userId,
'action' => $action,
'comment' => $comment,
'result' => ($action == 'approve' ? 'agree' : 'disagree'),
'created_at' => date('Y-m-d H:i:s'),
]);
// 3. 根据动作更新实例状态
if ($action == 'approve') {
// 同意:流转到下一节点
$nextNode = ApprovalFlowNode::where('flow_id', $instance->flow_id)
->where('sort_order', '>', $currentNode->sort_order)
->orderBy('sort_order', 'asc')
->first();
if ($nextNode) {
// 有下一节点:更新当前节点
$instance->current_node_id = $nextNode->id;
$instance->status = 'pending';
// 通知下一节点审批人
// $this->notifyApprover($nextNode, $instance);
} else {
// 无下一节点:流程结束
$instance->status = 'approved';
$instance->current_node_id = null;
// 触发业务逻辑完成(如请假单变为已批准)
}
} elseif ($action == 'reject') {
// 驳回:流程直接结束,回到提交人
$instance->status = 'rejected';
$instance->current_node_id = null;
} elseif ($action == 'transfer') {
// 转交:逻辑类似,但需要记录转交对象
// $instance->current_node_id 可以不变,但审批人变更
}
$instance->save();
// 4. 返回日志记录ID或状态
return $logEntry;
}
查询审批历史
通过 approval_log 表,按实例ID获取所有日志,即可得到完整的、按时间排序的自定义审批历史。
// 获取某个审批实例的完整历史
public function getApprovalHistory($instanceId) {
$logs = ApprovalLog::where('instance_id', $instanceId)
->join('users', 'approval_log.operator_id', '=', 'users.id')
->join('approval_flow_node', 'approval_log.node_id', '=', 'approval_flow_node.id')
->orderBy('approval_log.created_at', 'asc')
->select(
'approval_log.id',
'users.name as operator_name',
'approval_flow_node.node_name',
'approval_log.action',
'approval_log.comment',
'approval_log.result',
'approval_log.created_at'
)
->get();
// 可以对数据进行格式化,比如将action翻译为中文
$actionMap = [
'submit' => '提交申请',
'approve' => '同意',
'reject' => '驳回',
'transfer' => '转交',
'recall' => '撤回',
];
return $logs->map(function ($log) use ($actionMap) {
$log->action_name = $actionMap[$log->action] ?? $log->action;
return $log;
});
}
第三阶段:进阶与优化
处理多人审批
- 会签(并行):一个节点需要多个人同时审批,所有都同意才通过,这时候需要在
approval_instance中增加current_node_multi_status字段,记录哪些人批了。 - 或签(串行):一个人同意即通过,当前实现已经支持(只要第一个审批人同意就跳到下一节点)。
条件分支
如果需要“金额小于1000 -> 经理审批,否则 -> 总监审批”,需要在 approval_flow_node 中添加 condition_expression 字段,存储一个可执行的表达式(如 ['field' => 'amount', 'operator' => '>', 'value' => 1000]),在流转时动态计算决定走哪条分支。
- 实现方式:让用户配置节点时,设置条件判断,代码在
processApproval中,在查找下一节点时,通过sort_order和condition_expression来动态选择。
性能优化
extra_data字段使用JSON类型,灵活存储额外信息(如转交人ID、抄送人列表)。- 为
approval_log表创建instance_id和created_at的复合索引,提高历史查询速度。 - 使用 队列 异步发送审批通知(邮件、钉钉、企业微信等),避免同步阻塞。
用户体验
- 前端动态渲染审批流程图(可以基于
approval_flow_node的sort_order生成)。 - 使用 Websocket 或 SSE 实时推送审批状态变化给用户。
为什么这样设计就实现了“自定义”?
- 数据库层面:通过
approval_flow_node表,用户可以在后台任意增删改查节点,流程的可变性完全来自配置。 - 逻辑层面:审批路由(
processApproval中查找下一节点)不是 if-else 硬编码,而是基于sort_order和条件动态计算。 - 历史层面:
approval_log表记录每一次操作,无论是标准流程还是特殊动作(转交、撤回),都能忠实记录,用户查询instance_id即可获得该业务单的完整、可排序、可追溯的自定义审批历史。
这套方案足够支撑大部分企业级OA、CRM、ERP系统中的审批流需求,你可以根据实际业务复杂度,选择在这个基础上扩展(比如添加图形化拖拽设计器、代码生成器)或简化为一个更轻量的版本。