如何用PHP项目实现用户自定义审批历史?

wen java案例 1

本文目录导读:

如何用PHP项目实现用户自定义审批历史?

  1. 核心设计思路
  2. 第一阶段:数据库设计(基石)
  3. 第二阶段:PHP核心逻辑实现
  4. 第三阶段:进阶与优化
  5. 总结:为什么这样设计就实现了“自定义”?

这是一个很有价值的问题,在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_flowapproval_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_ordercondition_expression 来动态选择。

性能优化

  • extra_data 字段使用 JSON 类型,灵活存储额外信息(如转交人ID、抄送人列表)。
  • approval_log 表创建 instance_idcreated_at 的复合索引,提高历史查询速度。
  • 使用 队列 异步发送审批通知(邮件、钉钉、企业微信等),避免同步阻塞。

用户体验

  • 前端动态渲染审批流程图(可以基于 approval_flow_nodesort_order 生成)。
  • 使用 WebsocketSSE 实时推送审批状态变化给用户。

为什么这样设计就实现了“自定义”?

  1. 数据库层面:通过 approval_flow_node 表,用户可以在后台任意增删改查节点,流程的可变性完全来自配置。
  2. 逻辑层面:审批路由(processApproval 中查找下一节点)不是 if-else 硬编码,而是基于 sort_order 和条件动态计算。
  3. 历史层面approval_log 表记录每一次操作,无论是标准流程还是特殊动作(转交、撤回),都能忠实记录,用户查询 instance_id 即可获得该业务单的完整、可排序、可追溯的自定义审批历史。

这套方案足够支撑大部分企业级OA、CRM、ERP系统中的审批流需求,你可以根据实际业务复杂度,选择在这个基础上扩展(比如添加图形化拖拽设计器、代码生成器)或简化为一个更轻量的版本。

抱歉,评论功能暂时关闭!