beego中递归查询菜单树应使用getmenu(pid)而非全表加载,因后者易致cpu飙升;须用指针切片children []*treelist、权限过滤前置、空route需后端统一处理。

Beego 中递归查询菜单树必须用 getMenu(pid) 而不是一次性查全表再组装
很多人一上来就 QueryTable("auth_menu").All(&menus) 拿全量数据,然后在内存里用 map 建父子关系——这在测试环境看似可行,但上线后容易因菜单层级深、并发高导致 CPU 占用飙升。Beego ORM 的 Filter("pid", pid) + 递归调用才是稳定解法。
关键点在于:数据库已建好索引(KEY `pid` (`pid`)),每次只查某一级的直接子节点,IO 和内存压力可控;而全表加载再遍历,既浪费带宽,又让 Go runtime 频繁分配 slice 内存。
-
getMenu(0)入口必须传pid=0(或NULL,取决于你数据库中根节点的约定) - 递归终止条件靠
QueryTable(...).Filter("pid", pid).All()返回空 slice 自然结束,不用额外判空 - 如果菜单有「排序字段」如
sort,务必加.OrderBy("sort"),否则前端渲染顺序错乱
TreeList 结构体里 Children []*TreeList 必须是指针切片
Beego ORM 查询结果是值拷贝,如果定义成 Children []TreeList(非指针),递归赋值时子节点会复制一份新结构体,导致父节点里的 Children 实际指向的是副本,最终返回的树形数据里 Children 全为空。
正确写法必须是 Children []*TreeList,且每次递归构造子节点时用 &TreeList{...} 取地址:
node := &TreeList{
Id: v.Id,
Name: v.Name,
Pid: v.Pid,
Sort: v.Sort,
Route: v.Route,
Children: child, // child 是 []*TreeList 类型
}
- Beego 的
All()方法不会自动处理嵌套结构体的指针关系,必须手动控制引用 - 前端接收 JSON 时,只要字段名匹配(如
children对应Children),就能正常解析多层嵌套 - 别为了省一个
*改成值类型——这是 Beego + 树形结构最常踩的坑
权限过滤必须在递归前做,不能等树建完再裁剪
用户没有权限的菜单项,不是“前端不显示”就能解决的。如果后端把整棵树都返回,只是前端用 JS 过滤掉无权菜单,攻击者可直接调用接口拿到全部菜单路径,暴露系统结构。
正确做法是在每次 getMenu(pid) 查询前,先查出当前用户角色拥有的权限 ID 列表,然后在 QueryTable 中加权限过滤:
o.QueryTable("auth_menu").
Filter("pid", pid).
Filter("id__in", permIDs). // permIDs 来自用户角色关联的权限集合
OrderBy("sort").
All(&menu)
-
permIDs应该从RolePermission或类似中间表预查出来,缓存在 request context 或 local var 中,避免递归里反复查库 - 注意 Beego ORM 的
__in语法要求传入的是[]interface{},不是[]int,需做类型转换 - 如果菜单表本身有
status字段(启用/禁用),记得叠加Filter("status", 1)
前端渲染时 route 字段为空的菜单项要特殊处理
像「设置」这种一级分组菜单(route="" 或 NULL),后端返回的 route 是空字符串,但前端 Vue/React 路由跳转时会报错或白屏。不能靠前端判断 if (!route) return null 就完事。
更稳妥的做法是在 Beego 层统一补默认值或标记类型:
- 在
TreeList结构体里加一个IsGroup bool字段,递归时根据route==""自动设为true - 或者后端返回时把空
route替换为"#",前端用v-if="item.route !== '#'"控制可点击性 - 千万别让前端去猜哪个是分组菜单——菜单配置可能随时增减,逻辑必须收口在后端
多级菜单树的难点不在递归本身,而在权限裁剪和空路由的语义一致性。这两个地方一旦松动,轻则菜单错位,重则越权访问路径泄露。











