/** * MSW Handlers - P7 扩展(组卷 + 题库 + 教材 + 考试分析) * * 维护者:ai13(teacher-portal) * 关联:graphql-p7-exams.ts、workline.md §3 P7 交付物 * * 设计:本文件独立于 handlers.ts,不修改共享文件,最后统一合并。 * - 拦截 POST /api/v1/teacher/graphql,按 operationName 分发 P7-exams 操作。 * - 未覆盖的 operation(P2/P3/P4/P5/P7-grades)返回 undefined → MSW fallthrough 至下游 handler。 * * 注册顺序:p7ExamsHandlers 必须在 handlers 之前注册(见 browser.ts / server.ts)。 * * 覆盖 operationName(对应 graphql-p7-exams.ts): * - ExamBuild → 试卷结构 + 题库候选分页 * - SaveExamBuild → 保存组卷 * - ExamAnalytics → 考后分析 * - QuestionsLibrary → 题库列表(六维筛选 + 分页) * - QuestionCreate / QuestionUpdate / QuestionDelete → 题目 CRUD * - QuestionBatchImport → 批量导入 * - QuestionBatchExport → 批量导出 * - Textbooks → 教材列表 * - TextbookDetail → 教材详情 * - TextbookCreate → 创建教材 */ import { http, HttpResponse } from "msw"; import { filterQuestions, findQuestion, createMockQuestion, updateMockQuestion, deleteMockQuestion, batchImportQuestions, batchExportQuestions, } from "./fixtures/questions"; import { filterTextbooks, findTextbookDetail, createMockTextbook, } from "./fixtures/textbooks"; import { generateExamAnalytics, generateExamBuildStructure, } from "./fixtures/exam-analytics"; import { findMockExamDetail } from "./fixtures/exams"; import type { QuestionsLibraryFilter, QuestionCreateInput, QuestionUpdateInput, QuestionBatchImportInput, SaveExamBuildInput, TextbooksFilter, TextbookCreateInput, } from "@/lib/graphql-p7-exams"; /** GraphQL 响应体(urql 期望 { data: ... }) */ function graphqlData(data: T) { return HttpResponse.json({ data }); } function extractOperationName(query?: string): string { if (!query) return "Unknown"; const match = query.match(/(?:query|mutation)\s+(\w+)/); return match?.[1] ?? "Unknown"; } export const p7ExamsHandlers = [ // POST /api/v1/teacher/graphql → P7-exams operations(未覆盖的 fallthrough 至 handlers.ts) http.post("*/api/v1/teacher/graphql", async ({ request }) => { const body = (await request.json()) as { operationName?: string; query?: string; variables?: Record; }; const operationName = body.operationName ?? extractOperationName(body.query); const variables = body.variables ?? {}; switch (operationName) { case "ExamBuild": { const examId = (variables.examId as string) ?? ""; const candidatePage = (variables.candidatePage as number) ?? 1; const candidatePageSize = (variables.candidatePageSize as number) ?? 10; const filter = (variables.filter as QuestionsLibraryFilter | undefined) ?? undefined; // 调用 exams.ts 的 findMockExamDetail 拿基础信息 const examDetail = examId ? findMockExamDetail(examId) : null; // 同时叠加题库筛选(filter 影响 candidates) const structure = generateExamBuildStructure( examId, examDetail, candidatePage, candidatePageSize, ); if (!structure) { return HttpResponse.json( { errors: [ { message: `试卷结构不存在: ${examId}`, extensions: { code: "BFF_TEACHER_NOT_FOUND" }, }, ], }, { status: 200 }, ); } // 叠加 filter 到 candidates if (filter) { const filtered = filterQuestions({ ...filter, page: candidatePage, pageSize: candidatePageSize, }); return graphqlData({ examBuild: { ...structure, candidates: filtered, }, }); } return graphqlData({ examBuild: structure }); } case "SaveExamBuild": { const input = variables.input as SaveExamBuildInput; const totalScore = input.questions.reduce( (acc, q) => acc + (q.score || 0), 0, ); const now = new Date().toISOString(); return graphqlData({ saveExamBuild: { examId: input.examId, totalScore, questionCount: input.questions.length, savedAt: now, }, }); } case "ExamAnalytics": { const examId = (variables.examId as string) ?? ""; const analytics = generateExamAnalytics(examId); if (!analytics) { // 兜底:未匹配到 EXAM_DEFS 时按 examId 生成(使用第一场考试的 totalScore) return HttpResponse.json( { errors: [ { message: `考试分析不存在: ${examId}`, extensions: { code: "BFF_TEACHER_NOT_FOUND" }, }, ], }, { status: 200 }, ); } return graphqlData({ examAnalytics: analytics }); } case "QuestionsLibrary": { const filter = (variables.filter as QuestionsLibraryFilter | undefined) ?? {}; return graphqlData({ questionsLibrary: filterQuestions(filter), }); } case "QuestionCreate": { const input = variables.input as QuestionCreateInput; const created = createMockQuestion(input); return graphqlData({ questionCreate: created }); } case "QuestionUpdate": { const input = variables.input as QuestionUpdateInput; const updated = updateMockQuestion(input); if (!updated) { return HttpResponse.json( { errors: [ { message: `题目不存在: ${input.questionId}`, extensions: { code: "BFF_TEACHER_NOT_FOUND" }, }, ], }, { status: 200 }, ); } return graphqlData({ questionUpdate: updated }); } case "QuestionDelete": { const questionId = variables.questionId as string; // 删除前确认存在 const existed = findQuestion(questionId); if (!existed) { return HttpResponse.json( { errors: [ { message: `题目不存在: ${questionId}`, extensions: { code: "BFF_TEACHER_NOT_FOUND" }, }, ], }, { status: 200 }, ); } deleteMockQuestion(questionId); return graphqlData({ questionDelete: { questionId, deleted: true }, }); } case "QuestionBatchImport": { const input = variables.input as QuestionBatchImportInput; const result = batchImportQuestions(input); return graphqlData({ questionBatchImport: result }); } case "QuestionBatchExport": { const filter = (variables.filter as QuestionsLibraryFilter | undefined) ?? {}; const result = batchExportQuestions(filter); return graphqlData({ questionBatchExport: result }); } case "Textbooks": { const filter = (variables.filter as TextbooksFilter | undefined) ?? {}; return graphqlData({ textbooks: filterTextbooks(filter) }); } case "TextbookDetail": { const id = (variables.id as string) ?? ""; const detail = findTextbookDetail(id); if (!detail) { return HttpResponse.json( { errors: [ { message: `教材不存在: ${id}`, extensions: { code: "BFF_TEACHER_NOT_FOUND" }, }, ], }, { status: 200 }, ); } return graphqlData({ textbookDetail: detail }); } case "TextbookCreate": { const input = variables.input as TextbookCreateInput; const created = createMockTextbook(input); return graphqlData({ textbookCreate: created }); } default: // 未覆盖的 P2/P3/P4/P5/P7-grades operation:返回 undefined → fallthrough 至 handlers.ts return undefined; } }), ];