如何用PHP项目实现用户自定义审批看板?一篇从零到实战的完整指南
目录导读
- 为什么需要自定义审批看板?
- 系统架构设计要点
- 数据库表结构设计(含核心字段)
- PHP后端核心实现逻辑
- 前端动态看板渲染方案
- 常见问题问答(FAQ)
- 性能优化与安全建议
为什么需要自定义审批看板?
传统OA系统中的审批看板往往是固定布局,用户只能被动查看“待办”、“已办”、“抄送”等预设分类,但在实际业务中,不同部门、不同角色的审批关注点差异巨大——

财务总监希望一眼看到“金额大于5万的待审批报销单”,而项目经理则想汇总“本周所有待审批的采购申请”。固定看板无法满足这种差异化需求。
通过PHP实现用户自定义审批看板,核心价值在于:
- 用户可自由组合筛选条件(审批类型、时间范围、金额阈值、申请人等)
- 支持拖拽调整看板卡片布局
- 看板配置自动保存,跨设备同步
本文将从数据库设计、PHP业务逻辑编码、前端交互三个维度,给出可落地的完整实现方案。
系统架构设计要点
采用 PHP(ThinkPHP/Laravel)+ MySQL + Redis + Vue.js 的组合:
| 层级 | 技术选择 | 职责 |
|---|---|---|
| 数据层 | MySQL(InnoDB) | 存储用户配置、审批单据数据 |
| 缓存层 | Redis | 加速看板配置读取,存储临时筛选结果 |
| 业务层 | PHP 8.x | 解析用户配置、动态构建SQL查询 |
| 展示层 | Vue 3 + SortableJS | 拖拽式看板布局管理与实时数据渲染 |
关键设计原则:
- 看板配置以JSON格式存储,而非固化字段,便于扩展
- 所有筛选条件由后端动态解析,防止SQL注入
- 使用队列异步更新看板数据(如新审批单出现时自动推送)
数据库表结构设计(含核心字段)
表1:审批单据主表 approval_records
CREATE TABLE `approval_records` ( `id` int(11) NOT NULL AUTO_INCREMENT, varchar(200) DEFAULT NULL COMMENT '审批标题', `type` varchar(50) DEFAULT NULL COMMENT '审批类型(报销/请假/采购等)', `applicant` varchar(50) DEFAULT NULL COMMENT '申请人', `amount` decimal(10,2) DEFAULT '0.00' COMMENT '涉及金额', `status` tinyint(4) DEFAULT '0' COMMENT '0待审 1通过 2驳回', `create_time` datetime DEFAULT NULL, `finish_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_type` (`type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表2:用户看板配置表 user_dashboard
CREATE TABLE `user_dashboard` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `board_config` json DEFAULT NULL COMMENT '看板配置JSON', `is_default` tinyint(1) DEFAULT '0' COMMENT '是否默认看板', `updated_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_default` (`user_id`, `is_default`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
JSON配置示例(board_config字段):
[
{
"name": "高额待审单",
"position": 1,
"filters": {
"status": 0,
"amount_min": 50000
},
"columns": ["title", "applicant", "amount", "create_time"],
"sort": { "field": "amount", "order": "DESC" }
}
]
PHP后端核心实现逻辑
1 保存看板配置(伪代码核心片段)
// 路径:/api/dashboard/save
public function saveConfig(Request $request)
{
$userId = auth()->id();
$config = $request->input('board_config'); // 前端传来的JSON
// 校验JSON格式合法性
if (!$this->isValidConfig($config)) {
return response()->json(['code' => 400, 'msg' => '配置格式错误']);
}
// 存入数据库,使用upsert避免重复
Dashboard::updateOrCreate(
['user_id' => $userId, 'is_default' => 1],
['board_config' => json_encode($config), 'updated_at' => now()]
);
// 清理用户看板缓存
Cache::forget("dashboard:user:{$userId}");
return success('保存成功');
}
2 动态生成看板数据(核心难点)
// 路径:/api/dashboard/render
public function render(Request $request)
{
$userId = auth()->id();
$cacheKey = "dashboard:user:{$userId}";
// 优先从Redis缓存读取
$cached = Cache::remember($cacheKey, 300, function () use ($userId) {
$dashboard = Dashboard::where('user_id', $userId)
->where('is_default', 1)
->first();
if (!$dashboard) return [];
$cards = json_decode($dashboard->board_config, true);
$results = [];
foreach ($cards as $card) {
// 动态拼接查询条件
$query = ApprovalRecord::query();
if (!empty($card['filters']['status'])) {
$query->where('status', $card['filters']['status']);
}
if (!empty($card['filters']['amount_min'])) {
$query->where('amount', '>=', $card['filters']['amount_min']);
}
// 此处可扩展更多动态条件...
$results[] = [
'name' => $card['name'],
'count' => $query->count(),
'list' => $query->limit(20)->get(),
'columns' => $card['columns']
];
}
return $results;
});
return response()->json($cached);
}
安全性关键点:$card['filters']中的字段名必须做白名单校验,防止用户传入delete from user等危险字符。
前端动态看板渲染方案
使用 Vue 3 + SortableJS 实现拖拽排序,核心步骤:
// 拖拽结束事件
const onDragEnd = (evt) => {
const newOrder = [...cards.value];
const [movedItem] = newOrder.splice(evt.oldIndex, 1);
newOrder.splice(evt.newIndex, 0, movedItem);
// 更新排序字段
newOrder.forEach((item, index) => item.position = index + 1);
cards.value = newOrder;
// 自动保存
saveConfigToServer(newOrder);
};
// 过滤条件实时切换
const handleFilterChange = (cardIndex) => {
// 调用后端API重新获取该卡片数据
axios.get('/api/dashboard/card-detail', {
params: { filters: cards.value[cardIndex].filters }
}).then((res) => {
cards.value[cardIndex].list = res.data;
});
};
前端展示效果:
- 每个看板卡片可独立拖动调整位置
- 卡片右上角提供“编辑过滤条件”功能弹窗
- 支持展开/折叠卡片内容
常见问题问答(FAQ)
Q1:看板配置JSON太复杂,普通用户不会配置怎么办?
A:提供可视化筛选器组件——用户点击“添加过滤条件”,选择字段、操作符(大于/等于/包含等)、输入值,系统自动生成JSON,相当于用UI操作替代手写JSON。
Q2:每次渲染都要查所有卡片数据,性能扛不住怎么办?
A:采取“懒加载+分页”策略:
- 页面首次只加载每个卡片的数量(count)
- 点击展开卡片时,再请求具体列表数据(limit 20)
- 对于超大数据量,提前在Redis中存储各过滤条件的统计结果
Q3:如何保证不同用户看到的看板数据权限正确?
A:查询时强制加上 where('create_user_id', auth()->id()) 或根据角色过滤,且后端不做“全局查询”,每个SQL都绑定了当前用户上下文。
性能优化与安全建议
| 优化方向 | 具体措施 |
|---|---|
| 数据库 | 为type、status、create_time建立联合索引;对于金额查询使用BETWEEN代替>=提高命中率 |
| 缓存 | 看板配置缓存1200秒,卡片统计缓存300秒;使用Redis的HASH结构存储,手动处理缓存雪崩 |
| 防注入 | 所有用户传入的字段名必须通过whiteList校验,值使用参数绑定 |
| 防误操作 | 同一用户最多保存5个自定义看板;删除看板前二次确认 |
扩展建议:
- 如果公司有AI能力,可以接入“智能推荐看板”——分析用户上次查看记录,自动生成建议看板
- 支持看板模板市场:管理员上传标准看板模板,用户一键套用
通过以上方案,一个完全由用户定义、满足个性化需求的审批看板即可在PHP项目中顺利落地,关键不在于框架本身,而在于灵活的配置存储结构与安全的动态查询解析引擎。