import "server-only"; import { and, desc, eq, inArray, max } from "drizzle-orm"; import { createId } from "@paralleldrive/cuid2"; import { db } from "@/shared/db"; import { lessonPlanVersions, lessonPlans } from "@/shared/db/schema"; import { normalizeDocument } from "./data-access"; import type { LessonPlanDocument, LessonPlanVersion } from "./types"; // ---- 类型映射:Drizzle 行 → LessonPlanVersion(Date → ISO string)---- function mapRowToVersion(row: { id: string; planId: string; versionNo: number; label: string | null; content: unknown; isAuto: boolean; creatorId: string; createdAt: Date; }): LessonPlanVersion { return { id: row.id, planId: row.planId, versionNo: row.versionNo, label: row.label, content: normalizeDocument(row.content), isAuto: row.isAuto, creatorId: row.creatorId, createdAt: row.createdAt.toISOString(), }; } export async function getLessonPlanVersions( planId: string, userId: string, ): Promise { // 校验归属 const plan = await db .select({ id: lessonPlans.id }) .from(lessonPlans) .where( and(eq(lessonPlans.id, planId), eq(lessonPlans.creatorId, userId)), ) .limit(1); if (plan.length === 0) return []; const rows = await db .select() .from(lessonPlanVersions) .where(eq(lessonPlanVersions.planId, planId)) .orderBy(desc(lessonPlanVersions.versionNo)); return rows.map(mapRowToVersion); } export async function createLessonPlanVersion(input: { planId: string; content: LessonPlanDocument; userId: string; isAuto: boolean; label?: string; }): Promise<{ versionNo: number } | null> { // P0 修复:max(versionNo)+1 必须在事务内完成,避免并发产生重复版本号 return await db.transaction(async (tx) => { // 校验 planId 归属 const plan = await tx .select({ id: lessonPlans.id }) .from(lessonPlans) .where( and(eq(lessonPlans.id, input.planId), eq(lessonPlans.creatorId, input.userId)), ) .limit(1); if (plan.length === 0) return null; const maxRow = await tx .select({ maxNo: max(lessonPlanVersions.versionNo) }) .from(lessonPlanVersions) .where(eq(lessonPlanVersions.planId, input.planId)); const nextNo = (maxRow[0]?.maxNo ?? 0) + 1; await tx.insert(lessonPlanVersions).values({ id: createId(), planId: input.planId, versionNo: nextNo, label: input.label ?? null, content: input.content, isAuto: input.isAuto, creatorId: input.userId, }); return { versionNo: nextNo }; }); } export async function getVersionContent( planId: string, versionNo: number, userId: string, ): Promise { // 校验归属 const plan = await db .select({ id: lessonPlans.id }) .from(lessonPlans) .where( and(eq(lessonPlans.id, planId), eq(lessonPlans.creatorId, userId)), ) .limit(1); if (plan.length === 0) return null; const rows = await db .select({ content: lessonPlanVersions.content }) .from(lessonPlanVersions) .where( and( eq(lessonPlanVersions.planId, planId), eq(lessonPlanVersions.versionNo, versionNo), ), ) .limit(1); if (rows.length === 0) return null; return normalizeDocument(rows[0].content); } export async function revertToVersion( planId: string, versionNo: number, userId: string, // V3 修复:由 actions 层传入 i18n 翻译的回退标签,避免 data-access 硬编码中文 revertLabel: string, ): Promise<{ newVersionNo: number } | null> { const content = await getVersionContent(planId, versionNo, userId); if (!content) return null; // P0 修复:update 与 createLessonPlanVersion 必须在同一事务内, // 避免回退后 content 已覆盖但版本记录未生成的不一致状态 return await db.transaction(async (tx) => { await tx .update(lessonPlans) .set({ content, lastSavedAt: new Date() }) .where( and(eq(lessonPlans.id, planId), eq(lessonPlans.creatorId, userId)), ); const maxRow = await tx .select({ maxNo: max(lessonPlanVersions.versionNo) }) .from(lessonPlanVersions) .where(eq(lessonPlanVersions.planId, planId)); const newNo = (maxRow[0]?.maxNo ?? 0) + 1; await tx.insert(lessonPlanVersions).values({ id: createId(), planId, versionNo: newNo, label: revertLabel, content, isAuto: false, creatorId: userId, }); return { newVersionNo: newNo }; }); } export async function pruneAutoVersions( planId: string, userId: string, keep = 50, ): Promise { // 校验 planId 归属 const plan = await db .select({ id: lessonPlans.id }) .from(lessonPlans) .where(and(eq(lessonPlans.id, planId), eq(lessonPlans.creatorId, userId))) .limit(1); if (plan.length === 0) return 0; const rows = await db .select({ id: lessonPlanVersions.id, isAuto: lessonPlanVersions.isAuto, versionNo: lessonPlanVersions.versionNo, }) .from(lessonPlanVersions) .where(eq(lessonPlanVersions.planId, planId)) .orderBy(desc(lessonPlanVersions.versionNo)); if (rows.length <= keep) return 0; // 保留前 keep 条;超出部分只删 isAuto=true 的 const toDelete = rows.slice(keep).filter((r) => r.isAuto); if (toDelete.length === 0) return 0; await db .delete(lessonPlanVersions) .where( and( eq(lessonPlanVersions.planId, planId), inArray( lessonPlanVersions.id, toDelete.map((r) => r.id), ), ), ); return toDelete.length; }