独立的高中数学题库管理系统,C/S 架构。服务端 Python + FastAPI,客户端 Flutter 跨平台(Web + Android)。 核心区别:题目以 docx 原生格式 存储(每题一个 docx),图片和公式天然内嵌,不再经过 Markdown 中间格式。
两个现有题库软件位于同级目录:
题库/ — Python CLI + Web 版(FastAPI + SQLite + pandoc + fzf),Markdown 存储题库C++/ — Qt6 桌面版,C++17,直接兼容 题库/ 的数据格式| # | 决策 | 选择 |
|---|---|---|
| 1 | 全新开发 vs 改造现有 | 全新开发(C/S 架构需要重做) |
| 2 | 服务端技术栈 | Python + FastAPI |
| 3 | 题目存储格式 | 每题一个独立 docx |
| 4 | docx 内题干/答案区分 | 【答案】 标记行分隔,元数据存自定义属性 |
| 5 | 客户端形态 | Flutter 跨平台(Web + Android) |
| 6 | 数据库 | SQLite + 文件即真相 |
| 7 | 用户模型 | 单用户,客户端首次启动输入服务器地址 |
| 8 | API 协议 | REST + JSON + 二进制流(multipart 上传) |
| 9 | 知识点编码 | 完整沿用 现有 5 层编码体系(1811 叶子) |
| 10 | 导入流程 | 服务端拆分(整卷 docx → N 个单题 docx)+ 目录监控 |
| 11 | 全文搜索 | 提取纯文本建 FTS5 索引 |
| 12 | 客户端题目渲染 | 混合方案 — Flutter 原生 UI + WebView(pandoc docx→HTML+MathML) |
| 13 | 导出组卷 | python-docx 拼接,不经过 pandoc |
| 14 | 图片处理 | 完全内嵌在 docx 中,不提取 |
| 15 | 元数据存储 | docx 自定义属性(Custom Document Properties) |
| 16 | 数据迁移 | 无需迁移,用户提供原始 Word 文件重新导入 |
| 17 | 增量索引 | document.xml SHA256(忽略 Word 自动元数据变更) |
| 18 | API 认证 | 简单 Token(服务端启动生成,客户端首次输入) |
| 19 | 试题篮 | 服务端多篮命名 |
| 20 | 知识点标注交互 | 树+搜索混合 + 逐题标注入口 |
| 21 | 部署方式 | 裸机安装脚本(install.sh) |
| 22 | 服务端目录结构 | 数据和代码分离,data/ 独立 |
| 23 | 单题 docx 格式 | 【答案】 前=题干,后=答案区;【难度】【知识点】 提取后从正文删除 |
| 24 | pandoc 渲染管线 | pandoc docx→HTML + python-docx 微调(base64 图片、清理 artifact) |
| 25 | 知识点树管理 | 保留 知识点树.md 源文件 + CLI knowledge build |
| 26 | 学生版/教师版导出 | 标记驱动一次遍历(遇 【答案】 跳过生成学生版) |
| 27 | fzf 角色 | 仅服务端 CLI 管理,教师日常走客户端 |
| 28 | 本地备份 | DB + docx 一起备份,保留 30 个快照 |
| 29 | 远程备份 | WebDAV 客户端(推送到 NAS/NextCloud) |
| 30 | Flutter 状态管理 | Riverpod |
| 31 | 文件传输 | multipart 上传 + 二进制流下载 |
| 32 | 抬头信息 | 全部丢弃 |
| 33 | 题目 ID | document.xml SHA256 前 8 位 |
| 34 | 导入拆分 | python-docx 段落遍历(题号正则 + 题型标题双驱动) |
| 35 | 服务端代码结构 | 按功能域分包(routers/ + services/) |
| 36 | CLI 命令 | serve / knowledge build / rebuild-index / stats / backup / token |
| 37 | 客户端平台 | Web + Android |
| 38 | 导航结构 | 4 标签底部导航(题库/搜索/试题篮/设置) |
| 39 | 首次配置 | 配置页输入服务器地址 + token,Web 版免配置 |
| 40 | 导入进度 | 异步 + 轮询(POST → task_id → 轮询 status) |
| 41 | 分页 | 传统分页(page + page_size) |
| 42 | 错误处理 | 统一 JSON + 全局异常中间件 |
| 43 | 客户端部署 | FastAPI serve Flutter Web + APK 下载页 |
| 44 | 服务端端口 | 8765(避开常用端口) |
| 45 | pandoc 安装 | install.sh 检测 + 提示 |
| 46 | 解答题小题 | 一整题不拆 |
| 47 | 文件名 | problems/<sha256-8>.docx |
| 48 | Web 部署 | install.sh 检测 Flutter SDK 自动构建 |
| 49 | Flutter 项目结构 | 按功能域分包(pages/models/services/providers/widgets) |
| 50 | 缺失元数据 | 部分默认 + 报告警告 |
| 51 | 开发阶段 | 1.服务端核心 → 2.Flutter Web → 3.Android → 4.打磨 |
[题目正文段落,可能含公式(OMML)、图片、选项]
> A.... B.... C.... D.... (选择题选项,blockquote 缩进或普通段落)
【答案】X ← 分割线:以上是题干,以下是答案区
【分析】解题思路... (可选)
【详解】详细步骤... (可选)
【点睛】... (可选)
自定义属性(python-docx Custom Document Properties,Word 不可见):
question_type:单选题/多选题/填空题/解答题difficulty:0-1 浮点数,越大越容易knowledge_codes:JSON 数组字符串,如 '["01.01.03.02.01"]'import_type:精选题/经典题/高考题comment:评语字符串(高考题自动从文件名生成)导入时 【难度】 和 【知识点】 文本从正文提取后删除(存入自定义属性),【分析】【详解】【点睛】 保留在正文。
tiku-server/
├── app/
│ ├── __init__.py
│ ├── config.py # 配置加载 (config.yaml)
│ ├── database.py # DB 连接 + CRUD
│ ├── models.py # Pydantic schemas
│ ├── main.py # FastAPI 入口 + 生命周期
│ ├── routers/
│ │ ├── auth.py # token 验证
│ │ ├── problems.py # 题目 CRUD + 搜索
│ │ ├── import_.py # docx 导入拆分
│ │ ├── export_.py # 导出组卷
│ │ ├── knowledge.py # 知识点树 API
│ │ ├── carts.py # 试题篮
│ │ └── admin.py # 管理端点
│ └── services/
│ ├── docx_parser.py # docx 拆分 + 元数据提取
│ ├── docx_builder.py # docx 合并 + 排版
│ ├── indexer.py # 增量索引 + FTS
│ ├── renderer.py # pandoc docx→HTML 渲染
│ ├── knowledge.py # 知识点树解析编码
│ ├── backup.py # 备份 + WebDAV
│ └── search.py # FTS5 搜索封装
├── data/ # 运行时数据(可单独备份)
│ ├── problems/ # 单题 docx (<sha256-8>.docx)
│ ├── bank.db # SQLite 数据库
│ ├── papers/ # 导出试卷
│ └── backups/ # 自动备份(保留 30 个)
├── src/
│ └── 知识点树.md # 知识点层级树(输入源)
├── web/ # Flutter Web 构建产物
├── config.yaml
├── tiku-server # CLI 入口脚本
├── install.sh
└── requirements.txt
5 层 Markdown 层级(# → ## → ### → #### → -),共 1811 叶子。
编码:每层两位数字,各层独立,用 . 连接。
示例:01.01.03.02.01 → 集合与常用逻辑用语/集合/集合的基本运算/并集/并集的概念及运算
CREATE TABLE problems (
id TEXT PRIMARY KEY, -- document.xml SHA256 前8位
file_path TEXT NOT NULL UNIQUE, -- problems/<id>.docx
mtime REAL NOT NULL,
checksum TEXT NOT NULL, -- document.xml SHA256
chapters TEXT, -- JSON 数组
difficulty REAL, -- 0-1,越大越容易
question_type TEXT, -- 单选题|多选题|填空题|解答题
import_type TEXT, -- 经典题|高考题|精选题
knowledge_codes TEXT, -- JSON 数组
comment TEXT,
source TEXT, -- 来源文件名
created_at TEXT,
updated_at TEXT
);
CREATE VIRTUAL TABLE problems_fts USING fts5(
problem_id UNINDEXED,
body, -- docx 纯文本提取
tokenize='trigram'
);
CREATE TABLE knowledge_points (
code TEXT PRIMARY KEY,
path TEXT NOT NULL,
leaf_name TEXT NOT NULL,
chapter_code TEXT,
chapter_name TEXT
);
CREATE TABLE carts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at TEXT
);
CREATE TABLE cart_items (
cart_id INTEGER REFERENCES carts(id),
problem_id TEXT REFERENCES problems(id),
added_at TEXT,
PRIMARY KEY (cart_id, problem_id)
);
CREATE TABLE import_tasks (
id TEXT PRIMARY KEY,
status TEXT, -- pending|processing|done|error
total INTEGER,
completed INTEGER,
message TEXT,
result TEXT, -- JSON 结果
created_at TEXT
);
GET /api/health — 健康检查(无需 auth)POST /api/auth/verify — 验证 tokenGET /api/stats — 仪表盘统计GET /api/problems?page=&page_size=&chapter=&type=&question_type=&annotated= — 题目列表GET /api/problems/{id} — 题目元数据GET /api/problems/{id}/render — 题目 HTML 渲染(WebView 用)GET /api/problems/{id}/image — 图片内嵌访问GET /api/problems/next-unannotated?current_id=&direction=next|prev — 连续标注导航GET /api/search?q=&page=&page_size= — 全文搜索POST /api/import — 上传整卷 docx(multipart)GET /api/import/{task_id}/status — 轮询导入进度POST /api/export/chapter — 按章节+类型导出POST /api/export/cart/{cart_id} — 导出试题篮GET /api/exports/{filename} — 下载 docxPUT /api/problems/{id}/metadata — 更新元数据PUT /api/problems/{id}/knowledge — 更新知识点编码GET /api/knowledge/tree — 完整知识树GET /api/knowledge/search?q= — 搜索知识点GET /api/carts — 篮子列表POST /api/carts — 创建篮子DELETE /api/carts/{id} — 删除篮子GET /api/carts/{id} — 篮子详情POST /api/carts/{id}/items — 添加题目DELETE /api/carts/{id}/items/{pid} — 移除题目GET /api/admin/backup-config — 查看 WebDAV 配置PUT /api/admin/backup-config — 设置 WebDAV 配置POST /api/admin/backup/trigger — 手动触发备份GET /api/admin/backup/history — 备份历史tiku-server serve — 启动 FastAPI 服务
tiku-server knowledge build — 重建知识点编码
tiku-server rebuild-index — 手动增量索引
tiku-server stats — 终端统计
tiku-server backup — 手动备份
tiku-server backup --remote — 推送到 WebDAV
tiku-server token — 显示/重新生成 token
**X、题型名** 段落(含关键词:单选题/多选题/填空题/解答题)→ 设置 current_type^\d+[..] 开头段落 → 新题开始,完成上一题【答案】 → 标记答案区起点(同一段落之后的归答案区)【难度】 【知识点】 并删除,保留 【分析】 【详解】 【点睛】knowledge_map.json 转编码,不匹配的跳过并警告【答案】 就跳过该段及之后所有内容;教师版保留全部N. 标记 → cleanup 识别 → Word 自动编号papers/<标题>_<时间戳>_学生版.docx / _教师版.docxlib/
├── main.dart
├── app.dart # MaterialApp + GoRouter + 主题
├── models/ # Problem, KnowledgeNode, Cart, ...
├── services/ # API 调用封装
├── providers/ # Riverpod providers
├── pages/
│ ├── home/ # 题库首页(三栏)
│ ├── search/ # 搜索页
│ ├── problem_detail/ # 题目详情
│ ├── carts/ # 试题篮列表 + 详情
│ ├── export/ # 导出操作
│ └── settings/ # 设置 + 首次配置
└── widgets/ # 共享组件
├── knowledge_tree.dart # 知识点树
├── problem_card.dart # 题目卡片
└── webview_render.dart # WebView 题目渲染
bank.db + 被修改 docx → data/backups/<timestamp>/,保留 30 个tiku-server backup [--remote]