在PHP项目中实现用户自定义审批统计:从架构到实战
目录导读
-
为什么需要用户自定义审批统计?

-
核心架构设计思路
-
数据库表结构搭建
-
动态统计引擎的实现
-
前端交互与可视化
-
性能优化与缓存策略
-
常见问题与解决方案(Q&A)
-
总结与最佳实践
为什么需要用户自定义审批统计?
在企业级应用中,审批流程往往不是一成不变的,不同部门、不同层级的管理者可能需要从不同维度“看”审批数据:人力看考勤审批通过率,财务看报销周期,销售看合同审批延迟率,如果写死统计SQL,每次需求变更都要改代码,导致开发资源浪费。用户自定义审批统计正是为了解决这种“千人千面”的数据分析需求。
核心价值:
- 降低重复开发成本
- 提升业务部门自主分析能力
- 支持灵活的权限控制
核心架构设计思路
实现自定义审批统计,关键在于 “将统计条件、统计指标与代码逻辑解耦”,推荐采用以下分层架构:
┌─────────────┐
│ 前端配置层 │ (拖拽式配置统计条件)
├─────────────┤
│ 动态规则层 │ (解析配置,生成查询参数)
├─────────────┤
│ 数据查询层 │ (基于参数动态组装SQL)
├─────────────┤
│ 缓存加速层 │ (Redis/Memcached)
└─────────────┘
设计原则:
- 统计字段、筛选条件、聚合函数全部可配置
- 统计结果以JSON/数组形式返回,前端自由渲染
- 支持保存统计模板,方便复用
数据库表结构搭建
必要的三张核心表(以MySQL为例):
-- 审批主表 CREATE TABLE `approval_records` ( `id` int(11) NOT NULL AUTO_INCREMENT, `type` varchar(50) NOT NULL COMMENT '审批类型:leave, reimbursement, contract', `applicant_id` int(11) NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待审 1通过 2驳回', `amount` decimal(10,2) DEFAULT NULL, `created_at` datetime NOT NULL, `processed_at` datetime DEFAULT NULL, `duration_hours` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_type_status` (`type`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 统计配置表 CREATE TABLE `stat_configs` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '所属用户', `config_name` varchar(200) NOT NULL COMMENT '统计名称', `select_fields` json NOT NULL COMMENT '要查询的字段列表', `filter_conditions` json NOT NULL COMMENT '筛选条件', `group_by` json DEFAULT NULL COMMENT '分组字段', `aggregate_functions` json DEFAULT NULL COMMENT '聚合函数配置', `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 统计结果缓存表 CREATE TABLE `stat_results_cache` ( `config_id` int(11) NOT NULL, `result_data` json NOT NULL, `expired_at` datetime NOT NULL, PRIMARY KEY (`config_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计要点:
- JSON字段存储配置,支持动态字段扩展
filter_conditions结构示例:[{"field":"status","operator":"in","value":[1,2]}]- 权限控制通过
user_id隔离
动态统计引擎的实现
这是核心代码部分,用PHP实现一个动态SQL生成器:
<?php
class DynamicStatEngine {
// 组装查询字段
public function buildSelect($config): string {
$selected = [];
foreach ($config['select_fields'] ?? [] as $field) {
$selected[] = "`$field`";
}
// 处理聚合函数
foreach ($config['aggregate_functions'] ?? [] as $func) {
$selected[] = "{$func['function']}(`{$func['field']}`) AS `{$func['alias']}`";
}
return implode(', ', $selected) ?: '*';
}
// 构建WHERE条件
public function buildWhere($filters): string {
$clauses = [];
foreach ($filters as $filter) {
$field = $filter['field'];
$op = $filter['operator'] ?? '=';
$value = $filter['value'];
switch ($op) {
case 'in':
$values = implode(',', array_map(fn($v) => "'$v'", $value));
$clauses[] = "`$field` IN ($values)";
break;
case 'between':
$clauses[] = "`$field` BETWEEN '{$value[0]}' AND '{$value[1]}'";
break;
default:
$clauses[] = "`$field` $op '$value'";
}
}
return $clauses ? 'WHERE ' . implode(' AND ', $clauses) : '';
}
// 执行统计
public function execute($configId, $userId) {
$config = $this->loadConfig($configId, $userId);
$sql = sprintf(
"SELECT %s FROM approval_records %s %s",
$this->buildSelect($config),
$this->buildWhere($config['filter_conditions']),
$config['group_by'] ? "GROUP BY `{$config['group_by']}`" : ''
);
// 从数据库查询
return $this->db->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
}
特别提醒: 动态拼接SQL务必做好防注入,上述代码基于内部配置生成,如果允许用户输入,必须使用参数化查询或白名单验证。
前端交互与可视化
建议采用前后端分离架构,前端负责:
- 拖拽式配置界面:用户可以拖拽字段到“筛选条件区”“统计字段区”(可参考Ant Design Pro 的配置面板)
- 图表联动:统计结果传递到ECharts或Chart.js渲染柱状图/折线图
- 模板保存:通过API保存配置至
stat_configs表
典型交互流程:
用户选择“审批类型=请假” → 拖拽“状态”到分组 → 选择“计数”聚合
→ 点击“统计” → AJAX POST /api/stat/run 传入配置ID → 返回JSON结果
→ 前端渲染表格+图表
性能优化与缓存策略
当审批数据达到百万级,自定义统计必须考虑性能,优化方案:
- 预聚合表:每天晚上通过Cron Job生成常用维度的汇总数据
- Redis缓存:对同一配置ID的统计结果缓存5-10分钟(缓存时间根据数据实时性调整)
- 索引优化:为筛选条件常用字段(
type,status,created_at)建立联合索引 - 分页加载:>1000条的结果做分页返回,避免内存溢出
缓存实现示例:
public function getCachedResult($configId) {
$cacheKey = "stat_result_{$configId}";
if ($cached = Redis::get($cacheKey)) {
return json_decode($cached, true);
}
$result = $this->execute($configId);
Redis::setex($cacheKey, 300, json_encode($result));
return $result;
}
常见问题与解决方案(Q&A)
Q1:用户配置的统计SQL注入风险如何防止?
A:所有字段名、运算符必须在白名单中定义,不允许用户直接输入SQL片段,使用参数化查询绑定变量值。
Q2:统计结果返回慢,页面打开卡顿怎么办?
A:优先使用缓存;其次对大数据量开启流式查询(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY=false);最后考虑将统计结果存入ElasticSearch做全文检索。
Q3:不同用户看到的审批数据权限不同,如何隔离?
A:在 filter_conditions 自动追加用户的部门/角色过滤条件。WHERE applicant_dept IN('技术部', '产品部'),建议在服务层做权限断言。
Q4:统计配置变更后,如何更新缓存?
A:在更新统计配置时,同时删除对应缓存key(Redis::del("stat_result_{$configId}")),下次查询自动拉取最新数据。
Q5:是否支持跨报表的统计(如请假+报销合并统计)?
A:可以,需要设计数据源表(data_sources),允许用户选择多个审批类型表,通过UNION ALL合并后统计,注意字段名需统一映射。
总结与最佳实践
用户自定义审批统计的设计,本质上是将“统计需求”从代码层提到配置层,遵循以下原则能事半功倍:
- 配置即代码:所有统计规则用结构化数据存储,而非硬编码
- 分层清晰:数据库只存原始数据,聚合计算放在应用层或中间件
- 防御性编程:对所有动态SQL做双重安全检查(白名单+参数化)
- 渐进式优化:先简单实现功能,出现性能瓶颈后再引入缓存、预聚合
最佳实践清单:
- ✅ 提供24种常用聚合函数(COUNT, SUM, AVG, MAX, MIN, COUNT DISTINCT)
- ✅ 支持时间范围筛选(本周/本月/自定义区间)
- ✅ 结果导出为CSV/Excel功能
- ✅ 统计模板的权限共享(高级用户可发布模板给下属使用)
通过以上架构,开发者只需搭建一次统计引擎,后续所有统计需求由业务方自助完成,真正实现降本增效。