From 5f72149ceb75d22e5d8bfbb17d6f6808e2e75d89 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Fri, 18 Sep 2026 14:56:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8F=AF=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=E8=A1=A5=E9=BD=90=E7=AB=A0=E8=8A=82=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E3=80=81=E6=89=BE=E5=9B=9E=E7=94=B5=E5=AD=90=E4=B9=A6=E3=80=81?= =?UTF-8?q?=E5=8A=A0=E5=A4=87=E4=BB=BD=E4=B8=8E=E5=86=85=E5=AE=B9=E5=BF=AB?= =?UTF-8?q?=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 库文件 data/*.db 一直不进 git,seed --force 之后章节和题目就跟着没了。 这次把内容找回来,并且让远端始终留有一份可恢复的快照。 * seed/catalog.py:预置 4 本书的正式章节目录(29 章),启动时按 ISBN 补齐缺失章节,并把未归章的题目挂到对应章下(不覆盖人工改过的归章) * seed/ebooks.py:按「大小 + SHA-256」认领 data/ebooks 里的孤儿电子书, 重建教材条目并把文件挪回该书目录(已找回教材 7《柯西-施瓦茨大师课》) * services/ebooks.py:同内容文件复用,避免种子重复复制电子书 * dbtool.py + scripts/db.sh:backup/restore 本机时间戳备份, dump/load 把内容表导成 data/dump/catalog.sql(可提交、可灌回空库) * seed --force 前自动备份并打印路径;README 增加「数据备份与恢复」 --- .gitignore | 1 + README.md | 135 ++++++-- backend/dbtool.py | 290 ++++++++++++++++++ backend/seed/__main__.py | 8 + backend/seed/catalog.py | 172 ++++++----- backend/seed/ebooks.py | 68 ++++ backend/services/ebooks.py | 47 +++ data/dump/catalog.sql | 212 +++++++++++++ .../4559e9e2fa084fb7a8e8a06aea5196bc.epub | Bin .../8dc7e48036ac4db8abf0020334b47b07.pdf | Bin scripts/db.sh | 23 ++ 11 files changed, 851 insertions(+), 105 deletions(-) create mode 100644 backend/dbtool.py create mode 100644 data/dump/catalog.sql rename data/ebooks/{10 => 6}/4559e9e2fa084fb7a8e8a06aea5196bc.epub (100%) rename data/ebooks/{9 => 7}/8dc7e48036ac4db8abf0020334b47b07.pdf (100%) create mode 100755 scripts/db.sh diff --git a/.gitignore b/.gitignore index 5210193..a05bd10 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ node_modules/ frontend/dist/ .DS_Store data/*.db +data/backups/ .env diff --git a/README.md b/README.md index 42763f5..269b5b1 100644 --- a/README.md +++ b/README.md @@ -27,15 +27,22 @@ nex_math/ │ ├── schemas/ # Pydantic 请求响应模型 │ ├── routers/ # overview / practice / data / analytics / archive / catalog / question-bank / llm │ ├── services/ # llm_client / question_generator / ai_teacher / question_engine / mastery_engine / recommendation -│ └── seed/ # 目录与题库种子数据 +│ ├── seed/ # 目录与题库种子数据 +│ └── dbtool.py # 数据库备份 / 恢复 / 内容快照(python -m dbtool) ├── frontend/ # Vite + React 前端 │ └── src/ -│ ├── pages/ # 九个页面 -│ ├── components/ +│ ├── pages/ # 学习端 9 个页面 + 管理端页面 + 登录 / 电子书阅读器 +│ ├── components/ # SplitView / Pane(分屏工作台)、Paper(答题卡·题块·解析)、 +│ │ # PaperReview(整卷回看)、useApi、ui.tsx 等 │ ├── api/ # API 客户端 +│ ├── theme.ts # 设计令牌:标签色 / 状态色 / 掌握度色 +│ ├── i18n.ts # 中英双语文案(新增键必须 zh、en 同时补) │ └── types/ -├── data/math.db # SQLite 数据库(首次初始化后生成) -└── scripts/ # init.sh / start.sh +├── data/ # math.db(本机库,被 gitignore) +│ ├── ebooks/ # 教材电子书(随仓库提交) +│ ├── backups/ # db.sh backup 生成的时间戳备份(gitignore) +│ └── dump/ # catalog.sql:内容快照,提交进仓库=远端存了一份库 +└── scripts/ # init.sh(建环境)/ start.sh(起服务)/ db.sh(备份与恢复) ``` ## 界面与导航 @@ -49,10 +56,35 @@ nex_math/ - 全站样式与控件收敛到三层:**theme.ts(设计令牌)→ ui.tsx(Page / Panel / Toolbar / DataTable / RowActions / MetricGrid / CountTag / EmptyState 等) → 页面**,新页面只组合统一控件,不各写一套尺寸与颜色。 +- **题库管理 / 练习记录 / 错题本 / 章节测试采用「清单 + 试卷」分屏工作台** + (`components/SplitView.tsx` + `Pane`):一栏是可搜索的清单,另一栏按真实试卷版式 + 排题目;两栏各自独立滚动,清单不会把试卷顶走,整页不再是一条长滚动条。 + - 题库管理:左「教材→章节」树 + 搜索/难度/来源/使用状态筛选,右栏题目预览卷; + 侧栏首行显示题库总数,并提示还有多少题没挂到章节(计数来自 `/api/question-bank/summary`)。 + - 练习记录:左练习清单(得分进度条 + 错题数徽标),右该次练习的完整试卷回看, + 顶部答题卡按题号标色(正确 / 错误 / 未答),点题号跳题,可切「全部 / 只看错题」与「展开全部解析」。 + - 错题本:左错题清单(按知识点筛选、搜索题干),右错题精讲,逐题对照 + 我的作答 / 正确答案 / 解析,底部上一题、下一题翻页。 + - 章节测试:试卷在左,答题卡与本次小结在右,答题中随时点题号跳题、查看已答未答; + 交卷后同一套布局直接变成判卷结果。 + - 中缝可拖拽调节清单栏宽度(248–560px),偏好写入 localStorage + (`nex_math.split..aside`);窄屏(<992px)自动改为上下堆叠。 +- 章节测试按章节组卷,前置条件是**教材有章节目录、章节下挂了题目**。缺任何一步都只给 + 引导空态,不会变成红色报错,也不会写出 0 题的记录: + - 教材还没有章节 → 提示请管理员在「教材管理」中添加教材、整理章节目录; + - 选了还没挂题的章节 → 组出的是空卷,试卷位置显示「请管理员在题库管理里把题目关联到 + 本章」,答题卡提示本章暂无题目,提交按钮禁用; + - 章节题量不足 5 题时,组卷条件那行挂黄色徽标提示实际题量。 ## 快速开始(本机) -要求:Python 3.10+(推荐使用 [uv](https://docs.astral.sh/uv/))、Node.js 18+。 +要求: + +- Python 3.10+。macOS 自带的 `python3` 往往是 3.9,`init.sh` 会自动改找 + `python3.13 / 3.12 / 3.11 / 3.10`;最省事的做法是先装 + [uv](https://docs.astral.sh/uv/)(脚本会在 `PATH`、`~/.local/bin`、`~/.cargo/bin` 里找它, + 并用它拉一个 CPython 3.12),或 `brew install python@3.12`。 +- Node.js 20.19+ / 22.12+(Vite 8 的要求;本机 `node -v` 低于 20 时脚本会给出提示)。 ```bash ./scripts/init.sh # 创建虚拟环境、安装依赖、写入种子数据 @@ -61,6 +93,13 @@ nex_math/ 打开 http://localhost:5173。 +- `init.sh` 可重复执行:虚拟环境已存在就复用,已有 `data/math.db` 就不重写种子数据。 +- 依赖装不上时会自动改用国内镜像重试(pip → 清华源,npm → npmmirror); + 也可以显式指定:`PIP_INDEX_URL=… NPM_REGISTRY=… ./scripts/init.sh`; + 只想准备后端环境用 `./scripts/init.sh --no-web`。 +- `start.sh` 启动前会检查 8000 / 5173 是否被占用并直接告诉你是哪个进程, + 换端口:`BACKEND_PORT=8010 FRONTEND_PORT=5180 ./scripts/start.sh`。 + 默认账号: | 账号 | 密码 | 角色 | 说明 | @@ -94,15 +133,15 @@ docker compose up --build | --- | --- | --- | | 学习总览 | `/` 学习总览 | 指标卡 + 掌握度 + 推荐计划 | | 知识地图 | 知识地图 | 知识点掌握度;点击卡片查看定义、掌握度、相关知识点,以及讲了它的教材章节与视频课程(可一键进入本章测试) | -| 章节测试 | 章节测试 | 选书→选章→按掌握度动态抽取 5 题,可“换一套”;唯一的组卷与练习入口 | -| 错题本 | 错题本 | 记录每道错题的题干、你的错误回答、正确答案与解析 | -| 练习记录 | 练习记录 | 展开记录可回看每题作答与详细解析 | +| 章节测试 | 章节测试 | 选书→选章→按掌握度动态抽取 5 题,可“换一套”;唯一的组卷与练习入口。试卷 + 右侧答题卡分屏,需要教材已有章节目录 | +| 错题本 | 错题本 | 左清单 + 右错题精讲分屏:逐题对照我的作答、正确答案与解析,可按知识点筛选 | +| 练习记录 | 练习记录 | 左清单 + 右整卷回看:答题卡跳题、只看错题、展开全部解析 | | 今日任务 | 今日任务 | 推荐今日章节 → 做章节测试 → 对章节提交掌握反馈 | | 学习分析 | 学习分析 | 正确率、错题、诊断排序 | | 导出 / 导入档案 | 个人设置 → 学习档案 | `/api/archive/export`、`/api/archive/import` | | 教材 / 课程 | 教材 / 课程 | 盖尔范德丛书书目(ISBN)+ 在线课程链接 | | 教材详情 | 点击教材卡片 | 查看章节目录、每章知识点、题目数量并直接开始本章测试 | -| 题库管理 | 题库管理 | 列出/筛选/删除题目,按教材章节调用模型生成 | +| 题库管理 | 题库管理 | 左章节树 + 筛选、右题目预览卷的分屏;编辑删除题目,按教材章节调用模型生成 | | 模型配置 | 模型配置 | 多通道模型配置、通道命名、默认通道与连通测试 | | 用户管理 | 用户管理(管理员) | 创建用户、启停账号、切换角色、重置密码 | | 个人设置 | 左侧栏底部用户卡片 → 个人设置(主内容区整页) | 修改昵称/密码,导出与导入学习档案 | @@ -135,15 +174,35 @@ docker compose up --build | 分册 | ISBN | | --- | --- | -| 函数和图像(主教材,已挂章节与题库) | 978-7-312-05005-3 | +| 函数和图像(主教材,已上传 PDF 电子书) | 978-7-312-05005-3 | | 代数 | 978-7-312-04893-7 | -| 三角函数 | 978-7-312-04695-7 | +| 三角函数(已上传 PDF) | 978-7-312-04695-7 | | 坐标方法 | 978-7-312-05006-0 | | 几何 | 978-7-312-05779-3 | -《函数和图像》的章节(引言、第1章 例子 … 第7章 有理函数)已写入数据库, -可据此按章节生成新题;其余分册目录可在 `backend/seed/catalog.py` 中扩充后重启生效 -(幂等同步,不清空学习记录)。 +**有正式目录的书,章节由种子预置**:`seed/catalog.py` 里 `OFFICIAL_*_CHAPTERS` +是核对过原书的正式目录,按 ISBN 绑到 `CATALOG[*].chapters`,`python -m seed` +与后端启动都会幂等地补齐章节,并按 `KNOWLEDGE_CHAPTER_MAP` / +`DEFAULT_CHAPTER_KNOWLEDGE` 把演示题与知识点挂上去(只补空,不覆盖人工改动)。 +目前已落地:函数和图像 8 章、三角函数 10 章、坐标方法 7 章、几何 4 章。 + +**没有正式目录的书不由种子编造**(代数、普林斯顿微积分读本、柯西-施瓦茨大师课)。 +管理员在「教材 / 课程」打开某本教材的章节面板后,有三种方式: + +1. 「添加章节」手动逐条录入名称与内容提示; +2. 「AI 整理章节」:把书的目录原文(版权页/目录页文字,或在别处查到的正式目录)粘进 + 「目录参考数据」,任务在后台解析目录并为每章挂上知识图谱节点;模型只做整理, + 不新增、不补造素材里没有的章节; +3. 纯接口方式 `POST /api/textbooks/{id}/chapters/generate`:先按 ISBN 查 Open Library / + Google Books 的正式目录,再让模型按正式目录还原,查不到且模型没把握时直接拒绝写入。 + +确认无误的正式目录也可以回填到 `backend/seed/catalog.py` 的 +`OFFICIAL_*_CHAPTERS` / `CATALOG[*].chapters`,再执行 +`cd backend && .venv/bin/python -m seed`(幂等:只补缺,不覆盖界面上的修改,也不清空学习记录), +这样换机器或库丢了都能一并重建。 + +> 种子题库的 18 道演示题会按知识点自动挂到《函数和图像》的章节下(当前:引言 3 题、 +> 第6章 幂函数 15 题),所以「章节测试」开箱就能组卷;管理员手工改过的章节归属不会被覆盖。 ## 模型配置与题目生成 @@ -163,17 +222,17 @@ docker compose up --build `![图注](图片地址)`;题库、练习、错题本与练习记录中的题目内容均由前端 KaTeX 渲染公式并展示图片。 -### 教材章节的 AI 生成与题库跳转 +### 教材章节目录的整理与题库跳转 -在“教材课程管理”中打开某本教材的“章节”抽屉后: +在「教材 / 课程」中打开某本教材的章节面板后: -1. “添加章节”仍可手动输入名称与内容提示; -2. “AI 生成章节”按钮位于“添加章节”之后;无需选择数量,选择模型通道与可选说明后, - 系统会先查找内置官方目录(函数与图像、三角函数、坐标方法、几何等正式章节), - 再按 ISBN 查询 Open Library / Google Books;查到正式目录时直接按正式章节补齐, - 不会让模型自由发挥;如果无法核实正式目录,接口会明确拒绝生成并提示手动添加, - 避免产生与原书无关的章节。新章节入库后可继续编辑或删除; -3. 每个章节行都有“题库”入口,点击后跳转到题库管理并自动筛选该书与章节。 +1. 「添加章节」逐个录入名称与内容提示; +2. 面板右上角的「AI 整理章节」:选模型通道,把该书目录原文粘进「目录参考数据」, + 点「开始整理」。任务在后台执行并显示进度,模型只整理素材里真实出现的目录条目 + (不新增、不凭印象补造),同时为每章从知识图谱挑选知识点;与已有章节同名的条目自动跳过, + 入库后仍可编辑或删除; +3. 每个章节行都有「题库」入口,点击后跳转到题库管理并自动筛选该书与章节, + 可以在那里直接按章节生成新题,或把已有的未挂章节题目编辑到该章下。 题库管理每道题会标注“使用状态”(已使用次数 / 未使用)。使用过的题目和被 章节挂载的题目不允许删除,只能编辑;题目编辑会保留历史答题快照。 @@ -214,6 +273,7 @@ docker compose up --build | POST/PUT/DELETE | `/textbooks` 与 `/textbooks/{id}` | 教材/课程增删改(管理员) | | POST/PUT/DELETE | `/textbooks/.../chapters` | 章节管理(管理员) | | POST | `/textbooks/{id}/chapters/generate` | 按教材 AI 生成章节(管理员) | +| GET | `/question-bank/summary` | 题库侧栏计数:总数 / 未挂章节 / AI 生成 / 已使用(管理员) | | GET/POST/PUT/DELETE | `/question-bank/questions`、`…/generate` | 题库管理:列表/生成/编辑/删除(管理员),已使用题禁止删除 | | GET | `/llm/settings` | 模型通道列表与服务商预设(管理员) | | POST/PUT/DELETE | `/llm/channels` 与 `/llm/channels/{id}` | 模型通道增删改(管理员) | @@ -245,3 +305,30 @@ cd backend 会清空全部用户的学习数据与题库(含 AI 生成题)并重新写入种子数据; 用户、角色、教材目录与模型配置保留。演示账号的学习历史(练习记录、错题本) 现在包含逐题明细,可直接展开查看每题作答。 + +`--force` 执行前会自动把当前库备份到 `data/backups/`(打印路径),误操作可用 +`./scripts/db.sh restore` 回退。 + +## 数据备份与恢复 + +`data/*.db` 在 `.gitignore` 里,仓库只有代码和电子书,**数据库文件从来没有进过远端**: +库一旦被重置,章节、题目这些内容就跟着没了。为此有两层保险: + +```bash +./scripts/db.sh path # 数据库文件位置 +./scripts/db.sh backup 升级前 # 时间戳备份(用 SQLite backup API,服务在写也安全) +./scripts/db.sh list # 本机备份列表(默认保留最近 20 份) +./scripts/db.sh restore [文件|latest] # 回滚;覆盖前先把当前库再备份一份 +./scripts/db.sh dump # 导出 data/dump/catalog.sql(内容快照,建议随改动提交) +./scripts/db.sh load # 把 data/dump/catalog.sql 灌回当前库 +``` + +* `dump` 只导**内容**:教材、章节、知识点与关联、课程、题库。 + 这份文本提交进 git,远端仓库里就始终有一份可恢复的数据库快照。 +* 账号、密码散列、答题记录、错题本属于个人数据,**不进 dump、不进 git**; + 需要整机迁移时用 `./scripts/db.sh dump --full`(写到 `data/backups/math.full.sql`) + 或直接用 `backup` 的二进制备份。 +* 新机器上的恢复顺序:`./scripts/init.sh`(建库 + 种子)→ `./scripts/db.sh load` → 重启后端。 +* 电子书与库分离:`data/ebooks/` 随仓库提交,库只存文件名。 + 若库丢了,`seed/ebooks.py` 会按「大小 + SHA-256」认领磁盘上的已知电子书 + (`KNOWN_EBOOKS`),重建教材条目并把文件挪回该书目录,不必重新上传。 diff --git a/backend/dbtool.py b/backend/dbtool.py new file mode 100644 index 0000000..1ad1aa7 --- /dev/null +++ b/backend/dbtool.py @@ -0,0 +1,290 @@ +"""数据库备份 / 恢复 / 快照工具。 + +``data/*.db`` 在 .gitignore 里,仓库只带代码与电子书,不带数据库文件: +库一旦重置,章节、题目这些"内容"就跟着一起没了。这个工具补上两件事: + +* **backup / restore**:本机时间戳备份,seed --force 前自动执行; +* **dump / load**:把目录类内容(教材、章节、知识点、课程、题库)导成 + 可直接提交的 SQL 文本,放进 git 就等于远端存了一份数据库快照, + 而且不含账号、答题记录等个人数据。 + +用法见 scripts/db.sh(backend/ 目录下也可以 python -m dbtool)。 +""" +from __future__ import annotations + +import argparse +import sqlite3 +from datetime import datetime +from pathlib import Path + +from database import DATABASE_URL, DEFAULT_DB_PATH, REPO_ROOT + +BACKUP_DIR = REPO_ROOT / "data" / "backups" +DUMP_DIR = REPO_ROOT / "data" / "dump" +DEFAULT_DUMP = DUMP_DIR / "catalog.sql" + +# 目录类内容:可以公开提交的部分(不含用户、答题记录、错题本) +CATALOG_TABLES = [ + "textbooks", + "chapters", + "knowledge", + "chapter_knowledge", + "courses", + "knowledge_relations", + "knowledge_resources", + "questions", + "question_knowledge", +] + +# 个人学习数据:只进本机备份,不进 git +PRIVATE_TABLES = [ + "users", + "roles", + "permissions", + "role_permissions", + "user_roles", + "user_knowledge", + "questions", + "question_knowledge", + "attempt_sessions", + "attempt_items", + "error_entries", + "daily_completions", + "ebook_progress", + "llm_settings", +] + + +def db_path() -> Path: + """当前 SQLite 文件路径(非 sqlite 后端时退回默认路径)。""" + prefix = "sqlite:///" + if DATABASE_URL.startswith(prefix): + return Path(DATABASE_URL[len(prefix):]) + return DEFAULT_DB_PATH + + +def backup(label: str = "", keep: int = 20) -> Path: + """在线备份当前库(用 SQLite backup API,服务正在写也安全)。""" + source = db_path() + if not source.is_file(): + raise FileNotFoundError(f"数据库不存在:{source}") + BACKUP_DIR.mkdir(parents=True, exist_ok=True) + stamp = datetime.now().strftime("%Y%m%d-%H%M%S") + suffix = f"-{_slug(label)}" if label else "" + target = BACKUP_DIR / f"{source.stem}-{stamp}{suffix}.db" + src = sqlite3.connect(source) + try: + dst = sqlite3.connect(target) + try: + src.backup(dst) + finally: + dst.close() + finally: + src.close() + prune(keep) + return target + + +def restore(source_file: Path, protect: bool = True) -> Path | None: + """用备份覆盖当前库;覆盖前先把当前库存一份。""" + if not source_file.is_file(): + raise FileNotFoundError(f"备份不存在:{source_file}") + saved = backup(label="pre-restore") if protect else None + src = sqlite3.connect(source_file) + try: + dst = sqlite3.connect(db_path()) + try: + src.backup(dst) + finally: + dst.close() + finally: + src.close() + return saved + + +def prune(keep: int = 20) -> None: + """只保留最近 keep 份备份。""" + if keep <= 0 or not BACKUP_DIR.is_dir(): + return + files = sorted(BACKUP_DIR.glob("*.db"), key=lambda p: p.stat().st_mtime) + for old in files[:-keep]: + old.unlink(missing_ok=True) + + +def list_backups() -> list[Path]: + if not BACKUP_DIR.is_dir(): + return [] + return sorted(BACKUP_DIR.glob("*.db"), key=lambda p: p.name, reverse=True) + + +def _literal(value: object) -> str: + if value is None: + return "NULL" + if isinstance(value, (int, float)): + return repr(value) + if isinstance(value, (bytes, bytearray)): + return f"X'{bytes(value).hex()}'" + return "'" + str(value).replace("'", "''") + "'" + + +def dump_sql( + out: Path = DEFAULT_DUMP, tables: list[str] | None = None +) -> tuple[Path, int]: + """把内容表导成 SQL 文本(先清表再按原 id 插回,可重复执行)。""" + wanted = tables or CATALOG_TABLES + conn = sqlite3.connect(db_path()) + conn.row_factory = sqlite3.Row + try: + existing = { + row[0] + for row in conn.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ) + } + lines = [ + "-- nex_math 内容快照(教材 / 章节 / 知识点 / 课程 / 题库)", + f"-- 生成时间:{datetime.now().isoformat(timespec='seconds')}", + "-- 恢复:./scripts/db.sh load data/dump/catalog.sql", + "PRAGMA foreign_keys=OFF;", + "BEGIN TRANSACTION;", + ] + rows_total = 0 + for table in reversed(wanted): + if table in existing: + lines.append(f"DELETE FROM {table};") + for table in wanted: + if table not in existing: + continue + count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] + if not count: + continue + lines.append(f"-- {table}: {count} 行") + for row in conn.execute(f"SELECT * FROM {table}"): + columns = row.keys() + values = ", ".join(_literal(row[key]) for key in columns) + lines.append( + f"INSERT INTO {table} ({', '.join(columns)}) " + f"VALUES ({values});" + ) + rows_total += 1 + lines += ["COMMIT;", "VACUUM;"] + finally: + conn.close() + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text("\n".join(lines) + "\n", encoding="utf-8") + return out, rows_total + + +def _has_tables(conn: sqlite3.Connection) -> bool: + return ( + conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' LIMIT 1" + ).fetchone() + is not None + ) + + +def load_sql(script: Path) -> bool: + """把 dump 出来的 SQL 文本灌回当前库(内容表按快照里的状态覆盖)。 + + 库文件不存在或还没有表时,先按 ORM 模型建好表结构再灌数据, + 这样新克隆的仓库也能一条命令导入远端快照。返回值表示本次是否新建了库。 + """ + if not script.is_file(): + raise FileNotFoundError(f"快照不存在:{script}") + target = db_path() + target.parent.mkdir(parents=True, exist_ok=True) + created = False + conn = sqlite3.connect(target) + try: + if not _has_tables(conn): + conn.close() + # models 必须先导入,Base.metadata 才知道要建哪些表 + import models # noqa: F401 + from database import ensure_schema # 建表(含旧库补列) + + ensure_schema() + created = True + conn = sqlite3.connect(target) + conn.executescript(script.read_text(encoding="utf-8")) + conn.commit() + finally: + conn.close() + return created + + +def _slug(text: str) -> str: + cleaned = "".join(ch if ch.isalnum() or ch in "-_" else "-" for ch in text) + return cleaned.strip("-")[:32] or "backup" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="nex_math 数据库备份工具") + sub = parser.add_subparsers(dest="cmd", required=True) + + p_backup = sub.add_parser("backup", help="备份当前库") + p_backup.add_argument("label", nargs="?", default="", help="备份标签") + + p_restore = sub.add_parser("restore", help="用备份覆盖当前库") + p_restore.add_argument("file", nargs="?", default="latest", help="备份文件") + + sub.add_parser("list", help="列出本机备份") + + p_dump = sub.add_parser("dump", help="导出可提交的内容快照") + p_dump.add_argument("out", nargs="?", default=str(DEFAULT_DUMP)) + p_dump.add_argument("--full", action="store_true", help="含用户与学习数据") + + p_load = sub.add_parser("load", help="把内容快照灌回当前库") + p_load.add_argument("file", nargs="?", default=str(DEFAULT_DUMP)) + + sub.add_parser("path", help="打印数据库文件路径") + + args = parser.parse_args(argv) + + if args.cmd == "path": + print(db_path()) + elif args.cmd == "backup": + print(backup(label=args.label)) + elif args.cmd == "list": + items = list_backups() + if not items: + print(f"({BACKUP_DIR} 下暂无备份)") + for item in items: + print(f"{item} {item.stat().st_size // 1024} KB") + elif args.cmd == "restore": + target = ( + list_backups()[0] + if args.file == "latest" + else Path(args.file) + ) + if not target.is_absolute(): + target = (BACKUP_DIR / args.file).resolve() + saved = restore(target) + print(f"已恢复:{target}") + if saved: + print(f"覆盖前的库已备份到:{saved}") + print("提示:重启后端后生效。") + elif args.cmd == "dump": + if args.full: + # 含账号与学习数据:只写进本机备份目录,不进 git + tables = CATALOG_TABLES + [ + t for t in PRIVATE_TABLES if t not in CATALOG_TABLES + ] + out = BACKUP_DIR / "math.full.sql" + else: + tables = CATALOG_TABLES + out = Path(args.out) + written, rows = dump_sql(out, tables) + print(f"{written} {rows} 行") + elif args.cmd == "load": + created = load_sql(Path(args.file)) + print(f"已导入:{args.file}") + if created: + print("提示:这是一个新建的库。快照只含内容(教材/章节/知识点/题目)," + "账号与学习数据请用 ./scripts/init.sh 或 python -m seed 生成。") + print("提示:重启后端后生效。") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/seed/__main__.py b/backend/seed/__main__.py index 5d6ec9b..43f7432 100644 --- a/backend/seed/__main__.py +++ b/backend/seed/__main__.py @@ -21,6 +21,7 @@ from models import ( ) from seed.catalog import ensure_catalog from seed.data import KNOWLEDGE, QUESTIONS +from seed.ebooks import ensure_ebooks from seed.rbac import DEFAULT_USERS, ensure_rbac from services.learning_profile import ( ensure_default_profiles, @@ -129,6 +130,11 @@ def load_seed(force: bool = False) -> bool: ensure_llm_channels(db) if force: + # --force 会清掉题库与学习数据:先留一份可回退的备份 + from dbtool import backup as backup_database + + saved = backup_database(label="pre-force") + print(f"[seed] --force 将清空题库与学习数据,已先备份:{saved}") db.query(AttemptItem).delete() db.query(DailyCompletion).delete() db.query(ErrorEntry).delete() @@ -156,6 +162,8 @@ def load_seed(force: bool = False) -> bool: db.commit() ensure_catalog(db, force_sync=force) + # 电子书与教材目录同源:种子跑完就把 res/ 的书和磁盘上的存量电子书挂好 + ensure_ebooks(db) for user in users.values(): if user and user_has_admin_role(db, user.id): remove_unused_admin_profile(db, user.id) diff --git a/backend/seed/catalog.py b/backend/seed/catalog.py index a7b4be4..385c3d7 100644 --- a/backend/seed/catalog.py +++ b/backend/seed/catalog.py @@ -171,7 +171,13 @@ COURSES = [ }, ] -OFFICIAL_CHAPTERS_BY_ISBN = {} +# 官方章节模板 → 教材:按 ISBN 绑定,seed 在空库或补齐时据此写入章节目录。 +OFFICIAL_CHAPTERS_BY_ISBN = { + "9787312050053": OFFICIAL_PRIMARY_CHAPTERS, # 函数和图像 + "9787312057793": OFFICIAL_GEOMETRY_CHAPTERS, # 几何 + "9787312046957": OFFICIAL_TRIG_CHAPTERS, # 三角函数 + "9787312050060": OFFICIAL_COORDINATE_CHAPTERS, # 坐标方法 +} CATALOG = [ @@ -189,7 +195,7 @@ CATALOG = [ "英译本 Functions and Graphs 由 Dover 出版。现有函数与对称性题库" "以本书为依据。" ), - "chapters": [], + "chapters": OFFICIAL_PRIMARY_CHAPTERS, }, { "name": "代数(盖尔范德中学生数学思维丛书)", @@ -217,7 +223,7 @@ CATALOG = [ "以直观可视的方式从图形构造逐步建立几何,突出“先看见、再证明”" "的思维路线。" ), - "chapters": [], + "chapters": OFFICIAL_GEOMETRY_CHAPTERS, }, { "name": "三角函数(盖尔范德中学生数学思维丛书)", @@ -230,7 +236,7 @@ CATALOG = [ "description": ( "从初等几何直觉进入正弦、余弦与正切,解释周期与波动现象的数学本质。" ), - "chapters": [], + "chapters": OFFICIAL_TRIG_CHAPTERS, }, { "name": "坐标方法(盖尔范德中学生数学思维丛书)", @@ -244,98 +250,102 @@ CATALOG = [ "讲解如何用坐标把几何问题转化为代数问题,是解析几何与函数图像" "方法的基础。" ), - "chapters": [], + "chapters": OFFICIAL_COORDINATE_CHAPTERS, }, ] def ensure_catalog(db: Session, force_sync: bool = False) -> None: - """仅在空库时写入默认目录;管理员后续可自由增删改并持久保留。""" + """写入默认教材目录。 + + 空库(或 ``--force``)时按下述顺序全量登记教材与官方章节;存量库只 + *补齐缺失的章节*,既不覆盖管理员改过的教材信息,也不改动已有章节顺序, + 因此可以随每次启动安全重放。 + """ _ensure_courses(db) - if db.query(Textbook).count() > 0 and not force_sync: - # 存量库不覆盖用户修改,但会补齐新增的“官方正式目录”章节 + if db.query(Textbook).count() == 0 or force_sync: + for position, item in enumerate(CATALOG): + textbook = _upsert_textbook(db, item, position) + _backfill_chapters(db, textbook, item["chapters"]) + else: for item in CATALOG: textbook = ( - db.query(Textbook) - .filter(Textbook.name == item["name"]) - .first() + db.query(Textbook).filter(Textbook.name == item["name"]).first() ) - if textbook is None or not item["chapters"]: + if textbook is None: continue - existing = { - chapter.name: chapter - for chapter in db.query(Chapter) - .filter(Chapter.textbook_id == textbook.id) - .all() - } - position = len(existing) - for chapter in item["chapters"]: - if chapter["name"] not in existing: - db.add( - Chapter( - textbook_id=textbook.id, - name=chapter["name"], - position=position, - summary=chapter["summary"], - ) - ) - position += 1 - _sync_default_chapter_knowledge(db) - _sync_default_course_resources(db) - db.commit() - return - - for position, item in enumerate(CATALOG): - textbook = db.query(Textbook).filter(Textbook.name == item["name"]).first() - if textbook is None: - textbook = Textbook(name=item["name"], position=position) - db.add(textbook) - db.flush() - textbook.author = item["author"] - textbook.publisher = item["publisher"] - textbook.isbn = item["isbn"] - textbook.edition = item["edition"] - textbook.grade = item["grade"] - textbook.link = item["link"] - textbook.description = item["description"] - textbook.position = position - - existing = { - chapter.name: chapter - for chapter in db.query(Chapter) - .filter(Chapter.textbook_id == textbook.id) - .all() - } - for chapter_index, chapter in enumerate(item["chapters"]): - if chapter["name"] not in existing: - db.add( - Chapter( - textbook_id=textbook.id, - name=chapter["name"], - position=chapter_index, - summary=chapter["summary"], - ) - ) - - # 按最新知识点-章节映射重新关联所有题目 - primary = db.query(Textbook).filter(Textbook.name == CATALOG[0]["name"]).first() - if primary: - chapter_map = { - chapter.name: chapter - for chapter in db.query(Chapter) - .filter(Chapter.textbook_id == primary.id) - .all() - } - for question in db.query(Question).all(): - chapter = chapter_map.get( - KNOWLEDGE_CHAPTER_MAP.get(question.knowledge_name, "") - ) - question.chapter_id = chapter.id if chapter else None + _backfill_chapters(db, textbook, item["chapters"]) + db.flush() + _link_unassigned_questions(db) _sync_default_chapter_knowledge(db) _sync_default_course_resources(db) db.commit() +def _upsert_textbook(db: Session, item: dict, position: int) -> Textbook: + textbook = db.query(Textbook).filter(Textbook.name == item["name"]).first() + if textbook is None: + textbook = Textbook(name=item["name"], position=position) + db.add(textbook) + db.flush() + textbook.author = item["author"] + textbook.publisher = item["publisher"] + textbook.isbn = item["isbn"] + textbook.edition = item["edition"] + textbook.grade = item["grade"] + textbook.link = item["link"] + textbook.description = item["description"] + textbook.position = position + return textbook + + +def _backfill_chapters(db: Session, textbook: Textbook, chapters: list[dict]) -> None: + """按官方目录补齐缺失章节(已有的同名章节原样保留)。""" + if not chapters: + return + existing = { + chapter.name + for chapter in db.query(Chapter) + .filter(Chapter.textbook_id == textbook.id) + .all() + } + position = len(existing) + for chapter in chapters: + if chapter["name"] in existing: + continue + db.add( + Chapter( + textbook_id=textbook.id, + name=chapter["name"], + position=position, + summary=chapter["summary"], + ) + ) + position += 1 + + +def _link_unassigned_questions(db: Session) -> None: + """把尚未归入章节的题目按知识点-章节映射挂到主教材对应章节。 + + 只处理 ``chapter_id`` 为空的题目:管理员手工挂过或改过的题目不动, + 重放种子脚本不会冲掉人工维护结果。 + """ + primary = db.query(Textbook).filter(Textbook.name == CATALOG[0]["name"]).first() + if primary is None: + return + chapter_map = { + chapter.name: chapter + for chapter in db.query(Chapter) + .filter(Chapter.textbook_id == primary.id) + .all() + } + pending = db.query(Question).filter(Question.chapter_id.is_(None)).all() + for question in pending: + chapter = chapter_map.get(KNOWLEDGE_CHAPTER_MAP.get(question.knowledge_name, "")) + if chapter is not None: + question.chapter_id = chapter.id + + def _sync_default_chapter_knowledge(db: Session) -> None: """仅为还没有知识点关联的章节写入默认知识点(不覆盖管理员维护)。""" for chapter in db.query(Chapter).all(): diff --git a/backend/seed/ebooks.py b/backend/seed/ebooks.py index 5563436..ec81b33 100644 --- a/backend/seed/ebooks.py +++ b/backend/seed/ebooks.py @@ -4,6 +4,7 @@ """ from __future__ import annotations +import shutil from pathlib import Path from sqlalchemy.orm import Session @@ -47,6 +48,71 @@ def _find_resource(prefix: str, suffix: str) -> Path | None: return None +# 旧库里的教材:数据库只是引用磁盘上的文件,库一旦丢失,这些文件就成了孤儿。 +# 按“大小 + SHA-256”认领已知电子书,重建教材条目并把文件挪进该书自己的目录, +# 管理员就不必重新上传,种子也不会再往 res/ 里复制第二份。 +KNOWN_EBOOKS = [ + { + "size": 44110741, + "sha256": "f432fa058420f1032a169a91fe2dcefbc95c61a6793f1b0ec5fd1fcc84ee67da", + "display": "柯西-施瓦茨大师课:不等式的艺术.pdf", + "textbook": { + "name": "柯西-施瓦茨大师课:不等式的艺术", + "author": "J. Michael Steele 著;欧阳顺湘 译", + "publisher": "高等教育出版社 · 世界数学精品译丛 7", + "isbn": "9787040278678", + "grade": "高中至大学", + "description": ( + "Cambridge 出版的不等式专题大师课:以 Cauchy-Schwarz 不等式为主线," + "串起凸性、排序、切比雪夫、Young、Hölder、Minkowski 等常用不等式," + "每章配大量例题与习题,适合竞赛与数学分析进阶训练。" + ), + }, + }, +] + + +def _referenced_files(db: Session) -> set[Path]: + used: set[Path] = set() + rows = db.query(Textbook).filter(Textbook.ebook_file != "").all() + for row in rows: + used.add((store.book_dir(row.id) / row.ebook_file).resolve()) + return used + + +def recover_lost_ebooks(db: Session) -> list[str]: + """认领 data/ebooks 下未被引用的已知电子书,返回恢复的教材名。""" + if not store.EBOOK_ROOT.is_dir(): + return [] + referenced = _referenced_files(db) + recovered: list[str] = [] + for book in KNOWN_EBOOKS: + meta = book["textbook"] + if db.query(Textbook).filter(Textbook.name == meta["name"]).first(): + continue + for path in sorted(store.EBOOK_ROOT.glob("*/*")): + if not path.is_file() or path.suffix.lower() not in {".pdf", ".epub"}: + continue + if path.stat().st_size != book["size"] or path.resolve() in referenced: + continue + if store.sha256_file(path) != book["sha256"]: + continue + textbook = Textbook( + **meta, position=db.query(Textbook).count() + ) + db.add(textbook) + db.commit() + target_dir = store.book_dir(textbook.id) + target_dir.mkdir(parents=True, exist_ok=True) + shutil.move(str(path), str(target_dir / path.name)) + store.adopt_file( + db, textbook, path.name, display_name=book["display"] + ) + recovered.append(textbook.name) + break + return recovered + + def ensure_ebooks(db: Session) -> None: created = False textbook = ( @@ -86,3 +152,5 @@ def ensure_ebooks(db: Session) -> None: if pdf is None: continue store.attach_local_file(db, textbook, pdf, display_name=display) + + recover_lost_ebooks(db) diff --git a/backend/services/ebooks.py b/backend/services/ebooks.py index 76275fb..a8364c6 100644 --- a/backend/services/ebooks.py +++ b/backend/services/ebooks.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +import hashlib import os import re import shutil @@ -133,6 +134,48 @@ def clear_ebook(db: Session, textbook: Textbook) -> Textbook: return textbook +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as reader: + for chunk in iter(lambda: reader.read(_CHUNK), b""): + digest.update(chunk) + return digest.hexdigest() + + +def find_identical(textbook_id: int, source: Path) -> Path | None: + """该书目录下是否已经有一模一样的文件(按大小 + SHA-256 判定)。""" + target_dir = book_dir(textbook_id) + if not target_dir.is_dir() or not source.is_file(): + return None + size = source.stat().st_size + digest = sha256_file(source) + for candidate in sorted(target_dir.iterdir()): + if not candidate.is_file() or candidate.stat().st_size != size: + continue + if sha256_file(candidate) == digest: + return candidate + return None + + +def adopt_file( + db: Session, + textbook: Textbook, + filename: str, + display_name: str = "", +) -> Textbook: + """登记该书目录下已经在磁盘上的文件(认领孤儿电子书,不重新复制)。""" + path = resolve_path(textbook.id, filename) + fmt, _ = file_kind(path.name) + textbook.ebook_file = path.name + textbook.ebook_format = fmt + textbook.ebook_name = _safe_name(display_name or path.name) + textbook.ebook_size = path.stat().st_size + textbook.ebook_pages = count_pages(path, fmt) + textbook.ebook_uploaded_at = datetime.utcnow() + db.commit() + return textbook + + def attach_local_file( db: Session, textbook: Textbook, @@ -145,6 +188,10 @@ def attach_local_file( raise FileNotFoundError(str(source)) target_dir = book_dir(textbook.id) target_dir.mkdir(parents=True, exist_ok=True) + # 同一本书重复导入(例如换库重跑种子)时复用已有文件,避免多占几十 MB + identical = find_identical(textbook.id, source) + if identical is not None: + return adopt_file(db, textbook, identical.name, display_name=display_name) final = target_dir / f"{uuid.uuid4().hex}.{fmt}" with source.open("rb") as reader, final.open("wb") as writer: head = reader.read(len(magic)) diff --git a/data/dump/catalog.sql b/data/dump/catalog.sql new file mode 100644 index 0000000..3ef38ba --- /dev/null +++ b/data/dump/catalog.sql @@ -0,0 +1,212 @@ +-- nex_math 内容快照(教材 / 章节 / 知识点 / 课程 / 题库) +-- 生成时间:2026-09-18T14:52:08 +-- 恢复:./scripts/db.sh load data/dump/catalog.sql +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +DELETE FROM question_knowledge; +DELETE FROM questions; +DELETE FROM knowledge_resources; +DELETE FROM knowledge_relations; +DELETE FROM courses; +DELETE FROM chapter_knowledge; +DELETE FROM knowledge; +DELETE FROM chapters; +DELETE FROM textbooks; +-- textbooks: 7 行 +INSERT INTO textbooks (id, name, author, publisher, isbn, edition, grade, link, description, position, ebook_file, ebook_name, ebook_format, ebook_size, ebook_pages, ebook_uploaded_at) VALUES (1, '函数和图像(盖尔范德中学生数学思维丛书)', 'I. M. Gelfand, E. G. Glagoleva, E. E. Shnol 著;麻希南、胡长青 译', '中国科学技术大学出版社', '9787312050053', '2020年12月第1版', '初中至高中低年级', '', '本系统的主教材。以大量真实图像(地震图、心电图等)引入函数,强调“画图就是看见公式”,系统训练函数概念、图像与性质直觉;英译本 Functions and Graphs 由 Dover 出版。现有函数与对称性题库以本书为依据。', 0, 'da4d4ff2d0b0448fac3719cb7d536d5b.pdf', '函数和图像(盖尔范德中学生数学思维丛书).pdf', 'pdf', 2352775, 108, '2026-09-18 05:52:20.133460'); +INSERT INTO textbooks (id, name, author, publisher, isbn, edition, grade, link, description, position, ebook_file, ebook_name, ebook_format, ebook_size, ebook_pages, ebook_uploaded_at) VALUES (2, '代数(盖尔范德中学生数学思维丛书)', 'I. M. Gelfand, A. Shen 著;胡长青、麻希南 译', '中国科学技术大学出版社', '9787312048937', '2020年第1版', '初中至高中低年级', '', '用问题驱动的叙述串联自然数、负数、代数式与方程,重视“为什么可以这样变形”的代数思维,适合作为代数学基础。', 1, '', '', '', 0, 0, NULL); +INSERT INTO textbooks (id, name, author, publisher, isbn, edition, grade, link, description, position, ebook_file, ebook_name, ebook_format, ebook_size, ebook_pages, ebook_uploaded_at) VALUES (3, '几何(盖尔范德中学生数学思维丛书)', 'I. M. Gelfand 等 著', '中国科学技术大学出版社', '9787312057793', '2023年第1版', '初中至高中低年级', '', '以直观可视的方式从图形构造逐步建立几何,突出“先看见、再证明”的思维路线。', 2, '', '', '', 0, 0, NULL); +INSERT INTO textbooks (id, name, author, publisher, isbn, edition, grade, link, description, position, ebook_file, ebook_name, ebook_format, ebook_size, ebook_pages, ebook_uploaded_at) VALUES (4, '三角函数(盖尔范德中学生数学思维丛书)', 'I. M. Gelfand, M. Saul 著;王作勤 译', '中国科学技术大学出版社', '9787312046957', '2020年第1版', '初中至高中低年级', '', '从初等几何直觉进入正弦、余弦与正切,解释周期与波动现象的数学本质。', 3, 'c1945315d608455eb429eacc3378d7d5.pdf', '三角函数(盖尔范德中学生数学思维丛书).pdf', 'pdf', 4804517, 228, '2026-09-18 05:52:20.155476'); +INSERT INTO textbooks (id, name, author, publisher, isbn, edition, grade, link, description, position, ebook_file, ebook_name, ebook_format, ebook_size, ebook_pages, ebook_uploaded_at) VALUES (5, '坐标方法(盖尔范德中学生数学思维丛书)', 'I. M. Gelfand, E. G. Glagoleva, A. A. Kirillov 著;王作勤 译', '中国科学技术大学出版社', '9787312050060', '2020年12月第1版', '初中至高中低年级', '', '讲解如何用坐标把几何问题转化为代数问题,是解析几何与函数图像方法的基础。', 4, '', '', '', 0, 0, NULL); +INSERT INTO textbooks (id, name, author, publisher, isbn, edition, grade, link, description, position, ebook_file, ebook_name, ebook_format, ebook_size, ebook_pages, ebook_uploaded_at) VALUES (6, '普林斯顿微积分读本(修订版)', '阿德里安·班纳 (Adrian Banner)', '人民邮电出版社', '', '', '高中至大学先修', '', '面向微积分入门的系统辅导书,覆盖极限、导数、积分与级数,按考点组织,适合自学与考前查漏补缺。', 5, '4559e9e2fa084fb7a8e8a06aea5196bc.epub', '普林斯顿微积分读本(修订版).epub', 'epub', 9842024, 190, '2026-09-18 05:52:20.122225'); +INSERT INTO textbooks (id, name, author, publisher, isbn, edition, grade, link, description, position, ebook_file, ebook_name, ebook_format, ebook_size, ebook_pages, ebook_uploaded_at) VALUES (7, '柯西-施瓦茨大师课:不等式的艺术', 'J. Michael Steele 著;欧阳顺湘 译', '高等教育出版社 · 世界数学精品译丛 7', '9787040278678', '', '高中至大学', '', 'Cambridge 出版的不等式专题大师课:以 Cauchy-Schwarz 不等式为主线,串起凸性、排序、切比雪夫、Young、Hölder、Minkowski 等常用不等式,每章配大量例题与习题,适合竞赛与数学分析进阶训练。', 6, '8dc7e48036ac4db8abf0020334b47b07.pdf', '柯西-施瓦茨大师课:不等式的艺术.pdf', 'pdf', 44110741, 268, '2026-09-18 06:44:14.147181'); +-- chapters: 29 行 +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (1, 1, '引言', 0, '函数、自变量、定义域与函数值:从真实图像理解函数。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (2, 1, '第1章 例子', 1, '不同函数图像的直观例子与作图方法。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (3, 1, '第2章 线性函数', 2, '一次函数的图像、斜率与直线。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (4, 1, '第3章 函数 y=|x|', 3, '绝对值函数的图像与性质,关于 y 轴对称的典型例子。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (5, 1, '第4章 二次三项式', 4, '二次函数图像、配方、判别式与零点。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (6, 1, '第5章 分式线性函数', 5, 'y=1/x 类函数图像与渐近行为。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (7, 1, '第6章 幂函数', 6, 'y=x、y=x²、y=x³ 等幂函数的奇偶性与对称性。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (8, 1, '第7章 有理函数', 7, '更一般的有理函数图像与性质综合。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (9, 3, '点与直线:走进射影几何', 0, '从点与直线的位置关系出发理解射影几何的基本思想。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (10, 3, '平行线:走进仿射几何', 1, '以平行关系为线索建立仿射几何的直观框架。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (11, 3, '面积:走进辛几何', 2, '通过面积概念与保持面积变换认识辛几何。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (12, 3, '圆:走进欧氏几何', 3, '以圆为核心回到经典欧氏几何的结构与性质。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (13, 4, '第1章 三角函数', 0, '从角与旋转进入三角函数,建立正弦、余弦、正切的基本概念。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (14, 4, '第2章 三角形中的三角比', 1, '在直角三角形中定义正弦、余弦与正切,理解锐角三角比。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (15, 4, '第3章 三角比之间的关系', 2, '同角三角比的关系、互余关系与基本恒等式推导。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (16, 4, '第4章 三角形内的关系', 3, '正弦定理、余弦定理以及三角形中边角关系的应用。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (17, 4, '第5章 角与旋转', 4, '把角推广到任意旋转角,定义任意角的三角函数。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (18, 4, '第6章 弧度法', 5, '引入弧度制,理解弧长、扇形与角度制换算。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (19, 4, '第7章 加法公式', 6, '两角和与差的三角函数公式及其几何与代数证明。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (20, 4, '第8章 三角恒等式', 7, '倍角、半角与和差化积等常用三角恒等式。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (21, 4, '第9章 三角函数的图形', 8, '正弦、余弦、正切函数的图像、周期与变换。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (22, 4, '第10章 反函数与三角方程', 9, '反三角函数的基本概念与三角方程的求解方法。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (23, 5, '绪论', 0, '用坐标把几何问题转化为代数问题的方法引言。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (24, 5, '第1章 直线上点的坐标', 1, '数轴、数的绝对值与两点之间的距离。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (25, 5, '第2章 平面里点的坐标', 2, '坐标平面、坐标间关系、距离公式、确定平面图形与其他坐标系。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (26, 5, '第3章 三维空间里点的坐标', 3, '空间坐标轴与坐标平面,确定空间中的图形。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (27, 5, '第4章 引言', 4, '从三维推广到四维空间的动机:几何、计算与物理。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (28, 5, '第5章 四维空间', 5, '四维空间中的坐标轴、坐标平面与距离问题。', 0); +INSERT INTO chapters (id, textbook_id, name, position, summary, ebook_page) VALUES (29, 5, '第6章 四维方体', 6, '球与方体的定义、四维方体的结构与相关问题。', 0); +-- knowledge: 48 行 +INSERT INTO knowledge (id, name, domain, category, description) VALUES (1, '偶函数', '初等', '函数与图像', '满足 f(-x)=f(x) 的函数,其图像关于 y 轴对称。'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (2, '奇函数', '初等', '函数与图像', '满足 f(-x)=-f(x) 的函数,其图像关于原点中心对称;若 0 在定义域内,则 f(0)=0。'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (3, '定义域', '初等', '函数与图像', '使函数表达式有意义的所有自变量取值组成的集合,常见限制有分母不为零、偶次根号下非负、对数的真数为正等。'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (4, '图像对称性', '初等', '函数与图像', '函数图像的轴对称与中心对称性质,与奇偶性、平移伸缩等变换密切相关。'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (5, '初等数学', '初等', '总纲', '中学与竞赛基础的数学知识总图'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (6, '数与式', '初等', '数与式', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (7, '方程与不等式', '初等', '方程与不等式', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (8, '函数与图像', '初等', '函数与图像', '以图像直观呈现自变量与函数值对应关系的分支:先“看见”图像,再回到公式与性质。'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (9, '三角函数', '初等', '三角函数', '以角为自变量、以三角比为函数值的函数,包括正弦、余弦、正切等,具有周期性与有界性。'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (10, '平面几何', '初等', '平面几何', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (11, '坐标与解析几何', '初等', '坐标与解析几何', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (12, '数列与证明', '初等', '数列与证明', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (13, '概率统计', '初等', '概率统计', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (14, '高等数学', '高等', '总纲', '大学基础数学知识总图'); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (15, '分析与微积分', '高等', '分析与微积分', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (16, '线性代数', '高等', '线性代数', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (17, '概率论与数理统计', '高等', '概率论与数理统计', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (18, '离散与优化', '高等', '离散与优化', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (19, '函数概念', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (20, '函数值', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (21, '正负性', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (22, '零点', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (23, '点线面的基本关系', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (24, '几何变换', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (25, '平行与垂直', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (26, '面积与周长', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (27, '圆', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (28, '三角比', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (29, '直角三角形', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (30, '三角恒等式', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (31, '同角关系', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (32, '正弦定理', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (33, '余弦定理', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (34, '任意角', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (35, '弧度制', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (36, '弧长与扇形', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (37, '和角公式', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (38, '差角公式', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (39, '倍角公式', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (40, '三角函数图像', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (41, '周期函数', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (42, '反三角函数', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (43, '三角方程', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (44, '数轴', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (45, '平面直角坐标系', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (46, '实数', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (47, '两点间距离', '初等', '', ''); +INSERT INTO knowledge (id, name, domain, category, description) VALUES (48, '曲线与方程', '初等', '', ''); +-- chapter_knowledge: 48 行 +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (1, 1, 19); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (2, 1, 20); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (3, 1, 3); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (4, 1, 21); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (5, 5, 22); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (6, 7, 1); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (7, 7, 2); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (8, 7, 4); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (9, 9, 23); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (10, 9, 24); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (11, 10, 25); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (12, 11, 26); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (13, 12, 27); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (14, 13, 9); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (15, 14, 28); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (16, 14, 29); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (17, 15, 30); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (18, 15, 31); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (19, 16, 32); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (20, 16, 33); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (21, 17, 34); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (22, 17, 35); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (23, 18, 35); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (24, 18, 36); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (25, 19, 37); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (26, 19, 38); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (27, 20, 39); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (28, 20, 30); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (29, 21, 40); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (30, 21, 41); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (31, 22, 42); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (32, 22, 43); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (33, 23, 44); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (34, 23, 45); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (35, 24, 44); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (36, 24, 46); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (37, 24, 47); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (38, 25, 45); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (39, 25, 47); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (40, 25, 48); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (41, 26, 45); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (42, 26, 23); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (43, 27, 45); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (44, 27, 48); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (45, 28, 45); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (46, 28, 48); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (47, 29, 45); +INSERT INTO chapter_knowledge (id, chapter_id, knowledge_id) VALUES (48, 29, 48); +-- courses: 3 行 +INSERT INTO courses (id, name, provider, url, grade, description, position) VALUES (1, 'Khan Academy · Algebra 1: Functions', 'Khan Academy', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', '初中衔接 / 高中', '函数单元含函数求值、定义域值域、函数图像与性质等大量免费练习,英文界面,适合配合盖尔范德丛书做自动化巩固。', 0); +INSERT INTO courses (id, name, provider, url, grade, description, position) VALUES (2, '3Blue1Brown · Essence of Calculus', '3Blue1Brown', 'https://www.youtube.com/playlist?list=PLZHQObOWTQDMsr9K-rj53DwVRMYO3t5Yr', '高中高年级 / 大学先修', '微积分本质系列:从几何直觉理解导数与积分,适合函数图像直觉建立后进入微积分阶段使用。', 1); +INSERT INTO courses (id, name, provider, url, grade, description, position) VALUES (3, '国家中小学智慧教育平台 · 高中数学(人教A版)', '教育部教育技术与资源发展中心', 'https://basic.smartedu.cn/syncClassroom', '高中全学段', '官方免费同步课程与配套课件,可按教材版本与册次点播,适合做课内进度对照。', 2); +-- knowledge_relations: 2 行 +INSERT INTO knowledge_relations (id, source_id, target_id, relation_type) VALUES (1, 1, 2, '前置'); +INSERT INTO knowledge_relations (id, source_id, target_id, relation_type) VALUES (2, 8, 9, '前置'); +-- knowledge_resources: 14 行 +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (1, 3, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (2, 1, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (3, 2, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (4, 4, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (5, 8, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (6, 9, 'video', '国家中小学智慧教育平台 · 高中数学(人教A版)', 'https://basic.smartedu.cn/syncClassroom', NULL, 3, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (7, 19, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (8, 20, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (9, 22, 'video', 'Khan Academy · Algebra 1: Functions', 'https://www.khanacademy.org/math/algebra/x2f8bb11595b61c86:functions', NULL, 1, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (10, 34, 'video', '国家中小学智慧教育平台 · 高中数学(人教A版)', 'https://basic.smartedu.cn/syncClassroom', NULL, 3, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (11, 35, 'video', '国家中小学智慧教育平台 · 高中数学(人教A版)', 'https://basic.smartedu.cn/syncClassroom', NULL, 3, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (12, 28, 'video', '国家中小学智慧教育平台 · 高中数学(人教A版)', 'https://basic.smartedu.cn/syncClassroom', NULL, 3, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (13, 30, 'video', '国家中小学智慧教育平台 · 高中数学(人教A版)', 'https://basic.smartedu.cn/syncClassroom', NULL, 3, 0); +INSERT INTO knowledge_resources (id, knowledge_id, kind, title, url, textbook_id, course_id, position) VALUES (14, 40, 'video', '国家中小学智慧教育平台 · 高中数学(人教A版)', 'https://basic.smartedu.cn/syncClassroom', NULL, 3, 0); +-- questions: 18 行 +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (1, 'bank', '已知函数 f(x) 满足 f(-x)=f(x),且 f(3)=5。下列哪个点一定在图像上?', '["A. (-3,5)", "B. (-3,-5)", "C. (3,-5)", "D. (5,-3)"]', 0, '偶函数', '由偶函数定义 f(-x)=f(x) 可知,点 (3,5) 关于 y 轴的镜像点 (-3,5) 一定在图像上。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (2, 'bank', '已知函数 f(x) 满足 f(-x)=-f(x),且 f(2)=4。下列哪个点一定在图像上?', '["A. (-2,4)", "B. (-2,-4)", "C. (2,-4)", "D. (4,-2)"]', 1, '奇函数', '由奇函数定义 f(-x)=-f(x) 可知,(2,4) 关于原点的对称点 (-2,-4) 一定在图像上。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (3, 'bank', '下列函数的图像关于 $y$ 轴对称的是?', '["A. $y=x+1$", "B. $y=x^3$", "C. $y=x^2+2$", "D. $y=x^2+x$"]', 2, '偶函数', '$y=x^2+2$ 满足 $f(-x)=f(x)$,因此图像关于 $y$ 轴对称,属于偶函数。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (4, 'bank', '函数 y=1/(x²-9) 的图像不可能经过哪个点?', '["A. (0,-1/9)", "B. (4,1/7)", "C. (-4,1/7)", "D. (3,1)"]', 3, '定义域', '分母 x²-9 不能为 0,x=±3 不在定义域内,所以图像不可能经过 (3,1)。', 1, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (5, 'bank', '若函数图像关于原点对称,则下列关系一定成立的是?', '["A. f(-x)=f(x)", "B. f(-x)=-f(x)", "C. f(x)=0", "D. f(x)>0"]', 1, '奇函数', '图像关于原点对称对应奇函数,即对任意定义域内的 x 有 f(-x)=-f(x)。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (6, 'bank', '已知 $f(-x)=f(x)$,则函数图像的几何性质是?', '["A. 关于 x 轴对称", "B. 关于 y 轴对称", "C. 关于原点对称", "D. 无法判断"]', 1, '偶函数', 'f(-x)=f(x) 是偶函数的定义式,几何上等价于图像关于 y 轴对称。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (7, 'bank', '函数 y=2/(x²-1) 的图像不可能经过哪个点?', '["A. (0,-2)", "B. (2,2/3)", "C. (-2,2/3)", "D. (1,2)"]', 3, '定义域', '分母 x²-1 不能为 0,x=±1 不在定义域内,所以图像不可能经过 (1,2)。', 1, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (8, 'bank', '若 f(-x)=-f(x),且图像经过 (5,-2),则一定经过?', '["A. (-5,-2)", "B. (-5,2)", "C. (5,2)", "D. (2,5)"]', 1, '奇函数', '奇函数图像关于原点对称,(5,-2) 关于原点的对称点是 (-5,2)。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (9, 'bank', '若 f(-x)=f(x),且图像经过 (4,7),则下列点一定在图像上的是?', '["(-4,7)", "(-4,-7)", "(4,-7)", "(7,4)"]', 0, '偶函数', '由偶函数定义得到关于 y 轴的镜像点。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (10, 'bank', '若 f(-x)=-f(x),且图像经过 (3,5),则下列点一定在图像上的是?', '["(-3,5)", "(-3,-5)", "(3,-5)", "(5,3)"]', 1, '奇函数', '由奇函数定义得到关于原点的对称点。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (11, 'bank', '下列函数中属于偶函数的是?', '["$y=x^3$", "$y=x^2+1$", "$y=x+1$", "$y=x^3+x$"]', 1, '偶函数', '代入 $-x$ 检验 $f(-x)=f(x)$。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (12, 'bank', '下列函数中属于奇函数的是?', '["y=x²", "y=x^3", "y=x²+1", "y=x^3+1"]', 1, '奇函数', '代入 -x 检验 f(-x)=-f(x)。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (13, 'bank', '函数 y=1/(x-2) 的图像不可能经过哪个点?', '["(0,-1/2)", "(3,1)", "(2,0)", "(4,1/2)"]', 2, '定义域', '分母不能为 0,因此 x=2 不属于定义域。', 1, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (14, 'bank', '若一个函数图像关于 y 轴对称,则该函数通常满足?', '["f(-x)=f(x)", "f(-x)=-f(x)", "f(x)=0", "f(x)>0"]', 0, '图像对称性', '关于 y 轴对称对应偶函数。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (15, 'bank', '若一个函数图像关于原点对称,则该函数通常满足?', '["f(-x)=f(x)", "f(-x)=-f(x)", "f(x)=1", "f(x)>0"]', 1, '图像对称性', '关于原点对称对应奇函数。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (16, 'bank', '若 f(x) 是偶函数,且 f(-5)=9,则 f(5)=?', '["-9", "0", "9", "无法确定"]', 2, '偶函数', '偶函数的两个对称自变量对应相同函数值。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (17, 'bank', '若 f(x) 是奇函数,且 f(-6)=4,则 f(6)=?', '["4", "-4", "0", "无法确定"]', 1, '奇函数', '奇函数的两个相反自变量对应相反函数值。', 7, 1, 0, 1); +INSERT INTO questions (id, source, stem, options, correct_index, knowledge_name, explanation, chapter_id, difficulty, is_generated, used_count) VALUES (18, 'bank', '判断函数 y=x⁴-x² 的奇偶性。', '["偶函数", "奇函数", "既奇又偶", "非奇非偶"]', 0, '偶函数', '偶次幂组成,代入 -x 后表达式不变。', 7, 1, 0, 1); +-- question_knowledge: 18 行 +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (1, 1, 1); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (2, 2, 2); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (3, 3, 1); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (4, 4, 3); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (5, 5, 2); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (6, 6, 1); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (7, 7, 3); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (8, 8, 2); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (9, 9, 1); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (10, 10, 2); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (11, 11, 1); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (12, 12, 2); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (13, 13, 3); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (14, 14, 4); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (15, 15, 4); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (16, 16, 1); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (17, 17, 2); +INSERT INTO question_knowledge (id, question_id, knowledge_id) VALUES (18, 18, 1); +COMMIT; +VACUUM; diff --git a/data/ebooks/10/4559e9e2fa084fb7a8e8a06aea5196bc.epub b/data/ebooks/6/4559e9e2fa084fb7a8e8a06aea5196bc.epub similarity index 100% rename from data/ebooks/10/4559e9e2fa084fb7a8e8a06aea5196bc.epub rename to data/ebooks/6/4559e9e2fa084fb7a8e8a06aea5196bc.epub diff --git a/data/ebooks/9/8dc7e48036ac4db8abf0020334b47b07.pdf b/data/ebooks/7/8dc7e48036ac4db8abf0020334b47b07.pdf similarity index 100% rename from data/ebooks/9/8dc7e48036ac4db8abf0020334b47b07.pdf rename to data/ebooks/7/8dc7e48036ac4db8abf0020334b47b07.pdf diff --git a/scripts/db.sh b/scripts/db.sh new file mode 100755 index 0000000..7dc0300 --- /dev/null +++ b/scripts/db.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# nex_math 数据库运维:备份 / 恢复 / 内容快照。 +# +# ./scripts/db.sh backup [标签] 备份 data/math.db(seed --force 也会自动备份) +# ./scripts/db.sh list 列出本机备份 +# ./scripts/db.sh restore [文件|latest] 用备份覆盖当前库(覆盖前先自动备份) +# ./scripts/db.sh dump [输出文件] 导出内容快照(教材/章节/知识点/课程/题库),可提交进 git +# ./scripts/db.sh load [快照文件] 把内容快照灌回当前库 +# ./scripts/db.sh path 数据库文件位置 +# +# 为什么要 dump:.gitignore 里 data/*.db 让库文件从来就没进过远端仓库, +# 库一重置,章节和题目就没了。把内容表导成 SQL 文本提交后, +# 远端仓库里始终有一份可恢复的数据库快照(且不含账号、答题记录等个人数据)。 +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PY="$ROOT_DIR/backend/.venv/bin/python" +if [ ! -x "$PY" ]; then + echo "[db] 未找到 backend/.venv,请先执行 ./scripts/init.sh" >&2 + exit 1 +fi +cd "$ROOT_DIR/backend" +exec "$PY" -m dbtool "$@"