| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- #!/usr/bin/env node
- import assert from "node:assert/strict";
- import fs from "node:fs";
- import path from "node:path";
- import { fileURLToPath } from "node:url";
- import mysql from "mysql2/promise";
- import config from "../../src/config/dev.js";
- const taskDir = path.dirname(fileURLToPath(import.meta.url));
- const sourcePath = path.join(taskDir, "source_chinese_2026_fall.json");
- const applyReportPath = path.join(taskDir, "apply_report_2026_fall.json");
- const jsonReportPath = path.join(
- taskDir,
- "miaoguo_literacy_dependency_audit_current_2026_fall.json",
- );
- const markdownReportPath = path.join(taskDir, "MiaoguoLiteracy依赖检查_2026年秋季.md");
- const sources = JSON.parse(fs.readFileSync(sourcePath, "utf8")).filter(
- (source) => source.unitType === 2,
- );
- const applyReport = JSON.parse(fs.readFileSync(applyReportPath, "utf8"));
- const backup = JSON.parse(fs.readFileSync(applyReport.backupPath, "utf8"));
- function splitWords(example) {
- return example.split(",").filter(Boolean);
- }
- function inspectJson(row) {
- if (!row) return { status: "NO_ROW", detail: "MiaoguoLiteracy 中没有可用记录" };
- if (!row.JSONString) return { status: "EMPTY_JSON", detail: "JSONString 为空" };
- try {
- const json = JSON.parse(row.JSONString);
- if (!json.CHN) return { status: "MISSING_CHN", detail: "JSONString 中不存在 CHN" };
- if (
- !Array.isArray(json.CHN.PinYin) ||
- !json.CHN.PinYin[0]?.pinyin ||
- !json.CHN.PinYin[0]?.explain
- ) {
- return {
- status: "INVALID_CHN_STRUCTURE",
- detail: "CHN.PinYin[0].pinyin/explain 不完整,现有接口无法使用",
- };
- }
- return { status: "OK", detail: "存在可用 CHN" };
- } catch (error) {
- return { status: "INVALID_JSON", detail: error.message };
- }
- }
- function writeReports(report) {
- fs.writeFileSync(jsonReportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
- const lines = [
- "# MiaoguoLiteracy 缺失清单(2026 年秋季)",
- "",
- `检查时间:${report.generatedAt}`,
- "",
- "检查口径:检查三套新版词语表的全部最终词语;按线上接口过滤掉 `SearchType=shici/eng`,并按 `Word,SearchType DESC` 选择每个词的首条记录。除检查 `CHN` 外,还检查了接口实际使用的 `CHN.PinYin[0].pinyin/explain`。",
- "",
- `- 新版词语总数:${report.summary.finalOccurrences}`,
- `- 不重复词语数:${report.summary.uniqueFinalWords}`,
- `- 相比更新前新出现的不重复词语:${report.summary.uniqueNewlyIntroducedWords}`,
- `- 已有可用 CHN:${report.summary.usableWords}`,
- `- 缺少可用 CHN:${report.summary.problemWords}`,
- "",
- "## 需要新增的词语",
- "",
- "| 词语 | 数据集 | 单元 | 状态 | 相对旧版 |",
- "|---|---|---|---|---|",
- ];
- for (const problem of report.problems) {
- lines.push(
- `| ${problem.word} | ${problem.locations.map((item) => item.label).join("、")} | ${problem.locations.map((item) => item.unit).join("、")} | ${problem.status}: ${problem.detail} | ${problem.isNewlyIntroduced ? "新出现" : "旧版已有"} |`,
- );
- }
- lines.push(
- "",
- "## 补充说明",
- "",
- `另发现 ${report.nonBlockingLowerPriorityIssues.length} 条低优先级 \`SearchType=list\` 记录自身没有 CHN;这些词同时存在排序更靠前且具有完整 CHN 的 \`zici\` 记录,线上接口实际会使用 \`zici\`,所以没有列入待新增清单。`,
- "",
- "本报告只做查询和分析,没有向 `MiaoguoLiteracy` 写入任何数据。",
- "",
- );
- fs.writeFileSync(markdownReportPath, `${lines.join("\n")}\n`, "utf8");
- }
- async function main() {
- assert.equal(sources.length, 3, "本批次应有三套语文词语表");
- const oldWordsByBook = new Map(
- sources.map((source) => [
- source.bookIdOld,
- new Set(
- backup.hanziRows
- .filter((row) => row.HanziBookID === source.bookIdOld)
- .flatMap((row) => splitWords(row.Example ?? "")),
- ),
- ]),
- );
- const references = new Map();
- for (const source of sources) {
- const oldWords = oldWordsByBook.get(source.bookIdOld);
- for (const unit of source.units) {
- for (const word of splitWords(unit.example)) {
- const items = references.get(word) ?? [];
- items.push({
- label: source.label,
- bookIdOld: source.bookIdOld,
- unit: unit.name,
- isNewlyIntroduced: !oldWords.has(word),
- });
- references.set(word, items);
- }
- }
- }
- const words = [...references.keys()];
- const connection = await mysql.createConnection(config.database);
- try {
- const [rows] = await connection.query(
- `SELECT ID,Word,SearchType,JSONString
- FROM MiaoguoLiteracy
- WHERE Word IN (?)
- AND SearchType<>?
- AND SearchType<>?
- ORDER BY Word,SearchType DESC,ID`,
- [words, "shici", "eng"],
- );
- const rowsByWord = new Map();
- for (const row of rows) {
- const items = rowsByWord.get(row.Word) ?? [];
- items.push(row);
- rowsByWord.set(row.Word, items);
- }
- const problems = [];
- const nonBlockingLowerPriorityIssues = [];
- for (const word of words) {
- const wordRows = rowsByWord.get(word) ?? [];
- const selectedRow = wordRows[0];
- const selectedCheck = inspectJson(selectedRow);
- const locations = references.get(word);
- if (selectedCheck.status !== "OK") {
- problems.push({
- word,
- status: selectedCheck.status,
- detail: selectedCheck.detail,
- selectedRow: selectedRow
- ? { id: selectedRow.ID, searchType: selectedRow.SearchType }
- : null,
- rowCount: wordRows.length,
- isNewlyIntroduced: locations.some((item) => item.isNewlyIntroduced),
- locations,
- });
- }
- for (const row of wordRows.slice(1)) {
- const check = inspectJson(row);
- if (check.status !== "OK") {
- nonBlockingLowerPriorityIssues.push({
- word,
- id: row.ID,
- searchType: row.SearchType,
- status: check.status,
- detail: check.detail,
- selectedRowId: selectedRow?.ID ?? null,
- });
- }
- }
- }
- const uniqueNewlyIntroducedWords = words.filter((word) =>
- references.get(word).some((item) => item.isNewlyIntroduced),
- ).length;
- const report = {
- generatedAt: new Date().toISOString(),
- readOnly: true,
- querySemantics:
- "与 GetMiaoguoLiteracyWords 一致:排除 SearchType=shici/eng,按 Word,SearchType DESC 取词语首条记录",
- summary: {
- finalOccurrences: sources.reduce((total, source) => total + source.wordNum, 0),
- uniqueFinalWords: words.length,
- uniqueNewlyIntroducedWords,
- eligibleDatabaseRows: rows.length,
- usableWords: words.length - problems.length,
- problemWords: problems.length,
- },
- datasets: sources.map((source) => ({
- label: source.label,
- bookIdOld: source.bookIdOld,
- finalWords: source.wordNum,
- oldUniqueWords: oldWordsByBook.get(source.bookIdOld).size,
- newlyIntroducedOccurrences: source.units
- .flatMap((unit) => splitWords(unit.example))
- .filter((word) => !oldWordsByBook.get(source.bookIdOld).has(word)).length,
- })),
- problems,
- nonBlockingLowerPriorityIssues,
- };
- writeReports(report);
- console.log(
- JSON.stringify(
- {
- summary: report.summary,
- missingWords: problems.map((problem) => problem.word),
- jsonReportPath,
- markdownReportPath,
- },
- null,
- 2,
- ),
- );
- if (problems.length > 0) process.exitCode = 2;
- } finally {
- await connection.end();
- }
- }
- main().catch((error) => {
- console.error(error);
- process.exitCode = 1;
- });
|