运行控制台动态菜单
状态:Current — 运行控制台菜单由菜单管理(
menu_items)唯一驱动:菜单节点构成导航树,挂载到菜单且已发布的页面才出现在控制台。详细模型见 Dashboard Resource/Page 模型,操作向导(建菜单→发布→挂载→控制台出现)见 运行控制台导航与页面挂载。实现索引:菜单生成internal/api/console/(generateMenuFromMenuItems,GET /api/v1/console/menu)、菜单树与权限过滤internal/api/menu/(AccessibleTree)、菜单模型internal/model/menu.go、挂载字段internal/model/page_spec.go(MenuID)、前端路由web/config/routes.ts、侧边菜单组装web/src/utils/consoleMenu.ts(buildMenuFromConsoleSpec)、canonical 仲裁resolveConsolePageCanonicalPath。
结论
运行控制台左侧菜单不是静态路由配置,不是函数目录的直接投影,也不是 PageSpec category 的聚合。
菜单来源只有一个:
menu_items(菜单树结构) + page_specs.menu_id(挂载映射) + 最新已发布快照(页面内容)
-> ConsoleMenuSpec前端不得为动态分类修改 web/src/locales/*/menu.ts。菜单标题来自菜单系统的 menu_items.labels(菜单管理页维护),页面标题来自已发布 PageSpec 的 title。
挂载与过滤规则
页面出现在运行控制台由两个事实共同决定:
- 挂载:draft 表
page_specs.menu_id指向同 scope 的菜单。挂载关系读 draft 表——改挂载即时生效,无需重发页面。 - 已发布:该
pageKey存在 active 发布快照(published_page_specs)。draft-only 页面不上控制台。
组装规则:
- 菜单树结构(层级、排序、图标、可见性、权限)完全由
menu_items决定:menu.AccessibleTree按当前用户权限剪枝(不可见剪掉、菜单 permission 不持有剪掉、父级不可达剪整枝、孤儿提升为根)。 - 挂载页面作为菜单节点的 children 与子菜单混排,排序
order → 本地化标题 → key(组内同级比较)。 - 页面内容(title/icon/order)取最新发布快照;未挂载的已发布页面不出现在控制台导航,但直达 URL 仍可渲染。
- 无任何菜单时
items为空,控制台首页展示「去菜单管理」引导空态。
PageSpec.category 的现状
PageSpec 的 category.key 保留为页面侧元数据(页面工作台分组展示),不再驱动运行控制台菜单。其推导规则(显式声明 > resourceKey 前缀 > 主 binding functionId 前缀 > pageKey 前缀;详见 ProComponents 页面生成与运行时)仍用于新页面草稿的默认值,但只影响工作台侧分组,与控制台导航无关。
多语言
动态菜单显示名分两个事实源:
// menu_items(菜单管理页维护,菜单标题唯一事实)
{
"menuKey": "support",
"labels": {
"zh-CN": "客服",
"en-US": "Support"
}
}// PublishedPageSpec(页面侧只携带自身标题)
{
"title": {
"zh-CN": "封禁玩家",
"en-US": "Ban Player"
}
}规则:
menu_items.labels用于菜单标题;labels 为空时回落显示menuKey。- 已发布 PageSpec 的
title用于页面菜单标题。 - 静态 locale 只用于固定系统菜单,例如“运行控制台”。
- 动态菜单项必须设置
locale: false。 - 页面
title缺少系统默认语言时发布失败(菜单 labels 不在此校验范围)。 - 菜单 labels 在菜单管理页维护,与页面发布链解耦;改菜单名不需要重发页面。
路由
运行控制台保留固定参数路由承载动态菜单:
/console/home
/console/:categoryKey
/console/:categoryKey/:pageKey/console/:categoryKey 的 categoryKey 段是挂载菜单 key(menuKey),展示该菜单下的挂载页面(含子菜单组)。
/console/:categoryKey/:pageKey 渲染具体 PageSpec。canonical 仲裁以菜单树为唯一事实:resolveConsolePageCanonicalPath 在 ConsoleMenuSpec 树中递归查找 pageKey 的挂载路径;URL 与规范路径不一致时前端跳转到规范路径。页面未挂任何菜单(canonical 为空串)时不重定向,直达 URL 正常渲染。URL 不是 scope:页面、菜单和执行都按全局 game_id + env context 查询;同一个 pageKey 可以存在于不同 scope。
默认菜单种子(T-M10)
菜单读路径(GET /menus、GET /menus/accessible,含控制台导航组装共用的 menu.AccessibleTree)在 scope 首次访问时触发惰性种子,导入 bootstrap 目录(bootstrapData.baseDir,与 admins.json 同目录)default-menus.json 中的顶级菜单骨架。实现:internal/svc/menu_seeder.go(MenuSeeder.EnsureSeeded)。
设计边界(有意行为):
- 文件存在且非空即启用,缺失/空数组即禁用(零配置开关);任一条目非法(menuKey 不合规、labels 全空)整体禁用并 Error 日志,不留半套骨架。
- 空 scope → 全量种入:永不更新/覆盖已存在的 key;用户删光全部菜单并重启会重新种一次,彻底禁用需删种子文件。
- 非空 scope → 仅一次「按 menuKey 补缺」backfill(
platform_settings持久标记menuSeedBackfill/<gameId>/<env>,每 scope 只执行一次):修复 T-M10 上线前已有数据的旧 scope 永远缺骨架的问题(旧行为「scope 非空即整体跳过」)。标记写于首次种子处理后(含空 scope 全量种入),因此用户删除过的默认分类重启后不会被补回;标记写失败仅退化为重启后再尝试一次(key 幂等不会重复创建)。 - 种子是系统行为,不写用户审计(与 AdminManager 默认管理员同语义)。
- 默认骨架(
configs/default-menus.json):玩家管理 / 运营 / 支付订单 / 公告 / 审计五个空组,仅是挂载位置骨架——三要素(菜单+已发布+挂载)不变,空组在控制台渲染为指向/console/<menuKey>的单链接。
边界
禁止:
- 维护硬编码分类表。
- 为动态分类新增静态 i18n key。
- 从前端页面里重复实现菜单推断。
- 从函数目录或 PageSpec
category聚合直接生成运行控制台菜单。 - 把未发布 PageSpec(draft-only)或函数注册草稿展示到运行控制台。
- 在 PageSpec、前端组件或静态字典中引入菜单文案(菜单名称只属于菜单系统)。
允许:
- PageSpec 保存时按推导规则生成
category.key默认值(仅工作台侧分组用途)。 - Server 根据函数能力契约生成 PageSpec 建议;建议不是菜单事实源。
- 用户在页面工作台中覆盖页面标题、图标和排序;在菜单管理中维护树结构、labels、图标、排序、权限与可见性。
验收规则
- 新增菜单不需要改前端代码。
- 切换语言后,菜单标题来自
menu_items.labels,页面标题来自已发布 PageSpectitle。 - 已发布但未挂载的页面不出现在控制台导航;挂载后无需重发页面即出现。
- 挂载后下线(unpublish)的页面从控制台消失;解除挂载同样消失(菜单组保留为空组)。
- 无权限菜单(
menu_items.permission不持有或不可见)不出现在控制台。 - 没有
title默认语言时页面发布失败。 - 函数目录、Page Studio 草稿和运行控制台菜单之间不存在第二套菜单逻辑。
- 切换全局 game/env 后,菜单只显示新 scope 的菜单树与挂载页面。
