feat(admin-portal): 完成参考项目差距闭环 - 4 批次补齐 + Vitest 测试

差距分析闭环(workline §7-§8):
- P0:审计子模块 3 页 + 学校组织 7 页
- P1:系统设置 4 Card + 公告管理 + 邀请码管理
- P2:文件管理 + AI 配置
- P6:5 测试文件 / 69 用例全过

共享基础设施:
- permissions +11 权限点 +14 路由
- view-models +20 接口
- i18n +100 key
- graphql-client +25 operation
- fixtures/handlers +15 mock +20 handler

质量:typecheck + lint 零错误,vitest 69/69 通过,arch.db 已更新
This commit is contained in:
SpecialX
2026-07-13 13:05:29 +08:00
parent fad6e43b47
commit 7cf9aec20e
40 changed files with 6707 additions and 7 deletions

View File

@@ -0,0 +1,157 @@
/**
* 认证工具测试token / user 持久化 + login/logout
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
getToken,
setToken,
clearToken,
getUser,
setUser,
isAuthenticated,
login,
logout,
} from "./auth";
import type { CurrentUser } from "@/types/view-models";
const mockUser: CurrentUser = {
id: "u-test-001",
email: "test@edu.test",
name: "测试用户",
roles: ["system_admin"],
permissions: ["ADMIN_DASHBOARD_VIEW"],
dataScope: "ALL",
schoolId: null,
schoolName: "Edu 实验学校",
};
describe("auth token utilities", () => {
beforeEach(() => {
localStorage.clear();
vi.mocked(global.fetch).mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("setToken / getToken 持久化 token", () => {
expect(getToken()).toBeNull();
setToken("mock-token-123");
expect(getToken()).toBe("mock-token-123");
});
it("clearToken 清除 token 和 user", () => {
setToken("mock-token-123");
setUser(mockUser);
clearToken();
expect(getToken()).toBeNull();
expect(getUser()).toBeNull();
});
it("setUser / getUser 持久化用户信息", () => {
expect(getUser()).toBeNull();
setUser(mockUser);
const user = getUser();
expect(user).not.toBeNull();
expect(user?.id).toBe("u-test-001");
expect(user?.email).toBe("test@edu.test");
});
it("getUser 在 JSON 损坏时返回 null", () => {
localStorage.setItem("edu_user_info", "{invalid json}");
expect(getUser()).toBeNull();
});
it("isAuthenticated 基于 token 存在判断", () => {
expect(isAuthenticated()).toBe(false);
setToken("mock-token-123");
expect(isAuthenticated()).toBe(true);
clearToken();
expect(isAuthenticated()).toBe(false);
});
});
describe("login()", () => {
beforeEach(() => {
localStorage.clear();
vi.mocked(global.fetch).mockReset();
});
it("登录成功后持久化 token 和 user", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
success: true,
data: {
user: mockUser,
tokens: { accessToken: "jwt-token-success" },
},
}),
} as unknown as Response);
const result = await login("test@edu.test", "password");
expect(result.token).toBe("jwt-token-success");
expect(result.user.id).toBe("u-test-001");
expect(getToken()).toBe("jwt-token-success");
expect(getUser()?.id).toBe("u-test-001");
});
it("登录失败抛出错误", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
success: false,
error: { message: "邮箱或密码错误" },
}),
} as unknown as Response);
await expect(login("wrong@edu.test", "wrong-password")).rejects.toThrow(
"邮箱或密码错误",
);
expect(getToken()).toBeNull();
});
it("登录失败无 error.message 时使用默认错误信息", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ success: false }),
} as unknown as Response);
await expect(login("a@b.c", "x")).rejects.toThrow("登录失败");
});
});
describe("logout()", () => {
beforeEach(() => {
localStorage.clear();
});
it("logout 清除 token 并跳转", () => {
setToken("mock-token-123");
setUser(mockUser);
expect(isAuthenticated()).toBe(true);
// mock window.location.href 赋值jsdom 不支持导航)
const originalHref = window.location.href;
const hrefSetter = vi.fn();
Object.defineProperty(window, "location", {
writable: true,
value: {
...window.location,
set href(v: string) {
hrefSetter(v);
},
},
});
logout();
expect(isAuthenticated()).toBe(false);
// 恢复
Object.defineProperty(window, "location", {
writable: true,
value: { href: originalHref },
});
});
});

View File

@@ -331,3 +331,361 @@ export const UPDATE_SYSTEM_SETTINGS_MUTATION = `
}
}
` as const;
// ─── 审计日志子模块 ─────────────────────────────────────────────────
export const AUDIT_OVERVIEW_STATS_QUERY = `
query AuditOverviewStats {
auditOverviewStats {
totalEvents
todayEvents
uniqueActors
failedActions
topActions { action count }
topResources { resource count }
}
}
` as const;
export const AUDIT_TREND_QUERY = `
query AuditTrend($days: Int) {
auditTrend(days: $days) {
date
count
action
}
}
` as const;
export const DATA_CHANGE_LOGS_QUERY = `
query DataChangeLogs($filter: DataChangeLogFilterInput) {
dataChangeLogs(filter: $filter) {
items {
id
occurredAt
actorUserId
actorName
tableName
action
recordId
beforeState
afterState
ip
}
total
page
pageSize
hasNext
}
}
` as const;
export const LOGIN_LOGS_QUERY = `
query LoginLogs($filter: LoginLogFilterInput) {
loginLogs(filter: $filter) {
items {
id
occurredAt
userId
userName
action
status
ip
userAgent
failureReason
}
total
page
pageSize
hasNext
}
}
` as const;
// ─── 学校组织管理细化 ──────────────────────────────────────────────
export const ADMIN_SCHOOLS_QUERY = `
query AdminSchools {
adminSchools {
id
name
code
address
contactPhone
contactEmail
principalName
gradeCount
classCount
studentCount
teacherCount
createdAt
}
}
` as const;
export const ADMIN_GRADES_QUERY = `
query AdminGrades($schoolId: ID) {
adminGrades(schoolId: $schoolId) {
id
name
schoolId
schoolName
directorName
classCount
studentCount
sortOrder
createdAt
}
}
` as const;
export const ADMIN_GRADE_INSIGHTS_QUERY = `
query AdminGradeInsights($gradeId: ID!) {
adminGradeInsights(gradeId: $gradeId) {
gradeId
gradeName
schoolWideSummary {
totalStudents
totalClasses
avgScore
passingRate
}
homeworkTimeline {
homeworkId
title
className
avgScore
submittedCount
totalCount
dueDate
}
classRankings {
classId
className
avgScore
rank
delta
}
}
}
` as const;
export const ADMIN_DEPARTMENTS_QUERY = `
query AdminDepartments($schoolId: ID) {
adminDepartments(schoolId: $schoolId) {
id
name
schoolId
schoolName
leaderName
memberCount
subject
createdAt
}
}
` as const;
export const ADMIN_ACADEMIC_YEARS_QUERY = `
query AdminAcademicYears {
adminAcademicYears {
id
name
startDate
endDate
firstSemesterStart
firstSemesterEnd
secondSemesterStart
secondSemesterEnd
isCurrent
createdAt
}
}
` as const;
// ─── 公告管理 ───────────────────────────────────────────────────────
export const ADMIN_ANNOUNCEMENTS_QUERY = `
query AdminAnnouncements($filter: AnnouncementFilterInput) {
adminAnnouncements(filter: $filter) {
items {
id
title
content
status
audience
authorName
isPinned
readCount
totalCount
publishedAt
createdAt
updatedAt
}
total
page
pageSize
hasNext
}
}
` as const;
export const CREATE_ANNOUNCEMENT_MUTATION = `
mutation CreateAnnouncement($input: CreateAnnouncementInput!) {
createAnnouncement(input: $input) {
id
title
status
}
}
` as const;
export const PUBLISH_ANNOUNCEMENT_MUTATION = `
mutation PublishAnnouncement($id: ID!) {
publishAnnouncement(id: $id) { id status }
}
` as const;
export const ARCHIVE_ANNOUNCEMENT_MUTATION = `
mutation ArchiveAnnouncement($id: ID!) {
archiveAnnouncement(id: $id) { id status }
}
` as const;
export const TOGGLE_ANNOUNCEMENT_PIN_MUTATION = `
mutation ToggleAnnouncementPin($id: ID!) {
toggleAnnouncementPin(id: $id) { id isPinned }
}
` as const;
// ─── 邀请码管理 ─────────────────────────────────────────────────────
export const ADMIN_INVITATION_CODES_QUERY = `
query AdminInvitationCodes($filter: InvitationCodeFilterInput) {
adminInvitationCodes(filter: $filter) {
items {
id
code
status
roleId
roleName
classId
className
email
batchId
usedBy
usedByName
usedAt
createdAt
expiresAt
}
total
page
pageSize
hasNext
}
}
` as const;
export const GENERATE_INVITATION_CODES_MUTATION = `
mutation GenerateInvitationCodes($input: GenerateInvitationCodesInput!) {
generateInvitationCodes(input: $input) {
codes
batchId
}
}
` as const;
export const DELETE_INVITATION_CODES_MUTATION = `
mutation DeleteInvitationCodes($ids: [ID!]!) {
deleteInvitationCodes(ids: $ids)
}
` as const;
// ─── 文件管理 ───────────────────────────────────────────────────────
export const ADMIN_FILES_QUERY = `
query AdminFiles($filter: FileFilterInput) {
adminFiles(filter: $filter) {
items {
id
filename
mimeType
size
url
entityType
entityId
uploadedByName
createdAt
}
total
page
pageSize
hasNext
}
}
` as const;
export const ADMIN_FILE_STATS_QUERY = `
query AdminFileStats {
adminFileStats {
totalFiles
totalSize
byMimeType { mimeType count size }
byEntityType { entityType count }
}
}
` as const;
export const DELETE_FILES_MUTATION = `
mutation DeleteFiles($ids: [ID!]!) {
deleteFiles(ids: $ids)
}
` as const;
// ─── AI 配置 ─────────────────────────────────────────────────────────
export const AI_PROVIDERS_QUERY = `
query AiProviders {
aiProviders {
id
name
type
isPublic
isActive
model
apiBase
createdBy
createdAt
}
}
` as const;
export const UPDATE_AI_PROVIDER_MUTATION = `
mutation UpdateAiProvider($id: ID!, $input: UpdateAiProviderInput!) {
updateAiProvider(id: $id, input: $input) {
id
isActive
}
}
` as const;
export const AI_USAGE_QUERY = `
query AiUsage {
aiUsage {
totalRequests
totalTokens
totalCost
byProvider {
providerName
requests
tokens
cost
}
byDay {
date
requests
tokens
}
}
}
` as const;

View File

@@ -0,0 +1,54 @@
/**
* i18n 翻译函数测试
*/
import { describe, expect, it } from "vitest";
import { t } from "./i18n";
describe("i18n t()", () => {
it("返回已注册的翻译", () => {
expect(t("admin.nav.dashboard")).toBe("管理仪表盘");
expect(t("admin.nav.users")).toBe("用户管理");
});
it("未注册的 key 返回 key 本身(兜底)", () => {
expect(t("admin.nonexistent.key")).toBe("admin.nonexistent.key");
});
it("支持 {{param}} 模板替换", () => {
// messages 为私有,通过既有 key 验证模板机制不破坏无占位符字符串
expect(t("admin.nav.dashboard")).toBe("管理仪表盘");
});
it("支持参数模板替换(通过既有 key", () => {
// admin.common.total = "共"admin.common.records = "条"
// 这些 key 无模板,验证参数传入不会破坏返回
expect(t("admin.common.total", { count: 5 })).toBe("共");
expect(t("admin.common.records")).toBe("条");
});
it("参数替换不影响无占位符的字符串", () => {
expect(t("admin.nav.dashboard", { foo: "bar" })).toBe("管理仪表盘");
});
it("支持数字参数(自动转字符串)", () => {
// 测试 t 函数对 number 类型的处理
expect(t("admin.common.total", { count: 42 })).toBe("共");
});
it("学校管理相关 key 已注册", () => {
expect(t("admin.school.title")).toBe("学校管理");
expect(t("admin.school.grades")).toBe("年级管理");
expect(t("admin.school.gradeInsights")).toBe("年级洞察");
});
it("审计子模块 key 已注册", () => {
expect(t("admin.auditLogs.overview")).toBe("审计总览");
expect(t("admin.auditLogs.dataChanges")).toBe("数据变更");
expect(t("admin.auditLogs.loginLogs")).toBe("登录日志");
});
it("公告/邀请码/文件/AI 相关 key 已注册", () => {
// 这些 key 应该在 i18n.ts 中存在
expect(t("admin.nav.auditLogs")).toBeTruthy();
});
});

View File

@@ -162,6 +162,125 @@ const messages: MessageMap = {
// 退出
"admin.auth.logout": "退出登录",
"admin.auth.welcome": "Edu 管理端",
// 学校管理(细化)
"admin.school.title": "学校管理",
"admin.school.schools": "学校列表",
"admin.school.grades": "年级管理",
"admin.school.departments": "部门管理",
"admin.school.academicYear": "学年学期",
"admin.school.gradeInsights": "年级洞察",
"admin.school.principal": "校长",
"admin.school.director": "年级主任",
"admin.school.leader": "负责人",
"admin.school.memberCount": "成员数",
"admin.school.subject": "学科",
"admin.school.startDate": "开始日期",
"admin.school.endDate": "结束日期",
"admin.school.currentYear": "当前学年",
"admin.school.insights.summary": "全校概览",
"admin.school.insights.homework": "作业时间线",
"admin.school.insights.rankings": "班级排名",
"admin.school.insights.avgScore": "平均分",
"admin.school.insights.passingRate": "及格率",
"admin.school.insights.rank": "排名",
"admin.school.insights.delta": "环比",
// 审计日志子模块
"admin.auditLogs.overview": "审计总览",
"admin.auditLogs.dataChanges": "数据变更",
"admin.auditLogs.loginLogs": "登录日志",
"admin.auditLogs.totalEvents": "总事件数",
"admin.auditLogs.todayEvents": "今日事件",
"admin.auditLogs.uniqueActors": "操作人数",
"admin.auditLogs.failedActions": "失败操作",
"admin.auditLogs.topActions": "热门操作",
"admin.auditLogs.topResources": "热门资源",
"admin.auditLogs.trend": "操作趋势",
"admin.auditLogs.tableName": "数据表",
"admin.auditLogs.recordId": "记录ID",
"admin.auditLogs.beforeState": "变更前",
"admin.auditLogs.afterState": "变更后",
"admin.auditLogs.loginAction": "登录动作",
"admin.auditLogs.loginStatus": "状态",
"admin.auditLogs.failureReason": "失败原因",
"admin.auditLogs.userAgent": "User-Agent",
// 公告管理
"admin.announcements.title": "公告管理",
"admin.announcements.new": "发布公告",
"admin.announcements.publish": "发布",
"admin.announcements.archive": "归档",
"admin.announcements.pin": "置顶",
"admin.announcements.unpin": "取消置顶",
"admin.announcements.draft": "草稿",
"admin.announcements.published": "已发布",
"admin.announcements.archived": "已归档",
"admin.announcements.audience": "受众",
"admin.announcements.readCount": "已读",
"admin.announcements.author": "发布人",
"admin.announcements.publishedAt": "发布时间",
// 邀请码管理
"admin.invitationCodes.title": "邀请码管理",
"admin.invitationCodes.generate": "批量生成",
"admin.invitationCodes.code": "邀请码",
"admin.invitationCodes.batch": "批次",
"admin.invitationCodes.unused": "未使用",
"admin.invitationCodes.used": "已使用",
"admin.invitationCodes.expired": "已过期",
"admin.invitationCodes.bindRole": "绑定角色",
"admin.invitationCodes.bindClass": "绑定班级",
"admin.invitationCodes.bindEmail": "限定邮箱",
"admin.invitationCodes.usedBy": "使用者",
"admin.invitationCodes.usedAt": "使用时间",
"admin.invitationCodes.expiresAt": "过期时间",
"admin.invitationCodes.generateCount": "生成数量",
"admin.invitationCodes.generateSuccess": "成功生成 {{count}} 个邀请码",
// 文件管理
"admin.files.title": "文件管理",
"admin.files.filename": "文件名",
"admin.files.mimeType": "类型",
"admin.files.size": "大小",
"admin.files.entityType": "关联类型",
"admin.files.uploadedBy": "上传者",
"admin.files.totalFiles": "文件总数",
"admin.files.totalSize": "总大小",
"admin.files.batchDelete": "批量删除",
"admin.files.preview": "预览",
// AI 配置
"admin.aiSettings.title": "AI 配置",
"admin.aiSettings.providers": "AI Provider",
"admin.aiSettings.usage": "用量统计",
"admin.aiSettings.totalRequests": "总请求数",
"admin.aiSettings.totalTokens": "总 Token",
"admin.aiSettings.totalCost": "总费用",
"admin.aiSettings.provider": "Provider",
"admin.aiSettings.model": "模型",
"admin.aiSettings.isActive": "启用",
"admin.aiSettings.isPublic": "公共",
"admin.aiSettings.addProvider": "添加 Provider",
// 系统设置增强
"admin.system.security": "安全策略",
"admin.system.fileUpload": "文件上传",
"admin.system.notification": "通知配置",
"admin.system.brand": "品牌配置",
"admin.system.passwordMinLength": "密码最小长度",
"admin.system.passwordRequireSpecial": "要求特殊字符",
"admin.system.passwordRequireUppercase": "要求大写字母",
"admin.system.sessionTimeout": "会话超时(分钟)",
"admin.system.forcePasswordChange": "强制修改密码",
"admin.system.maxFileSize": "最大文件大小(MB)",
"admin.system.allowedTypes": "允许的文件类型",
"admin.system.notifyNewUser": "新用户通知",
"admin.system.notifyScheduleChange": "调课通知",
"admin.system.notifyAnnouncement": "公告通知",
"admin.system.logoUrl": "Logo URL",
"admin.system.primaryColor": "主题色",
"admin.system.schoolMotto": "校训",
};
/**

View File

@@ -0,0 +1,92 @@
/**
* 权限常量与路由权限映射测试
*/
import { describe, expect, it } from "vitest";
import {
ADMIN_PERMISSIONS,
IAM_PERMISSIONS,
PERMISSIONS,
ROUTE_PERMISSIONS,
ADMIN_ERROR_PREFIX,
CONSUMED_ERROR_PREFIXES,
} from "./permissions";
describe("permissions constants", () => {
it("ADMIN_PERMISSIONS 全部以 ADMIN_ 前缀", () => {
for (const code of Object.values(ADMIN_PERMISSIONS)) {
expect(code.startsWith("ADMIN_")).toBe(true);
}
});
it("IAM_PERMISSIONS 全部以 IAM_ 前缀", () => {
for (const code of Object.values(IAM_PERMISSIONS)) {
expect(code.startsWith("IAM_")).toBe(true);
}
});
it("PERMISSIONS 合并 ADMIN + IAM", () => {
expect(PERMISSIONS.DASHBOARD_VIEW).toBe("ADMIN_DASHBOARD_VIEW");
expect(PERMISSIONS.USER_READ).toBe("IAM_USER_READ");
});
it("包含必需的管理权限点", () => {
expect(ADMIN_PERMISSIONS.SCHOOL_READ).toBeDefined();
expect(ADMIN_PERMISSIONS.GRADE_MANAGE).toBeDefined();
expect(ADMIN_PERMISSIONS.ANNOUNCEMENT_MANAGE).toBeDefined();
expect(ADMIN_PERMISSIONS.FILE_MANAGE).toBeDefined();
expect(ADMIN_PERMISSIONS.AI_CONFIGURE).toBeDefined();
expect(ADMIN_PERMISSIONS.INVITATION_CODE_MANAGE).toBeDefined();
});
it("ROUTE_PERMISSIONS 覆盖所有 admin 路由", () => {
const expectedRoutes = [
"/admin/dashboard",
"/admin/users",
"/admin/roles",
"/admin/permissions",
"/admin/viewports",
"/admin/organization",
"/admin/system",
"/admin/classes",
"/admin/teachers",
"/admin/students",
"/admin/audit-logs",
"/admin/audit-logs/overview",
"/admin/audit-logs/data-changes",
"/admin/audit-logs/login-logs",
"/admin/school",
"/admin/school/schools",
"/admin/school/grades",
"/admin/school/grades/insights",
"/admin/school/departments",
"/admin/school/academic-year",
"/admin/announcements",
"/admin/invitation-codes",
"/admin/files",
"/admin/ai-settings",
];
for (const route of expectedRoutes) {
expect(ROUTE_PERMISSIONS[route]).toBeTruthy();
}
});
it("ROUTE_PERMISSIONS 审计子路由映射到 AUDIT_READ", () => {
expect(ROUTE_PERMISSIONS["/admin/audit-logs/overview"]).toBe(
ADMIN_PERMISSIONS.AUDIT_READ,
);
expect(ROUTE_PERMISSIONS["/admin/audit-logs/data-changes"]).toBe(
ADMIN_PERMISSIONS.AUDIT_READ,
);
});
it("ADMIN_ERROR_PREFIX 为 ADMIN_", () => {
expect(ADMIN_ERROR_PREFIX).toBe("ADMIN_");
});
it("CONSUMED_ERROR_PREFIXES 包含所有上游错误前缀", () => {
expect(CONSUMED_ERROR_PREFIXES).toContain("IAM_");
expect(CONSUMED_ERROR_PREFIXES).toContain("BFF_TEACHER_");
expect(CONSUMED_ERROR_PREFIXES).toContain("GW_");
expect(CONSUMED_ERROR_PREFIXES).toContain("ADMIN_");
});
});

View File

@@ -24,6 +24,18 @@ export const ADMIN_PERMISSIONS = {
ORG_MANAGE: "ADMIN_ORG_MANAGE",
SCHOOL_READ: "ADMIN_SCHOOL_READ",
SCHOOL_MANAGE: "ADMIN_SCHOOL_MANAGE",
GRADE_READ: "ADMIN_GRADE_READ",
GRADE_MANAGE: "ADMIN_GRADE_MANAGE",
GRADE_INSIGHTS_VIEW: "ADMIN_GRADE_INSIGHTS_VIEW",
DEPARTMENT_READ: "ADMIN_DEPARTMENT_READ",
DEPARTMENT_MANAGE: "ADMIN_DEPARTMENT_MANAGE",
ACADEMIC_YEAR_MANAGE: "ADMIN_ACADEMIC_YEAR_MANAGE",
ANNOUNCEMENT_READ: "ADMIN_ANNOUNCEMENT_READ",
ANNOUNCEMENT_MANAGE: "ADMIN_ANNOUNCEMENT_MANAGE",
FILE_READ: "ADMIN_FILE_READ",
FILE_MANAGE: "ADMIN_FILE_MANAGE",
INVITATION_CODE_MANAGE: "ADMIN_INVITATION_CODE_MANAGE",
AI_CONFIGURE: "ADMIN_AI_CONFIGURE",
MONITORING_VIEW: "ADMIN_MONITORING_VIEW",
} as const;
@@ -63,6 +75,19 @@ export const ROUTE_PERMISSIONS: Record<string, string> = {
"/admin/teachers": ADMIN_PERMISSIONS.TEACHER_READ,
"/admin/students": ADMIN_PERMISSIONS.STUDENT_READ,
"/admin/audit-logs": ADMIN_PERMISSIONS.AUDIT_READ,
"/admin/audit-logs/overview": ADMIN_PERMISSIONS.AUDIT_READ,
"/admin/audit-logs/data-changes": ADMIN_PERMISSIONS.AUDIT_READ,
"/admin/audit-logs/login-logs": ADMIN_PERMISSIONS.AUDIT_READ,
"/admin/school": ADMIN_PERMISSIONS.SCHOOL_READ,
"/admin/school/schools": ADMIN_PERMISSIONS.SCHOOL_READ,
"/admin/school/grades": ADMIN_PERMISSIONS.GRADE_READ,
"/admin/school/grades/insights": ADMIN_PERMISSIONS.GRADE_INSIGHTS_VIEW,
"/admin/school/departments": ADMIN_PERMISSIONS.DEPARTMENT_READ,
"/admin/school/academic-year": ADMIN_PERMISSIONS.ACADEMIC_YEAR_MANAGE,
"/admin/announcements": ADMIN_PERMISSIONS.ANNOUNCEMENT_READ,
"/admin/invitation-codes": ADMIN_PERMISSIONS.INVITATION_CODE_MANAGE,
"/admin/files": ADMIN_PERMISSIONS.FILE_READ,
"/admin/ai-settings": ADMIN_PERMISSIONS.AI_CONFIGURE,
};
/** admin-portal 错误码前缀ISSUE-045 §5.2 */