菜单接口必须做权限过滤、树形转换、高亮标记三件事:先用where('status',1)和where('is_del',0)过滤禁用与删除项;再buildTree转树形并逐层usort排序;最后基于路由名匹配设置is_active,且仅对已授权菜单生效。

菜单数据接口不能只返回原始列表,必须做权限过滤、树形转换、高亮标记三件事,否则前端拿到的是全量脏数据,既不安全也不可用。
ThinkPHP 6 菜单接口必须用 where('status', 1) 过滤禁用项
数据库里 menu 表的 status 字段是开关,0 表示禁用。常见错误是查完就直接 json($list) 返回,结果被禁用的菜单在前端仍能点开或显示高亮。
- 务必在查询时加上
where('status', 1),且该条件要在关联权限前执行,避免无效数据进入后续流程 - 如果用了软删除(
is_del字段),还得加where('is_del', 0) - 不要依赖前端 JS 隐藏 —— 这等于把权限逻辑暴露给用户,有越权风险
菜单接口返回前必须 buildTree 转树形结构
数据库查出来是一维数组,但左侧导航需要嵌套层级。直接 for 循环渲染会导致父子菜单平铺、无法折叠,也丢失层级语义。
- ThinkPHP 6 没内置
buildTree(),需自己写递归函数或用引用方式组装,核心是按pid归类子节点 - 每层子菜单排序不能只靠数据库
ORDER BY sort,树形生成后要对每个children数组单独usort(),否则二级菜单顺序错乱 - 若菜单深度超过 4 层,建议加
level字段限制最大嵌套,防前端渲染卡顿
接口需返回当前路由匹配状态,而非让前端算 active
高亮逻辑放前端容易出错:URL 含参数、入口文件名、查询字符串都会干扰匹配;更严重的是,没权限的菜单也可能被误标为 active。
立即学习“PHP免费学习笔记(深入)”;
- 后端应在菜单数组每个节点加
is_active字段,值为布尔型,判断依据是str_starts_with(request()->rule()->getName(), $menu['url']) -
$menu['url']必须存路由名(如admin/user/list),不是完整 URL 或带参数路径 - 必须在计算
is_active前完成权限校验,即:仅当用户有该菜单权限,且路由名匹配,才设为 true
别把权限校验甩给前端,接口必须只返回已授权菜单
很多项目图省事,在接口里查出全部菜单,再让前端用 JS 过滤掉没权限的。这等于把 auth_rule 表结构和规则名全暴露出去,攻击者可直接构造请求绕过。
- 正确做法是:先查用户角色拥有的菜单 ID 列表(从
role_menu或auth_rule表),再用这些 ID 去menu表中 in 查询 - 如果用了 path 字段(如
"0-1-5-12"),可用WHERE path LIKE '0-1-%' AND id IN (...)一次查出整棵子树 - 缓存 key 必须包含角色 ID 和菜单版本号(如
menu_tree_{$roleId}_v20260424),改菜单后不清缓存,前端永远看不到新结构
最易被忽略的一点:菜单接口的响应数据里,url 字段不是用来跳转的,而是用于路由名匹配和权限比对的标识符;真正跳转地址应由前端根据路由系统动态 resolve,而不是拼接 url 字段值。硬编码跳转路径会破坏多应用、子目录部署等场景。



















