import "server-only"; import { eq } from "drizzle-orm"; import { createId } from "@paralleldrive/cuid2"; import { db } from "@/shared/db"; import { lessonPlans } from "@/shared/db/schema"; import { createQuestionWithRelations } from "@/modules/questions/data-access"; import { persistExamDraft, addExamQuestions } from "@/modules/exams/data-access"; import { createHomeworkAssignment } from "@/modules/homework/data-access-write"; import { getStudentIdsByClassIds } from "@/modules/classes/data-access"; import { normalizeDocument } from "./data-access"; import type { LessonPlanDocument, ExerciseBlockData } from "./types"; interface PublishInput { planId: string; blockId: string; userId: string; classIds: string[]; availableAt?: Date; dueAt?: Date; } interface PublishResult { examId: string; assignmentId: string; updatedContent: LessonPlanDocument; } export async function publishLessonPlanHomework( input: PublishInput, ): Promise { // 1. 读取课案 const rows = await db .select() .from(lessonPlans) .where(eq(lessonPlans.id, input.planId)) .limit(1); if (rows.length === 0) throw new Error("课案不存在"); const row = rows[0] as unknown as { id: string; content: unknown; creatorId: string; title: string; textbookId: string | null; chapterId: string | null; subjectId: string | null; gradeId: string | null; }; const plan = { ...row, content: normalizeDocument(row.content), }; if (plan.creatorId !== input.userId) throw new Error("无权发布"); // 2. 定位 exercise block const block = plan.content.nodes.find((b) => b.id === input.blockId); if (!block || block.type !== "exercise") throw new Error("练习块不存在"); const data = block.data as ExerciseBlockData; if (data.items.length === 0) throw new Error("练习块无题目"); if (data.publishedAssignmentId) throw new Error("该练习块已发布,请使用'重新发布'"); // 3. inline 题目入库,替换占位 ID const newContent: LessonPlanDocument = structuredClone(plan.content); const newBlock = newContent.nodes.find((b) => b.id === input.blockId); if (!newBlock || newBlock.type !== "exercise") throw new Error("练习块不存在"); const newData = newBlock.data as ExerciseBlockData; for (let i = 0; i < newData.items.length; i++) { const item = newData.items[i]; if (item.source === "inline" && item.inlineContent) { const questionId = await createQuestionWithRelations( { content: item.inlineContent.content, type: item.inlineContent.type as never, difficulty: item.inlineContent.difficulty, knowledgePointIds: item.inlineContent.knowledgePointIds, }, input.userId, ); newData.items[i] = { ...item, questionId, inlineContent: undefined, }; } } // 4. 打包 exam 草稿 const examId = createId(); if (!plan.subjectId || !plan.gradeId) { throw new Error("课案缺少学科或年级信息,无法发布"); } await persistExamDraft({ examId, title: `${plan.title} - 作业`, creatorId: input.userId, subjectId: plan.subjectId, gradeId: plan.gradeId, scheduledAt: undefined, description: `来自课案:${plan.title}`, }); // 插入 examQuestions(通过 exams data-access 跨模块接口) await addExamQuestions( examId, newData.items.map((it, i) => ({ questionId: it.questionId, score: it.score, order: i, })), ); // 5. 下发作业 const assignmentId = createId(); const targetStudentIds = await getStudentIdsByClassIds(input.classIds); if (targetStudentIds.length === 0) { throw new Error("所选班级无学生"); } await createHomeworkAssignment({ assignmentId, sourceExamId: examId, title: `${plan.title} - 作业`, description: `来自课案:${plan.title}`, structure: null, status: "published", creatorId: input.userId, availableAt: input.availableAt ?? null, dueAt: input.dueAt ?? null, allowLate: false, lateDueAt: null, maxAttempts: 1, publish: true, questions: newData.items.map((it, i) => ({ questionId: it.questionId, score: it.score, order: i, })), targetStudentIds, }); // 6. 回写溯源标记 newData.publishedExamId = examId; newData.publishedAssignmentId = assignmentId; newData.publishedAt = new Date().toISOString(); await db .update(lessonPlans) .set({ content: newContent }) .where(eq(lessonPlans.id, input.planId)); return { examId, assignmentId, updatedContent: newContent }; }