AuthRule表需同时支持权限与菜单,推荐字段:id、pid、title、name(唯一标识)、path、component、icon、sort、status、is_menu;菜单树须先查用户授权name列表,再按is_menu=1且在授权内筛选并递归组装。

ThinkPHP 6 的 AuthRule 表怎么设计才支持动态菜单
菜单不是写死在模板里的,必须从数据库读取并按用户权限过滤 —— 这就要求 AuthRule 表结构得同时承载「权限规则」和「菜单项」两类语义。常见错误是只存 URL(如 /admin/user/list),结果菜单渲染时缺图标、排序、父级关系,前端根本拼不出树形。
推荐字段至少包含:id、pid(0 表示顶级)、title(菜单名)、name(唯一标识,如 user:list 或 admin.user.index)、path(前端路由路径,如 /user)、component(对应 Vue 组件路径)、icon、sort、status(是否启用)、is_menu(布尔,0=纯权限点,1=显示为菜单)。
关键点:is_menu = 1 的记录才参与菜单生成;name 必须全局唯一,且建议用「模块.控制器.方法」或「资源.动作」风格,方便后续做 RBAC 权限校验。
如何用 think\Model 递归查出带权限过滤的菜单树
不能直接 select() 全量再 PHP 层筛 —— 用户没权限的菜单项,连数据都不该查出来,否则有信息泄露风险(比如看到“删除订单”菜单但点不开,反而暴露了功能存在)。
立即学习“PHP免费学习笔记(深入)”;
正确做法:先查出当前用户所有已授权的 name 列表(从 AuthRoleAccess + AuthRole 关联查),再用 where in 筛 AuthRule 中 is_menu = 1 且在授权列表内的记录,最后用 PHP 递归组装树。
示例关键逻辑:
$allowedNames = Db::name('auth_role_access')
->alias('a')
->join('auth_role r', 'a.role_id = r.id')
->where('r.status', 1)
->where('a.role_id', $roleId)
->column('a.rule_name'); // 注意:rule_name 应与 AuthRule.name 对齐
$menuList = Db::name('auth_rule')
->where('is_menu', 1)
->where('status', 1)
->where('name', 'in', $allowedNames)
->order('sort ASC')
->select();
$menuTree = $this->buildMenuTree($menuList->toArray()); // 自定义递归函数
注意:buildMenuTree() 要按 pid 归类,别漏掉根节点(pid = 0);如果后端返回给前端的是扁平数组,那前端也得有对应解析逻辑,别指望后端包办一切。
think-auth 扩展和自己手写权限中间件,哪个更适合动态菜单
如果你用的是 ThinkPHP 6,think-auth 是最省事的方案 —— 它自带 AuthRule 模型和菜单生成方法(getMenu()),但默认不区分「权限」和「菜单」,需要你手动加 is_menu 字段并重写查询条件。
自己写中间件更可控,尤其当菜单要联动系统配置(比如 SaaS 多租户下菜单可开关)或需对接前端微前端路由时。但要注意两点:
- 权限校验中间件(如检查
user:delete)和菜单生成逻辑必须共用同一套name命名规则,否则会出「能看菜单但点不开」或「能操作但没菜单入口」的问题 - 菜单接口务必加缓存(如 Redis),键名建议含
$roleId和timestamp(关联菜单更新时间),避免每次请求都查库
别迷信扩展,think-auth 的 getMenu() 默认不走权限过滤,直接返回全部 is_menu=1 的项 —— 这个坑很多人踩过。
前端拿到菜单数据后,为什么 router.addRoute() 动态注册总失败
ThinkPHP 后端只管吐 JSON,但前端 Vue Router 的 addRoute() 对路由格式很敏感:组件路径(component 字段)必须是函数形式(() => import('@/views/User.vue')),不能是字符串;嵌套路由的 children 必须显式声明,不能靠 path 自动推导。
所以后端返回的菜单数据里,component 字段建议存相对路径字符串(如 "User/index"),前端统一映射为动态 import:
const componentMap = {
'User/index': () => import('@/views/User/Index.vue'),
'Order/list': () => import('@/views/Order/List.vue'),
};
另外,动态添加的路由不会触发 router.beforeEach,首次进入新菜单页前,得手动调用 router.push() 触发导航守卫 —— 否则权限校验逻辑压根不执行。
最易忽略的一点:菜单数据中若存在重复 name(比如两个菜单都叫 user:list),addRoute() 会静默失败,控制台无报错,页面白屏。务必保证后端返回的每条菜单 name 唯一。



















