- 内容底座改为你提供的语法体系(入门/基础/进阶 69 知识点,中文讲解 + 中英对照例句 + 六步教学法),整体更适合小学生 - 内容存库(grammar_point),家长中心新增"编辑英语课程"可改标题/目标/ 课文,保存即时生效 - 取消顺序解锁,所有课程可直接学习,通过测验即点亮 - 测验引擎支持 选择/判断/填空/连词成句/连线 五种题型,达标自动联动 当日英语打卡 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
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<PointRow[]> {
|
|
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<number> {
|
|
const [r] = await db.select({ n: sql<number>`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<Set<string>> {
|
|
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<number> {
|
|
return (await passedByPoint(childId)).size;
|
|
}
|
|
|
|
const norm = (s: unknown) => String(s ?? "").trim().toLowerCase();
|
|
|
|
// 服务端判分:五种题型 choice/judge/fill/order/match
|
|
export type AnswerValue = number | string | string[];
|
|
export function gradeAnswers(questions: QuizQ[], answers: AnswerValue[]) {
|
|
let correct = 0;
|
|
const detail = questions.map((q, i) => {
|
|
const a = answers[i];
|
|
let ok = false;
|
|
if (q.type === "fill") ok = (q.answers ?? []).some((x) => norm(x) === norm(a));
|
|
else if (q.type === "order")
|
|
ok = Array.isArray(a) && a.join(" ").trim() === (q.answer as string[]).join(" ").trim();
|
|
else if (q.type === "match")
|
|
ok = Array.isArray(a) && (q.pairs ?? []).every((p, pi) => norm(a[pi]) === norm(p[1]));
|
|
else ok = Number(a) === (q.answer as number);
|
|
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,
|
|
});
|
|
}
|
|
}
|