import_ielts.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /**
  2. * 雅思词汇导入秒过知识库
  3. * 用法:
  4. * node ielts/import_ielts.js --dry-run # 预检,不写库
  5. * node ielts/import_ielts.js --import # 正式导入
  6. *
  7. * 数据源: ielts/ielts-xdf-random.json (新东方3050词,有音标/词性/中文释义)
  8. * ielts/ielts-4000.json (526词,仅英文释义)
  9. * 合并去重后 3346 词, 分6本书 (BookID 212-217)
  10. * 4000独有的296词: 优先从现有四六级/高考词库取中文释义+音标, 否则用英文释义
  11. */
  12. import { getConnection, query } from '../../../src/util/db.js';
  13. import fs from 'fs';
  14. const DRY_RUN = process.argv.includes('--dry-run');
  15. const IMPORT = process.argv.includes('--import');
  16. if (!DRY_RUN && !IMPORT) {
  17. console.error('请指定 --dry-run 或 --import');
  18. process.exit(1);
  19. }
  20. const BOOK_COUNT = 6;
  21. const WORDS_PER_LESSON = 10;
  22. const START_BOOK_ID = 212; // WordBooks 新ID (当前最大211)
  23. const SORT = 14; // 排在六级(13)之后
  24. const CATEGORY = '雅思英语';
  25. const CATEGORY2 = '雅思英语';
  26. const CATEGORY3 = '雅思考试单词';
  27. const GRADE = '大学';
  28. const BOOK_IMAGE_NAME = 'ieltsWords';
  29. const REMARK = '雅思备考核心词汇,提升考试通过率。';
  30. const TEST_FUNCTION = JSON.stringify([
  31. { ID: 1, N: "read", N2: "念单词说含义", C: 0, Img: "picJygs_13", Remark: "题目为单词,答出它的发音,配释义等信息。" },
  32. { ID: 2, N: "write", N2: "听写单词", C: 0, Img: "picJygs_14", Remark: "题目为单词发音和释义,写出单词。" }
  33. ]);
  34. const PART_NAMES = ['第一部分', '第二部分', '第三部分', '第四部分', '第五部分', '第六部分'];
  35. const norm = (w) => String(w || '').trim().toLowerCase();
  36. function normalizeSoundmark(p) {
  37. if (!p) return null;
  38. let s = String(p).trim();
  39. if (!s) return null;
  40. // 统一为 [xxx] 格式 (参照四级 "[əˈbændən]")
  41. if (s.startsWith('/') && s.endsWith('/')) s = s.slice(1, -1);
  42. else if (s.startsWith('[') && s.endsWith(']')) s = s.slice(1, -1);
  43. s = s.trim();
  44. if (!s) return null;
  45. return '[' + s + ']';
  46. }
  47. function buildTranslate(pos, zh) {
  48. let t = '';
  49. const p = String(pos || '').trim();
  50. const z = String(zh || '').trim();
  51. if (p && z) t = p + z; // 参照四级 "vt.丢弃 放弃 抛弃"
  52. else t = z || p;
  53. return t || null;
  54. }
  55. async function main() {
  56. // ---------- 1. 读取并合并数据 ----------
  57. const xdf = JSON.parse(fs.readFileSync(new URL('./ielts-xdf-random.json', import.meta.url), 'utf8'))
  58. .filter(x => norm(x.word) !== 'word' && norm(x.word) !== ''); // 去掉表头行
  59. const awl = JSON.parse(fs.readFileSync(new URL('./ielts-4000.json', import.meta.url), 'utf8'));
  60. const merged = new Map(); // key: norm word -> record
  61. for (const item of xdf) {
  62. const key = norm(item.word);
  63. if (merged.has(key)) continue;
  64. merged.set(key, {
  65. word: String(item.word).trim(),
  66. soundmark: normalizeSoundmark(item.phonetic),
  67. translate: buildTranslate(item.pos, item.zh),
  68. source: 'xdf'
  69. });
  70. }
  71. const xdfCount = merged.size;
  72. const onlyAwl = [];
  73. for (const item of awl) {
  74. const key = norm(item.word);
  75. if (merged.has(key)) continue;
  76. onlyAwl.push({ word: String(item.word).trim(), def: String(item.def || '').trim() });
  77. merged.set(key, {
  78. word: String(item.word).trim(),
  79. soundmark: null,
  80. translate: null,
  81. def: String(item.def || '').trim(),
  82. source: 'awl'
  83. });
  84. }
  85. // ---------- 2. 从现有四六级/高考词库补释义和音标 ----------
  86. const dbRef = await query(
  87. "SELECT Word, Translate, Soundmark FROM kylx365_db.Words WHERE BookID BETWEEN 169 AND 183 AND LOWER(Word) IN (?)",
  88. [onlyAwl.map(x => norm(x.word))]
  89. );
  90. const refMap = new Map();
  91. for (const r of dbRef) {
  92. const k = norm(r.Word);
  93. if (!refMap.has(k)) refMap.set(k, r);
  94. }
  95. let refFilled = 0, defFilled = 0;
  96. for (const item of onlyAwl) {
  97. const rec = merged.get(norm(item.word));
  98. const ref = refMap.get(norm(item.word));
  99. if (ref) {
  100. rec.translate = ref.Translate;
  101. rec.soundmark = ref.Soundmark ? normalizeSoundmark(ref.Soundmark) : null;
  102. refFilled++;
  103. } else {
  104. rec.translate = rec.def; // 英文释义填入
  105. defFilled++;
  106. }
  107. }
  108. // ---------- 3. 分书 ----------
  109. const ordered = [...xdf].map(x => merged.get(norm(x.word))).filter(Boolean); // 保持xdf乱序源顺序
  110. for (const item of onlyAwl) ordered.push(merged.get(norm(item.word))); // 4000独有词按原字母序追加在后
  111. const total = ordered.length;
  112. const base = Math.floor(total / BOOK_COUNT); // 557
  113. const extra = total - base * BOOK_COUNT; // 4
  114. // 前 extra 本 base+1 词, 其余 base 词 → 558*4 + 557*2 = 3346
  115. const books = [];
  116. let idx = 0;
  117. for (let i = 0; i < BOOK_COUNT; i++) {
  118. const n = i < extra ? base + 1 : base;
  119. books.push({ part: i + 1, bookID: START_BOOK_ID + i, words: ordered.slice(idx, idx + n) });
  120. idx += n;
  121. }
  122. console.log(`合并统计: xdf=${xdfCount}, awl独有=${onlyAwl.length}(四六级补释义=${refFilled}, 英文释义=${defFilled}), 总计=${total}`);
  123. for (const b of books)
  124. console.log(` BookID=${b.bookID} ${PART_NAMES[b.part - 1]}: ${b.words.length}词, ${Math.ceil(b.words.length / WORDS_PER_LESSON)}个Lesson`);
  125. // ---------- 4. 校验 ----------
  126. const [maxWB] = await query("SELECT MAX(ID) m FROM kylx365_db.WordBooks;");
  127. const [maxMG] = await query("SELECT MAX(ID) m FROM MiaoguoBook;");
  128. const [exist] = await query("SELECT COUNT(*) c FROM kylx365_db.WordBooks WHERE Category=?;", [CATEGORY]);
  129. console.log(`当前最大ID: WordBooks=${maxWB.m}, MiaoguoBook=${maxMG.m}; 已存在"${CATEGORY}"记录=${exist.c}`);
  130. if (maxWB.m >= START_BOOK_ID || exist.c > 0) {
  131. console.error('中止: ID已被占用或已存在雅思记录, 请人工检查!');
  132. process.exit(1);
  133. }
  134. if (DRY_RUN) {
  135. console.log('\n=== 预览: 每本书前3词 ===');
  136. for (const b of books) {
  137. console.log(`BookID=${b.bookID}:`);
  138. for (const w of b.words.slice(0, 3))
  139. console.log(` ${w.word} ${w.soundmark || '(无音标)'} ${w.translate || '(无释义)'}`);
  140. }
  141. console.log('\n[dry-run] 校验通过, 未写库.');
  142. process.exit(0);
  143. }
  144. // ---------- 5. 导入 (事务) ----------
  145. const conn = await getConnection();
  146. try {
  147. await conn.beginTransaction();
  148. // 5.1 WordBooks
  149. for (const b of books) {
  150. const lessonNum = Math.ceil(b.words.length / WORDS_PER_LESSON);
  151. await conn.query(
  152. `INSERT INTO kylx365_db.WordBooks (ID,Sort,Category,Name,Name2,Total,Image,Grade,Category2,Category3,WordType,Remark,Package,BookImageName,WordNum,Flag)
  153. VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)`,
  154. [b.bookID, SORT, CATEGORY, PART_NAMES[b.part - 1], `词汇${b.words.length}`, lessonNum,
  155. `../images/examine_subject_a00${b.part}.png`, GRADE, CATEGORY2, CATEGORY3, '单词', REMARK,
  156. PART_NAMES[b.part - 1], BOOK_IMAGE_NAME, b.words.length]
  157. );
  158. }
  159. // 5.2 MiaoguoBook
  160. for (const b of books) {
  161. const lessonNum = Math.ceil(b.words.length / WORDS_PER_LESSON);
  162. await conn.query(
  163. `INSERT INTO MiaoguoBook (BookIDOld,Category,LibraryName1,LibraryName2,Grade,Category2,BookName,Remark,WordType,UnitNum,StartID,WordNum,BookImageName,KnowledgeImageName,TestFunction,Sort,Flag)
  164. VALUES (?,?,?,?,?,?,?,?,?,?,0,?,?,?,?,?,0)`,
  165. [b.bookID, 'English', CATEGORY2, CATEGORY3, GRADE, '课外拓展', PART_NAMES[b.part - 1], REMARK,
  166. '单词', lessonNum, b.words.length, BOOK_IMAGE_NAME, '', TEST_FUNCTION, SORT]
  167. );
  168. }
  169. // 5.3 Words (批量插入, 每批500条)
  170. let inserted = 0;
  171. const BATCH = 500;
  172. const rows = [];
  173. for (const b of books) {
  174. for (let i = 0; i < b.words.length; i++) {
  175. const w = b.words[i];
  176. rows.push([w.word, b.bookID, Math.floor(i / WORDS_PER_LESSON) + 1, w.soundmark, w.translate]);
  177. }
  178. }
  179. for (let i = 0; i < rows.length; i += BATCH) {
  180. const chunk = rows.slice(i, i + BATCH);
  181. await conn.query(
  182. `INSERT INTO kylx365_db.Words (Word,BookID,LessonID,Soundmark,Translate,Sort)
  183. VALUES ${chunk.map(() => '(?,?,?,?,?,0)').join(',')}`,
  184. [chunk.flat()]
  185. );
  186. inserted += chunk.length;
  187. console.log(` Words 已插入 ${inserted}/${rows.length}`);
  188. }
  189. await conn.commit();
  190. console.log(`\n导入完成: WordBooks 6条, MiaoguoBook 6条, Words ${inserted}条.`);
  191. } catch (e) {
  192. await conn.rollback();
  193. console.error('导入失败, 已回滚:', e.message);
  194. process.exit(1);
  195. } finally {
  196. conn.release();
  197. }
  198. // ---------- 6. 验证 ----------
  199. const wb = await query("SELECT ID,Category,Name,Name2,WordNum,BookImageName,Sort,Flag FROM kylx365_db.WordBooks WHERE ID BETWEEN 212 AND 217;");
  200. const mg = await query("SELECT ID,BookIDOld,Category,LibraryName1,LibraryName2,BookName,UnitNum,WordNum,Sort,Flag FROM MiaoguoBook WHERE BookIDOld BETWEEN 212 AND 217;");
  201. const wc = await query("SELECT BookID,COUNT(*) c,COUNT(DISTINCT LOWER(Word)) d FROM kylx365_db.Words WHERE BookID BETWEEN 212 AND 217 GROUP BY BookID;");
  202. console.log('\n=== 验证 WordBooks ===\n' + JSON.stringify(wb));
  203. console.log('=== 验证 MiaoguoBook ===\n' + JSON.stringify(mg));
  204. console.log('=== 验证 Words ===\n' + JSON.stringify(wc));
  205. process.exit(0);
  206. }
  207. main().catch(e => { console.error(e); process.exit(1); });