动态菜单在CodeIgniter中是权限驱动的菜单裁剪,非递归渲染树;菜单表须含permission_code、is_hidden、sort_order字段;查询须用INNER JOIN一次获取授权项并ORDER BY排序;构树禁用递归,应扁平查库+循环限深(如≤4层);url存相对路径,前端跳转前必用site_url()包裹;缓存key须含user_id并支持主动失效;菜单仅作入口提示,访问控制需前后端双校验。

动态菜单在 CodeIgniter 中不是“渲染一棵树”,而是“权限驱动的菜单裁剪”——菜单项是否出现,由 permission_code 与当前用户角色的关联结果决定,不是靠 PHP 递归拼 HTML。
菜单表必须带 permission_code 和 is_hidden 字段
很多项目只建了 id、parent_id、name、url,结果权限一加就崩:普通用户点开菜单看到空列表,或进页面直接 403。真实可用的菜单表至少要包含:
-
permission_code(如user:list、order:export),用于和角色权限表对齐 -
is_hidden(tinyint(1)),显式控制该菜单是否参与前端渲染,避免“有权限但不想暴露入口” -
sort_order(int),数据库ORDER BY sort_order排序,不依赖 PHPusort
get_user_menus() 必须用 INNER JOIN 一次查出授权菜单
常见错误是控制器里先调 get_all_menus() 拿全量,再 foreach 去查权限——N+1 查询会让首屏慢 300ms 以上。正确做法是在模型中一条 SQL 完成:
SELECT m.id, m.parent_id, m.name, m.url, m.icon, m.sort_order FROM menus m INNER JOIN role_permissions rp ON m.permission_code = rp.permission_code INNER JOIN user_roles ur ON rp.role_id = ur.role_id WHERE ur.user_id = ? AND m.is_hidden = 0 ORDER BY m.sort_order
注意:INNER JOIN 天然过滤无权限项;is_hidden = 0 防止配置开关失效;若用户可属多个角色,记得加 DISTINCT 去重。
树形结构必须用循环构建,禁止递归 + 限深
别用 build_menu($items, $parent_id) 这类递归函数,脏数据或逻辑漏洞可能触发 PHP 堆栈溢出。推荐扁平查库 + 循环构树,并硬编码最大深度(比如 4 层):
- 数据库返回的是带
id、parent_id的扁平数组 - PHP 中用两层
foreach构建层级映射,外层找根节点,内层挂子项 - 每层插入前检查当前深度,≥4 就跳过,不渲染也不报错
-
url字段存相对路径(如/admin/user/list),前端跳转前必须用site_url()包裹,否则可能跨域或 404
菜单缓存必须绑定 user_id 并支持主动失效
不能用全局缓存(如 cache->save('menus', $data, 3600)),否则 A 用户登录后缓存菜单,B 用户接着访问会看到 A 的菜单。正确方式:
- 缓存 key 必须含
user_id,例如menus_123 - 用户修改角色或权限时,主动删除对应
menus_{user_id}缓存项 - 不要依赖缓存过期自动清理——权限变更必须实时生效
最易被忽略的是:菜单渲染完成后,前端 JS 用 location.href 跳转前没校验 URL 是否真属于当前用户权限范围,这会导致绕过菜单隐藏但依然能手动输入地址访问——权限校验必须前后端双保险,菜单只是入口提示,不是访问控制本身。


















