ThinkPHP6 后台统计与权限实战:从表设计到图表

先给结论: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/UVdate、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_userid, username, password(用 password_hash), status后台用户
roleid, name, slug角色(如 超级管理员 / 运营)
permissionid, name, slug, type, path权限节点,type 区分「菜单」和「按钮/接口」
role_permissionrole_id, permission_id角色与权限多对多
user_roleuser_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 官方文档和你所用版本为准。

相关推荐

相关推荐

评论(0)
发表评论