本文目录导读:

在PHP项目中实现用户自定义审批流程是一个比较复杂但常见的需求,核心在于将审批逻辑从代码中解耦,变成可由用户配置的数据,下面提供一个相对完整的设计思路和实现方案。
总体架构设计
采用 “节点(Node)+ 连线(Transition)” 的工作流引擎思想,核心数据结构包括:
- 流程模板:定义审批流程的骨架(如:请假审批模板)。
- 流程节点:模板中的每一步(如:发起人提交、部门经理审批、HR审批等)。
- 连线/路由:定义节点之间的流转条件(如:金额 > 5000 走老板审批)。
- 流程实例:根据模板发起的一次具体审批(如:张三2024-01-01的请假)。
- 审批记录:实例中每个节点的处理记录。
第一步:核心数据库表设计(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_node和workflow_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();
}
第三步:用户交互与前端配合
-
流程配置界面:
- 使用拖拽组件(如 Vue 的
vue-draggable或 React 的react-flow)。 - 每次添加节点时,让用户选择节点类型(审批、条件、抄送、结束)。
- 每个审批节点配置:审批人来源(指定人、部门主管、角色等)、会签/或签。
- 条件节点配置:字段比较(如金额 > 5000)。
- 连线时,前端记录
fromNodeId和toNodeId。
- 使用拖拽组件(如 Vue 的
-
发起审批界面:
- 用户选择模板,动态渲染表单(也可在模板中定义表单字段)。
- 提交后调用API创建实例。
-
审批处理界面:
- 显示当前实例的流程图(高亮当前节点)。
- 显示历史审批记录。
- 提供同意、驳回、转交等按钮,附带意见输入框。
关键优化点与注意事项
- 条件表达式安全:
eval()有安全风险,建议使用symfony/expression-language或自己实现树解析器,仅允许预定义的函数和变量。 - 并发控制:审批操作可能多人同时处理,需要加锁(如
SELECT ... FOR UPDATE或 Redis锁),防止状态混乱。 - 会签/或签:
- 或签:任意一人同意即通过。
- 会签:需全部人同意,实现时可在
workflow_log和节点config中记录总人数和已同意数,当达到阈值时才流转到下一步。
- 超时与提醒:利用消息队列(如RabbitMQ)配合定时任务,检查超时的节点并推送通知。
- 撤回功能:如果下一个节点还没有人处理,允许发起人撤回。
- 审计日志:所有操作都记录到
workflow_log,包括节点配置的变更(可另建日志表)。 - 性能优化:当审批实例非常多时,避免全表扫描待办列表,可以为
workflow_instance的current_node_id、status建立组合索引。
简化版:不使用图数据库
对于大多数业务系统,直接用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 # 事件通知
通过以上设计,你可以实现一个相对灵活、可扩展的用户自定义审批流程系统,后续可根据业务需求逐步增强(如加签、会签、传阅、委托等)。