import "server-only"; import { and, eq } from "drizzle-orm"; import { createId } from "@paralleldrive/cuid2"; import { db } from "@/shared/db"; import { lessonPlanTemplates, lessonPlans } from "@/shared/db/schema"; import { SYSTEM_TEMPLATES } from "./constants"; import { normalizeDocument, LessonPlanDataError } from "./data-access"; import { normalizeTemplateBlocks, isTemplateType, isTemplateScope, } from "./lib/type-guards"; import type { LessonPlanTemplate, TemplateBlockSkeleton, } from "./types"; // ---- 类型映射:Drizzle 行 → LessonPlanTemplate(Date → ISO string)---- function mapRowToTemplate(row: { id: string; name: string; type: string; scope: string; blocks: unknown; creatorId: string | null; createdAt: Date; updatedAt: Date; }): LessonPlanTemplate { return { id: row.id, name: row.name, type: isTemplateType(row.type) ? row.type : "personal", scope: isTemplateScope(row.scope) ? row.scope : "custom", // P1 修复:使用 normalizeTemplateBlocks 安全转换 DB JSON 字段,替代 as 断言 blocks: normalizeTemplateBlocks(row.blocks), creatorId: row.creatorId, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } export async function getLessonPlanTemplates( userId: string, ): Promise { // system 模板(内存)+ personal 模板(DB) const systemTemplates: LessonPlanTemplate[] = SYSTEM_TEMPLATES.map((t) => ({ id: t.id, name: t.name, type: "system", scope: t.scope, blocks: t.blocks, creatorId: null, createdAt: "", updatedAt: "", })); const personalRows = await db .select() .from(lessonPlanTemplates) .where( and( eq(lessonPlanTemplates.type, "personal"), eq(lessonPlanTemplates.creatorId, userId), ), ); const personalTemplates = personalRows.map(mapRowToTemplate); return [...systemTemplates, ...personalTemplates]; } export async function saveAsTemplate(input: { sourcePlanId: string; name: string; userId: string; }): Promise<{ templateId: string }> { // 从课案 content 提取 block 骨架 const plan = await db .select({ content: lessonPlans.content }) .from(lessonPlans) .where( and( eq(lessonPlans.id, input.sourcePlanId), eq(lessonPlans.creatorId, input.userId), ), ) .limit(1); if (plan.length === 0) throw new LessonPlanDataError("NOT_FOUND"); const doc = normalizeDocument(plan[0].content); const skeleton: TemplateBlockSkeleton[] = doc.nodes .filter((b): b is import("./types").LessonPlanNode => b.type !== "textbook_content") .map((b) => ({ type: b.type, title: b.title, })); const templateId = createId(); await db.insert(lessonPlanTemplates).values({ id: templateId, name: input.name, type: "personal", scope: "custom", blocks: skeleton, creatorId: input.userId, }); return { templateId }; } export async function deletePersonalTemplate( templateId: string, userId: string, ): Promise { await db.delete(lessonPlanTemplates).where( and( eq(lessonPlanTemplates.id, templateId), eq(lessonPlanTemplates.type, "personal"), eq(lessonPlanTemplates.creatorId, userId), ), ); }