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 type { LessonPlanTemplate, TemplateBlockSkeleton, TemplateType, TemplateScope, } from "./types"; // ---- 类型守卫:安全地将 DB string 收窄为联合类型 ---- const TEMPLATE_TYPES = ["system", "personal"] as const; function isTemplateType(v: string): v is TemplateType { return (TEMPLATE_TYPES as readonly string[]).includes(v); } const TEMPLATE_SCOPES = ["regular", "review", "experiment", "inquiry", "blank", "custom"] as const; function isTemplateScope(v: string): v is TemplateScope { return (TEMPLATE_SCOPES as readonly string[]).includes(v); } // ---- 类型映射: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", // 从 unknown 转换为 TemplateBlockSkeleton[](DB JSON 字段) blocks: row.blocks as LessonPlanTemplate["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), ), ); }