本文目录导读:

这是一个非常典型且复杂的企业级需求,所谓的“用户自定义审批大屏”,核心在于低代码/无代码的配置化思想,允许用户(通常是非技术管理员)通过拖拽或表单配置,来定义审批流程的各个节点、条件以及最终展示在屏幕上的图表样式。
要实现这个功能,不能只靠简单的代码逻辑,需要一套完整的数据模型和前端渲染引擎。
以下是一套可行的、分层次的 PHP 实现架构方案:
第一层:核心数据模型设计(数据库层面)
这是最基础、最关键的,你需要几张核心表来存储用户“自定义”的结果,而不是硬编码。
流程定义表 approval_flows
CREATE TABLE `approval_flows` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL COMMENT '流程名称',
`icon` VARCHAR(50) DEFAULT NULL COMMENT '大屏显示的图标',
`description` TEXT COMMENT '描述',
`status` TINYINT DEFAULT 1 COMMENT '1启用 0停用',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
节点定义表 approval_nodes
核心字段:type 和 config_json
CREATE TABLE `approval_nodes` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`flow_id` INT UNSIGNED NOT NULL COMMENT '所属流程ID',
`node_order` INT UNSIGNED NOT NULL COMMENT '节点顺序,如1,2,3',
`type` VARCHAR(50) NOT NULL COMMENT '节点类型: start, approve, condition, notify, end',
`name` VARCHAR(100) NOT NULL COMMENT '节点名称(如: 部门经理审批)',
-- 关键配置字段:存储该节点的所有自定义属性(JSON格式)
`config_json` JSON DEFAULT NULL COMMENT '节点配置,具体见下方详解',
`parent_node_id` INT DEFAULT NULL COMMENT '条件分支时指向父节点',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
大屏配置表 approval_dashboards
记录用户对于“大屏”本身的可视化配置。
CREATE TABLE `approval_dashboards` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`flow_id` INT UNSIGNED NOT NULL,
`name` VARCHAR(100) NOT NULL COMMENT '大屏名称',
`layout_config` JSON NOT NULL COMMENT '布局配置,如各组件的位置、大小',
`theme` VARCHAR(50) DEFAULT 'default' COMMENT '主题',
`refresh_interval` INT DEFAULT 30 COMMENT '自动刷新秒数',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
大屏组件配置表 dashboard_components
存储每一个图表/卡片/指标的配置。
CREATE TABLE `dashboard_components` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`dashboard_id` INT UNSIGNED NOT NULL,
`type` VARCHAR(50) NOT NULL COMMENT '组件类型: pie_chart, bar_chart, number_card, list_table, status_timeline',
`x` INT DEFAULT 0 COMMENT '网格布局X坐标',
`y` INT DEFAULT 0 COMMENT '网格布局Y坐标',
`w` INT DEFAULT 4 COMMENT '网格宽度',
`h` INT DEFAULT 4 COMMENT '网格高度',
`source_config` JSON NOT NULL COMMENT '数据源配置,详见下方',
`style_config` JSON DEFAULT NULL COMMENT '样式配置,如颜色、标题、单位',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
第二层:核心数据模型设计(PHP 与 JSON 结构)
用户自定义的核心就在于 JSON 字段,你需要定义好这些 JSON 的内部结构。
节点 config_json 示例:
{
"assignee_type": "role",
"assignee_value": 5,
"timeout_hours": 48,
"sign_type": "single",
"condition": null
}
或对于条件节点:
{
"condition_logic": "AND",
"rules": [
{"field": "amount", "operator": ">", "value": 10000},
{"field": "department", "operator": "==", "value": "finance"}
]
}
组件 source_config 示例:
{
"data_source": "sql",
"sql": "SELECT status, COUNT(*) as count FROM approval_requests WHERE flow_id = ? GROUP BY status",
"params": ["flow_id"],
"mapping": {
"name": "status",
"value": "count"
}
}
或引用预定义的查询(推荐,更安全):
{
"data_source": "predefined_query",
"query_key": "approval_status_distribution",
"filter_params": {
"start_date": "-30",
"end_date": "today"
}
}
第三层:PHP 实现逻辑
配置视图层(供管理员使用)
- 路由:
/admin/flow/{id}/design - PHP 控制器:
FlowDesignController@index- 从数据库加载
approval_flows、approval_nodes。 - 返回给前端一个 JSON 树状结构(前端使用 Vue/React 的 antv/g6 或类似流程图库进行拖拽渲染)。
- 关键点: PHP 只负责提供原始数据(
flow+nodes),前端的交互逻辑(拖拽添加节点、修改配置、保存JSON)完全由前端完成。
- 从数据库加载
- 保存操作: 控制器
FlowDesignController@save- 接收前端传来的完整
nodes数组。 - 开启事务,先删除该流程下旧节点,再逐条插入新节点。
- 注意防注入: 尽管是 JSON,但在 SQL 查询中务必使用参数绑定。
- 接收前端传来的完整
运行时解析层(分析引擎)
这是最复杂的一环,当一个人发起一个审批请求时,系统需要根据配置走的路径。
- 核心服务类:
ApprovalEngineService
// 伪代码
class ApprovalEngineService {
public function startFlow($flowId, $formData) {
$flow = ApprovalFlows::find($flowId);
$firstNode = ApprovalNodes::where('flow_id', $flowId)
->where('node_order', 1)->first();
// 创建审批请求记录
$request = new ApprovalRequest();
$request->flow_id = $flowId;
$request->form_data = json_encode($formData); // 用户提交的表单数据
$request->current_node_id = $firstNode->id;
$request->status = 'pending';
$request->save();
// 根据 $firstNode->config_json 动态生成待办任务
$this->createTaskForNode($request, $firstNode);
}
public function approve($requestId, $action, $userId) {
$request = ApprovalRequest::find($requestId);
$node = ApprovalNodes::find($request->current_node_id);
// 根据 $node->type 执行不同逻辑
switch ($node->type) {
case 'approve':
$this->handleApproveNode($request, $node, $action, $userId);
break;
case 'condition':
$this->handleConditionNode($request, $node, $formData);
break;
}
// 移动到下一个节点
$nextNode = $this->determineNextNode($request, $node);
$request->current_node_id = $nextNode->id;
$request->save();
}
private function determineNextNode($request, $currentNode) {
// 核心:根据当前节点 type 和 config 解析下一步
if ($currentNode->type == 'condition') {
$rules = json_decode($currentNode->config_json, true)['rules'];
$formData = json_decode($request->form_data, true);
// 解析条件逻辑,判断走哪个分支(例如走true分支还是false分支)
// 通过 parent_node_id 或分支字段来找到对应子节点
$nextOrder = $this->evaluateCondition($rules, $formData)
? $currentNode->true_node_id
: $currentNode->false_node_id;
return ApprovalNodes::find($nextOrder);
} else {
// 正常顺序流
return ApprovalNodes::where('flow_id', $currentNode->flow_id)
->where('node_order', $currentNode->node_order + 1)
->first();
}
}
}
大屏渲染层(后端数据提供)
- 路由:
/api/dashboard/{dashboardId}/data(支持轮询或WebSocket) - 控制器:
DashboardDataController
class DashboardDataController {
public function getData($dashboardId) {
$components = DashboardComponents::where('dashboard_id', $dashboardId)->get();
$result = [];
foreach ($components as $component) {
// 根据 component->source_config 的不同 type 执行不同查询
if ($component->source_config['data_source'] == 'predefined_query') {
$queryKey = $component->source_config['query_key'];
$filterParams = $component->source_config['filter_params'];
$data = $this->executePredefinedQuery($queryKey, $filterParams);
} elseif ($component->source_config['data_source'] == 'sql') {
// 注意极其危险!必须严格过滤用户输入的SQL片段,最好只允许选择预定义字段
$sql = $component->source_config['sql'];
// 使用参数绑定执行
$data = DB::select($sql, [$dashboardId]);
}
$result[] = [
'component_id' => $component->id,
'type' => $component->type,
'style' => $component->style_config,
'data' => $data
];
}
return response()->json($result);
}
// 安全的预定义查询示例
private function executePredefinedQuery($key, $params) {
switch ($key) {
case 'approval_status_distribution':
$start = now()->subDays($params['start_date'] ?? 30);
$end = now()->addDays(1);
return DB::table('approval_requests')
->select('status', DB::raw('count(*) as count'))
->where('created_at', '>=', $start)
->where('created_at', '<=', $end)
->groupBy('status')
->get();
// ...
}
}
}
第四层:前端封装(用户交互关键)
用户自定义的核心体验在于前端,后端 PHP 只需提供配置数据。
- 工作流设计器: 使用 vue-flow / react-flow / LogicFlow 类似的开源库,配合
approval_nodes的 type 和 config_json,让用户能:- 拖出一个“审批节点”。
- 点击节点,弹出一个PHP渲染的表单(或纯前端Vue组件)配置
config_json(比如选择审批人角色、超时时间)。
- 大屏配置器: 使用 GridStack / React-Grid-Layout 实现拖拽布局。
- 用户添加“饼图组件”。
- 选择数据源(如:选择预定义的
按状态统计审批单数查询,并从status字段映射到name)。 - 保存时,前端将
x,y,w,h和source_config传给 PHP 存入dashboard_components表。
- 大屏展示页: 静态页面,使用 ECharts / AntV 等图表库。
- 启动时调用
GET /api/dashboard/{id}/data。 - 根据返回的
type和data,实例化对应图表。 - 通过
setInterval( refreshInterval )定期重新请求数据,实现动态效果。
- 启动时调用
安全性考量(极其重要)
当涉及“用户自定义SQL”或“用户自定义条件”时,安全风险极高。
- 绝对禁止直接执行用户输入的SQL。 只能执行预定义的、硬编码在PHP中的查询方法(如
executePredefinedQuery方法中的switch语句),用户只能选择“用哪个查询”和“映射哪些字段”。 - 条件规则必须在前端和后端双重校验。 用户在
config_json里保存的{"rules": [{"field": "amount", "operator": ">", "value": 10000}]},在运行时,PHP 必须有一个白名单,只允许field是预设的安全字段(如amount,department_id),operator只允许>,<, ,in等,防止 SQL 注入。 - 配置数据缓存: 这类配置通常变化不频繁,将用户的配置(
approval_nodes,dashboard_components)缓存到 Redis 或 Memcached 中,减少数据库查询,提升大屏加载速度。
实现 PHP 用户自定义审批大屏,核心思路是:
用数据结构(表 + JSON)定义规则,用引擎(PHP 类)解析规则,用组件(前端库)渲染配置。
- 后端(PHP): 负责存储配置、解析流程走向(引擎)、提供图表数据(安全预定义查询)、维护用户权限。
- 前端(JS): 负责将配置以图形化的形式可视化给用户拖拽配置,以及最终将数据渲染成图表。
这种架构下,PHP 更多扮演配置管理器和安全的数据查询网关的角色,而非硬编码逻辑。