新增数学思维自学板块与考试板块(9模块154课+初一上5套真实试卷客观题)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-06 08:00:46 +08:00
co-authored by Claude Opus 4.8
parent 3473ecbf2b
commit bc53e2b2a5
38 changed files with 3393 additions and 10 deletions
+97
View File
@@ -0,0 +1,97 @@
// xuchao: 校验数学内容 JSON——外层可解析、每课有 md、quiz 块内层可解析且题目字段齐全
// 用法:node scripts/check-math-content.mjs <file.json> [more.json ...](不传则扫 src/content/math/*.json)
import fs from "node:fs";
import path from "node:path";
const TYPE_OK = new Set(["choice", "fill", "judge", "order", "match"]);
function checkQuizBlock(raw, where, errs) {
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
errs.push(`${where}: quiz 块 JSON 解析失败 — ${e.message}`);
return;
}
const qs = parsed.questions;
if (!Array.isArray(qs) || qs.length === 0) {
errs.push(`${where}: quiz 块没有 questions 数组`);
return;
}
if (qs.length < 8) errs.push(`${where}: 题目只有 ${qs.length} 题(建议 ≥8)`);
qs.forEach((q, i) => {
const tag = `${where} 第${i + 1}题`;
if (!TYPE_OK.has(q.type)) errs.push(`${tag}: 非法 type=${q.type}`);
if (!q.stem || typeof q.stem !== "string") errs.push(`${tag}: 缺 stem`);
if (!q.explain) errs.push(`${tag}: 缺 explain`);
if (q.type === "choice") {
if (!Array.isArray(q.options) || q.options.length < 2) errs.push(`${tag}: choice 选项不足`);
if (typeof q.answer !== "number" || q.answer < 0 || q.answer >= (q.options?.length ?? 0))
errs.push(`${tag}: choice answer 越界`);
} else if (q.type === "judge") {
if (q.answer !== 0 && q.answer !== 1) errs.push(`${tag}: judge answer 必须是 0 或 1`);
} else if (q.type === "fill") {
if (!Array.isArray(q.answers) || q.answers.length === 0) errs.push(`${tag}: fill 缺 answers 数组`);
} else if (q.type === "order") {
if (!Array.isArray(q.words) || !Array.isArray(q.answer)) errs.push(`${tag}: order 缺 words/answer`);
} else if (q.type === "match") {
if (!Array.isArray(q.pairs) || q.pairs.length === 0) errs.push(`${tag}: match 缺 pairs`);
}
});
}
function checkFile(file) {
const errs = [];
let data;
try {
data = JSON.parse(fs.readFileSync(file, "utf8"));
} catch (e) {
console.log(`❌ ${file}: 外层 JSON 解析失败 — ${e.message}`);
return 1;
}
if (!data.module || !data.moduleName || !Array.isArray(data.points)) {
console.log(`❌ ${file}: 缺 module / moduleName / points`);
return 1;
}
const ids = new Set();
let lessons = 0;
data.points.forEach((p, idx) => {
const tag = `${file} points[${idx}](${p.id ?? "?"})`;
if (!p.id) errs.push(`${tag}: 缺 id`);
else if (ids.has(p.id)) errs.push(`${tag}: id 重复`);
else ids.add(p.id);
if (!p.title) errs.push(`${tag}: 缺 title`);
if (!p.md || typeof p.md !== "string") {
errs.push(`${tag}: 缺 md`);
return;
}
lessons++;
const quizRe = /```quiz\s*\n([\s\S]*?)```/g;
let m, count = 0;
while ((m = quizRe.exec(p.md))) {
count++;
checkQuizBlock(m[1], `${tag} quiz#${count}`, errs);
}
if (count === 0) errs.push(`${tag}: md 中没有 quiz 块`);
});
if (errs.length) {
console.log(`❌ ${file}: ${lessons} 课,${errs.length} 处问题`);
errs.forEach((e) => console.log(" - " + e));
return 1;
}
console.log(`✅ ${file}: ${data.module} / ${lessons} 课,全部通过`);
return 0;
}
const args = process.argv.slice(2);
const files = args.length
? args
: fs
.readdirSync(path.join(process.cwd(), "src/content/math"))
.filter((f) => f.endsWith(".json"))
.map((f) => path.join(process.cwd(), "src/content/math", f));
let bad = 0;
for (const f of files) bad += checkFile(f);
console.log(bad ? `\n${bad} 个文件有问题` : "\n全部文件校验通过");
process.exit(bad ? 1 : 0);
+80
View File
@@ -0,0 +1,80 @@
// xuchao: 考试试卷种子——src/content/exam/*.json 灌入 exam_paper;表空全量,否则增量(按 paperId 跳过,seq 续接)
import fs from "node:fs";
import path from "node:path";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
import { examPapers } from "../src/db/schema";
type ExamPaper = {
id: string;
subject?: string;
grade: string;
semester: string;
month: number;
examType: string;
title: string;
region?: string;
year?: number;
duration?: number;
fullScore?: number;
sourceUrl?: string;
summary?: string;
questions: unknown[];
};
type ExamFile = { papers: ExamPaper[] };
async function main() {
const dir = path.join(process.cwd(), "src/content/exam");
if (!fs.existsSync(dir)) {
console.log("src/content/exam 不存在,跳过考试种子");
return;
}
const files: ExamFile[] = fs
.readdirSync(dir)
.filter((f) => f.endsWith(".json"))
.sort()
.map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")));
const conn = await mysql.createConnection(process.env.DATABASE_URL!);
const db = drizzle(conn);
const [cnt] = await db.select({ n: sql<number>`COUNT(*)` }).from(examPapers);
if (cnt.n > 0) console.log("exam_paper 已有", cnt.n, "条,增量模式(跳过已存在 paperId)");
const [mx] = await db.select({ m: sql<number>`COALESCE(MAX(seq),0)` }).from(examPapers);
let seq = mx.m;
const existing = new Set(
(await db.select({ paperId: examPapers.paperId }).from(examPapers)).map((r) => r.paperId),
);
let added = 0;
for (const file of files) {
for (const p of file.papers) {
if (existing.has(p.id)) continue;
seq += 1;
await db.insert(examPapers).values({
paperId: p.id,
subject: p.subject ?? "math",
grade: p.grade,
semester: p.semester,
month: p.month,
examType: p.examType,
title: p.title,
region: p.region ?? null,
year: p.year ?? null,
duration: p.duration ?? null,
fullScore: p.fullScore ?? null,
sourceUrl: p.sourceUrl ?? null,
summary: p.summary ?? null,
questions: JSON.stringify(p.questions),
seq,
});
added++;
}
}
const [tot] = await db.select({ n: sql<number>`COUNT(*)` }).from(examPapers);
console.log("新增试卷:", added, "| 试卷总数:", tot.n);
await conn.end();
}
main();
+64
View File
@@ -0,0 +1,64 @@
// xuchao: 数学内容种子——src/content/math/*.json 灌入 math_point;表空全量,否则增量(按 pointId 跳过,seq 续接)
import fs from "node:fs";
import path from "node:path";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
import { mathPoints } from "../src/db/schema";
type MathPoint = { id: string; title: string; grade?: string; difficulty: number; summary?: string; md: string };
type MathModuleFile = { module: string; moduleName: string; points: MathPoint[] };
// 模块顺序即学习顺序,与 src/lib/math.ts 的 MODULES 保持一致
const MODULE_ORDER = ["calc", "apply", "geo", "num", "travel", "eq", "combo"];
async function main() {
const dir = path.join(process.cwd(), "src/content/math");
if (!fs.existsSync(dir)) {
console.log("src/content/math 不存在,跳过数学种子");
return;
}
const files: MathModuleFile[] = fs
.readdirSync(dir)
.filter((f) => f.endsWith(".json"))
.sort()
.map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")));
const conn = await mysql.createConnection(process.env.DATABASE_URL!);
const db = drizzle(conn);
const [cnt] = await db.select({ n: sql<number>`COUNT(*)` }).from(mathPoints);
if (cnt.n > 0) console.log("math_point 已有", cnt.n, "条,增量模式(跳过已存在 pointId)");
const [mx] = await db.select({ m: sql<number>`COALESCE(MAX(seq),0)` }).from(mathPoints);
let seq = mx.m;
const existing = new Set(
(await db.select({ pointId: mathPoints.pointId }).from(mathPoints)).map((r) => r.pointId),
);
let added = 0;
for (const mod of MODULE_ORDER) {
for (const file of files.filter((f) => f.module === mod)) {
for (const p of file.points) {
if (existing.has(p.id)) continue;
seq += 1;
await db.insert(mathPoints).values({
pointId: p.id,
module: file.module,
moduleName: file.moduleName,
title: p.title,
grade: p.grade ?? null,
difficulty: p.difficulty || 1,
summary: p.summary ?? null,
md: p.md,
seq,
});
added++;
}
}
}
const [tot] = await db.select({ n: sql<number>`COUNT(*)` }).from(mathPoints);
console.log("新增数学课:", added, "| 数学知识点总数:", tot.n);
await conn.end();
}
main();