Menu 是平台的全局导航目录 owner。它管理 SysModule 的结构与展示字段,并根据当前用户的角色授权生成平铺列表、菜单树和前端导航。它不决定目标 API 是否可访问,也不是 Feature 管理器或前端路由注册中心。
1. 当前产品面
当前代码提供三个项目:Contracts 仅含四种输出 DTO;Application 持有九个用例与树策略;Infrastructure 持有全局目录行、IEntitySet store、seed step 和缓存同步消费者。
2. 九个 HTTP 用例
| 方法与路径 | 用例 | 资源权限 |
|---|---|---|
POST /api/menus | CreateMenu | create |
PUT /api/menus/{id} | UpdateMenu | update |
DELETE /api/menus/{id} | DeleteMenu | delete |
PATCH /api/menus/{id}/sort | SortMenu | update |
PATCH /api/menus/{id}/toggle | ToggleMenu | update |
GET /api/menus/{id} | GetMenuDetail | view |
GET /api/menus/flat | GetMenuFlat | view |
GET /api/menus/tree | GetMenuTree | view |
GET /api/menus/navigation | GetNavigation | 仅认证,不实现 IAuthorizedRequest |
GenerateEndpointAttribute.RequireAuthorization 默认为 true,因此 navigation 仍要求登录。它跳过的是 menus.menu.view 资源决策,不是认证。
3. 权限与治理
模块标记是 AppModule("Menu", Code="menus"),声明允许依赖 Authorization 和 Identity。当前权限目录只有四项:
menus.menu.viewmenus.menu.createmenus.menu.updatemenus.menu.deleteMenu 没有 Feature catalog。旧文档所说的“Feature-aware visibility”并无源码依据;当前可见性只由 admin 角色短路或 Authorization 返回的模块码决定。
4. 请求到导航的真实链路
管理员角色名按不区分大小写比较;只要角色集合包含 admin,解析器就返回 null,表示目录全量可见。其他用户无角色时得到空导航。
5. 正确消费示例
// ① 该查询由生成端点要求认证,但不要求菜单管理权限。var result = await mediator.Send( new GetNavigation.Query(), cancellationToken);
// ② 返回值已经按当前用户角色过滤并补齐可见节点的祖先。if (result.IsFailure) return result.Error;
// ③ 客户端只把它用于导航;目标 API 仍执行自己的权限策略。return result.Value.Select(RenderNavigationNode).ToList();导航 DTO 只有 Id、Name、Icon、LinkUrl 和 Children;Code、Scope、Enabled 与授权信息不会出现在导航响应中。
6. 全局目录而非租户目录
MenuModuleCatalogRecord 继承 EntityBase,表元数据没有 IsTenant=true。目录结构对所有租户共享;租户差异来自 Authorization 中的租户化角色—模块关系。
这意味着修改名称、链接、父级、排序或启停会影响所有租户。若产品需要租户自定义菜单,不能直接把当前全局行当租户行使用,需要显式覆盖模型和合并策略。
7. 种子基线
MenuModuleSeedStep 的顺序为 220,以 Code 为自然键。当前资产有九行根节点:Operations、PlatformBilling、Files、Notifications、Webhooks、Catalog、Tickets、Chat 和 Sandbox。
seed 会更新父级、展示、旧 MVC 字段、排序、说明、IsMenu、Enabled、Scope 与软删状态,但不会更新作为匹配键的 Code。
8. 当前保证
- Code 有数据库唯一索引。
- ParentId 有普通索引。
- store 通过
IEntitySet<MenuModuleCatalogRecord>保持 ORM 中立。 - Authorization 关系只经
IAuthorizationAssignmentReader窄端口读取。 - 查询只加载 Enabled 且未软删的目录行。
- flat、tree、navigation 都按用户与可见集合缓存。
- 写用例在成功持久化后清理本地菜单 tag,并发送同步通知。
- API Shell 未配置数据库时为
IMenuStore提供 fail-closed unavailable adapter。
9. 当前不保证
- 创建/更新不验证父节点存在、自引用或祖先环。
- 更新不验证 Name、Code 非空,也不预查 Code 冲突。
- 删除不是单事务级联,也不校验授权关系引用。
- 排序只是写入整数,不重排同级节点。
- 启停父节点不级联修改子节点状态。
- 树递归没有环检测或最大深度。
- 没有 Menu Feature、审计时间线、版本历史或租户覆盖。
- 数据库写入与缓存广播之间没有 outbox 原子性。
10. 何时使用 Menu
适合放入 Menu 的是稳定的产品导航目录、模块入口与旧控制器动作兼容信息。仅用于某个页面内部的 tab、按钮或临时引导,不应被提升为全局 SysModule 行。
新增节点前必须先确定稳定 Code、父级、目标 URL、作用域、是否是菜单、默认启用状态以及哪些角色获得模块可见性。
11. 文档导航
12. 源码核查
# 核对九个用例和生成端点声明。find src/Platform/Menu -path '*Commands*' -o -path '*Queries*' | sort
# 核对全局目录元数据、种子与授权窄端口。rg -n "BitzTable|WhereColumns|IAuthorizationAssignmentReader" src/Platform/Menu -g '*.cs'
# 不应存在 Menu Feature catalog。rg -n "FeatureCatalog|FeatureDefinition" src/Platform/Menu -g '*.cs'