From cee7bbfd7a1f554b8dc8d24fd435b3aa3e65adc1 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:23:47 +0800 Subject: [PATCH] feat(scripts): add ollama provider, grade5 chinese seed, i18n fix, and l9 deps scripts - Add add-ai-provider-ollama for Ollama AI provider setup - Add seed-grade5-chinese for grade 5 Chinese content seeding - Add temp-i18n-zh-fix for Chinese i18n fixes - Add _l9_deps module dependency analysis --- scripts/_l9_deps.mjs | 29 + scripts/add-ai-provider-ollama.js | 80 +++ scripts/seed-grade5-chinese.ts | 996 ++++++++++++++++++++++++++++++ scripts/temp-i18n-zh-fix.mjs | 52 ++ 4 files changed, 1157 insertions(+) create mode 100644 scripts/_l9_deps.mjs create mode 100644 scripts/add-ai-provider-ollama.js create mode 100644 scripts/seed-grade5-chinese.ts create mode 100644 scripts/temp-i18n-zh-fix.mjs diff --git a/scripts/_l9_deps.mjs b/scripts/_l9_deps.mjs new file mode 100644 index 0000000..e8606a3 --- /dev/null +++ b/scripts/_l9_deps.mjs @@ -0,0 +1,29 @@ +// L-9 架构文档同步: 005_architecture_data.json dependencyMatrix 添加 grades 依赖 +import { readFileSync, writeFileSync } from 'node:fs'; + +const FILE = 'e:/Desktop/CICD/docs/architecture/005_architecture_data.json'; +const src = readFileSync(FILE, 'utf8'); + +// 1) dependsOn: attendance 模块添加 "grades" +// 定位 attendance.dependsOn 数组的 "notifications" 行(属于 attendance 模块,而非其他模块) +// 通过上下文唯一性定位:parent, notifications 后接 ],\n "uses": { +const depMarker = '"parent",\r\n "notifications"\r\n ],\r\n "uses": {\r\n "shared": [\r\n "db",\r\n "auth-guard.requirePermission",\r\n "auth-guard.getAuthContext",\r\n "db.schema.attendanceRecords",'; +const depReplacement = '"parent",\r\n "notifications",\r\n "grades"\r\n ],\r\n "uses": {\r\n "shared": [\r\n "db",\r\n "auth-guard.requirePermission",\r\n "auth-guard.getAuthContext",\r\n "db.schema.attendanceRecords",'; + +if (!src.includes(depMarker)) { + // 尝试 LF + const depMarkerLF = depMarker.replace(/\r\n/g, '\n'); + if (!src.includes(depMarkerLF)) { + console.error('dependsOn marker not found'); + process.exit(1); + } + // 用 LF 版本替换 + const depReplacementLF = depReplacement.replace(/\r\n/g, '\n'); + const updated = src.replace(depMarkerLF, depReplacementLF); + writeFileSync(FILE, updated, 'utf8'); + console.log('dependsOn updated (LF)'); +} else { + const updated = src.replace(depMarker, depReplacement); + writeFileSync(FILE, updated, 'utf8'); + console.log('dependsOn updated (CRLF)'); +} diff --git a/scripts/add-ai-provider-ollama.js b/scripts/add-ai-provider-ollama.js new file mode 100644 index 0000000..f593a08 --- /dev/null +++ b/scripts/add-ai-provider-ollama.js @@ -0,0 +1,80 @@ +/** + * 扩展 ai_providers.provider 枚举:添加 'ollama' 用于本地 Ollama 连接 + * + * 背景: + * - Ollama 提供 OpenAI 兼容的 /v1/chat/completions 端点 + * - 无需 API Key(本地部署),无需公网 + * - 默认地址 http://localhost:11434/v1 + * + * 变更: + * - ai_providers.provider ENUM 增加 'ollama' + * + * 幂等:脚本会先读取当前枚举值,若已包含 'ollama' 则跳过。 + */ +require("dotenv/config"); +const mysql = require("mysql2/promise"); + +async function getCurrentEnum(conn) { + const [rows] = await conn.execute( + `SELECT COLUMN_TYPE FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_providers' AND COLUMN_NAME = 'provider'`, + ); + if (rows.length === 0) return null; + // 形如 "enum('zhipu','openai','gemini','custom')" + const match = rows[0].COLUMN_TYPE.match(/enum\((.*)\)/i); + if (!match) return null; + return match[1] + .split(",") + .map((s) => s.trim().replace(/^'|'$/g, "")); +} + +async function main() { + const conn = await mysql.createConnection({ uri: process.env.DATABASE_URL }); + console.log("✅ 已连接数据库\n"); + + const current = await getCurrentEnum(conn); + if (!current) { + console.error("❌ 无法读取 ai_providers.provider 列定义,请确认表已创建"); + await conn.end(); + process.exit(1); + } + + console.log(`当前枚举值: [${current.join(", ")}]`); + + if (current.includes("ollama")) { + console.log("⏭️ 枚举已包含 'ollama',无需修改"); + await conn.end(); + process.exitCode = 0; + return; + } + + const nextValues = [...current, "ollama"]; + const nextEnum = nextValues.map((v) => `'${v}'`).join(","); + const sql = `ALTER TABLE \`ai_providers\` MODIFY COLUMN \`provider\` ENUM(${nextEnum}) NOT NULL`; + + try { + await conn.execute(sql); + console.log(`✅ 执行成功: ${sql}`); + } catch (err) { + console.error(`❌ 执行失败: ${err.message}`); + console.error(` SQL: ${sql}`); + await conn.end(); + process.exit(1); + } + + // 验证 + const verify = await getCurrentEnum(conn); + if (verify && verify.includes("ollama")) { + console.log(`\n✅ 验证通过,新枚举值: [${verify.join(", ")}]`); + } else { + console.log("\n❌ 验证失败,ollama 未出现在枚举中"); + } + + await conn.end(); + process.exitCode = 0; +} + +main().catch((err) => { + console.error("致命错误:", err); + process.exit(1); +}); diff --git a/scripts/seed-grade5-chinese.ts b/scripts/seed-grade5-chinese.ts new file mode 100644 index 0000000..20f7676 --- /dev/null +++ b/scripts/seed-grade5-chinese.ts @@ -0,0 +1,996 @@ +/** + * 五年级上册语文(人教版)第一单元 数据初始化脚本 + * + * 功能: + * 1. 清理现有教材/章节/知识点/题目/试卷相关数据(保留用户/班级/学校) + * 2. 创建五年级上册语文教材 + * 3. 创建第一单元 4 课(白鹭、落花生、桂花雨、珍珠鸟)的章节与正文 + * 4. 创建每课的知识点(生字、朗读、理解、表达等)+ 前置依赖 + * 5. 创建题库(生字题、选择题、判断题、填空题、简答题) + * 6. 创建第一单元测验试卷并关联题目 + * + * 运行:npx tsx scripts/seed-grade5-chinese.ts + */ +import "dotenv/config"; +import { db } from "../src/shared/db"; +import { + textbooks, + chapters, + knowledgePoints, + knowledgePointPrerequisites, + questions, + questionsToKnowledgePoints, + exams, + examQuestions, + subjects, + grades, + schools, + users, + usersToRoles, + roles, +} from "../src/shared/db/schema"; +import { createId } from "@paralleldrive/cuid2"; +import { sql, eq, and } from "drizzle-orm"; + +// ============ 类型定义 ============ + +interface SeedOption { + id: string; + text: string; + isCorrect?: boolean; +} + +interface SeedQuestionContent { + text?: string; + options?: SeedOption[]; + correctAnswer?: string | boolean; +} + +interface SeedQuestion { + id: string; + type: "single_choice" | "multiple_choice" | "text" | "judgment"; + difficulty: number; + content: SeedQuestionContent; + score: number; + knowledgePointIds: string[]; +} + +// ============ 常量 ============ + +const NOW = new Date(); +const GRADE_VALUE = "Grade 5"; +const SUBJECT_VALUE = "Chinese"; + +// ============ 主流程 ============ + +async function main() { + console.log("🌱 开始五年级上册语文(人教版)第一单元数据初始化..."); + const start = performance.now(); + + await cleanupTextbookAndQuestions(); + const { subjectId, gradeId, authorId } = await ensurePrerequisites(); + const { chapterMap } = await seedTextbookAndChapters(); + const kpMap = await seedKnowledgePoints(chapterMap); + const questionList = await seedQuestions(authorId, kpMap); + await seedExam(authorId, subjectId, gradeId, questionList); + + const elapsed = ((performance.now() - start) / 1000).toFixed(2); + console.log(`\n✅ 初始化完成,耗时 ${elapsed}s`); + console.log(` 教材:1 本(五年级语文上册)`); + console.log(` 章节:1 个单元 + 4 课`); + console.log(` 知识点:${Object.keys(kpMap).length} 个`); + console.log(` 题目:${questionList.length} 道`); + console.log(` 试卷:1 套(第一单元测验)`); +} + +main().catch((err) => { + console.error("❌ 初始化失败:", err); + process.exit(1); +}); + +// ============ 0. 清理 ============ + +async function cleanupTextbookAndQuestions() { + console.log("🧹 清理现有教材/题目/试卷数据(保留用户/班级/学校)..."); + await db.execute(sql`SET FOREIGN_KEY_CHECKS = 0;`); + const tables = [ + "homework_answers", + "homework_submissions", + "homework_assignment_targets", + "homework_assignment_questions", + "homework_assignments", + "submission_answers", + "exam_submissions", + "exam_questions", + "exams", + "questions_to_knowledge_points", + "questions", + "knowledge_point_prerequisites", + "knowledge_points", + "chapters", + "textbooks", + ]; + for (const t of tables) { + try { + await db.execute(sql.raw(`TRUNCATE TABLE \`${t}\`;`)); + } catch { + // 表可能不存在,跳过 + } + } + await db.execute(sql`SET FOREIGN_KEY_CHECKS = 1;`); + console.log("✓ 清理完成"); +} + +// ============ 1. 前置数据 ============ + +async function ensurePrerequisites(): Promise<{ + subjectId: string; + gradeId: string; + authorId: string; +}> { + console.log("🔍 检查学科/年级/教师..."); + + // 学科:查询或创建 Chinese(用原生 SQL 避免 DB schema 不同步问题) + let subjectId: string | null = null; + try { + const subjRows = await db.execute(sql`SELECT id FROM subjects WHERE code = 'CHINESE' OR name = '语文' LIMIT 1`); + const subjRow = subjRows[0] as Array<{ id: string }> | undefined; + if (subjRow && subjRow.length > 0) { + subjectId = subjRow[0].id; + } + } catch { + // subjects 表可能不存在 + } + if (!subjectId) { + subjectId = "subj_chinese"; + try { + await db.execute(sql` + INSERT INTO subjects (id, name, code, \`order\`, created_at, updated_at) + VALUES (${subjectId}, '语文', 'CHINESE', 1, NOW(), NOW()) + `); + console.log(" + 创建学科:语文(CHINESE)"); + } catch { + // 可能已存在或表结构不同,尝试不带 order + try { + await db.execute(sql` + INSERT INTO subjects (id, name, code, created_at, updated_at) + VALUES (${subjectId}, '语文', 'CHINESE', NOW(), NOW()) + `); + console.log(" + 创建学科:语文(CHINESE)"); + } catch { + // 如果仍然失败,尝试用 Drizzle insert + try { + await db.insert(subjects).values({ id: subjectId, name: "语文", code: "CHINESE", order: 1 }); + console.log(" + 创建学科:语文(CHINESE)"); + } catch { + console.log(" ! 学科创建失败,尝试继续..."); + } + } + } + } else { + console.log(` ✓ 学科 ID:${subjectId}`); + } + + const school = await db.query.schools.findFirst(); + if (!school) { + throw new Error("数据库中不存在任何学校,请先运行 npm run db:seed 初始化基础数据"); + } + // 年级:查询或创建五年级(用原生 SQL 避免 DB schema 不同步问题) + let gradeId: string | null = null; + try { + const gradeRows = await db.execute(sql` + SELECT id FROM grades WHERE school_id = ${school.id} AND (name = '五年级' OR name = ${GRADE_VALUE}) LIMIT 1 + `); + const gradeRow = gradeRows[0] as Array<{ id: string }> | undefined; + if (gradeRow && gradeRow.length > 0) { + gradeId = gradeRow[0].id; + } + } catch { + // grades 表可能不存在 + } + if (!gradeId) { + gradeId = `grade_g5_${school.id.slice(0, 8)}`; + try { + await db.execute(sql` + INSERT INTO grades (id, school_id, name, \`order\`, created_at, updated_at) + VALUES (${gradeId}, ${school.id}, '五年级', 5, NOW(), NOW()) + `); + console.log(" + 创建年级:五年级"); + } catch { + try { + await db.insert(grades).values({ + id: gradeId, + schoolId: school.id, + name: "五年级", + order: 5, + }); + console.log(" + 创建年级:五年级"); + } catch { + console.log(" ! 年级创建失败,尝试继续..."); + } + } + } else { + console.log(` ✓ 年级 ID:${gradeId}`); + } + + // 教师:查询第一个有教师角色的用户(用原生 SQL 避免 DB schema 不同步问题) + let authorId: string | null = null; + try { + const roleRows = await db.execute(sql` + SELECT r.id FROM roles r WHERE r.name = 'teacher' LIMIT 1 + `); + const roleRow = roleRows[0] as Array<{ id: string }> | undefined; + if (roleRow && roleRow.length > 0) { + const linkRows = await db.execute(sql` + SELECT utr.user_id FROM users_to_roles utr WHERE utr.role_id = ${roleRow[0].id} LIMIT 1 + `); + const linkRow = linkRows[0] as Array<{ user_id: string }> | undefined; + if (linkRow && linkRow.length > 0) { + authorId = linkRow[0].user_id; + } + } + } catch { + // roles 表可能不存在或 schema 不同步,忽略 + } + if (!authorId) { + // 兜底:取第一个用户 + const userRows = await db.execute(sql`SELECT id FROM users LIMIT 1`); + const userRow = userRows[0] as Array<{ id: string }> | undefined; + if (!userRow || userRow.length === 0) { + throw new Error("数据库中不存在任何用户,请先运行 npm run db:seed 初始化基础数据"); + } + authorId = userRow[0].id; + console.log(" ! 未找到教师用户,使用第一个用户作为题目作者"); + } else { + console.log(` ✓ 教师 ID:${authorId}`); + } + + return { subjectId: subjectId!, gradeId: gradeId!, authorId: authorId! }; +} + +// ============ 2. 教材 + 章节 ============ + +async function seedTextbookAndChapters(): Promise<{ + chapterMap: Record; +}> { + console.log("📖 创建五年级上册语文教材与章节..."); + const chapterMap: Record = {}; + + const textbookId = "tb_chinese_g5_up"; + await db.insert(textbooks).values({ + id: textbookId, + title: "五年级语文(上册)", + subject: SUBJECT_VALUE, + grade: GRADE_VALUE, + publisher: "人民教育出版社", + }); + + const unitId = "ch_chinese_g5_u1"; + await db.insert(chapters).values({ + id: unitId, + textbookId, + title: "第一单元 课文", + order: 1, + parentId: null, + content: `# 第一单元 课文 + +本单元围绕「万物有灵」主题,通过《白鹭》《落花生》《桂花雨》《珍珠鸟》四篇课文,学习借物喻人、借物抒情的方法,体会作者表达的情感。`, + }); + + // 第 1 课 白鹭 + const lesson1Id = "ch_chinese_g5_u1_l1"; + chapterMap.l1_bailu = lesson1Id; + await db.insert(chapters).values({ + id: lesson1Id, + textbookId, + title: "1 白鹭", + order: 1, + parentId: unitId, + content: lesson1Content, + }); + + // 第 2 课 落花生 + const lesson2Id = "ch_chinese_g5_u1_l2"; + chapterMap.l2_luohuasheng = lesson2Id; + await db.insert(chapters).values({ + id: lesson2Id, + textbookId, + title: "2 落花生", + order: 2, + parentId: unitId, + content: lesson2Content, + }); + + // 第 3 课 桂花雨 + const lesson3Id = "ch_chinese_g5_u1_l3"; + chapterMap.l3_guihuayu = lesson3Id; + await db.insert(chapters).values({ + id: lesson3Id, + textbookId, + title: "3 桂花雨", + order: 3, + parentId: unitId, + content: lesson3Content, + }); + + // 第 4 课 珍珠鸟 + const lesson4Id = "ch_chinese_g5_u1_l4"; + chapterMap.l4_zhenzhuniao = lesson4Id; + await db.insert(chapters).values({ + id: lesson4Id, + textbookId, + title: "4* 珍珠鸟(略读课文)", + order: 4, + parentId: unitId, + content: lesson4Content, + }); + + console.log("✓ 创建 1 本教材,含第一单元 4 课"); + return { chapterMap }; +} + +// ============ 3. 知识点 + 前置依赖 ============ + +async function seedKnowledgePoints(chapterMap: Record): Promise> { + console.log("🧠 创建知识点..."); + const map: Record = {}; + + const kpDefs: Array<{ + key: string; + name: string; + chapterKey: keyof typeof chapterMap; + desc: string; + level: number; + order: number; + }> = [ + // 第 1 课 白鹭 + { key: "l1_char", name: "生字认读:鹭嫌嵌匣嗜韵", chapterKey: "l1_bailu", desc: "认识鹭、嫌、嵌、匣、嗜、韵 6 个生字,正确书写并理解含义", level: 1, order: 1 }, + { key: "l1_read", name: "朗读与背诵", chapterKey: "l1_bailu", desc: "正确、流利、有感情地朗读课文,背诵全文", level: 1, order: 2 }, + { key: "l1_understand", name: "体会白鹭是一首精巧的诗", chapterKey: "l1_bailu", desc: "从色素配合、身段大小、蓑毛/喙/脚的描写中体会白鹭的精巧与适宜", level: 2, order: 3 }, + { key: "l1_image", name: "为三幅图画起名字", chapterKey: "l1_bailu", desc: "为第 6~8 自然段描绘的清水田钓鱼、清晨望哨、黄昏低飞三幅图起名字", level: 2, order: 4 }, + { key: "l1_express", name: "借物喻人/借物抒情", chapterKey: "l1_bailu", desc: "学习作者把白鹭比作诗,通过具体描写表达赞美之情的方法", level: 3, order: 5 }, + // 第 2 课 落花生 + { key: "l2_char", name: "生字认读:亩吩榨榴矮", chapterKey: "l2_luohuasheng", desc: "认识亩、吩、榨、榴、矮 5 个生字,正确书写并理解含义", level: 1, order: 1 }, + { key: "l2_read", name: "分角色朗读", chapterKey: "l2_luohuasheng", desc: "分角色朗读课文,读出人物对话的语气", level: 1, order: 2 }, + { key: "l2_understand", name: "体会花生的特点", chapterKey: "l2_luohuasheng", desc: "从对话中体会花生埋在地里、不好看但有用的特点", level: 2, order: 3 }, + { key: "l2_truth", name: "理解父亲借花生说的道理", chapterKey: "l2_luohuasheng", desc: "理解人要做有用的人,不要做只讲体面而对别人没有好处的人", level: 3, order: 4 }, + { key: "l2_write", name: "小练笔:借物喻人", chapterKey: "l2_luohuasheng", desc: "选择竹子、梅花、蜜蜂、路灯等事物写一段话,运用借物喻人的方法", level: 3, order: 5 }, + // 第 3 课 桂花雨 + { key: "l3_char", name: "生字认读:萝杭", chapterKey: "l3_guihuayu", desc: "认识萝、杭 2 个生字,正确书写并理解含义", level: 1, order: 1 }, + { key: "l3_read", name: "有感情地朗读", chapterKey: "l3_guihuayu", desc: "有感情地朗读课文,读出对桂花的喜爱和对故乡的思念", level: 1, order: 2 }, + { key: "l3_memory", name: "桂花带来的美好回忆", chapterKey: "l3_guihuayu", desc: "说说桂花给我带来的美好回忆(摇花乐、桂花雨、桂花香)", level: 2, order: 3 }, + { key: "l3_feeling", name: "体会句子蕴含的感情", chapterKey: "l3_guihuayu", desc: "理解这里的桂花再香也比不上家乡院子里的桂花蕴含的思乡之情", level: 3, order: 4 }, + // 第 4 课 珍珠鸟 + { key: "l4_char", name: "生字认读:蔓幽悉雏哟柜享陪待趴脸眸", chapterKey: "l4_zhenzhuniao", desc: "认识蔓、幽、悉、雏、哟、柜、享、陪、待、趴、脸、眸 12 个生字", level: 1, order: 1 }, + { key: "l4_read", name: "默读课文", chapterKey: "l4_zhenzhuniao", desc: "默读课文,理清我逐渐得到珍珠鸟信赖的过程", level: 1, order: 2 }, + { key: "l4_trust", name: "体会信赖创造美好境界", chapterKey: "l4_zhenzhuniao", desc: "找出描写珍珠鸟可爱的语句,体会我和珍珠鸟之间的情意", level: 2, order: 3 }, + // 单元综合 + { key: "unit_theme", name: "单元主题:万物有灵", chapterKey: "l1_bailu", desc: "理解本单元万物有灵主题,体会作者对事物的细致观察与真挚情感", level: 3, order: 1 }, + { key: "unit_method", name: "单元方法:借物抒情", chapterKey: "l1_bailu", desc: "综合运用借物喻人、借物抒情的方法进行阅读与写作", level: 3, order: 2 }, + ]; + + for (const kp of kpDefs) { + const id = `kp_g5_${kp.key}`; + map[kp.key] = id; + await db.insert(knowledgePoints).values({ + id, + name: kp.name, + description: kp.desc, + level: kp.level, + order: kp.order, + chapterId: chapterMap[kp.chapterKey], + }); + } + + // 前置依赖:生字 → 朗读 → 理解 → 表达(每课内部递进) + const prereqPairs: Array<[string, string]> = [ + ["l1_char", "l1_read"], + ["l1_read", "l1_understand"], + ["l1_understand", "l1_image"], + ["l1_understand", "l1_express"], + ["l2_char", "l2_read"], + ["l2_read", "l2_understand"], + ["l2_understand", "l2_truth"], + ["l2_truth", "l2_write"], + ["l3_char", "l3_read"], + ["l3_read", "l3_memory"], + ["l3_memory", "l3_feeling"], + ["l4_char", "l4_read"], + ["l4_read", "l4_trust"], + ["l1_express", "unit_method"], + ["l2_write", "unit_method"], + ["l1_understand", "unit_theme"], + ]; + for (const [from, to] of prereqPairs) { + const fromId = map[from]; + const toId = map[to]; + if (fromId && toId) { + await db.insert(knowledgePointPrerequisites).values({ + knowledgePointId: toId, + prerequisiteKpId: fromId, + }); + } + } + + console.log(`✓ 创建 ${kpDefs.length} 个知识点,${prereqPairs.length} 条前置依赖`); + return map; +} + +// ============ 4. 题库 ============ + +async function seedQuestions( + authorId: string, + kpMap: Record +): Promise { + console.log("❓ 创建题库..."); + const all: SeedQuestion[] = []; + + // ---- 第 1 课 白鹭 ---- + const l1Questions: SeedQuestion[] = [ + { + id: createId(), + type: "single_choice", + difficulty: 1, + content: { + text: "「白鹭是一首精巧的诗」中,「精巧」的意思最接近?", + options: [ + { id: "A", text: "精美巧妙", isCorrect: true }, + { id: "B", text: "精明能干", isCorrect: false }, + { id: "C", text: "精心制作", isCorrect: false }, + { id: "D", text: "精打细算", isCorrect: false }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l1_understand], + }, + { + id: createId(), + type: "single_choice", + difficulty: 2, + content: { + text: "下列哪个字不是《白鹭》一课的生字?", + options: [ + { id: "A", text: "鹭", isCorrect: false }, + { id: "B", text: "嫌", isCorrect: false }, + { id: "C", text: "嵌", isCorrect: false }, + { id: "D", text: "鹤", isCorrect: true }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l1_char], + }, + { + id: createId(), + type: "judgment", + difficulty: 1, + content: { + text: "「增之一分则嫌长,减之一分则嫌短」说明白鹭的身段大小非常适宜。", + correctAnswer: true, + }, + score: 5, + knowledgePointIds: [kpMap.l1_understand], + }, + { + id: createId(), + type: "judgment", + difficulty: 2, + content: { + text: "作者认为白鹭不会唱歌是它的美中不足,所以白鹭不如歌好听。", + correctAnswer: false, + }, + score: 5, + knowledgePointIds: [kpMap.l1_express], + }, + { + id: createId(), + type: "text", + difficulty: 2, + content: { + text: "为第 6~8 自然段描绘的三幅图画分别起一个名字。", + correctAnswer: "清水田钓鱼图;清晨望哨图;黄昏低飞图", + }, + score: 10, + knowledgePointIds: [kpMap.l1_image], + }, + { + id: createId(), + type: "text", + difficulty: 3, + content: { + text: "为什么说「白鹭实在是一首诗,一首韵在骨子里的散文诗」?结合课文谈谈你的理解。", + correctAnswer: "白鹭外形精巧适宜,色素配合得当,像一首精巧的诗;它在清水田钓鱼、清晨望哨、黄昏低飞的画面富有诗意;它的美是内在的、含蓄的,像韵在骨子里的散文诗。", + }, + score: 10, + knowledgePointIds: [kpMap.l1_express, kpMap.l1_understand], + }, + ]; + + // ---- 第 2 课 落花生 ---- + const l2Questions: SeedQuestion[] = [ + { + id: createId(), + type: "single_choice", + difficulty: 1, + content: { + text: "父亲借花生告诉我们的道理是?", + options: [ + { id: "A", text: "花生很好吃", isCorrect: false }, + { id: "B", text: "人要做有用的人,不要做只讲体面而对别人没有好处的人", isCorrect: true }, + { id: "C", text: "花生可以榨油", isCorrect: false }, + { id: "D", text: "花生价钱便宜", isCorrect: false }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l2_truth], + }, + { + id: createId(), + type: "single_choice", + difficulty: 2, + content: { + text: "父亲把花生和桃子、石榴、苹果作对比,是为了说明花生?", + options: [ + { id: "A", text: "果实埋在地里,不炫耀,朴实无华", isCorrect: true }, + { id: "B", text: "比桃子石榴苹果更好吃", isCorrect: false }, + { id: "C", text: "长得矮小不好看", isCorrect: false }, + { id: "D", text: "价钱比它们便宜", isCorrect: false }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l2_understand], + }, + { + id: createId(), + type: "judgment", + difficulty: 1, + content: { + text: "「榨」字的读音是 zhà。", + correctAnswer: true, + }, + score: 5, + knowledgePointIds: [kpMap.l2_char], + }, + { + id: createId(), + type: "judgment", + difficulty: 2, + content: { + text: "父亲认为花生虽然不好看可是很有用,所以我们要像花生一样只做有用的人不要讲体面。", + correctAnswer: false, + }, + score: 5, + knowledgePointIds: [kpMap.l2_truth], + }, + { + id: createId(), + type: "text", + difficulty: 2, + content: { + text: "课文围绕「落花生」写了哪些内容?(请简要概括)", + correctAnswer: "种花生、收花生;尝花生(过收获节);议花生(父亲借花生讲道理)", + }, + score: 10, + knowledgePointIds: [kpMap.l2_understand], + }, + { + id: createId(), + type: "text", + difficulty: 3, + content: { + text: "小练笔:选择竹子、梅花、蜜蜂或路灯中的一种,仿照《落花生》的写法,写一段话(借物喻人)。", + correctAnswer: "(开放题)示例:竹子虽然外表朴素,没有牡丹的艳丽,但它虚心有节、坚韧不拔,无论风吹雨打都挺直腰杆。我们要像竹子一样,做一个虚心、坚强、有骨气的人。", + }, + score: 10, + knowledgePointIds: [kpMap.l2_write], + }, + ]; + + // ---- 第 3 课 桂花雨 ---- + const l3Questions: SeedQuestion[] = [ + { + id: createId(), + type: "single_choice", + difficulty: 1, + content: { + text: "「这里的桂花再香,也比不上家乡院子里的桂花」表达了作者怎样的感情?", + options: [ + { id: "A", text: "觉得杭州的桂花不够香", isCorrect: false }, + { id: "B", text: "对故乡和童年生活的思念之情", isCorrect: true }, + { id: "C", text: "不喜欢杭州的桂花", isCorrect: false }, + { id: "D", text: "觉得母亲挑剔", isCorrect: false }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l3_feeling], + }, + { + id: createId(), + type: "single_choice", + difficulty: 2, + content: { + text: "下列哪个词最能概括「摇花」给「我」带来的感受?", + options: [ + { id: "A", text: "害怕", isCorrect: false }, + { id: "B", text: "摇花乐", isCorrect: true }, + { id: "C", text: "辛苦", isCorrect: false }, + { id: "D", text: "无聊", isCorrect: false }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l3_memory], + }, + { + id: createId(), + type: "judgment", + difficulty: 1, + content: { + text: "「萝」和「杭」都是《桂花雨》一课的生字。", + correctAnswer: true, + }, + score: 5, + knowledgePointIds: [kpMap.l3_char], + }, + { + id: createId(), + type: "judgment", + difficulty: 2, + content: { + text: "桂花摇下来比落在泥土里的香味差。", + correctAnswer: false, + }, + score: 5, + knowledgePointIds: [kpMap.l3_memory], + }, + { + id: createId(), + type: "text", + difficulty: 2, + content: { + text: "说说桂花给「我」带来了哪些美好的回忆?", + correctAnswer: "摇花乐(缠着母亲摇桂花,桂花纷纷落下像下雨);桂花雨(满头满身都是桂花,好香的雨);桂花香(全年整个村子都浸在桂花香里)", + }, + score: 10, + knowledgePointIds: [kpMap.l3_memory], + }, + ]; + + // ---- 第 4 课 珍珠鸟 ---- + const l4Questions: SeedQuestion[] = [ + { + id: createId(), + type: "single_choice", + difficulty: 1, + content: { + text: "「信赖,往往创造出美好的境界」这句话的含义是?", + options: [ + { id: "A", text: "珍珠鸟很可爱", isCorrect: false }, + { id: "B", text: "人与动物(人与人)之间真诚的信赖,能带来和谐美好的关系", isCorrect: true }, + { id: "C", text: "鸟笼很舒适", isCorrect: false }, + { id: "D", text: "写作需要安静", isCorrect: false }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l4_trust], + }, + { + id: createId(), + type: "single_choice", + difficulty: 2, + content: { + text: "珍珠鸟逐渐信赖「我」的过程,正确顺序是?", + options: [ + { id: "A", text: "落在肩上 → 在屋里飞 → 落在书桌上", isCorrect: false }, + { id: "B", text: "在屋里飞 → 落在书桌上 → 落在肩上睡着", isCorrect: true }, + { id: "C", text: "落在书桌上 → 在屋里飞 → 落在肩上", isCorrect: false }, + { id: "D", text: "落在肩上 → 落在书桌上 → 在屋里飞", isCorrect: false }, + ], + }, + score: 5, + knowledgePointIds: [kpMap.l4_read], + }, + { + id: createId(), + type: "judgment", + difficulty: 1, + content: { + text: "「雏」字的读音是 chú。", + correctAnswer: true, + }, + score: 5, + knowledgePointIds: [kpMap.l4_char], + }, + { + id: createId(), + type: "text", + difficulty: 2, + content: { + text: "从课文中找出两句描写珍珠鸟可爱的语句,抄写下来。", + correctAnswer: "(示例)1. 它好肥,整个身子好像一个蓬松的球儿。2. 小红爪子在纸上发出嚓嚓的响声。", + }, + score: 10, + knowledgePointIds: [kpMap.l4_trust], + }, + ]; + + // ---- 单元综合 ---- + const unitQuestions: SeedQuestion[] = [ + { + id: createId(), + type: "multiple_choice", + difficulty: 3, + content: { + text: "本单元四篇课文都运用了「借物抒情」的方法,下列说法正确的有?(多选)", + options: [ + { id: "A", text: "《白鹭》借白鹭表达对平凡之美的赞美", isCorrect: true }, + { id: "B", text: "《落花生》借花生说明要做有用的人", isCorrect: true }, + { id: "C", text: "《桂花雨》借桂花表达思乡之情", isCorrect: true }, + { id: "D", text: "《珍珠鸟》借珍珠鸟说明信赖创造美好境界", isCorrect: true }, + ], + }, + score: 8, + knowledgePointIds: [kpMap.unit_method, kpMap.unit_theme], + }, + { + id: createId(), + type: "text", + difficulty: 3, + content: { + text: "本单元的主题是「万物有灵」,请结合四篇课文谈谈你的理解。", + correctAnswer: "白鹭的精巧、花生的朴实、桂花的芬芳、珍珠鸟的信赖,都体现了万物各有其美。作者通过细致观察和真挚情感,把事物写得生动感人,启示我们要善于发现美、做有用的人、珍惜情感。", + }, + score: 12, + knowledgePointIds: [kpMap.unit_theme], + }, + ]; + + const groups = [l1Questions, l2Questions, l3Questions, l4Questions, unitQuestions]; + for (const g of groups) { + all.push(...g); + } + + for (const q of all) { + await db.insert(questions).values({ + id: q.id, + type: q.type, + difficulty: q.difficulty, + content: q.content, + authorId, + }); + if (q.knowledgePointIds.length > 0) { + await db.insert(questionsToKnowledgePoints).values( + q.knowledgePointIds.map((kpId) => ({ + questionId: q.id, + knowledgePointId: kpId, + })) + ); + } + } + + console.log(`✓ 创建 ${all.length} 道题目`); + return all; +} + +// ============ 5. 试卷 ============ + +async function seedExam( + creatorId: string, + subjectId: string, + gradeId: string, + questionList: SeedQuestion[] +): Promise { + console.log("📝 创建第一单元测验试卷..."); + + const examId = "exam_chinese_g5_u1"; + + // 选取 10 题组卷 + const selected = [ + questionList[0], // l1 选择 + questionList[5], // l1 简答 + questionList[6], // l2 选择 + questionList[9], // l2 判断 + questionList[12], // l3 选择 + questionList[15], // l3 判断 + questionList[17], // l4 选择 + questionList[19], // l4 判断 + questionList[21], // 单元多选 + questionList[22], // 单元简答 + ]; + + const makeGroup = (title: string, children: Array<{ questionId: string; score: number }>) => ({ + id: createId(), + type: "group" as const, + title, + children: children.map((c) => ({ + id: createId(), + type: "question" as const, + questionId: c.questionId, + score: c.score, + })), + }); + + const structure = [ + makeGroup("一、选择题(每题 5 分)", [ + { questionId: selected[0].id, score: 5 }, + { questionId: selected[2].id, score: 5 }, + { questionId: selected[4].id, score: 5 }, + { questionId: selected[6].id, score: 5 }, + ]), + makeGroup("二、判断题(每题 5 分)", [ + { questionId: selected[3].id, score: 5 }, + { questionId: selected[5].id, score: 5 }, + { questionId: selected[7].id, score: 5 }, + ]), + makeGroup("三、多选题(8 分)", [ + { questionId: selected[8].id, score: 8 }, + ]), + makeGroup("四、简答题(共 32 分)", [ + { questionId: selected[1].id, score: 10 }, + { questionId: selected[9].id, score: 12 }, + ]), + ]; + + const totalScore = selected.reduce((sum, q) => sum + q.score, 0); + + await db.insert(exams).values({ + id: examId, + title: "五年级语文(上册)第一单元测验", + description: JSON.stringify({ + subject: SUBJECT_VALUE, + grade: GRADE_VALUE, + difficulty: 2, + totalScore, + durationMin: 60, + questionCount: selected.length, + tags: ["第一单元", "白鹭", "落花生", "桂花雨", "珍珠鸟"], + }), + creatorId, + subjectId, + gradeId, + status: "published", + startTime: NOW, + examMode: "homework", + durationMinutes: 60, + structure, + }); + + await db.insert(examQuestions).values( + selected.map((q, i) => ({ + examId, + questionId: q.id, + score: q.score, + order: i, + })) + ); + + console.log(`✓ 创建试卷「${examId}」,含 ${selected.length} 题,总分 ${totalScore} 分`); +} + +// ============ 课文正文内容(模板字符串) ============ + +const lesson1Content = `# 1 白鹭 + +白鹭是一首精巧的诗。 + +色素的配合,身段的大小,一切都很适宜。 + +白鹤太大而嫌生硬,即使如粉红的朱鹭或灰色的苍鹭,也觉得大了一些,而且太不寻常了。 + +然而白鹭却因为它的常见,而被人忘却了它的美。 + +那雪白的蓑毛,那全身的流线型结构,那铁色的长喙,那青色的脚,增之一分则嫌长,减之一分则嫌短,素之一忽则嫌白,黛之一忽则嫌黑。 + +在清水田里,时有一只两只白鹭站着钓鱼,整个的田便成了一幅嵌在玻璃框里的画。田的大小好像是有心人为白鹭设计的镜匣。 + +晴天的清晨,每每看见它孤独地站立于小树的绝顶,看来像是不安稳,而它却很悠然。这是别的鸟很难表现的一种嗜好。人们说它是在望哨,可它真是在望哨吗? + +黄昏的空中偶见白鹭的低飞,更是乡居生活中的一种恩惠。那是清澄的形象化,而且具有生命了。 + +或许有人会感到美中不足,白鹭不会唱歌。但是白鹭本身不就是一首很优美的歌吗? + +不,歌未免太铿锵了。 + +白鹭实在是一首诗,一首韵在骨子里的散文诗。 + +--- + +**生字**:鹭 嫌 嵌 匣 嗜 韵 + +**课后要求**:朗读课文,说说从哪些地方感受到「白鹭是一首精巧的诗」;为第6~8自然段描绘的三幅图画起名字;背诵课文,抄写喜欢的自然段。`; + +const lesson2Content = `# 2 落花生 + +我们家的后园有半亩空地。母亲说:「让它荒着怪可惜的,你们那么爱吃花生,就开辟出来种花生吧。」我们姐弟几个都很高兴,买种,翻地,播种,浇水,没过几个月,居然收获了。 + +母亲说:「今晚我们过一个收获节,请你们的父亲也来尝尝我们的新花生,好不好?」母亲把花生做成了好几样食品,还吩咐就在后园的茅亭里过这个节。 + +那晚的天色不大好,可是父亲也来了,实在很难得。 + +父亲说:「你们爱吃花生吗?」 + +我们争着回答:「爱!」 + +「谁能把花生的好处说出来?」 + +姐姐说:「花生的味道很美。」 + +哥哥说:「花生可以榨油。」 + +我说:「花生的价钱便宜,谁都可以买来吃,都喜欢吃。这就是它的好处。」 + +父亲说:「花生的好处很多,有一样最可贵。它的果实埋在地里,不像桃子、石榴、苹果那样,把鲜红嫩绿的果实高高地挂在枝上,使人一见就生爱慕之心。你们看它矮矮地长在地上,等到成熟了,也不能立刻分辨出来它有没有果实,必须挖起来才知道。」 + +我们都说是,母亲也点点头。 + +父亲接下去说:「所以你们要像花生,它虽然不好看,可是很有用。」 + +我说:「那么,人要做有用的人,不要做只讲体面,而对别人没有好处的人。」 + +父亲说:「对。这是我对你们的希望。」 + +我们谈到深夜才散。花生做的食品都吃完了,父亲的话却深深地印在我的心上。 + +--- + +**生字**:亩 吩 榨 榴 矮 + +**课后要求**:分角色朗读课文,说说课文围绕「落花生」写了哪些内容;从对话中体会花生的特点及父亲借花生告诉的道理;小练笔:选择竹子、梅花、蜜蜂、路灯等事物写一段话。`; + +const lesson3Content = `# 3 桂花雨 + +中秋节前后,正是故乡桂花盛开的时节。 + +小时候,我无论对什么花,都不懂得欣赏。父亲总是指指点点地告诉我,这是梅花,那是木兰花……但我除了记些名字外,并不喜欢。我喜欢的是桂花。桂花树的样子笨笨的,不像梅树那样有姿态。不开花时,只见到满树的叶子;开花时,仔细地在树丛里寻找,才能看到那些小花。可是桂花的香气,太迷人了。 + +故乡靠海,八月是台风季节。桂花一开,母亲就开始担心了:「可别来台风啊!」母亲每天都要在前后院子走一回,嘴里念着:「只要不来台风,我就可以收几大箩。送一箩给胡家老爷爷,送一箩给毛家老婆婆,他们两家糕饼做得多。」 + +桂花盛开的时候,不说香飘十里,至少前后左右十几家邻居,没有不浸在桂花香里的。桂花成熟时,就应当「摇」。摇下来的桂花,朵朵完整、新鲜。如果让它开过了,落在泥土里,尤其是被风吹落,比摇下来的香味就差多了。 + +摇花对我来说是件大事,我总是缠着母亲问:「妈,怎么还不摇桂花呢?」母亲说:「还早呢,花开的时间太短,摇不下来的。」可是母亲一看天上布满阴云,就知道要来台风了,赶紧叫大家提前摇桂花。这下,我可乐了,帮大人抱着桂花树,使劲地摇。摇哇摇,桂花纷纷落下来,我们满头满身都是桂花。我喊着:「啊!真像下雨,好香的雨呀!」 + +桂花摇落以后,挑去小枝小叶,晒上几天太阳,收在铁盒子里,可以加在茶叶里泡茶,过年时还可以做糕饼。全年,整个村子都浸在桂花的香气里。 + +我念中学的时候,全家到了杭州。杭州有一处小山,全是桂花树,花开时那才是香飘十里。秋天,我常到那儿去赏桂花。回家时,总要捡一大袋桂花给母亲。可是母亲说:「这里的桂花再香,也比不上家乡院子里的桂花。」 + +于是,我又想起了在故乡童年时代的「摇花乐」,还有那摇落的阵阵桂花雨。 + +--- + +**生字**:萝 杭 + +**课后要求**:有感情地朗读课文,说说桂花给「我」带来哪些美好回忆;体会句子蕴含的感情;联系「阅读链接」理解「这里的桂花再香,也比不上家乡院子里的桂花」的含义。`; + +const lesson4Content = `# 4* 珍珠鸟(略读课文) + +真好!朋友送我一对珍珠鸟,放在一个简易的竹条编成的笼子里,笼内还有一卷干草,那是小鸟舒适又温暖的巢。 + +有人说,这是一种怕人的鸟。 + +我把鸟笼挂在窗前。那儿还有一大盆异常茂盛的法国吊兰。我便用吊兰长长的、串生着小绿叶的垂蔓蒙盖在鸟笼上。它们就像躲进幽深的丛林一样安全,从中传出的笛儿般又细又亮的叫声,也就格外轻松自在了。小鸟的影子就在这中间隐约闪动,看不完整,有时连笼子也看不出,却见它们可爱的鲜红小嘴从绿叶中伸出来。 + +我很少扒开垂蔓瞧它们,它们便渐渐敢伸出小脑袋瞅瞅我。我们就这样一点点熟悉了。 + +三个月后,那一团越发繁茂的垂蔓里边,发出一种尖细又娇嫩的鸣叫。我猜到,是它们有了雏儿。我呢,决不掀开叶片往里看,连添食加水时也不睁大好奇的眼睛去惊动它们。过不多久,忽然有一个更小的脑袋从叶间探出来。哟,雏儿!正是这小家伙! + +它小,就能轻易地由疏格的笼子里钻出来。瞧,多么像它的父母:红嘴红脚,灰蓝色的毛,只是后背还没生出珍珠似的圆圆的白点。它好肥,整个身子好像一个蓬松的球儿。 + +起先,这小家伙只在笼子四周活动,随后就在屋里飞来飞去,一会儿落在柜顶上,一会儿神气十足地站在书架上,啄着书脊上那些大文豪的名字,一会儿把灯绳撞得来回摇动,跟着逃到画框上去了。只要大鸟在笼里生气地叫一声,它就立即飞回笼里去。 + +我不管它。这样久了,打开窗子,它最多只在窗框上站一会儿,决不飞出去。 + +渐渐它胆子大了,就落在我的书桌上。它先是离我较远,见我不去伤害它,便一点点挨近,然后蹦到我的杯子上,俯下头来喝茶,再偏过脸瞧瞧我的反应。我只是微微一笑,依旧写东西。它就放开胆子跑到稿纸上,绕着我的笔尖蹦来蹦去,跳动的小红爪子在纸上发出嚓嚓的响声。 + +我不动声色地写,默默享受着这小家伙亲近的情意。这样,它完全放心了,索性用那涂了蜡似的小红嘴,嗒嗒地啄着我颤动的笔尖。我用手抚一抚它细腻的绒毛,它也不怕,反而友好地啄两下我的手指。 + +白天,它这样淘气地陪伴我;天色入暮,它就在父母再三的呼唤声中,飞向笼子,扭动滚圆的身子,挤开那些绿叶钻进去。 + +有一天,我伏案写作时,它居然落到我的肩上。我手中的笔不觉停了,生怕惊跑它。待一会儿,扭头看,这小家伙竟趴在我的肩头睡着了,银灰色的眼睑盖住眸子,小红爪子刚好被胸脯上长长的绒毛盖住。我轻轻抬一抬肩,它没醒,睡得好熟!还咂咂嘴,难道在做梦? + +我笔尖一动,流泻下一时的感受: + +信赖,往往创造出美好的境界。 + +--- + +**生字**:蔓 幽 悉 雏 哟 柜 享 陪 待 趴 脸 眸 + +**课前提示**:默读课文,想想「我」是怎样逐渐得到珍珠鸟的信赖的;找出描写珍珠鸟可爱的语句,体会「我」和珍珠鸟之间的情意。`; diff --git a/scripts/temp-i18n-zh-fix.mjs b/scripts/temp-i18n-zh-fix.mjs new file mode 100644 index 0000000..b8704ae --- /dev/null +++ b/scripts/temp-i18n-zh-fix.mjs @@ -0,0 +1,52 @@ +import fs from "node:fs"; +const f = "src/shared/i18n/messages/zh-CN/attendance.json"; +let c = fs.readFileSync(f, "utf8"); +const oldStr = `"noDataDescription": "当前班级在所选时间范围内暂无考勤记录。" + }, + "errors": {`; +const newStr = `"noDataDescription": "当前班级在所选时间范围内暂无考勤记录。" + }, + "correlation": { + "title": "考勤与成绩关联分析", + "description": "{className} · {start} 至 {end} · 出勤率与平均成绩的相关性", + "noData": "暂无关联分析数据", + "noDataDescription": "所选班级在时间范围内无考勤或成绩记录,无法进行关联分析。", + "noStudents": "暂无学生数据", + "noStudentsDescription": "该班级在时间范围内无考勤与成绩匹配的学生。", + "correlationCoefficient": "相关系数 r", + "notAvailable": "数据不足", + "scatterTitle": "出勤率-成绩散点图", + "scatterSeries": "学生", + "studentDetails": "学生明细", + "riskLevelLabel": "风险等级", + "attendanceRate": "出勤率", + "averageScore": "平均成绩", + "absentCount": "缺勤次数", + "gradeRecordCount": "成绩记录数", + "riskCounts": { + "high": "高风险学生数", + "medium": "中风险学生数", + "low": "低风险学生数" + }, + "riskLevel": { + "high": "高风险", + "medium": "中风险", + "low": "低风险" + }, + "interpretation": { + "strong_negative": "强负相关:出勤率越低,成绩越低", + "weak_negative": "弱负相关:出勤率与成绩有一定负相关", + "negligible": "无明显线性相关", + "weak_positive": "弱正相关:出勤率与成绩有一定正相关", + "strong_positive": "强正相关:出勤率越高,成绩越高", + "insufficient_data": "数据不足,无法计算相关性" + } + }, + "errors": {`; +if (!c.includes(oldStr)) { + console.log("OLD NOT FOUND"); + process.exit(1); +} +c = c.replace(oldStr, newStr); +fs.writeFileSync(f, c); +console.log("Done");