菜单权限必须与角色、用户权限联动,需单独建menus表管理标题、图标、排序、父子关系等,通过动态过滤+递归构建+缓存优化菜单树,且菜单显示逻辑须与gate权限校验解耦。

菜单权限需要和角色、用户权限联动,不能只靠 spatie/laravel-permission 默认字段
默认的 spatie/laravel-permission 只管“用户能不能访问某个路由”,但后台菜单是结构化展示需求,必须支持层级(一级菜单/二级菜单)、隐藏控制、排序、图标等。直接用 permissions 表的 name 字段当菜单名会很快失控——比如 users.index 是权限,不是菜单标题;dashboard.view 无法体现它该在顶部还是左侧导航。
常见错误现象:菜单硬编码在 Blade 模板里,改个顺序就得动代码;或者把权限名当菜单名渲染,结果页面显示 posts.create 这种字符串。
- 必须单独建一张
menus表(或扩展现有permissions表),加字段如title、icon、sort、pid、is_hide、route_name -
route_name字段要和 Laravel 路由定义的 name 严格一致(如admin.users.index),用于后续校验权限是否匹配 - 不要复用
permissions.name做菜单显示名,它应专注权限标识(如user.delete),而菜单标题由menus.title管理 - 如果沿用 spatie 扩展包,需手动修改其迁移文件,或新建迁移追加字段,不能依赖 vendor 发布的原始 migration
菜单数据要按角色动态过滤,且缓存用户可访问菜单树
每次请求都查数据库组装菜单树性能很差,尤其当菜单嵌套深、用户角色多时。关键是:用户看到的菜单 = 当前角色拥有的、is_hide = 0 的、且对应路由已被授权的节点。
容易踩的坑:只查 role_has_permissions 关联的权限名,再用这些名去 menus 表里找,却忽略菜单节点本身可能有父子关系,导致子菜单没父级就无法渲染;或者没检查 route_name 是否真实存在路由定义,造成空菜单项。
- 先查出当前用户所有有效权限名(通过
$user->getPermissionNames()) - 再用这些权限名 join
menus表的route_name,得到基础菜单集 - 对结果集做递归构建树(用
pid字段),确保父子关系完整;顶层菜单pid = 0,子菜单指向父id - 缓存整个菜单树(键建议用
"user_{$user->id}_menu_tree"),过期时间设为 1 小时,避免频繁重算 - 超级管理员(
is_super = 1)可跳过权限校验,直接返回全部非隐藏菜单
Gate 和中间件配合路由权限,但菜单显示逻辑必须独立处理
Gate::allows('user.delete', $user) 或 @can('user.delete') 解决的是“能不能操作”,不是“该不该显示”。很多开发者误以为给路由加了 @can 就自动控制菜单可见性,结果用户看不到菜单,却能直接输入 URL 访问——这是权限校验和菜单渲染两个逻辑混为一谈。
典型场景:一个按钮(如“删除”)需要 @can('user.delete') 控制,但整个“用户管理”菜单项是否显示,取决于用户是否有任意一个子权限(如 user.index 或 user.edit)。
- 菜单显示判断逻辑:只要用户拥有该菜单节点下任一子节点的权限,就显示该菜单(含父级)
- 按钮级控制仍用
@can或can()方法,和菜单渲染解耦 - 自定义中间件如
CheckMenuPermission不要替代 Gate,它只负责拦截无权访问的路由,不参与菜单生成 - 路由定义中必须带
name(),否则menus.route_name无法关联,例如Route::get('/users', [UserController::class, 'index'])->name('admin.users.index');
菜单模板渲染要注意递归深度和空节点过滤
Blade 里用 @foreach 递归渲染菜单时,若没控制好层级或没过滤掉无权限子项,很容易出现空白 <dd><ul></ul></dd>,破坏 UI 结构;更糟的是递归没终止条件,导致 PHP 内存溢出。
实际开发中,菜单层级通常不超过 3 层(顶级 → 子项 → 操作项),强行支持无限嵌套反而增加维护成本和前端兼容难度。
- 控制器传入的数据必须是已过滤、已排序、已补全父子关系的扁平数组或嵌套数组,不要把原始
menus表数据直接扔给模板 - 模板中用递归组件(如
@include('admin.menu-item', ['items' => $menuData])),并在组件内加@if($items)判断,避免空列表 - 每个菜单项渲染前检查
route_name是否在用户权限集合里(或该节点下存在任意子权限),否则 skip - 图标字段
icon建议存 class 名(如layui-icon-home),不要存 SVG 或 URL,便于前端统一 CSS 处理











