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:
157
apps/admin-portal/src/lib/auth.test.ts
Normal file
157
apps/admin-portal/src/lib/auth.test.ts
Normal 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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
54
apps/admin-portal/src/lib/i18n.test.ts
Normal file
54
apps/admin-portal/src/lib/i18n.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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": "校训",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
92
apps/admin-portal/src/lib/permissions.test.ts
Normal file
92
apps/admin-portal/src/lib/permissions.test.ts
Normal 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_");
|
||||
});
|
||||
});
|
||||
@@ -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) */
|
||||
|
||||
Reference in New Issue
Block a user