ThinkPHP6 后台统计与权限实战:从表设计到图表
PHP先给结论:ThinkPHP6 后台做统计,别每次都实时 count 全表——按天预先落表(如 visit_stats 存每日 PV/UV、article_view_daily 存文章×日),查的时候只 sum 小表,又快又稳;做权限就上经典五表 RBAC(admin_user/role/permission/role_permission/user_role),再用中间件统一鉴权,别在控制器里到处写 check。本文用本博客真实的访问统计模块当例子,从表设计讲到出数再到权限落地。
一、统计模块:实时聚合 vs 预先按天落表
- 实时聚合:每次看报表都
Db::name('visits')->count()。数据小还行,量大就慢,还把主表锁死。 - 预先按天落表(推荐):访问发生时往「日表」append 一行/加一,看报表时只查日表。本博客就是这么干的——
visit_stats记站点每日 PV/UV,article_view_daily记文章维度每日阅读。
一句话:写时增量累加,读时只查聚合小表,把重活分摊到平时,报表查询才会快。
二、表设计与字段
| 表 | 用途 | 核心字段 |
|---|---|---|
| visit_stats | 站点每日 PV/UV | date、pv、uv(按 date 唯一,每天一行) |
| article_view_daily | 文章×日 阅读 | article_id、date、views(文章 Top10 数据源) |
注意职责分离:访问记录列表(visits,按 IP 聚合)、站点每日统计(visit_stats)、文章 Top10(article_view_daily)三张表互不直接牵连,清空访问列表时绝不误清另外两张——这点本博客踩过坑,已在 D13 部署篇强调。
三、出数:模型 + 查询构造器
// 今日 PV/UV:直接读日表,不再 count 主表
$today = date('Y-m-d');
$row = Db::name('visit_stats')->where('date', $today)->find();
$todayPv = $row['pv'] ?? 0;
$todayUv = $row['uv'] ?? 0;
// 累计天数:从最早一条到今天
$first = Db::name('visit_stats')->order('date asc')->value('date');
$days = $first ? (int)ceil((time() - strtotime($first)) / 86400) : 0;
// 文章 Top10:按文章聚合并倒序
$top = Db::name('article_view_daily')
->field('article_id, sum(views) as total')
->group('article_id')
->order('total desc')
->limit(10)
->select();
要点:聚合结果已经躺在日表里,查询只是简单读取和 sum,再也不用每次扫描全量访问记录。
四、RBAC 五张表
后台权限用 RBAC(基于角色的访问控制),五张表是地基:
| 表 | 关键字段 | 作用 |
|---|---|---|
| admin_user | id, username, password(用 password_hash), status | 后台用户 |
| role | id, name, slug | 角色(如 超级管理员 / 运营) |
| permission | id, name, slug, type, path | 权限节点,type 区分「菜单」和「按钮/接口」 |
| role_permission | role_id, permission_id | 角色与权限多对多 |
| user_role | user_id, role_id | 用户与角色多对多 |
关键设计:permission 的 type 字段把「菜单」和「按钮/接口」分开——菜单用来渲染侧边栏,接口/按钮用来在中间件里校验,别混在一张表里否则会「看得见菜单点不开」或「进得去页面按钮不显示」。
五、中间件统一鉴权
别在每个控制器方法里写 checkAuth,漏一个就是越权漏洞。把鉴权收口到中间件:
// app/admin/middleware/CheckPermission.php(骨架思路)
public function handle($request, \Closure $next) {
$user = $request->userInfo; // 由登录中间件注入
if (!$user) return json(['code' => 401, 'msg' => '未登录'], 401);
// 合并用户所有角色的权限(原生 Auth::check 只认第一个角色,是坑)
$roleIds = Db::name('user_role')->where('user_id', $user['id'])->column('role_id');
$ruleIds = Db::name('role_permission')->whereIn('role_id', $roleIds)
->where('status', 1)->column('permission_id');
$slugs = Db::name('permission')->whereIn('id', $ruleIds)->column('slug');
$route = $request->rule()->getName(); // 用路由名而非 URL 路径
if (!in_array($route, $slugs)) {
return json(['code' => 403, 'msg' => '无权限'], 403);
}
return $next($request);
}
两个易错点:多角色要自己合并权限(别依赖只查第一个角色的原生方法);用路由名匹配而不是 URL 字符串(URL 带参数和入口文件,不稳定)。
六、五个常见坑
| 坑 | 原因 | 规避 |
|---|---|---|
| count 不吃全局作用域 | 模型 baseQuery 对 count() 不生效 | 封装 tenantCount 之类方法强制拼全条件 |
| 权限缓存错乱 | 菜单改了缓存还挂旧数据 | 缓存 key 带角色/用户更新时间戳,变更即失效 |
| 菜单高亮错位 | 用 URL 字符串全等匹配 | 用 request()->rule()->getName() 路由名匹配 |
| 多角色只认第一个 | 原生 Auth::check 单角色路径 | 自己查全部 role_id 再合并 rule_id |
| 统计越清越慢 | 每次实时 count 主表 | 按天落表,读时只查聚合小表 |
七、FAQ
Q:统计一定要按天落表吗? 量小可实时,量一大就必须预聚合。本博客的访问统计就是按天落表,报表查询才稳。
Q:菜单权限和按钮权限要分表吗? 不必分物理表,但 permission 表一定要加 type 字段区分「menu / action」,渲染和校验各取所需。
Q:缓存用 file 还是 redis? 开发用 file 方便,但改菜单/权限后记得清缓存;生产建议 redis,失效更可控。
Q:和 D13 上线部署什么关系? D13 讲怎么把 ThinkPHP6 部署上线;本篇是上线后「后台怎么出数、怎么管权限」的实战,正好衔接。
小结
ThinkPHP6 后台两件大事:统计靠「写时增量、读时查聚合小表」(visit_stats / article_view_daily 这套),权限靠五表 RBAC + 中间件统一鉴权,多角色权限自己合并、用路由名匹配。先把表设计和中间件收口做好,后面加菜单、加接口都是填表的事,不会越改越乱。具体表字段与中间件写法以 ThinkPHP6 官方文档和你所用版本为准。