新增数学思维自学板块与考试板块(9模块154课+初一上5套真实试卷客观题)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3473ecbf2b
commit
bc53e2b2a5
+141
@@ -0,0 +1,141 @@
|
||||
// xuchao: 考试板块——试卷查询与作答记录
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { examPapers, examRecords, taskInstances } from "@/db/schema";
|
||||
import { todayStr } from "@/lib/dates";
|
||||
import type { QuizQ } from "@/lib/quiz-parse";
|
||||
|
||||
// xuchao: 试卷题型标签(展示用)
|
||||
export const EXAM_TYPE_LABEL: Record<string, string> = {
|
||||
monthly: "月考",
|
||||
unit: "单元卷",
|
||||
midterm: "期中",
|
||||
final: "期末",
|
||||
};
|
||||
|
||||
export type ExamPaperRow = {
|
||||
paperId: string;
|
||||
subject: string;
|
||||
grade: string;
|
||||
semester: string;
|
||||
month: number;
|
||||
examType: string;
|
||||
title: string;
|
||||
region: string | null;
|
||||
year: number | null;
|
||||
duration: number | null;
|
||||
fullScore: number | null;
|
||||
seq: number;
|
||||
};
|
||||
|
||||
// xuchao: 某年级某学期的全部试卷(按月份+顺序)
|
||||
export async function listPapers(grade: string, semester: string, subject = "math"): Promise<ExamPaperRow[]> {
|
||||
return db
|
||||
.select({
|
||||
paperId: examPapers.paperId,
|
||||
subject: examPapers.subject,
|
||||
grade: examPapers.grade,
|
||||
semester: examPapers.semester,
|
||||
month: examPapers.month,
|
||||
examType: examPapers.examType,
|
||||
title: examPapers.title,
|
||||
region: examPapers.region,
|
||||
year: examPapers.year,
|
||||
duration: examPapers.duration,
|
||||
fullScore: examPapers.fullScore,
|
||||
seq: examPapers.seq,
|
||||
})
|
||||
.from(examPapers)
|
||||
.where(and(eq(examPapers.subject, subject), eq(examPapers.grade, grade), eq(examPapers.semester, semester)))
|
||||
.orderBy(examPapers.month, examPapers.seq);
|
||||
}
|
||||
|
||||
// xuchao: 单张试卷全文(含题目 JSON)
|
||||
export async function getPaper(paperId: string) {
|
||||
const [row] = await db.select().from(examPapers).where(eq(examPapers.paperId, paperId)).limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
// xuchao: 解析试卷题目 JSON
|
||||
export function paperQuestions(paper: { questions: string }): QuizQ[] {
|
||||
try {
|
||||
const arr = JSON.parse(paper.questions);
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// xuchao: 某孩子某卷的最近成绩
|
||||
export async function getExamRecord(childId: number, paperId: string) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(examRecords)
|
||||
.where(and(eq(examRecords.childUserId, childId), eq(examRecords.paperId, paperId)))
|
||||
.limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
// xuchao: 某孩子全部考试记录(列表页显示已考分数)
|
||||
export async function recordsByPaper(childId: number): Promise<Map<string, { score: number; passed: boolean }>> {
|
||||
const rows = await db
|
||||
.select({ paperId: examRecords.paperId, score: examRecords.score, passed: examRecords.passed })
|
||||
.from(examRecords)
|
||||
.where(eq(examRecords.childUserId, childId));
|
||||
return new Map(rows.map((r) => [r.paperId, { score: r.score ?? 0, passed: r.passed }]));
|
||||
}
|
||||
|
||||
// xuchao: 记录作答;达标联动当天数学打卡(允许重考覆盖成绩)
|
||||
export async function recordExam(
|
||||
childId: number,
|
||||
paperId: string,
|
||||
paperTitle: string,
|
||||
score: number,
|
||||
correct: number,
|
||||
total: number,
|
||||
passed: boolean,
|
||||
answers: unknown[],
|
||||
) {
|
||||
await db
|
||||
.insert(examRecords)
|
||||
.values({
|
||||
childUserId: childId,
|
||||
paperId,
|
||||
score,
|
||||
correct,
|
||||
total,
|
||||
passed,
|
||||
answers: JSON.stringify(answers),
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: { score, correct, total, passed, answers: JSON.stringify(answers), takenAt: sql`CURRENT_TIMESTAMP` },
|
||||
});
|
||||
|
||||
if (!passed) return;
|
||||
const today = todayStr();
|
||||
const ckTitle = `考试 · ${paperTitle.length > 24 ? paperTitle.slice(0, 24) + "…" : paperTitle}`;
|
||||
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: "math",
|
||||
status: "done",
|
||||
doneAt: new Date(),
|
||||
sortOrder: 96,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// xuchao: 数学每周检测——从已点亮数学课的题池 seeded 抽 10 题
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { mathPoints, mathProgress, mathWeeklyTest } from "@/db/schema";
|
||||
import { extractQuiz, type QuizQ } from "@/lib/quiz-parse";
|
||||
import { todayStr, weekKey } from "@/lib/dates";
|
||||
|
||||
export const currentMathWeekKey = () => weekKey(todayStr());
|
||||
|
||||
// xuchao: 确定性随机(同 seed 每次得到相同组卷,渲染与判分一致)
|
||||
function hashStr(s: string): number {
|
||||
let h = 5381;
|
||||
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
||||
return h >>> 0;
|
||||
}
|
||||
function mulberry32(a: number) {
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
function seededSample<T>(arr: T[], seed: string, n: number): T[] {
|
||||
const rnd = mulberry32(hashStr(seed));
|
||||
const pool = [...arr];
|
||||
for (let i = pool.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rnd() * (i + 1));
|
||||
[pool[i], pool[j]] = [pool[j], pool[i]];
|
||||
}
|
||||
return pool.slice(0, n);
|
||||
}
|
||||
|
||||
// xuchao: 从已点亮数学课组卷;题池不足 5 题时用计算模块兜底
|
||||
export async function buildMathWeeklyQuestions(childId: number, wk: string): Promise<QuizQ[]> {
|
||||
const lit = await db
|
||||
.select({ pointId: mathProgress.pointId })
|
||||
.from(mathProgress)
|
||||
.where(and(eq(mathProgress.childUserId, childId), eq(mathProgress.passed, true)));
|
||||
const litIds = new Set(lit.map((r) => r.pointId));
|
||||
|
||||
const all = await db.select().from(mathPoints);
|
||||
let pool: QuizQ[] = [];
|
||||
for (const p of all) {
|
||||
if (litIds.has(p.pointId)) pool.push(...extractQuiz(p.md));
|
||||
}
|
||||
if (pool.length < 5) {
|
||||
for (const p of all) if (p.module === "calc") pool.push(...extractQuiz(p.md));
|
||||
}
|
||||
return seededSample(pool, `${childId}:math:${wk}`, 10);
|
||||
}
|
||||
|
||||
export async function getMathWeeklyRecord(childId: number, wk: string) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(mathWeeklyTest)
|
||||
.where(and(eq(mathWeeklyTest.childUserId, childId), eq(mathWeeklyTest.weekKey, wk)))
|
||||
.limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// xuchao: 数学思维自学板块——结构对标英语语法模块(知识点+进度+打卡联动)
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { mathPoints, mathProgress, taskInstances } from "@/db/schema";
|
||||
import { todayStr } from "@/lib/dates";
|
||||
import { extractQuiz, type QuizQ } from "@/lib/quiz-parse";
|
||||
import { gradeAnswers, type AnswerValue } from "@/lib/grammar";
|
||||
|
||||
export { extractQuiz, type QuizQ };
|
||||
export { gradeAnswers, type AnswerValue };
|
||||
|
||||
// xuchao: 七大模块元信息(展示用),顺序即学习顺序
|
||||
export const MODULES = [
|
||||
{ id: "calc", name: "计算与计数", stars: 1, desc: "整数/分数/小数运算、运算律、巧算与计数原理,一切数学的地基。" },
|
||||
{ id: "apply", name: "应用题", stars: 2, desc: "和差倍、鸡兔同笼、植树盈亏、工程比例等经典应用题模型。" },
|
||||
{ id: "geo", name: "几何", stars: 2, desc: "平面图形周长面积、立体图形表面积体积、割补与等积变换。" },
|
||||
{ id: "num", name: "数论", stars: 3, desc: "整除特征、质数合数、位值原理、余数与同余。" },
|
||||
{ id: "travel", name: "行程", stars: 3, desc: "相遇追及、流水行船、火车过桥、环形跑道。" },
|
||||
{ id: "eq", name: "方程", stars: 3, desc: "用字母表示数、解方程、列方程解应用题。" },
|
||||
{ id: "combo", name: "组合", stars: 3, desc: "抽屉原理、最值问题、逻辑推理与策略游戏。" },
|
||||
] as const;
|
||||
|
||||
export type MathPointRow = {
|
||||
pointId: string;
|
||||
module: string;
|
||||
moduleName: string;
|
||||
title: string;
|
||||
difficulty: number;
|
||||
seq: number;
|
||||
};
|
||||
|
||||
// xuchao: 全部数学知识点(按展示顺序)
|
||||
export async function listMathPoints(): Promise<MathPointRow[]> {
|
||||
return db
|
||||
.select({
|
||||
pointId: mathPoints.pointId,
|
||||
module: mathPoints.module,
|
||||
moduleName: mathPoints.moduleName,
|
||||
title: mathPoints.title,
|
||||
difficulty: mathPoints.difficulty,
|
||||
seq: mathPoints.seq,
|
||||
})
|
||||
.from(mathPoints)
|
||||
.orderBy(mathPoints.seq);
|
||||
}
|
||||
|
||||
export async function totalMathPoints(): Promise<number> {
|
||||
const [r] = await db.select({ n: sql<number>`COUNT(*)` }).from(mathPoints);
|
||||
return r.n;
|
||||
}
|
||||
|
||||
// xuchao: 单个数学知识点全文
|
||||
export async function getMathPoint(pointId: string) {
|
||||
const [row] = await db.select().from(mathPoints).where(eq(mathPoints.pointId, pointId)).limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
// xuchao: 已通过的数学知识点集合
|
||||
export async function mathPassedByPoint(childId: number): Promise<Set<string>> {
|
||||
const rows = await db
|
||||
.select({ pointId: mathProgress.pointId })
|
||||
.from(mathProgress)
|
||||
.where(and(eq(mathProgress.childUserId, childId), eq(mathProgress.passed, true)));
|
||||
return new Set(rows.map((r) => r.pointId));
|
||||
}
|
||||
|
||||
// xuchao: 记录结果;通过则同步完成当天数学打卡
|
||||
export async function completeMathPoint(
|
||||
childId: number,
|
||||
pointId: string,
|
||||
title: string,
|
||||
score: number,
|
||||
passed: boolean,
|
||||
) {
|
||||
await db
|
||||
.insert(mathProgress)
|
||||
.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: "math",
|
||||
status: "done",
|
||||
doneAt: new Date(),
|
||||
sortOrder: 97,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,10 @@ export function correctText(q: QuizQ): string {
|
||||
}
|
||||
|
||||
// 错题入库(同一孩子同一来源同一题干去重)
|
||||
// sourceType:lesson/weekly=英语,mlesson/mweekly=数学,exam=考试卷
|
||||
export async function recordWrong(
|
||||
childId: number,
|
||||
sourceType: "lesson" | "weekly",
|
||||
sourceType: "lesson" | "weekly" | "mlesson" | "mweekly" | "exam",
|
||||
sourceId: string,
|
||||
sourceTitle: string,
|
||||
q: QuizQ,
|
||||
|
||||
Reference in New Issue
Block a user