import { and, eq, sql } from "drizzle-orm"; import { db } from "@/db"; import { grammarPoints, grammarProgress, taskInstances } from "@/db/schema"; import { todayStr } from "@/lib/dates"; import { extractQuiz, type QuizQ } from "@/lib/quiz-parse"; export { extractQuiz, type QuizQ }; // 三档级别元信息(展示用) export const LEVELS = [ { id: "starter", name: "入门", stars: 1, desc: "打地基:词与句子、四种句类、名词、代词、冠词、be 动词等。" }, { id: "base", name: "基础", stars: 2, desc: "时态、情态、比较级、介词、连词等核心用法。" }, { id: "pro", name: "进阶", stars: 3, desc: "复合句、非谓语、被动、虚拟等综合提升。" }, ] as const; export type PointRow = { pointId: string; level: string; levelName: string; title: string; difficulty: number; seq: number; }; // 全部知识点(按展示顺序) export async function listPoints(): Promise { return db .select({ pointId: grammarPoints.pointId, level: grammarPoints.level, levelName: grammarPoints.levelName, title: grammarPoints.title, difficulty: grammarPoints.difficulty, seq: grammarPoints.seq, }) .from(grammarPoints) .orderBy(grammarPoints.seq); } export async function totalPoints(): Promise { const [r] = await db.select({ n: sql`COUNT(*)` }).from(grammarPoints); return r.n; } // 单个知识点全文 export async function getPoint(pointId: string) { const [row] = await db.select().from(grammarPoints).where(eq(grammarPoints.pointId, pointId)).limit(1); return row ?? null; } // 已通过知识点集合 export async function passedByPoint(childId: number): Promise> { const rows = await db .select({ pointId: grammarProgress.pointId }) .from(grammarProgress) .where(and(eq(grammarProgress.childUserId, childId), eq(grammarProgress.passed, true))); return new Set(rows.map((r) => r.pointId)); } export async function passedCount(childId: number): Promise { return (await passedByPoint(childId)).size; } const norm = (s: unknown) => String(s ?? "").trim().toLowerCase(); // 服务端判分:六种题型 choice/judge/fill/order/match/solve(solve 不在此处判,走 AI) export type AnswerValue = number | string | string[]; // xuchao: 单题客观判分(solve 返回 false,由 AI 判分链路单独处理) export function gradeOne(q: QuizQ, a: AnswerValue | null | undefined): boolean { if (q.type === "fill") return (q.answers ?? []).some((x) => norm(x) === norm(a)); if (q.type === "order") return Array.isArray(a) && a.join(" ").trim() === (q.answer as string[]).join(" ").trim(); if (q.type === "match") return Array.isArray(a) && (q.pairs ?? []).every((p, pi) => norm(a[pi]) === norm(p[1])); if (q.type === "solve") return false; return Number(a) === (q.answer as number); } export function gradeAnswers(questions: QuizQ[], answers: AnswerValue[]) { let correct = 0; const detail = questions.map((q, i) => { const ok = gradeOne(q, answers[i]); if (ok) correct++; return ok; }); const score = questions.length ? Math.round((correct / questions.length) * 100) : 0; return { score, passed: score >= 80, correct, total: questions.length, detail }; } // 记录结果;通过则同步完成当天英语打卡 export async function completePoint( childId: number, pointId: string, title: string, score: number, passed: boolean, ) { await db .insert(grammarProgress) .values({ childUserId: childId, pointId, score, passed }) .onDuplicateKeyUpdate({ set: { score, passed, completedAt: sql`CURRENT_TIMESTAMP` } }); if (!passed) return; const today = todayStr(); const ckTitle = `英语语法 · ${title}`; const [existing] = await db .select({ id: taskInstances.id, status: taskInstances.status }) .from(taskInstances) .where( and(eq(taskInstances.childUserId, childId), eq(taskInstances.date, today), eq(taskInstances.title, ckTitle)), ) .limit(1); if (existing) { if (existing.status !== "done") await db .update(taskInstances) .set({ status: "done", doneAt: sql`CURRENT_TIMESTAMP` }) .where(eq(taskInstances.id, existing.id)); } else { await db.insert(taskInstances).values({ childUserId: childId, date: today, title: ckTitle, subject: "english", status: "done", doneAt: new Date(), sortOrder: 99, }); } }