thinkphp 6 实现菜单树形结构需一次查全菜单数据并用 php 组装树,结合用户权限白名单过滤及按用户 id 缓存,确保轻量、高效、稳定。

ThinkPHP 6 实现接口菜单树形结构,核心在于把菜单数据从数据库中按层级关系正确取出、组装,并与权限系统联动,而不是靠前端硬编码或递归查库。重点不是“能不能做”,而是“怎么做得轻、快、稳”。
菜单数据必须带 type=menu 且字段语义清晰
TP6 的 RBAC 不自带菜单模型,得自己扩展 auth_rule 表: - 新增字段 type,值为 menu(区别于 url 或 button) - name 用英文点号分隔的唯一标识,如 api.order.list、api.order.detail,便于前缀匹配和权限校验 - title 存中文名,如“订单列表”“订单详情”,供接口返回给前端渲染 - pid 指向上级菜单 ID,根菜单 pid = 0 - 可选扩展 icon、sort、is_show 字段,用于控制显示顺序和图标
一次查全 + PHP 组装树,避免 N+1
别在控制器里循环查子菜单。正确做法是: - 用一条 SQL 查出所有 type = 'menu' 且 status = 1 的记录 - 查询时加 ORDER BY pid ASC, sort ASC,确保父节点一定排在子节点前面 - 用 PHP 索引映射组装树结构,不依赖递归函数:
- $map[$id] = $item 缓存每条记录引用
- pid 为 0 的,直接推入 $tree
- 其余的,追加到 $map[$item['pid']]['children'] 数组中
接口返回前,先过滤用户可见菜单
菜单显隐 ≠ URL 权限校验。不要对每个菜单项调用 Auth::check(),开销太大。应该: - 登录后,根据当前 user_id 获取其全部有效菜单 rule_id 集合(缓存键建议为 auth_rule_list_user_{$uid}) - 把组装好的完整菜单树,用这个 ID 集合做白名单过滤 - 只保留用户有权限的节点及其祖先路径(保证父菜单不空悬)
缓存策略要区分用户,不能只按角色缓存
TP6 原生 Auth 缓存键只含角色 ID,多个用户共用同一角色但菜单不同(比如财务岗 A 能看报表,B 不能),就会串数据。必须: - 重写 getRuleList() 方法,缓存键带上 $userId - 用户修改角色或菜单权限后,主动清除 auth_rule_list_user_{$uid} - 树形菜单组装结果也可单独缓存,5–10 分钟足够,减少重复计算











