应将菜单/按钮权限预加载至redis并按用途分key缓存,数据权限缓存规则结构而非实时数据,用join替代in避免全表扫描,一次性查询菜单后php构建树,设置差异化过期时间并确保角色变更时及时失效对应缓存。

为什么后台列表页总在查权限表?
因为默认每次请求都走一遍 AuthRule 查询 + 权限判断逻辑,尤其当用户角色多、菜单嵌套深、数据权限规则复杂时,单次请求可能触发 5–10 次数据库查询。这不是代码写得烂,是没把「权限判定」和「数据过滤」拆开缓存。
实操建议:
- 把「用户能访问哪些菜单/按钮」(即路由/操作级权限)预加载进 Redis,key 设为
auth:menu:{user_id},有效期设为 24 小时;登录后一次性查完并序列化存储 - 「数据级权限」(比如只看本部门订单)不能全缓存,但可缓存规则结构:例如
data_rule:{role_id}存的是 SQL 片段或字段映射配置,而非实时数据 - 避免在
index()方法里调用checkRule()多次——改用一次$this->auth->getMenuList()拿全量,再用 PHP 数组过滤
数据权限 SQL 怎么拼才不拖慢列表查询?
常见错误是把权限条件硬塞进 where,比如 where('dept_id', 'in', $deptIds),结果导致 MySQL 无法命中索引,尤其是 $deptIds 是几十个 ID 时,执行计划变成全表扫描。
实操建议:
- 优先用关联表+JOIN 过滤,比如加一张
user_dept_access表,查询时join后直接on u.id = uda.user_id,比in更易走索引 - 若必须用
in,确保$deptIds是整数数组且已去重,避免传入字符串型 ID 导致隐式转换失效 - ThinkPHP 6 的
withWhere()不支持动态绑定权限条件,老老实实用whereRaw()拼接,但记得用str_replace()过滤掉单引号防注入
Redis 缓存权限数据时 key 设计容易踩什么坑?
最典型的是把所有权限塞进一个大 key,比如 auth:all:{user_id},表面省事,实际一改权限就得删整个 key,连带清掉菜单、按钮、数据规则,导致用户下次点任意页面都卡顿一秒。
实操建议:
- 按用途拆 key:
auth:menu:{user_id}、auth:button:{user_id}、auth:data:{role_id},更新时只删对应 key - 别用
serialize()存数组,改用json_encode(),Redis 中更易调试,也兼容其他语言服务读取 - 设置过期时间要区分场景:菜单类缓存设 86400(24h),数据规则类缓存设 3600(1h),避免规则改了但前端还显示旧数据
预加载菜单树时如何避免 N+1 查询?
ThinkPHP 默认的 tree() 或递归查子节点,会在循环中反复查 pid = ?,10 级菜单可能触发 30+ 次查询。不是 ORM 的锅,是没意识到「树形结构」该一次性拉平处理。
实操建议:
- 用
Db::name('auth_rule')->order('sort asc')->select()一次性查出全部菜单,再用 PHP 构建树(推荐thinkphp/helper的Collection::tree()) - 如果菜单量超 200 条,加个
status = 1条件过滤掉禁用项,别等 PHP 层再筛 - 不要在模板里用
{:action('getMenuTree')},这会触发额外请求;改成在控制器 assign 一个已处理好的$menuTree变量
权限缓存不是加个 Redis 就完事,关键在「哪些该缓、缓多久、怎么更新」——数据权限那块最容易被当成黑盒跳过测试,上线后才发现销售经理能看到财务报表,问题就出在 auth:data:{role_id} 缓存没随角色变更自动失效。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











