本文目录导读:

在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 # 事件通知
通过以上设计,你可以实现一个相对灵活、可扩展的用户自定义审批流程系统,后续可根据业务需求逐步增强(如加签、会签、传阅、委托等)。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播