PHP项目怎么实现用户自定义审批记录?

wen java案例 1

PHP项目实现用户自定义审批记录:从零构建灵活工作流引擎

目录导读

核心需求与设计挑战

在众多企业级PHP项目中(如OA系统、合同管理系统、报销平台),用户自定义审批记录是一项高频需求,传统硬编码审批流的方式维护成本极高——业务部门每调整一个审批节点,就需要开发人员修改代码、更新数据库。

PHP项目怎么实现用户自定义审批记录?

核心挑战在于:

  1. 动态性:审批层级、审批人、审批条件必须可配置,并且能实时生效
  2. 兼容性:已发起的审批记录不受后续规则变更影响
  3. 可靠性:支持驳回、转审、加签等复杂操作时的状态一致性

本文将基于PHP(Laravel/Symfony框架为例),结合关系型数据库,提供一套完整的、经过生产验证的用户自定义审批记录实现方案。

数据库模型设计:动态审批记录的基石

核心数据表结构(MySQL)

-- 1. 审批流程模板表:定义不同业务类型的审批规则
CREATE TABLE `approval_flows` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL COMMENT '流程名称(如:采购审批)',
  `business_type` varchar(50) DEFAULT NULL COMMENT '业务类型标识(如:purchase)',
  `is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 审批节点表:每个流程下的审批步骤
CREATE TABLE `approval_nodes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `flow_id` int(11) NOT NULL COMMENT '关联流程ID',
  `node_order` tinyint(3) DEFAULT 0 COMMENT '节点顺序(从1开始)',
  `node_type` varchar(20) DEFAULT 'user' COMMENT '审批类型(user用户/role角色/department部门)',
  `approver_id` int(11) DEFAULT NULL COMMENT '审批人ID(当node_type=user时)',
  `approver_role_id` int(11) DEFAULT NULL COMMENT '角色ID(当node_type=role时)',
  `condition_expression` text COMMENT '条件表达式(可选:部门ID>100)',
  `is_required` tinyint(1) DEFAULT 1 COMMENT '是否必须审批',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_flow_order` (`flow_id`, `node_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 审批实例表:每次发起审批生成的记录
CREATE TABLE `approval_records` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `flow_id` int(11) NOT NULL COMMENT '使用的流程模板ID',
  `business_id` int(11) DEFAULT NULL COMMENT '业务单据ID(如采购单ID)',
  `submitter_id` int(11) DEFAULT NULL COMMENT '发起人ID',
  `status` enum('pending','approved','rejected','cancelled') DEFAULT 'pending',
  `current_node_order` tinyint(3) DEFAULT 0 COMMENT '当前待审批节点序号',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `finished_at` timestamp NULL DEFAULT NULL COMMENT '审批完成时间',
  PRIMARY KEY (`id`),
  KEY `idx_business` (`business_type`, `business_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 审批日志表:记录每个节点的审批动作
CREATE TABLE `approval_logs` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `record_id` int(11) NOT NULL COMMENT '关联审批记录ID',
  `node_id` int(11) NOT NULL COMMENT '节点ID',
  `node_order` tinyint(3) DEFAULT 0 COMMENT '节点序号',
  `operator_id` int(11) DEFAULT NULL COMMENT '操作人ID',
  `action` enum('approve','reject','transfer') NOT NULL COMMENT '操作类型',
  `remark` text COMMENT '审批意见',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计关键点:

  • 使用flow_id+node_order组合解决多版本流程问题
  • 引入condition_expression字段支持条件分支(如:金额>5000时需总经理审批)
  • approval_records中的current_node_order字段直接存储当前节点序号,避免每步都查节点表

核心代码实现:三步构建自定义审批逻辑

第一步:提交审批时动态生成实例

// 示例代码:Laravel框架
public function submitApproval(SubmitApprovalRequest $request)
{
    $flow = ApprovalFlow::with('nodes')->findOrFail($request->flow_id);
    // 检查流程模板是否启用
    if (!$flow->is_active) {
        throw new \Exception('该审批流程已停用');
    }
    // 验证条件表达式(动态计算是否跳过某些节点)
    $firstNode = $flow->nodes->first(function ($node) {
        return $this->evaluateCondition($node->condition_expression);
    });
    if (!$firstNode) {
        throw new \Exception('未找到符合条件的审批节点');
    }
    // 创建审批实例
    $record = ApprovalRecord::create([
        'flow_id' => $flow->id,
        'business_id' => $request->business_id,
        'business_type' => $flow->business_type,
        'submitter_id' => Auth::id(),
        'status' => 'pending',
        'current_node_order' => $firstNode->node_order,
    ]);
    // 写入首节点审批日志(待审批状态)
    ApprovalLog::create([
        'record_id' => $record->id,
        'node_id' => $firstNode->id,
        'node_order' => $firstNode->node_order,
        'operator_id' => $firstNode->approver_id ?? null,
        'action' => 'pending',
        'remark' => '待审批'
    ]);
    return response()->json(['message' => '提交成功', 'record_id' => $record->id]);
}
// 条件表达式解析器(简化版)
private function evaluateCondition($expression, $context = [])
{
    if (empty($expression)) return true;
    // 实际项目建议使用表达式解析库,如 symfony/expression-language
    // 示例:简单解析 "$amount > 5000"
    preg_match('/^\$([a-z_]+)\s*(>|<|>=|<=|==)\s*(\d+)$/i', $expression, $matches);
    if ($matches) {
        $var = $context[$matches[1]] ?? 0;
        return eval("return $var $matches[2] $matches[3];");
    }
    return true;
}

第二步:审批操作逻辑(处理、驳回、转审)

public function processApproval(ProcessApprovalRequest $request, $recordId)
{
    $record = ApprovalRecord::with('logs')->findOrFail($recordId);
    $node = ApprovalNode::where('flow_id', $record->flow_id)
                        ->where('node_order', $record->current_node_order)
                        ->first();
    // 权限检查:当前登录用户必须是指定的审批人
    $allowApproverIds = $this->getActualApprovers($node);
    if (!in_array(Auth::id(), $allowApproverIds)) {
        throw new \Exception('您不是当前审批人');
    }
    // 记录操作日志
    ApprovalLog::create([
        'record_id' => $record->id,
        'node_id' => $node->id,
        'node_order' => $node->node_order,
        'operator_id' => Auth::id(),
        'action' => $request->action, // approve 或 reject
        'remark' => $request->remark
    ]);
    if ($request->action === 'reject') {
        // 驳回:直接结束流程
        $record->update(['status' => 'rejected', 'finished_at' => now()]);
        // 触发业务回调:通知发起人
        return response()->json(['message' => '已驳回']);
    }
    // 批准:查找下一节点
    $nextNode = ApprovalNode::where('flow_id', $record->flow_id)
                            ->where('node_order', '>', $record->current_node_order)
                            ->where('is_required', true)
                            ->orderBy('node_order')
                            ->first();
    if ($nextNode && $this->evaluateCondition($nextNode->condition_expression)) {
        // 有下一节点,更新当前节点
        $record->update(['current_node_order' => $nextNode->node_order]);
        // 通知下一审批人
        return response()->json(['message' => '已转至下一审批']);
    } else {
        // 所有节点完成
        $record->update(['status' => 'approved', 'finished_at' => now()]);
        // 触发业务状态变更
        return response()->json(['message' => '审批通过']);
    }
}
// 获取实际可审批人员(支持用户/角色/部门三种模式)
private function getActualApprovers($node)
{
    switch ($node->node_type) {
        case 'user':
            return [$node->approver_id];
        case 'role':
            return RoleUser::where('role_id', $node->approver_role_id)->pluck('user_id')->toArray();
        case 'department':
            return User::where('department_id', $node->approver_id)->pluck('id')->toArray();
        default:
            return [];
    }
}

第三步:审批记录展示与时间线

public function getApprovalTimeline($recordId)
{
    $record = ApprovalRecord::with(['logs.operator:id,name,avatar', 'flow.nodes'])->findOrFail($recordId);
    $timeline = [];
    $flow = $record->flow;
    // 按照节点顺序组织时间线
    foreach ($flow->nodes as $node) {
        $log = $record->logs->firstWhere('node_id', $node->id);
        $timeline[] = [
            'node_order' => $node->node_order,
            'node_name' => $node->name,
            'approver' => $log ? $log->operator->name : '待审批',
            'action' => $log ? $log->action : 'pending',
            'remark' => $log ? $log->remark : '',
            'time' => $log ? $log->created_at->format('Y-m-d H:i:s') : null,
        ];
    }
    return response()->json(['timeline' => $timeline]);
}

用户交互界面:配置审批流程的友好入口

前端配置界面建议(基于Vue.js)

// 流程配置组件核心数据结构
const flowConfig = {
  name: '采购审批',
  business_type: 'purchase',
  nodes: [
    {
      id: 1,
      node_order: 1,
      node_type: 'user',
      approver_id: 1001, // 用户选择器
      condition_expression: '', // 可选条件
      is_required: true
    },
    {
      id: 2,
      node_order: 2,
      node_type: 'role',
      approver_role_id: 5, // 角色选择器
      condition_expression: '$amount > 5000', // 仅金额>5000时触发
      is_required: false
    }
  ]
};
// 拖拽排序(使用 vuedraggable)
// 条件编辑器(支持变量提示:$amount, $department_id等)

关键交互细节:

  • 节点类型提供 用户选择角色选择部门选择 三种模式
  • 可设置 跳过条件(当条件不满足时自动跳过该节点)
  • 提供测试流程按钮,模拟提交流程验证节点逻辑

常见问题与性能优化

Q1:企业微信/钉钉集成时,审批人选择如何动态同步?
A:建议在管理员配置审批节点时,通过API同步通讯录并缓存到本地users表,在getActualApprovers()方法中,对于node_type=role的场景,实时查询角色-人员关联表。

Q2:已发起的审批记录还能修改流程吗?
A:不能,已发起的审批记录会clone当时流程模板的快照(可在approval_record表中增加snapshot字段存储JSON),修改流程模板不影响已有记录。

Q3:如何避免高并发下的审批状态错误?
A:使用数据库行级锁(SELECT ... FOR UPDATE)或者Redis分布式锁,审批操作前锁定approval_record行。

优化建议:

  • 数据库层面:对approval_logs按月分表
  • 缓存层面:常用流程模板缓存到Redis(flow:template:{flow_id}
  • 异步通知:使用消息队列(如RabbitMQ)处理审批通知

问答环节:开发者最关心的5个问题

问题1:审批节点支持多同意人(会签)吗?
答:支持,可以在approval_nodes表中增加approval_mode字段(any(或签)/ all(会签)),当all时,在approval_logs中统计当前节点通过人数,直到所有指定审批人同意后才进入下一节点,关键逻辑在processApproval方法中增加计数判断。

问题2:用户自定义审批条件表达式的安全性怎么保证?
答:禁止直接使用eval(),生产中推荐使用symfony/expression-language(支持沙箱模式)或编写自己的AST解析器,在管理后台对表达式做白名单校验,只允许访问预定义的变量(如$amount$department)。

问题3:如何在审批记录中展示驳回后重新提交的版本控制?
答:在approval_records表中增加version字段,驳回后生成新的version(+1),新versioncurrent_node_order重置为1,前端展示时按版本分组显示时间线。

问题4:如果审批人离职,如何处理卡在ta节点上的审批?
答:管理后台提供节点转交功能,使用转交(transfer)操作,在approval_logs记录原审批人,并修改当前节点approver_id为新的审批人,建议配合定时任务,定期扫描超过7天未处理的审批,发送提醒。

问题5:支持子父级审批流程嵌套吗?
答:可以,设计approval_nodesparent_node_id自引用字段,形成树形结构,父节点审批通过后,触发子节点审批,这种场景多用于二级审批(如金额超过10万需集团副总裁审批)。

总结与扩展建议

本文从数据库设计、代码实现、交互界面到性能优化,系统性地讲解了在PHP项目中如何实现用户自定义审批记录,核心思路是:将审批流程抽象为“流程模板”的节点序列,通过动态解析条件表达式控制节点跳转,用审批日志记录完整流转历史。

对于更高要求的场景,推荐扩展方向:

  1. 引入并行审批节点(多人同时审批)
  2. 增加超时自动审批机制
  3. 结合消息队列实现异步通知
  4. 提供RESTful API给第三方系统调用

你的下一个项目是否考虑将这个模型集成到现有的OA或ERP系统中?通过灵活的自定义审批引擎,可以为企业节省大量的重复开发成本。

上一篇PHP项目如何实现用户自定义审批报表?

下一篇当前分类已是最新一篇

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