怎样在PHP项目中实现用户自定义审批流程?

wen java案例 8

本文目录导读:

怎样在PHP项目中实现用户自定义审批流程?

  1. 总体架构设计
  2. 第一步:核心数据库表设计(MySQL)
  3. 第二步:核心PHP实现逻辑
  4. 第三步:用户交互与前端配合
  5. 关键优化点与注意事项
  6. 简化版:不使用图数据库
  7. 示例代码目录结构建议

在PHP项目中实现用户自定义审批流程是一个比较复杂但常见的需求,核心在于将审批逻辑从代码中解耦,变成可由用户配置的数据,下面提供一个相对完整的设计思路和实现方案。

总体架构设计

采用 “节点(Node)+ 连线(Transition)” 的工作流引擎思想,核心数据结构包括:

  1. 流程模板:定义审批流程的骨架(如:请假审批模板)。
  2. 流程节点:模板中的每一步(如:发起人提交、部门经理审批、HR审批等)。
  3. 连线/路由:定义节点之间的流转条件(如:金额 > 5000 走老板审批)。
  4. 流程实例:根据模板发起的一次具体审批(如:张三2024-01-01的请假)。
  5. 审批记录:实例中每个节点的处理记录。

第一步:核心数据库表设计(MySQL)

建议至少设计5张核心表:

-- 1. 流程模板表
CREATE TABLE `workflow_template` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(100) NOT NULL COMMENT '模板名称,如请假审批',
    `description` TEXT,
    `status` TINYINT DEFAULT 1 COMMENT '1启用 0停用',
    `created_at` DATETIME,
    `updated_at` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 流程节点表
CREATE TABLE `workflow_node` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `template_id` INT UNSIGNED NOT NULL COMMENT '关联模板',
    `node_name` VARCHAR(100) NOT NULL COMMENT '节点名称,如部门经理',
    `node_type` VARCHAR(20) NOT NULL COMMENT '类型: start(发起), approval(审批), cc(抄送), condition(条件), end(结束)',
    `approver_type` VARCHAR(20) DEFAULT NULL COMMENT '审批人类型: user(指定人), role(指定角色), supervisor(直属上级), hrbp/department_head(按组织架构)',
    `approver_ids` TEXT COMMENT '指定审批人ID列表,逗号分隔',
    `sort_order` INT DEFAULT 0 COMMENT '同层级的排序',
    `config` JSON COMMENT '扩展配置,如超时提醒、会签/或签等',
    `created_at` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 连线/路由表(决定节点流转方向)
CREATE TABLE `workflow_transition` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `template_id` INT UNSIGNED NOT NULL,
    `from_node_id` INT UNSIGNED NOT NULL COMMENT '起始节点',
    `to_node_id` INT UNSIGNED NOT NULL COMMENT '目标节点',
    `condition_expression` TEXT COMMENT '流转条件,如 amount > 5000,可以为空(无条件/默认路径)',
    `sort_order` INT DEFAULT 0 COMMENT '条件优先级,数字小优先匹配',
    `created_at` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 流程实例表(每次发起一个审批创建一个实例)
CREATE TABLE `workflow_instance` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `template_id` INT UNSIGNED NOT NULL, VARCHAR(200) COMMENT '审批标题,如张三请假申请',
    `applicant_id` INT UNSIGNED NOT NULL COMMENT '发起人用户ID',
    `form_data` JSON COMMENT '表单提交的数据',
    `status` TINYINT DEFAULT 0 COMMENT '0待提交 1审批中 2通过 3驳回 4已撤销',
    `current_node_id` INT UNSIGNED DEFAULT NULL COMMENT '当前所在节点ID',
    `created_at` DATETIME,
    `completed_at` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 5. 审批记录表(每个节点的审批操作)
CREATE TABLE `workflow_log` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `instance_id` INT UNSIGNED NOT NULL,
    `node_id` INT UNSIGNED NOT NULL,
    `operator_id` INT UNSIGNED DEFAULT NULL COMMENT '审批人ID',
    `action` VARCHAR(20) NOT NULL COMMENT '操作: approve(同意), reject(驳回), transfer(转交), add_approver(加签)',
    `comment` TEXT COMMENT '审批意见',
    `created_at` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第二步:核心PHP实现逻辑

构建流程模板(管理者后台)

用户通过拖拽或表单配置,前端将节点和连线数据存入workflow_nodeworkflow_transition表。

// WorkflowBuilder.php - 保存流程配置
class WorkflowBuilder {
    public function saveTemplateNodesAndTransitions($templateId, $nodes, $transitions) {
        // 1. 删除旧节点和连线
        DB::table('workflow_node')->where('template_id', $templateId)->delete();
        DB::table('workflow_transition')->where('template_id', $templateId)->delete();
        // 2. 批量插入节点(注意获取新ID)
        $nodeIdMap = []; // 前端临时ID -> 数据库真实ID
        foreach ($nodes as $node) {
            $newId = DB::table('workflow_node')->insertGetId([
                'template_id' => $templateId,
                'node_name' => $node['name'],
                'node_type' => $node['type'],
                'approver_type' => $node['approverType'] ?? null,
                'approver_ids' => json_encode($node['approverIds'] ?? []),
                'sort_order' => $node['sortOrder'] ?? 0,
                'config' => json_encode($node['config'] ?? []),
            ]);
            $nodeIdMap[$node['tempId']] = $newId;
        }
        // 3. 批量插入连线,替换ID
        foreach ($transitions as $trans) {
            DB::table('workflow_transition')->insert([
                'template_id' => $templateId,
                'from_node_id' => $nodeIdMap[$trans['fromTempId']],
                'to_node_id' => $nodeIdMap[$trans['toTempId']],
                'condition_expression' => $trans['condition'] ?? '',
                'sort_order' => $trans['sortOrder'] ?? 0,
            ]);
        }
    }
}

发起审批(创建流程实例)

用户填写表单并提交,系统创建workflow_instance,定位到第一个start节点后的下一个节点。

// WorkflowService.php - 发起流程
class WorkflowService {
    public function startWorkflow($templateId, $userId, $formData) {
        $template = DB::table('workflow_template')->where('id', $templateId)->first();
        if (!$template || $template->status != 1) {
            throw new \Exception('流程模板未启用');
        }
        // 找开始节点后的第一个审批节点
        $startNode = DB::table('workflow_node')
            ->where('template_id', $templateId)
            ->where('node_type', 'start')
            ->first();
        // 找从start节点出发的默认连线
        $transition = DB::table('workflow_transition')
            ->where('template_id', $templateId)
            ->where('from_node_id', $startNode->id)
            ->orderBy('sort_order')
            ->first();
        if (!$transition) {
            throw new \Exception('流程未配置起始路径');
        }
        // 创建实例
        $instanceId = DB::table('workflow_instance')->insertGetId([
            'template_id' => $templateId,
            'title' => $formData['title'] ?? '审批申请',
            'applicant_id' => $userId,
            'form_data' => json_encode($formData),
            'status' => 1, // 审批中
            'current_node_id' => $transition->to_node_id, // 指向第一个审批节点
            'created_at' => now(),
        ]);
        // 记录初始日志
        DB::table('workflow_log')->insert([
            'instance_id' => $instanceId,
            'node_id' => $startNode->id,
            'operator_id' => $userId,
            'action' => 'submit',
            'created_at' => now(),
        ]);
        // 自动通知第一个审批人
        $this->notifyNextApprovers($instanceId, $transition->to_node_id);
        return $instanceId;
    }
}

处理审批(同意/驳回/转交)

这是最核心的逻辑,需要解析节点配置并决定下一步走向。

// WorkflowService.php - 处理审批动作
public function processApproval($instanceId, $nodeId, $operatorId, $action, $comment = '') {
    $instance = DB::table('workflow_instance')->find($instanceId);
    if (!$instance || $instance->status != 1) {
        throw new \Exception('实例状态不正确');
    }
    // 记录审批日志
    DB::table('workflow_log')->insert([
        'instance_id' => $instanceId,
        'node_id' => $nodeId,
        'operator_id' => $operatorId,
        'action' => $action,
        'comment' => $comment,
        'created_at' => now(),
    ]);
    if ($action === 'reject') {
        // 驳回:流程结束,回到发起人
        DB::table('workflow_instance')->where('id', $instanceId)->update([
            'status' => 3, // 驳回
            'current_node_id' => null,
            'completed_at' => now(),
        ]);
        return;
    }
    if ($action === 'approve') {
        // 同意:需要找到下一步节点
        $currentNode = DB::table('workflow_node')->find($nodeId);
        // 查找从当前节点出发的连线
        $transitions = DB::table('workflow_transition')
            ->where('template_id', $instance->template_id)
            ->where('from_node_id', $nodeId)
            ->orderBy('sort_order')
            ->get();
        $nextNodeId = null;
        foreach ($transitions as $trans) {
            if (!$trans->condition_expression) {
                // 无条件的默认路线
                $nextNodeId = $trans->to_node_id;
                break;
            }
            // 解析条件表达式( amount > 5000)
            // 注意:生产环境应使用安全的方式,如脚本沙盒或预定义条件
            $conditionResult = $this->evaluateCondition($trans->condition_expression, json_decode($instance->form_data, true));
            if ($conditionResult) {
                $nextNodeId = $trans->to_node_id;
                break;
            }
        }
        if (!$nextNodeId) {
            throw new \Exception('无法找到下一步节点');
        }
        // 检查目标节点是否是结束节点
        $nextNode = DB::table('workflow_node')->find($nextNodeId);
        if ($nextNode->node_type === 'end') {
            DB::table('workflow_instance')->where('id', $instanceId)->update([
                'status' => 2, // 通过
                'current_node_id' => null,
                'completed_at' => now(),
            ]);
        } else {
            // 进入下一个节点
            DB::table('workflow_instance')->where('id', $instanceId)->update([
                'current_node_id' => $nextNodeId,
            ]);
            // 通知下一个审批人
            $this->notifyNextApprovers($instanceId, $nextNodeId);
        }
    }
    if ($action === 'transfer') {
        // 转交:节点不变,但更换审批人(可记录在日志或创建子节点)
        // 简单实现:更新当前节点的审批人
    }
}
// 条件判断辅助方法
private function evaluateCondition($expression, $formData) {
    // 简单示例:支持基本比较运算,实际项目建议使用表达式解析库
    // 如 symfony/expression-language 或 自定义安全解析器
    $expression = str_replace(
        ['amount', 'department'],
        ['$formData[\'amount\']', '$formData[\'department\']'],
        $expression
    );
    // 注意:eval 不安全,生产环境请用解析库或预定义条件
    // 此处仅为演示概念,实际应避免eval
    eval("\$result = $expression;");
    return $result;
}

获取待办列表与审批人匹配

// WorkflowService.php - 查找用户待办审批
public function getPendingApprovals($userId) {
    // 获取所有当前节点需要该用户审批的实例
    return DB::table('workflow_instance as i')
        ->join('workflow_node as n', 'i.current_node_id', '=', 'n.id')
        ->where('i.status', 1)
        ->where(function($query) use ($userId) {
            $query->where('n.approver_type', 'user')
                  ->whereRaw("JSON_CONTAINS(n.approver_ids, ?)", json_encode([$userId]))
                  ->orWhere('n.approver_type', 'supervisor')
                  ->whereExists(function($q) use ($userId) {
                      // 直属上级逻辑
                      $q->select(DB::raw(1))
                        ->from('users')
                        ->where('users.id', $userId)
                        ->whereColumn('users.supervisor_id', '?'); // 需要关联用户表
                  });
        })
        ->get();
}

第三步:用户交互与前端配合

  1. 流程配置界面

    • 使用拖拽组件(如 Vue 的 vue-draggable 或 React 的 react-flow)。
    • 每次添加节点时,让用户选择节点类型(审批、条件、抄送、结束)。
    • 每个审批节点配置:审批人来源(指定人、部门主管、角色等)、会签/或签。
    • 条件节点配置:字段比较(如金额 > 5000)。
    • 连线时,前端记录fromNodeIdtoNodeId
  2. 发起审批界面

    • 用户选择模板,动态渲染表单(也可在模板中定义表单字段)。
    • 提交后调用API创建实例。
  3. 审批处理界面

    • 显示当前实例的流程图(高亮当前节点)。
    • 显示历史审批记录。
    • 提供同意、驳回、转交等按钮,附带意见输入框。

关键优化点与注意事项

  1. 条件表达式安全eval() 有安全风险,建议使用 symfony/expression-language 或自己实现树解析器,仅允许预定义的函数和变量。
  2. 并发控制:审批操作可能多人同时处理,需要加锁(如 SELECT ... FOR UPDATE 或 Redis锁),防止状态混乱。
  3. 会签/或签
    • 或签:任意一人同意即通过。
    • 会签:需全部人同意,实现时可在workflow_log和节点config中记录总人数和已同意数,当达到阈值时才流转到下一步。
  4. 超时与提醒:利用消息队列(如RabbitMQ)配合定时任务,检查超时的节点并推送通知。
  5. 撤回功能:如果下一个节点还没有人处理,允许发起人撤回。
  6. 审计日志:所有操作都记录到workflow_log,包括节点配置的变更(可另建日志表)。
  7. 性能优化:当审批实例非常多时,避免全表扫描待办列表,可以为workflow_instancecurrent_node_idstatus建立组合索引。

简化版:不使用图数据库

对于大多数业务系统,直接用MySQL的关联查询已经足够,不需要引入Neo4j等图数据库,但如果流程极其复杂(如百节点、多分支),可以考虑:

  • 用Redis缓存整个模板的DAG结构。
  • 或使用专业的PHP工作流库如 Symfony Workflow 组件。

示例代码目录结构建议

app/
├── Workflow/
│   ├── Controllers/
│   │   ├── TemplateController.php      # 流程模板CRUD
│   │   └── InstanceController.php      # 实例处理
│   ├── Services/
│   │   ├── WorkflowEngine.php          # 核心引擎(节点流转)
│   │   ├── ApprovalMatcher.php         # 审批人匹配
│   │   └── ConditionEvaluator.php      # 条件安全解析
│   ├── Models/
│   │   ├── WorkflowTemplate.php
│   │   ├── WorkflowNode.php
│   │   └── WorkflowInstance.php
│   └── Listeners/
│       └── WorkflowEventSubscriber.php # 事件通知

通过以上设计,你可以实现一个相对灵活、可扩展的用户自定义审批流程系统,后续可根据业务需求逐步增强(如加签、会签、传阅、委托等)。

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