namespace MiGu.Server.Auth; /// /// 单个「权限页面」定义。Key 与前端 vue-router 的 route.name 一一对齐, /// 角色通过勾选 Key 集合决定可访问的页面(菜单 + 路由守卫据此放行)。 /// public sealed record PageDef(string Key, string Label, string Group, string Scope); /// /// 平台「权限页面清单」——RBAC 的最小授权单元。 /// /// 设计:页面是由前端路由静态决定的(相对稳定),因此后端维护一份与 /// frontends/.../router/index.ts 对齐的静态清单,通过 GET /api/rbac/pages /// 暴露给「权限与角色」管理页,让管理员可视化地把页面分配给角色。 /// /// Scope 含义: /// - Platform:管理端(/admin/*)页面; /// - RCSMonitor:运营监控端(/monitor/*)页面。 /// public static class PageCatalog { public const string ScopePlatform = "Platform"; public const string ScopeMonitor = "RCSMonitor"; /// 权限页面通配符:角色 Pages 含此值表示「该 scope 下全部页面」(超级管理员)。 public const string Wildcard = "*"; public static readonly IReadOnlyList All = new List { // ── 管理端 / Platform:概览 ── new("admin-dashboard", "总览", "概览", ScopePlatform), new("admin-setup", "初始配置", "概览", ScopePlatform), new("admin-map-monitor", "地图监控", "概览", ScopePlatform), new("admin-playback", "调度回放", "概览", ScopePlatform), new("admin-tasks", "任务管理", "概览", ScopePlatform), new("admin-alarms", "报警管理", "概览", ScopePlatform), new("admin-ops-log", "运维记录", "概览", ScopePlatform), new("admin-notes", "运维备注", "概览", ScopePlatform), new("admin-logs", "日志管理", "概览", ScopePlatform), // ── 管理端 / Platform:设计与编排 ── new("admin-maps", "地图管理", "设计与编排", ScopePlatform), new("admin-map-editor", "地图编辑", "设计与编排", ScopePlatform), new("admin-project-properties", "项目属性", "设计与编排", ScopePlatform), new("admin-tracks", "场景管理", "设计与编排", ScopePlatform), new("admin-cars", "车辆管理", "设计与编排", ScopePlatform), new("admin-processes", "进程管理", "设计与编排", ScopePlatform), new("admin-scripts", "脚本管理", "设计与编排", ScopePlatform), new("admin-task-templates", "任务编排", "设计与编排", ScopePlatform), new("admin-wcs-template-proto", "WCS模板原型", "设计与编排", ScopePlatform), new("admin-simple-fields", "字段管理", "设计与编排", ScopePlatform), // ── 管理端 / Platform:数据中心(scene.signal,选 SPS / Pack 时显示) ── new("admin-data-center", "数据中心", "数据中心", ScopePlatform), // ── 管理端 / Platform:平台配置中心(聚合页,每个 Key 对齐前端聚合路由 route.name) ── new("admin-vehicle-hub", "车辆运维", "平台配置中心", ScopePlatform), new("admin-config-facility", "设备接入", "平台配置中心", ScopePlatform), new("admin-config-warehouse", "库位管理", "平台配置中心", ScopePlatform), new("admin-config-ops-center", "监控配置", "平台配置中心", ScopePlatform), new("admin-config-system", "系统配置", "平台配置中心", ScopePlatform), new("admin-config-auth", "权限与角色", "平台配置中心", ScopePlatform), // ── 运营端 / RCSMonitor ── new("monitor-vehicle-hub", "车辆运维", "运营监控", ScopeMonitor), new("monitor-map", "地图监控", "运营监控", ScopeMonitor), new("monitor-ops", "运维记录", "运营监控", ScopeMonitor), new("monitor-notes", "运维备注", "运营监控", ScopeMonitor), }; private static readonly HashSet _keys = All.Select(p => p.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); /// 已下线页面 Key → 合并后的新 Key(角色数据迁移用)。 private static readonly IReadOnlyDictionary LegacyKeyAliases = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["admin-config-vehicle"] = "admin-vehicle-hub", ["admin-config-fleet"] = "admin-vehicle-hub", // 会话 16:平台配置中心入口按业务收敛为 6 个聚合页,旧 Key 迁移到对应聚合页 Key。 ["admin-config-device"] = "admin-config-facility", ["admin-config-location"] = "admin-config-warehouse", ["admin-config-ops"] = "admin-config-ops-center", ["admin-config-logs"] = "admin-logs", ["admin-config-map-monitor"] = "admin-config-ops-center", ["admin-config-system-center"] = "admin-config-system", // 运营总览页已下线:旧角色勾选迁到地图监控,避免权限清单里出现幽灵页面。 ["monitor-dashboard"] = "monitor-map", ["admin-data-center-stations"] = "admin-data-center", ["admin-data-center-docks"] = "admin-data-center", ["admin-data-center-handshake"] = "admin-data-center", ["admin-data-center-release"] = "admin-data-center", ["admin-data-center-mag-control"] = "admin-data-center", }; /// /// 管理端页面 → 运营端对应页面。给运营角色勾页时,只能从「当前管理员已有的管理端页」映射出来。 /// 无对应运营页的管理端入口(如任务/报警)不出现在映射里。 /// public static readonly IReadOnlyDictionary PlatformToMonitor = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["admin-map-monitor"] = "monitor-map", ["admin-vehicle-hub"] = "monitor-vehicle-hub", ["admin-ops-log"] = "monitor-ops", ["admin-notes"] = "monitor-notes", }; /// 仅运营端有的页面:不依赖管理端对应页,始终可授给运营角色。 public static readonly IReadOnlyList MonitorOnlyPages = new[] { "monitor-ops", "monitor-notes" }; /// /// 把角色里勾选的页面展开到指定登录域:勾了运营监控的「运维记录」时, /// 管理账号/通用角色进管理壳也能拿到对应的 admin-ops-log。 /// public static HashSet ExpandKeysForScope(IEnumerable keys, string scope) { var scopeKeys = KeysForScope(scope).ToHashSet(StringComparer.OrdinalIgnoreCase); var set = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var raw in keys ?? Array.Empty()) { if (string.Equals(raw, Wildcard, StringComparison.OrdinalIgnoreCase)) { foreach (var k in scopeKeys) set.Add(k); return set; } var key = NormalizeKey(raw); if (scopeKeys.Contains(key)) set.Add(key); var counterpart = CounterpartForScope(key, scope); if (counterpart != null && scopeKeys.Contains(counterpart)) set.Add(counterpart); } return set; } /// 管理页 ↔ 运营页互认:monitor-ops → admin-ops-log(当前域是 Platform 时)。 public static string? CounterpartForScope(string key, string targetScope) { if (string.Equals(targetScope, ScopePlatform, StringComparison.OrdinalIgnoreCase)) { foreach (var kv in PlatformToMonitor) { if (string.Equals(kv.Value, key, StringComparison.OrdinalIgnoreCase)) return kv.Key; } return null; } if (string.Equals(targetScope, ScopeMonitor, StringComparison.OrdinalIgnoreCase) && PlatformToMonitor.TryGetValue(key, out var monitorKey)) return monitorKey; return null; } /// 判断页面 Key 是否合法(用于角色保存时过滤掉脏数据 / 已下线页面)。 public static bool IsValidKey(string key) => _keys.Contains(key); /// 将旧页面 Key 映射为当前有效 Key;未知 Key 原样返回。 public static string NormalizeKey(string key) => LegacyKeyAliases.TryGetValue(key, out var mapped) ? mapped : key; /// 列出某 scope 下的全部页面 Key(用于把角色的 "*" 通配展开成具体页面集合)。 public static IReadOnlyList KeysForScope(string scope) => All.Where(p => string.Equals(p.Scope, scope, StringComparison.OrdinalIgnoreCase)) .Select(p => p.Key) .ToList(); /// 当前管理员在管理端拥有的页面 → 可授给运营端角色的页面集合。 public static HashSet GrantableMonitorPages(IEnumerable? actorPlatformPages) { var set = new HashSet(MonitorOnlyPages, StringComparer.OrdinalIgnoreCase); var list = (actorPlatformPages ?? Array.Empty()).ToList(); if (list.Contains(Wildcard, StringComparer.OrdinalIgnoreCase) || HasAllKeys(list, KeysForScope(ScopePlatform))) { foreach (var k in KeysForScope(ScopeMonitor)) set.Add(k); return set; } foreach (var p in list) { var key = NormalizeKey(p); if (PlatformToMonitor.TryGetValue(key, out var monitorKey) && IsValidKey(monitorKey)) set.Add(monitorKey); } return set; } /// 当前管理员可授给管理端角色的页面(不能超权:只能授自己已有的)。 public static HashSet GrantablePlatformPages(IEnumerable? actorPlatformPages) { var list = (actorPlatformPages ?? Array.Empty()).ToList(); var all = KeysForScope(ScopePlatform); if (list.Contains(Wildcard, StringComparer.OrdinalIgnoreCase) || HasAllKeys(list, all)) return all.ToHashSet(StringComparer.OrdinalIgnoreCase); return list .Select(NormalizeKey) .Where(k => IsValidKey(k) && all.Contains(k, StringComparer.OrdinalIgnoreCase)) .ToHashSet(StringComparer.OrdinalIgnoreCase); } /// 按角色归属域,算出当前管理员可勾选的页面 Key。 public static HashSet GrantablePagesForRoleScope( string roleScope, IEnumerable? actorPlatformPages) { if (string.Equals(roleScope, ScopeMonitor, StringComparison.OrdinalIgnoreCase)) return GrantableMonitorPages(actorPlatformPages); if (string.Equals(roleScope, ScopePlatform, StringComparison.OrdinalIgnoreCase)) return GrantablePlatformPages(actorPlatformPages); // 通用域:管理端可授 + 运营端可授 var set = GrantablePlatformPages(actorPlatformPages); set.UnionWith(GrantableMonitorPages(actorPlatformPages)); return set; } private static bool HasAllKeys(IEnumerable have, IEnumerable all) { var set = have.ToHashSet(StringComparer.OrdinalIgnoreCase); return all.All(k => set.Contains(k)); } }