差距分析闭环(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 已更新
805 lines
24 KiB
TypeScript
805 lines
24 KiB
TypeScript
/**
|
||
* MSW Handlers
|
||
*
|
||
* 仲裁 ARB-004 §4:MSW 拦截所有 GraphQL/HTTP 请求
|
||
* 端点:
|
||
* - POST /api/auth/login - mock 登录
|
||
* - POST /api/admin/graphql - admin 命名空间 GraphQL
|
||
* - GET /ws - WebSocket(mock-socket 在 use-websocket.ts 中处理)
|
||
*/
|
||
import { graphql, http, HttpResponse } from "msw";
|
||
import {
|
||
mockCurrentUser,
|
||
mockUsers,
|
||
mockRoles,
|
||
mockPermissions,
|
||
mockViewports,
|
||
mockOrganization,
|
||
mockClasses,
|
||
mockTeachers,
|
||
mockStudents,
|
||
mockAuditLogs,
|
||
mockDashboard,
|
||
mockSystemSettings,
|
||
mockSchools,
|
||
mockGrades,
|
||
mockGradeInsights,
|
||
mockDepartments,
|
||
mockAcademicYears,
|
||
mockAuditOverviewStats,
|
||
mockAuditTrend,
|
||
mockDataChangeLogs,
|
||
mockLoginLogs,
|
||
mockAnnouncements,
|
||
mockInvitationCodes,
|
||
mockFiles,
|
||
mockFileStats,
|
||
mockAiProviders,
|
||
mockAiUsage,
|
||
mockSecurityPolicy,
|
||
mockFileUploadConfig,
|
||
mockNotificationConfig,
|
||
mockBrandConfig,
|
||
genId,
|
||
} from "./fixtures";
|
||
import type {
|
||
UserViewModel,
|
||
RoleViewModel,
|
||
ViewportConfigViewModel,
|
||
PaginatedResult,
|
||
ListFilter,
|
||
} from "@/types/view-models";
|
||
|
||
const BASE_URL = "";
|
||
|
||
// 内存中可变副本(支持 mock 增删改)
|
||
const db = {
|
||
users: [...mockUsers] as UserViewModel[],
|
||
roles: [...mockRoles] as RoleViewModel[],
|
||
viewports: [...mockViewports] as ViewportConfigViewModel[],
|
||
permissions: [...mockPermissions],
|
||
organization: [...mockOrganization],
|
||
classes: [...mockClasses],
|
||
teachers: [...mockTeachers],
|
||
students: [...mockStudents],
|
||
auditLogs: [...mockAuditLogs],
|
||
systemSettings: { ...mockSystemSettings },
|
||
schools: [...mockSchools],
|
||
grades: [...mockGrades],
|
||
departments: [...mockDepartments],
|
||
academicYears: [...mockAcademicYears],
|
||
announcements: [...mockAnnouncements],
|
||
invitationCodes: [...mockInvitationCodes],
|
||
files: [...mockFiles],
|
||
aiProviders: [...mockAiProviders],
|
||
securityPolicy: { ...mockSecurityPolicy },
|
||
fileUploadConfig: { ...mockFileUploadConfig },
|
||
notificationConfig: { ...mockNotificationConfig },
|
||
brandConfig: { ...mockBrandConfig },
|
||
};
|
||
|
||
function paginate<T>(
|
||
items: T[],
|
||
filter: Partial<ListFilter>,
|
||
): PaginatedResult<T> {
|
||
const page = filter.page ?? 1;
|
||
const pageSize = filter.pageSize ?? 20;
|
||
const start = (page - 1) * pageSize;
|
||
const end = start + pageSize;
|
||
const sliced = items.slice(start, end);
|
||
return {
|
||
items: sliced,
|
||
total: items.length,
|
||
page,
|
||
pageSize,
|
||
hasNext: end < items.length,
|
||
};
|
||
}
|
||
|
||
function recordAudit(
|
||
action: string,
|
||
resourceType: string,
|
||
resourceId: string,
|
||
afterState?: string,
|
||
): void {
|
||
db.auditLogs.unshift({
|
||
id: genId("al"),
|
||
eventId: genId("evt"),
|
||
occurredAt: Date.now(),
|
||
actorUserId: mockCurrentUser.id,
|
||
actorName: mockCurrentUser.name,
|
||
action,
|
||
resourceType,
|
||
resourceId,
|
||
ip: "127.0.0.1",
|
||
userAgent: "MSW-Mock",
|
||
beforeState: null,
|
||
afterState: afterState ?? null,
|
||
traceId: genId("trace"),
|
||
});
|
||
}
|
||
|
||
export const handlers = [
|
||
// ─── 认证 ────────────────────────────────────────────
|
||
http.post(`${BASE_URL}/api/auth/login`, async ({ request }) => {
|
||
const body = (await request.json()) as { email: string; password: string };
|
||
if (body.email === "admin@edu.test" && body.password === "admin123") {
|
||
return HttpResponse.json({
|
||
success: true,
|
||
data: {
|
||
user: mockCurrentUser,
|
||
tokens: { accessToken: "mock-jwt-token-admin-001" },
|
||
},
|
||
});
|
||
}
|
||
return HttpResponse.json(
|
||
{
|
||
success: false,
|
||
error: { message: "邮箱或密码错误" },
|
||
},
|
||
{ status: 401 },
|
||
);
|
||
}),
|
||
|
||
// ─── GraphQL admin 命名空间 ──────────────────────────
|
||
graphql.operation(({ query, variables }) => {
|
||
const operationName =
|
||
(query.match(/^\s*(query|mutation)\s+(\w+)/) || [])[2] ?? "";
|
||
|
||
// ── currentUser ──
|
||
if (operationName === "CurrentUser") {
|
||
return HttpResponse.json({ data: { currentUser: mockCurrentUser } });
|
||
}
|
||
|
||
// ── adminUsers ──
|
||
if (operationName === "AdminUsers") {
|
||
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
|
||
let items = [...db.users];
|
||
if (filter.search) {
|
||
const q = filter.search.toLowerCase();
|
||
items = items.filter(
|
||
(u) =>
|
||
u.name.toLowerCase().includes(q) ||
|
||
u.email.toLowerCase().includes(q),
|
||
);
|
||
}
|
||
if (filter.status && filter.status !== "all") {
|
||
items = items.filter((u) => u.status === filter.status);
|
||
}
|
||
const result = paginate(items, filter);
|
||
return HttpResponse.json({ data: { adminUsers: result } });
|
||
}
|
||
|
||
// ── adminUser ──
|
||
if (operationName === "AdminUser") {
|
||
const id = variables.id as string;
|
||
const user = db.users.find((u) => u.id === id) ?? null;
|
||
return HttpResponse.json({ data: { adminUser: user } });
|
||
}
|
||
|
||
// ── createUser ──
|
||
if (operationName === "CreateUser") {
|
||
const input = variables.input as {
|
||
email: string;
|
||
name: string;
|
||
password: string;
|
||
roleIds?: string[];
|
||
dataScope?: string;
|
||
organizationId?: string;
|
||
};
|
||
const roles = db.roles.filter((r) => input.roleIds?.includes(r.id));
|
||
const newUser: UserViewModel = {
|
||
id: genId("u"),
|
||
email: input.email,
|
||
name: input.name,
|
||
roles,
|
||
status: "active",
|
||
dataScope: (input.dataScope as UserViewModel["dataScope"]) ?? "SCHOOL",
|
||
organizationId: input.organizationId ?? null,
|
||
schoolName: "Edu 实验学校",
|
||
lastLoginAt: null,
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now(),
|
||
};
|
||
db.users.push(newUser);
|
||
recordAudit(
|
||
"USER_CREATE",
|
||
"iam.user",
|
||
newUser.id,
|
||
JSON.stringify({ email: newUser.email, name: newUser.name }),
|
||
);
|
||
return HttpResponse.json({
|
||
data: {
|
||
createUser: {
|
||
id: newUser.id,
|
||
email: newUser.email,
|
||
name: newUser.name,
|
||
status: newUser.status,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── updateUser ──
|
||
if (operationName === "UpdateUser") {
|
||
const id = variables.id as string;
|
||
const input = variables.input as {
|
||
name?: string;
|
||
email?: string;
|
||
roleIds?: string[];
|
||
dataScope?: string;
|
||
organizationId?: string;
|
||
};
|
||
const user = db.users.find((u) => u.id === id);
|
||
if (!user) {
|
||
return HttpResponse.json(
|
||
{ errors: [{ message: "用户不存在" }] },
|
||
{ status: 404 },
|
||
);
|
||
}
|
||
if (input.name) user.name = input.name;
|
||
if (input.email) user.email = input.email;
|
||
if (input.roleIds)
|
||
user.roles = db.roles.filter((r) => input.roleIds!.includes(r.id));
|
||
if (input.dataScope)
|
||
user.dataScope = input.dataScope as UserViewModel["dataScope"];
|
||
if (input.organizationId !== undefined)
|
||
user.organizationId = input.organizationId;
|
||
user.updatedAt = Date.now();
|
||
recordAudit("USER_UPDATE", "iam.user", user.id, JSON.stringify(input));
|
||
return HttpResponse.json({
|
||
data: {
|
||
updateUser: {
|
||
id: user.id,
|
||
email: user.email,
|
||
name: user.name,
|
||
status: user.status,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── toggleUserStatus ──
|
||
if (operationName === "ToggleUserStatus") {
|
||
const id = variables.id as string;
|
||
const newStatus = variables.status as UserViewModel["status"];
|
||
const user = db.users.find((u) => u.id === id);
|
||
if (!user) {
|
||
return HttpResponse.json(
|
||
{ errors: [{ message: "用户不存在" }] },
|
||
{ status: 404 },
|
||
);
|
||
}
|
||
const before = user.status;
|
||
user.status = newStatus;
|
||
user.updatedAt = Date.now();
|
||
recordAudit(
|
||
"USER_TOGGLE_STATUS",
|
||
"iam.user",
|
||
user.id,
|
||
JSON.stringify({ before, after: newStatus }),
|
||
);
|
||
return HttpResponse.json({
|
||
data: { toggleUserStatus: { id: user.id, status: user.status } },
|
||
});
|
||
}
|
||
|
||
// ── adminRoles ──
|
||
if (operationName === "AdminRoles") {
|
||
return HttpResponse.json({ data: { adminRoles: db.roles } });
|
||
}
|
||
|
||
// ── createRole ──
|
||
if (operationName === "CreateRole") {
|
||
const input = variables.input as {
|
||
name: string;
|
||
code: string;
|
||
description?: string;
|
||
dataScope?: string;
|
||
};
|
||
const newRole: RoleViewModel = {
|
||
id: genId("r"),
|
||
name: input.name,
|
||
code: input.code,
|
||
description: input.description ?? "",
|
||
permissions: [],
|
||
userCount: 0,
|
||
dataScope: (input.dataScope as RoleViewModel["dataScope"]) ?? "SCHOOL",
|
||
isSystem: false,
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now(),
|
||
};
|
||
db.roles.push(newRole);
|
||
recordAudit(
|
||
"ROLE_CREATE",
|
||
"iam.role",
|
||
newRole.id,
|
||
JSON.stringify({ name: newRole.name, code: newRole.code }),
|
||
);
|
||
return HttpResponse.json({
|
||
data: {
|
||
createRole: {
|
||
id: newRole.id,
|
||
name: newRole.name,
|
||
code: newRole.code,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── updateRolePermissions ──
|
||
if (operationName === "UpdateRolePermissions") {
|
||
const roleId = variables.roleId as string;
|
||
const codes = variables.permissionCodes as string[];
|
||
const role = db.roles.find((r) => r.id === roleId);
|
||
if (!role) {
|
||
return HttpResponse.json(
|
||
{ errors: [{ message: "角色不存在" }] },
|
||
{ status: 404 },
|
||
);
|
||
}
|
||
role.permissions = db.permissions.filter((p) => codes.includes(p.code));
|
||
role.updatedAt = Date.now();
|
||
recordAudit(
|
||
"ROLE_UPDATE",
|
||
"iam.role",
|
||
role.id,
|
||
JSON.stringify({ permissionCodes: codes }),
|
||
);
|
||
return HttpResponse.json({
|
||
data: {
|
||
updateRolePermissions: {
|
||
id: role.id,
|
||
permissions: role.permissions.map((p) => ({
|
||
id: p.id,
|
||
code: p.code,
|
||
})),
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── adminPermissions ──
|
||
if (operationName === "AdminPermissions") {
|
||
return HttpResponse.json({ data: { adminPermissions: db.permissions } });
|
||
}
|
||
|
||
// ── adminViewports ──
|
||
if (operationName === "AdminViewports") {
|
||
const scope = variables.scope as string | undefined;
|
||
const items = scope
|
||
? db.viewports.filter((v) => v.scope === scope)
|
||
: db.viewports;
|
||
return HttpResponse.json({ data: { adminViewports: items } });
|
||
}
|
||
|
||
// ── updateViewport ──
|
||
if (operationName === "UpdateViewport") {
|
||
const id = variables.id as string;
|
||
const input = variables.input as {
|
||
label?: string;
|
||
sortOrder?: number;
|
||
isVisible?: boolean;
|
||
requiredPermission?: string | null;
|
||
};
|
||
const vp = db.viewports.find((v) => v.id === id);
|
||
if (!vp) {
|
||
return HttpResponse.json(
|
||
{ errors: [{ message: "视口不存在" }] },
|
||
{ status: 404 },
|
||
);
|
||
}
|
||
if (input.label !== undefined) vp.label = input.label;
|
||
if (input.sortOrder !== undefined) vp.sortOrder = input.sortOrder;
|
||
if (input.isVisible !== undefined) vp.isVisible = input.isVisible;
|
||
if (input.requiredPermission !== undefined)
|
||
vp.requiredPermission = input.requiredPermission;
|
||
recordAudit(
|
||
"VIEWPORT_UPDATE",
|
||
"iam.viewport",
|
||
vp.id,
|
||
JSON.stringify(input),
|
||
);
|
||
return HttpResponse.json({
|
||
data: {
|
||
updateViewport: {
|
||
id: vp.id,
|
||
key: vp.key,
|
||
label: vp.label,
|
||
sortOrder: vp.sortOrder,
|
||
isVisible: vp.isVisible,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── adminOrganization ──
|
||
if (operationName === "AdminOrganization") {
|
||
const parentId = variables.parentId as string | null | undefined;
|
||
const items =
|
||
parentId === undefined || parentId === null
|
||
? db.organization.filter((n) => n.parentId === null)
|
||
: db.organization.filter((n) => n.parentId === parentId);
|
||
return HttpResponse.json({ data: { adminOrganization: items } });
|
||
}
|
||
|
||
// ── adminClasses ──
|
||
if (operationName === "AdminClasses") {
|
||
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
|
||
let items = [...db.classes];
|
||
if (filter.search) {
|
||
const q = filter.search.toLowerCase();
|
||
items = items.filter(
|
||
(c) =>
|
||
c.name.toLowerCase().includes(q) ||
|
||
c.gradeName.toLowerCase().includes(q),
|
||
);
|
||
}
|
||
if (filter.schoolId) {
|
||
// mock:单一学校
|
||
}
|
||
const result = paginate(items, filter);
|
||
return HttpResponse.json({ data: { adminClasses: result } });
|
||
}
|
||
|
||
// ── adminTeachers ──
|
||
if (operationName === "AdminTeachers") {
|
||
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
|
||
let items = [...db.teachers];
|
||
if (filter.search) {
|
||
const q = filter.search.toLowerCase();
|
||
items = items.filter(
|
||
(t) =>
|
||
t.name.toLowerCase().includes(q) ||
|
||
t.email.toLowerCase().includes(q),
|
||
);
|
||
}
|
||
if (filter.status && filter.status !== "all") {
|
||
items = items.filter((t) => t.status === filter.status);
|
||
}
|
||
const result = paginate(items, filter);
|
||
return HttpResponse.json({ data: { adminTeachers: result } });
|
||
}
|
||
|
||
// ── adminStudents ──
|
||
if (operationName === "AdminStudents") {
|
||
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
|
||
let items = [...db.students];
|
||
if (filter.search) {
|
||
const q = filter.search.toLowerCase();
|
||
items = items.filter(
|
||
(s) =>
|
||
s.name.toLowerCase().includes(q) ||
|
||
s.email.toLowerCase().includes(q),
|
||
);
|
||
}
|
||
if (filter.status && filter.status !== "all") {
|
||
items = items.filter((s) => s.status === filter.status);
|
||
}
|
||
const result = paginate(items, filter);
|
||
return HttpResponse.json({ data: { adminStudents: result } });
|
||
}
|
||
|
||
// ── auditLogs ──
|
||
if (operationName === "AuditLogs") {
|
||
const filter = (variables.filter ?? {}) as Partial<ListFilter> & {
|
||
action?: string;
|
||
actorUserId?: string;
|
||
};
|
||
let items = [...db.auditLogs];
|
||
if (filter.search) {
|
||
const q = filter.search.toLowerCase();
|
||
items = items.filter(
|
||
(l) =>
|
||
l.actorName.toLowerCase().includes(q) ||
|
||
l.action.toLowerCase().includes(q) ||
|
||
l.resourceType.toLowerCase().includes(q),
|
||
);
|
||
}
|
||
if (filter.action) {
|
||
items = items.filter((l) => l.action === filter.action);
|
||
}
|
||
if (filter.actorUserId) {
|
||
items = items.filter((l) => l.actorUserId === filter.actorUserId);
|
||
}
|
||
const result = paginate(items, filter);
|
||
return HttpResponse.json({ data: { auditLogs: result } });
|
||
}
|
||
|
||
// ── adminDashboard ──
|
||
if (operationName === "AdminDashboard") {
|
||
return HttpResponse.json({ data: { adminDashboard: mockDashboard } });
|
||
}
|
||
|
||
// ── systemSettings ──
|
||
if (operationName === "SystemSettings") {
|
||
return HttpResponse.json({ data: { systemSettings: db.systemSettings } });
|
||
}
|
||
|
||
// ── updateSystemSettings ──
|
||
if (operationName === "UpdateSystemSettings") {
|
||
const input = variables.input as Partial<typeof mockSystemSettings>;
|
||
Object.assign(db.systemSettings, input);
|
||
recordAudit(
|
||
"SYSTEM_SETTINGS_UPDATE",
|
||
"admin.system",
|
||
"settings",
|
||
JSON.stringify(input),
|
||
);
|
||
return HttpResponse.json({
|
||
data: {
|
||
updateSystemSettings: {
|
||
schoolName: db.systemSettings.schoolName,
|
||
schoolCode: db.systemSettings.schoolCode,
|
||
academicYear: db.systemSettings.academicYear,
|
||
semester: db.systemSettings.semester,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── auditOverviewStats ──
|
||
if (operationName === "AuditOverviewStats") {
|
||
return HttpResponse.json({
|
||
data: { auditOverviewStats: mockAuditOverviewStats },
|
||
});
|
||
}
|
||
|
||
// ── auditTrend ──
|
||
if (operationName === "AuditTrend") {
|
||
return HttpResponse.json({ data: { auditTrend: mockAuditTrend } });
|
||
}
|
||
|
||
// ── dataChangeLogs ──
|
||
if (operationName === "DataChangeLogs") {
|
||
return HttpResponse.json({
|
||
data: {
|
||
dataChangeLogs: paginate(mockDataChangeLogs, variables.filter ?? {}),
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── loginLogs ──
|
||
if (operationName === "LoginLogs") {
|
||
return HttpResponse.json({
|
||
data: { loginLogs: paginate(mockLoginLogs, variables.filter ?? {}) },
|
||
});
|
||
}
|
||
|
||
// ── adminSchools ──
|
||
if (operationName === "AdminSchools") {
|
||
return HttpResponse.json({ data: { adminSchools: db.schools } });
|
||
}
|
||
|
||
// ── adminGrades ──
|
||
if (operationName === "AdminGrades") {
|
||
const schoolId = variables.schoolId;
|
||
const grades = schoolId
|
||
? db.grades.filter((g) => g.schoolId === schoolId)
|
||
: db.grades;
|
||
return HttpResponse.json({ data: { adminGrades: grades } });
|
||
}
|
||
|
||
// ── adminGradeInsights ──
|
||
if (operationName === "AdminGradeInsights") {
|
||
return HttpResponse.json({
|
||
data: { adminGradeInsights: mockGradeInsights },
|
||
});
|
||
}
|
||
|
||
// ── adminDepartments ──
|
||
if (operationName === "AdminDepartments") {
|
||
const schoolId = variables.schoolId;
|
||
const depts = schoolId
|
||
? db.departments.filter((d) => d.schoolId === schoolId)
|
||
: db.departments;
|
||
return HttpResponse.json({ data: { adminDepartments: depts } });
|
||
}
|
||
|
||
// ── adminAcademicYears ──
|
||
if (operationName === "AdminAcademicYears") {
|
||
return HttpResponse.json({
|
||
data: { adminAcademicYears: db.academicYears },
|
||
});
|
||
}
|
||
|
||
// ── adminAnnouncements ──
|
||
if (operationName === "AdminAnnouncements") {
|
||
return HttpResponse.json({
|
||
data: {
|
||
adminAnnouncements: paginate(
|
||
db.announcements,
|
||
variables.filter ?? {},
|
||
),
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── createAnnouncement ──
|
||
if (operationName === "CreateAnnouncement") {
|
||
const input = variables.input;
|
||
const newAnn = {
|
||
id: genId("ann"),
|
||
title: input.title,
|
||
content: input.content,
|
||
status: "draft" as const,
|
||
audience: input.audience ?? ["all"],
|
||
authorName: mockCurrentUser.name,
|
||
isPinned: false,
|
||
readCount: 0,
|
||
totalCount: 0,
|
||
publishedAt: null,
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now(),
|
||
};
|
||
db.announcements.unshift(newAnn);
|
||
recordAudit("ANNOUNCEMENT_CREATE", "announcement", newAnn.id);
|
||
return HttpResponse.json({
|
||
data: {
|
||
createAnnouncement: {
|
||
id: newAnn.id,
|
||
title: newAnn.title,
|
||
status: newAnn.status,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── publishAnnouncement ──
|
||
if (operationName === "PublishAnnouncement") {
|
||
const ann = db.announcements.find((a) => a.id === variables.id);
|
||
if (ann) {
|
||
ann.status = "published";
|
||
ann.publishedAt = Date.now();
|
||
ann.updatedAt = Date.now();
|
||
recordAudit("ANNOUNCEMENT_PUBLISH", "announcement", ann.id);
|
||
}
|
||
return HttpResponse.json({
|
||
data: {
|
||
publishAnnouncement: { id: variables.id, status: "published" },
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── archiveAnnouncement ──
|
||
if (operationName === "ArchiveAnnouncement") {
|
||
const ann = db.announcements.find((a) => a.id === variables.id);
|
||
if (ann) {
|
||
ann.status = "archived";
|
||
ann.updatedAt = Date.now();
|
||
recordAudit("ANNOUNCEMENT_ARCHIVE", "announcement", ann.id);
|
||
}
|
||
return HttpResponse.json({
|
||
data: { archiveAnnouncement: { id: variables.id, status: "archived" } },
|
||
});
|
||
}
|
||
|
||
// ── toggleAnnouncementPin ──
|
||
if (operationName === "ToggleAnnouncementPin") {
|
||
const ann = db.announcements.find((a) => a.id === variables.id);
|
||
if (ann) {
|
||
ann.isPinned = !ann.isPinned;
|
||
ann.updatedAt = Date.now();
|
||
}
|
||
return HttpResponse.json({
|
||
data: {
|
||
toggleAnnouncementPin: {
|
||
id: variables.id,
|
||
isPinned: ann?.isPinned ?? false,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── adminInvitationCodes ──
|
||
if (operationName === "AdminInvitationCodes") {
|
||
return HttpResponse.json({
|
||
data: {
|
||
adminInvitationCodes: paginate(
|
||
db.invitationCodes,
|
||
variables.filter ?? {},
|
||
),
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── generateInvitationCodes ──
|
||
if (operationName === "GenerateInvitationCodes") {
|
||
const input = variables.input;
|
||
const count = Math.min(input.count ?? 10, 1000);
|
||
const batchId = genId("bat");
|
||
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||
const codes: string[] = [];
|
||
for (let i = 0; i < count; i++) {
|
||
let code = "";
|
||
for (let j = 0; j < 8; j++) {
|
||
code += charset[Math.floor(Math.random() * charset.length)];
|
||
}
|
||
codes.push(code);
|
||
db.invitationCodes.unshift({
|
||
id: genId("ic"),
|
||
code,
|
||
status: "unused",
|
||
roleId: input.roleId ?? null,
|
||
roleName: null,
|
||
classId: input.classId ?? null,
|
||
className: null,
|
||
email: input.email ?? null,
|
||
batchId,
|
||
usedBy: null,
|
||
usedByName: null,
|
||
usedAt: null,
|
||
createdAt: Date.now(),
|
||
expiresAt: input.expiresAt ?? null,
|
||
});
|
||
}
|
||
recordAudit("INVITATION_CODE_GENERATE", "invitation_code", batchId);
|
||
return HttpResponse.json({
|
||
data: { generateInvitationCodes: { codes, batchId } },
|
||
});
|
||
}
|
||
|
||
// ── deleteInvitationCodes ──
|
||
if (operationName === "DeleteInvitationCodes") {
|
||
const ids = variables.ids as string[];
|
||
db.invitationCodes = db.invitationCodes.filter(
|
||
(ic) => !ids.includes(ic.id),
|
||
);
|
||
recordAudit("INVITATION_CODE_DELETE", "invitation_code", ids.join(","));
|
||
return HttpResponse.json({ data: { deleteInvitationCodes: true } });
|
||
}
|
||
|
||
// ── adminFiles ──
|
||
if (operationName === "AdminFiles") {
|
||
return HttpResponse.json({
|
||
data: { adminFiles: paginate(db.files, variables.filter ?? {}) },
|
||
});
|
||
}
|
||
|
||
// ── adminFileStats ──
|
||
if (operationName === "AdminFileStats") {
|
||
return HttpResponse.json({ data: { adminFileStats: mockFileStats } });
|
||
}
|
||
|
||
// ── deleteFiles ──
|
||
if (operationName === "DeleteFiles") {
|
||
const ids = variables.ids as string[];
|
||
db.files = db.files.filter((f) => !ids.includes(f.id));
|
||
recordAudit("FILE_DELETE", "file", ids.join(","));
|
||
return HttpResponse.json({ data: { deleteFiles: true } });
|
||
}
|
||
|
||
// ── aiProviders ──
|
||
if (operationName === "AiProviders") {
|
||
return HttpResponse.json({ data: { aiProviders: db.aiProviders } });
|
||
}
|
||
|
||
// ── updateAiProvider ──
|
||
if (operationName === "UpdateAiProvider") {
|
||
const provider = db.aiProviders.find((p) => p.id === variables.id);
|
||
if (provider) {
|
||
Object.assign(provider, variables.input);
|
||
}
|
||
return HttpResponse.json({
|
||
data: {
|
||
updateAiProvider: {
|
||
id: variables.id,
|
||
isActive: provider?.isActive ?? false,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
// ── aiUsage ──
|
||
if (operationName === "AiUsage") {
|
||
return HttpResponse.json({ data: { aiUsage: mockAiUsage } });
|
||
}
|
||
|
||
// ── 未匹配的 operation ──
|
||
return HttpResponse.json(
|
||
{ errors: [{ message: `未实现的 GraphQL operation: ${operationName}` }] },
|
||
{ status: 501 },
|
||
);
|
||
}),
|
||
];
|