// 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(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 { 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; }