主题
数据库架构设计
版本: Schema v57 最后更新: 2026-08-03
_schemaVersion仍为 57。下方「v57.1 变更摘要」是走_ensureLatestColumns幂等 ALTER-ADD 的增量列,按 RB v23/v24 的 ALTER-ADD precedent 不升版本号 (加一个可空墓碑列不值得触发开发期删库重建)。
⚠️ 文档债提示:本文件下半部分的表结构详情仍停留在 v38 之前(表名仍写
books、字段仍含 v39 删除的isbn/publisher/total_pages/language、cover_image_path等已废弃列),且尚未跟上 v56 三端表/列全量重命名 (notebook_entries/reading_sources/word_sources/excluded_words/recommended_excluded_words等旧表名在下方 ER 图和详细字段表里仍随处可见)。assets/sql/01_create_tables.sql是字段定义的权威(已全量改为 v56 新表名)。 本节仅维护版本号 + 变更摘要,详细表格清理排在 schema.md 大修 commit 中处理。
v57.1 变更摘要(2026-08-03)— reading_notes 补 deleted_at 软删列
改动:
| 表 | 变更 |
|---|---|
reading_notes | 新增 deleted_at TEXT(nullable)— 笔记级软删时间戳 |
⚠️ 落地方式:ALTER-ADD,不升 _schemaVersion(仍 57)。走 app_database._ensureLatestColumns 的幂等 ALTER 路径(新装从 01_create_tables.sql 建列,存量装机从 ALTER 补列),沿用 RB v23/v24 的 「ALTER-ADD precedent(不动 baseline)」。加一个可空墓碑列不值得触发开发期删库重建。
背景:补齐 RB v23「笔记四级删除闭环」缺失的第四级。此前 reading_notes 是 6 张同步表里唯一没有墓碑列的表,且双端均走硬删(RVH reading_note_datasource_impl.deleteBook / RB notes.rs::delete_reading_note), Supabase user_reading_notes 也无 deleted_at 列 —— 删除信号在整条链路上无处承载:
任一端删 → 本地硬删(无墓碑)→ push 取不到行 → 云端行永生 → 对端无条件 upsert两级后果:
- 删除永不上行:任一端删,对端笔记继续存在
- 删除端自己会被复活:云端活行 + 无条件 upsert。因红线 #5d 的
server_updated_at > watermarkfilter 而潜伏,触发条件为「对端 touch 该行 /server_updated_at_migration_v1兜底全量 pull / 换设备重装」 - 连带抹掉子表墓碑:
PRAGMA foreign_keys = ON,硬删经 FK CASCADE 物理删除reading_pages+word_page_links,把 v55 刚加的墓碑一并抹掉,子树同样永生
实现要点:
- 删除改软删事务:
deleteBook同事务软删 笔记 + 其reading_pages+ 其word_page_links。不能依赖 FK CASCADE(只在硬删触发,且会抹掉子表墓碑)。learning_entries不动 —— 词仍在生词本,只失去这个出处(镜像 RBremove_reading_page语义)。 - 8 处读查询加
deleted_at IS NULL:reading_note_datasource_impl5 处 (getBookById / getAllBooks / updateBook / searchBooks / bookExists)+statistics_repository_impl/reading_page_datasource/notebook_datasource各 1 处。 - sync push:dirty-check 加墓碑分支 + payload 加
deleted_at(传本地真值, 绝不能恒写 null —— PostgREST upsert 会把显式 null 写进去,远程复活对端刚删的笔记)。 仍不传 onConflict(user_reading_notes仅 PK 冲突,红线 #5f)。 - sync pull:三分支墓碑模板(模板同
_pullReadingPages)。分支① 连带软删本地子树 (防御对端漏传)。ON CONFLICT SET 子句不写deleted_at,且仍必须保留 cover 三字段(红线 #6c)+ 保持 ON CONFLICT DO UPDATE 风格(红线 #6b)。 _pullReadingPages父行守卫加AND deleted_at IS NULL:笔记改软删后行仍在, 裸COUNT(*)会放行远端活页、在已删笔记下重建子树。
部署顺序(硬约束,不可颠倒):
① Supabase ALTER → ② 双端 pull 侧上线 → ③ 双端 push 侧上线①必须最先:PostgREST 对未知列返回 PGRST204,整批 push 直接失败。 ②可安全早于③(pull 读缺失字段得 null,无害);反向部署会把「RB 写墓碑而 RVH 无视」 变成真实数据问题。DDL 见 supabase/migrations/20260803_v57_1_reading_notes_deleted_at.sql(该目录 2026-08-28 已删,DDL 原文见 git 历史)。
RB 侧待办:本仓不改 RB 运行时代码,交接见 docs/cross-end/19-reading-notes-tombstone-handoff.md。
已知遗留:ocr_word_positions(本地专属,不同步)FK CASCADE 挂在 reading_pages 上,页改软删后其行不再被清理。查询均经 reading_pages join 且带 deleted_at IS NULL,不会显示;仅本地行数轻微堆积。与 v55 页级软删的既有状态一致。
v57 变更摘要(2026-07-10)— word_cloze_contexts 复习卡真实语境表(跨端同步)
改动:新建本地表 word_cloze_contexts(表数 10 → 11 张)+ 索引 idx_cloze_word。
sql
CREATE TABLE IF NOT EXISTS word_cloze_contexts (
id TEXT PRIMARY KEY,
word TEXT NOT NULL COLLATE NOCASE, -- 归一 lemma,join learning_entries.word(红线 #9)
surface TEXT NOT NULL, -- 原始点击形,挖空精确替换目标
sentence TEXT NOT NULL, -- 真实语境句(应用层 8..220 字符护栏)
source_url TEXT,
user_id TEXT NOT NULL, -- 红线 #5b
created_at TEXT NOT NULL,
sense_gloss TEXT, -- 消歧出的贴合语境义项 gloss 文本
updated_at TEXT, -- 可空(对齐 RB 脏检查语义)
deleted_at TEXT, -- 软删墓碑(红线 #6)
synced_at TEXT
);
CREATE INDEX idx_cloze_word ON word_cloze_contexts(user_id, word COLLATE NOCASE, created_at DESC);背景:镜像 RB v24(word_cloze_contexts 纳入跨端同步矩阵)。此前 RB 独有 local-only,RVH 无此表也无 cloze 概念。
关键约束:
- 无 UNIQUE(一词多语境池,应用层封顶 5);无 FK(v16 松耦合,孤儿行无害)。
- 无
source_platform列:远端user_word_cloze_contexts亦无此列,push 不得携带(参考 known_words PGRST204)。 - Supabase 端业务键
UNIQUE(user_id, word, sentence)= push onConflict 目标(红线 #5f)。 - RVH pull-only:不新增本地采集路径;仍参与 sync push/pull + 本表独有
reconcile_cloze_pool(合并后去重 + 重新封顶)+ 删词/切用户软删墓碑传播。
同步矩阵:RVH 同步表 5 → 6 张(+word_cloze_contexts)。cloze 不进 vocabulary 预装库、不受 word PK 重算影响。
数据策略:开发期升 v57(表结构变)触发删库重建,无需迁移脚本。
详见 ~/reading-browser/docs/cross-end/16-rb-cloze-context-sync-handoff.md + docs/plans/archive/word-cloze-contexts-sync-rvh-plan.md。
v56 变更摘要(2026-07-09)— 三端表/列全量重命名
改动(本地表 → Supabase user_* 远端表同步跟改):
| 旧表名 | 新表名 | 说明 |
|---|---|---|
notebook_entries | learning_entries | 列 notebook_entry_id→learning_entry_id |
reading_sources | reading_pages | 列 reading_source_id→reading_page_id(含 ocr_word_positions 的同名列,表名不变) |
word_sources | word_page_links | — |
excluded_words | known_words | 语义由"排除"翻转为"已认识",i18n 文案同步改写 |
recommended_excluded_words | default_stopwords | 架构简化:删除 Supabase 拉取路径,改纯本地预装种子(03_init_data.sql 211 行确定性 sys-ew-* id),Supabase 公开表已 DROP |
保持不变:vocabulary、reading_notes、reference_words、ocr_word_positions(表名)、app_metadata、user_settings。RVH 无 sites 表,跳过 favorite_sites。
背景:镜像 RB 桌面端 + Supabase 已完成的统一改名,消除 notebook_entries↔reading_notes 的 "note" 撞车 + "排除词"负面命名。三端(RB/RVH/Supabase)表/列/代码标识符/i18n 文案同步统一。
数据策略:开发期升 v56(表结构变)触发删库重建,无需迁移脚本。预装词库版本不变(预装库只含 vocabulary/lemma_* 系统表,不含本次改名的用户数据表)。
实机验证:删库重装后触发 sync,default_stopwords 211 行种子正确写入 → 登录态 merge 进 known_words(+211 行,纯本地无网络请求)→ 与 Supabase 全表同步往返成功(user_reading_pages/user_learning_entries/user_word_page_links/user_known_words 均正确拉取)。
详见 docs/plans/archive/table-rename-rvh-handoff.md + table-rename-three-end-plan.md(2026-08-29 归档)。
v55 变更摘要(2026-07-07)— reading_sources / word_sources 补 deleted_at 软删列
改动:
| 表 | 变更 |
|---|---|
reading_sources | 新增 deleted_at TEXT(nullable)— 软删时间戳 |
word_sources | 新增 deleted_at TEXT(nullable)— 软删时间戳 |
背景:镜像 RB 端「笔记四级删除闭环」(commit 6fa0185),支持"移除来源页"(来源软删,词仍留生词本)和"词级解除关联"两级删除的跨端墓碑传播。RVH 无删除 UI 入口,仅 sync 层消费远端墓碑(pull-only):三分支模板(remote 删+本地有→标记;本地已删+remote 活→跳过不复活;本地无+remote 删→跳过不落墓碑,FK-safe),照抄既有 notebook_entries/excluded_words 同款结构。createWordSourceRelation 复活语义对齐红线 #7(ON CONFLICT DO UPDATE SET deleted_at=NULL)。
数据策略:开发期升 v55(表结构变)触发删库重建。不涉及预装词库版本变化。Supabase 端 user_word_sources/user_reading_sources 的 deleted_at 列已由 RB 会话在 Dashboard ALTER 完成。
详见 CLAUDE.md(本次会话)+ ~/reading-browser/docs/plans/notes-crud-phase2-plan.md §八。
v54 变更摘要(2026-06-08)— 习语归一治理 + canonical_surface 列
改动:
| 表 | 变更 |
|---|---|
vocabulary | 新增 canonical_surface TEXT — 习语可读引用形(如 'raining cats and dogs')/ NULL=普通词。归一键 word('rain cat and dog')逐 token lemmatize 后不可读,canonical_surface 保留原始引用形供显示。仅 phrase 行有值,正交于 CEFR / word_tags / emoji |
| lemmatizer 资产 | surface_to_base.json 修正错映射(as 删 / picked→pick / passed→pass / trying→try)→ 48 条含 as 习语 PK 还原(a a whole→as a whole 等)。base_forms.json 不变 |
来源 / 跨端契约(红线 #10):canonical_surface 由 RVH pipeline 单点产出(generate_db._applyPhraseOverlay 取 phrase_library_final.raw_forms[0]),RB byte-equal 只读消费,供 review 卡 / popup 显示。匹配/同步零参与(匹配只走归一键 word),不进 Supabase 同步矩阵。RB 读取建议 COALESCE(canonical_surface, word)(3 个复合名词习语如 good night/last minute/rush hour 归一无损,canonical_surface NULL,回退 word 即可读)。
资产修正(红线 #5e):surface_to_base.json SHA 1a468a82…→ca75b179…(140371→140370 mappings);base_forms.json 不变。RB 端复制同字节 + 移植 override 保 durability。详见 CLAUDE.md 红线 #5e + docs/cross-end/09-phrase-asset-fix-reseed-handoff.md。
⚠️ 上述 SHA/行数是 v54(v18 资产)当时的 as-built 值,已被 v26 取代(2026-08-18): 当前权威值只在 CLAUDE.md 红线 #5e 维护,本表不再跟随(v26 后又有 v27,数字会持续变动)。 落地见
docs/cross-end/25-rvh-lemmatizer-v26-confirmation.md(v26)、27-rvh-lemmatizer-residual-scan-handoff.md(v27)。
数据策略:开发期升 v54(表结构变)触发删库重建;预装词库重导出(_preinstalledVocabVersion 17→18),既存安装启动 UPSERT 补 canonical_surface 列。词数不变(18899)。⚠️ 48 旧 PK(a a whole 等)需 reseed prune(merge 路径残留),见交接文档。
v53 变更摘要(2026-06-06)— 词汇插图 emoji 列
改动:
| 表 | 变更 |
|---|---|
vocabulary | 新增 emoji TEXT — OpenMoji hexcode 大写(如 '1F436')/ NULL=无图。具象名词配图,词级一词一图,正交于 CEFR / word_tags |
来源 / 跨端契约(红线 #10):emoji 是词内禀、双端共需的数据 —— 由 RVH pipeline 单点产出进预装库 reading_vocab.db,RB byte-equal 只读消费。数据从 tools/vocabulary_builder_v3/data/word_emoji_mapping.tsv(726 候选经严格人工复核)回填,generate_db._applyEmojiMapping 在 Step 6 导出时写入。不进 Supabase 同步矩阵(与 vocabulary 表跨端策略一致,仅预装库权威)。
复核口径(质量优先 / 严格):tier1 直接采用(同形词主导义弱者弃);tier2 中心词命中过筛(图被修饰词主导 / 义项错位者弃);tier2 非中心词默认弃,仅白名单「emoji 真正描绘该词」者留;ambiguous 仅主导义强者(star/ship/train…)留。详见 docs/cross-end/06-word-illustration-emoji-reseed-handoff.md。
数据策略:开发期升 v53(表结构变)触发删库重建;预装词库重导出(_preinstalledVocabVersion 15→16),既存安装启动 UPSERT 覆盖 emoji 列。词数不变(12291)。
详见 CLAUDE.md 跨端红线 #10 + RB docs/cross-end/04-word-illustration-handoff.md。
v52 变更摘要(2026-05-22)— Sprint C-1 跨端续读 RVH 镜像
改动:
| 表 | 变更 |
|---|---|
reading_sources (RVH 本地) | 加 last_opened_at TEXT 列(RFC3339;仅 RB 端 touch 路径写入,RVH pull-only,红线 #6d) |
reading_sources 索引 | 加部分索引 idx_reading_sources_last_opened ON (user_id, last_opened_at DESC) WHERE last_opened_at IS NOT NULL |
镜像 RB 端 v12 migration(~/reading-browser/docs/plans/sprint-c1-cross-device-continue-reading-rb-plan.md「RVH 镜像协议清单」段)。Supabase 端 ALTER 由 RB 会话执行一次(user_reading_sources.last_opened_at),RVH 端不重复。
Sync 契约(红线 #6d):
- pull:
_pullReadingSourcesON CONFLICT DO UPDATE SET 加last_opened_at = CASE ... MAX(local, remote)(RFC3339 字典序,沿用红线 #5d watermark 同模式) - push:
_pushReadingSources字段白名单不含last_opened_at(RVH 无阅读功能,写入会用 NULL 覆盖 RB 端 touch 值)
UI:v52 当前无 UI 消费方(review 场景下时间戳与单词记忆无关,删除徽标渲染只保留数据层)。后续若书架页做 Continue on RB section 再启用。
数据策略:开发期升 v52 触发删库重建;预装词库(assets/databases/reading_vocab.db)不涉及(导入路径仅读 vocabulary 表)。
详见 CLAUDE.md 红线 #6d + plan ~/.claude/plans/sharded-inventing-dawn.md。
v51 变更摘要(2026-05-13)— vocabulary 跨端 schema 拉齐 + FK CASCADE→RESTRICT
双目标改动(一次性彻底):
| 表 | 变更 |
|---|---|
vocabulary (Supabase) | 重建表:加 cefr_inferred / cefr_source / word_tags 列;清掉 94063 旧 Kaikki 词(全 PENDING 无翻译,质量极低且不被命中)。Supabase 从此当「共享缓存」用 |
notebook_entries.word → vocabulary.word (RVH 本地) | FK 改 ON DELETE RESTRICT(原 CASCADE) |
user_notebook_entries.word → vocabulary.word (Supabase) | |
user_excluded_words.word → vocabulary.word (Supabase) |
2026-05-21 修订:缓冲池模型下 Supabase 两条 word FK 无业务价值(本地 FK 已守门 + 12291 预装词不在 Supabase vocabulary)。RB 端 push 撞 23503 触发整体 DROP。详见 decisions.md ADR 17 「修订 2026-05-21」 + supabase/migrations/20260521_drop_user_word_fkeys.sql(该目录 2026-08-28 已删,DDL 原文见 git 历史)。RVH 本地 SQLite 的 notebook_entries.word → vocabulary.word FK RESTRICT 保留不变。
FK CASCADE → RESTRICT 理由:
- 红线 #6b 实证:sync pull 用
INSERT OR REPLACE触发 CASCADE 误删子表 - 本次 vocabulary 重建讨论再次实证:DROP/TRUNCATE vocabulary 会 CASCADE 清掉 276 notebook + 15 excluded
- 业务上 vocabulary 是「字典层数据」,不应被业务删除
- RESTRICT 让 PostgreSQL 在误删时报错,强制 explicit 处理
保留 CASCADE 的关系(业务合理):
reading_sources.note_id → reading_notes.id:删 note 时清掉所有 sourcesword_sources.notebook_entry_id → notebook_entries.id:删 entry 时清孤立词源word_sources.reading_source_id → reading_sources.id:同上
数据策略(开发期):清空 Supabase user_notebook_entries (276 行) + user_excluded_words (15 行) 测试数据;本地 SQLite 升 v51 触发删库重建。客户端通过 UPDATE notebook_entries SET synced_at = NULL 可触发全量重 push 到 Supabase。
详见 supabase/migrations/20260513_v51_vocabulary_rebuild_fk_restrict.sql(该目录 2026-08-28 已删,DDL 原文见 git 历史) + plan cefr-llm-evaluation-and-ui-passthrough.md Track B。
v50 变更摘要(2026-05-12)— 词类型标签
正交于 CEFR:词的"难度等级" vs "词类型"是两个独立维度。LLM 评估 CEFR 时副产出 PROPER_NOUN / ABBREVIATION 信号,需独立字段承载。
| 表 | 变更 |
|---|---|
vocabulary | 新增 word_tags TEXT — JSON 数组(如 '["proper_noun"]' / '["proper_noun","abbreviation"]' / NULL) |
为什么用 JSON 数组而非 enum:NASA = proper_noun ∧ abbreviation 双标签场景,单字段 enum 强制选一个会丢信息。Tags 数组可扩展 symbol / archaic / medical 等未来类型,0 schema 改动。
tag 值现况:类型标签
proper_noun/abbreviation/symbol/basic/phrasal_verb/idiom;带前缀的属性值idiomaticity:always|context|literal(预装 v21 引入 / v22 / v23 retag,仅 phrase 行,短语非组合性三档,RB「只自动高亮 always 桶」)。本列纯内容增量、无 schema 变更(沿「0 schema 改动」原则)。RB 取 idiomaticity 用substr(value,14)。
详见 docs/plans/cefr-llm-evaluation-and-ui-passthrough.md「关键设计原则」段。
v49 变更摘要(2026-05-12)— CEFR 来源标识
正交建模:把 CEFR 的"值"和"可信度"分开存储。
| 表 | 变更 |
|---|---|
vocabulary | 新增 cefr_source TEXT — 'oxford' / 'cefr_j' / 'llm' / 'freq' / 'fallback' / NULL |
与 v48 cefr_inferred 互补:inferred 是布尔视图,source 是具体来源。NULL 允许(历史 backfilled 词无标记)。
详见 docs/plans/cefr-llm-evaluation-and-ui-passthrough.md。
v48 变更摘要(2026-05-12)— CEFR 推断标记
| 表 | 变更 |
|---|---|
vocabulary | 新增 cefr_inferred INTEGER NOT NULL DEFAULT 0 — 标记 primary_cefr_level 是否为推断值 |
C1/C2 词 7225 个由 Google web 频率反推(pipeline wordlist_builder.dart::c1Threshold),UI 可基此标识"约 X"。
v47 变更摘要(2026-05-03)— 封面字节直传重构
RB schema v7 镜像:消除 reading_notes.cover_image_path 跨端语义分裂(RB 写 HTTPS URL,RVH 写本地路径,sync 后渲染端永远只在创建端工作)。
| 表 | 变更 |
|---|---|
reading_notes | 删 cover_image_path TEXT;新增 cover_image_data BLOB + cover_image_mime TEXT + cover_source_url TEXT |
跨端契约(CLAUDE.md 红线 #6c):
- PostgREST bytea 走
\x<hex>transit(与 RB Rustformat!("\\x{}", hex::encode(b))/hex::decode严格对齐) - MIME 始终
image/jpeg,双端都压 192×192 JPEG q85,100KB 硬上限 - 三字段同进同出(
dataNULL →mime/source_url也 NULL) _pullReadingNotesON CONFLICT DO UPDATE SET 子句必须包含三 cover 列- 父表禁
INSERT OR REPLACE(继承红线 #6b)
配套:
- 新
lib/core/services/cover_image_processor.dart(Isolate.run 内 decode/resize/encodeJpg) _bytesToPostgresHex/_postgresHexToByteshelper(sync_repository_impl.dart)- 6 个 reading_note 封面渲染点统一改
Image.memory(coverImageData!)+ placeholder
关联:
- RB 实施计划
~/reading-browser/docs/plans/cover-image-blob-refactor-plan.md+cover-image-webview-refactor-plan.md - CLAUDE.md 红线 #6c(封面 BLOB 跨端契约)
v46 变更摘要(2026-04-29)— Bug D 修复 + 跨端契约镜像
RVH 4 张同步表 user_id 列 nullable → NOT NULL(红线 #5b RVH 镜像):
| 表 | 变更 |
|---|---|
notebook_entries | user_id TEXT → user_id TEXT NOT NULL |
reading_notes | user_id TEXT → user_id TEXT NOT NULL |
reading_sources | user_id TEXT → user_id TEXT NOT NULL |
word_sources | user_id TEXT → user_id TEXT NOT NULL |
excluded_words | 不变(已有 idx_excluded_words_user_word UNIQUE INDEX 与 RB 对齐) |
配套代码修复:
- save 路径:4 个 Model 加
required String userId字段;4 个 Repository 构造期绑定userId(参考ExcludedWordsRepositoryImpl模板);4 个 Provider 注入currentUserIdProvider - pull 路径:
sync_repository_impl.dart4 个_pull*INSERT 列表加user_id列 +_userId值
问题背景:vocab-pk Phase 3 用例 3 实证 RVH notebook_entries 出现 dup row(user_id NULL 时 SQLite UNIQUE 静默失效)。跨端契约镜像审计(temp/cross-end-contract-audit-2026-04-29.md)发现 4 张表的 save+pull 路径都漏写 user_id(只有 excluded_words 合规)。
关联:
- RB CLAUDE.md §4 红线 #5b(pull INSERT 必写 user_id)
- cross-end log [
docs/cross-end/archive/01-vocabulary-word-pk-log.md「Bug D」段] - roadmap 方向 2 [
docs/plans/cross-end-sync-evolution-roadmap.md]
v45 变更摘要(2026-04-27)
notebook_entries 加 UNIQUE(user_id, word COLLATE NOCASE)(双端协议决议,对齐 Supabase user_notebook_entries v43 migration):
- 双端独立 INSERT 同 word 时,本地 SQLite 直接拒绝插入(不再依赖 Supabase 端冲突回弹),sync 协议无需特殊处理
- 协议背景:见
~/reading-browser/docs/cross-end/01-vocabulary-word-pk-log.md"notebook_entries UNIQUE(user_id, word)" 决议
关联修复:本次同 PR 还修复了 DateTime.now() 写入 SQLite 时缺 toUtc() 的 bug(导致 updated_at 字典序与 Supabase UTC 字符串错位 → dirty 死循环),通过新增 lib/core/utils/datetime_utils.dart 统一封装 nowUtc() / nowUtcIso() 并在所有 entry CRUD 路径替换。
v44 变更摘要(2026-04-27)
删除 word_contexts 表(v35 双端整合时新建,从未被生产路径使用,Supabase 端 user_word_contexts 已不存在):
- 删除本地表
word_contexts+ 2 个索引 + 6 个 datasource 方法 - 删除 sync 模块中相关 push/pull/计数逻辑
- 删除 review session 中加载语境句子的代码及 UI 展示分支
- 删除
WordContextEntity/WordContextModel - 表数量:11 → 10 张
v43 变更摘要(2026-04-27)
跨端 vocabulary.word 升 PRIMARY KEY 重构(详见 ~/reading-browser/docs/cross-end/01-vocabulary-word-pk-plan.md):
vocabulary:删除id列,word TEXT PRIMARY KEY COLLATE NOCASEnotebook_entries:vocabulary_id→word(FK→vocabulary(word))excluded_words:删除vocabulary_id列 + FK- 应用层归一规则:
normalize(w) := nfc(lemmatize(w.trim().toLowerCase())) - 同步协议(Supabase user_*)同步改造,详见
supabase/migrations/20260426_v43_word_pk_rebuild.sql(该目录 2026-08-28 已删,DDL 原文见 git 历史)
⚠️ 本文档下方 ER 图和部分章节仍保留 v39 之前的旧表名(
vocabulary_items/books/word_source_relations)和未对齐 v43 word PK 的描述,是历史欠债,待后续重写。SQL 脚本(assets/sql/)是权威,本文档仅供参考。
📊 ER 关系图
mermaid
erDiagram
%% ==================== 核心学习流程 ====================
vocabulary_items ||--o{ notebook_entries : "词汇关联"
%% ❌ v27删除:notebook_entries ||--o{ learning_records : "学习记录"
%% ==================== 书籍-阅读来源-单词 三层架构 ====================
books ||--o{ reading_sources : "书籍包含来源"
reading_sources ||--o{ word_source_relations : "来源关联单词"
notebook_entries ||--o{ word_source_relations : "单词关联来源"
reading_sources ||--o{ ocr_word_positions : "OCR位置记录"
%% ==================== 排除词库 ====================
vocabulary_items ||--o{ excluded_words : "可选关联"
%% ==================== 参考词库(v30新增)====================
reference_words {
TEXT id PK
TEXT word UK
TEXT category
TEXT chinese_meaning
TEXT english_meaning
TEXT source
TEXT added_at
TEXT created_at
TEXT updated_at
}
%% ==================== 表定义 ====================
vocabulary_items {
TEXT id PK
TEXT word UK
TEXT ipa_pronunciation
TEXT pronunciation_url
TEXT primary_cefr_level
TEXT pos_definitions
INTEGER frequency_rank
TEXT source
TEXT last_accessed_at
TEXT etymology
TEXT word_forms
TEXT audio_local_path
TEXT word_family
TEXT created_at
TEXT updated_at
}
notebook_entries {
TEXT id PK
TEXT vocabulary_id FK
TEXT mastery_level
REAL easy_factor
INTEGER interval
INTEGER repetitions
INTEGER correct_count
INTEGER incorrect_count
REAL average_response_time
INTEGER last_review_quality
TEXT deleted_at
TEXT added_at
TEXT first_reviewed_at
TEXT last_review_date
TEXT next_review_date
TEXT created_at
TEXT updated_at
}
%% ❌ v27删除:learning_records 表
books {
TEXT id PK
TEXT title
TEXT author
TEXT isbn
TEXT publisher
TEXT cover_image_path
TEXT description
INTEGER total_pages
REAL reading_progress
TEXT language
TEXT note_type
TEXT full_text_path
TEXT created_at
TEXT updated_at
}
reading_sources {
TEXT id PK
TEXT book_id FK
TEXT name
TEXT source_type
TEXT source_ref
TEXT image_path
TEXT linked_at
TEXT created_at
TEXT updated_at
}
word_source_relations {
TEXT id PK
TEXT notebook_entry_id FK
TEXT reading_source_id FK
TEXT lemma
TEXT created_at
}
ocr_word_positions {
TEXT id PK
TEXT reading_source_id FK
TEXT word
TEXT lemma
TEXT line_text
INTEGER word_x
INTEGER word_y
INTEGER word_width
INTEGER word_height
INTEGER line_x
INTEGER line_y
INTEGER line_width
INTEGER line_height
REAL confidence
TEXT created_at
}
excluded_words {
TEXT id PK
TEXT word
TEXT source "recommended|user"
TEXT vocabulary_id FK
TEXT reason
TEXT added_at
TEXT created_at
TEXT updated_at
TEXT synced_at
TEXT deleted_at
TEXT user_id UK "UNIQUE(user_id, word COLLATE NOCASE)"
}
recommended_excluded_words {
TEXT id PK
TEXT word UK
INTEGER sort_order
}
user_settings {
TEXT id PK
TEXT user_id UK
TEXT theme_mode
TEXT language
INTEGER notifications_enabled
INTEGER sound_enabled
INTEGER vibration_enabled
INTEGER daily_reminder
TEXT daily_reminder_time
INTEGER auto_sync
INTEGER wifi_only_sync
TEXT selected_cefr_levels
INTEGER daily_learning_goal
INTEGER space_repetition_enabled
INTEGER space_repetition_days
TEXT notebook_group_sort
INTEGER show_pronunciation
INTEGER show_example
INTEGER page_size
INTEGER debug_mode
TEXT ocr_mode
INTEGER duplicate_similarity_threshold
INTEGER auto_document_crop
INTEGER image_enhancement
INTEGER edge_detection_sensitivity
TEXT preprocess_quality
INTEGER auto_orientation_correction
INTEGER orientation_detection_sensitivity
INTEGER auto_add_filtered_words
INTEGER review_batch_size
INTEGER show_crop_box_by_default
TEXT crop_mode
INTEGER enable_reading_notes
TEXT created_at
TEXT updated_at
}
app_metadata {
TEXT id PK
TEXT key UK
TEXT value
TEXT created_at
TEXT updated_at
}🗂️ 表结构详细说明
1. vocabulary_items(词汇库)
用途: 本地词汇库(预装核心词 + 用户查询回填词)
架构说明(v6):
- 预装词: 核心高频词(初始化时导入),source='preinstalled'
- 回填词: 用户查询过的词自动添加,source='backfilled'
- 更新策略: 预装词支持全量更新(DELETE WHERE source='preinstalled')
v38 更新说明(2026-03-29):
- ✅ notebook_entries 新增
deleted_at TEXT字段(soft-delete 时间戳) - ✅ 用途:删除操作通过同步传播到另一端(RVH↔RB),防止已删除词"复活"
- ✅ 所有查询:添加
WHERE deleted_at IS NULL过滤 - ✅ v37 跳过:直接从 v36→v38(v37 为 auth 相关 app_metadata,无表结构变更)
v36 更新说明(2026-03-27):
- ✅ notebook_entries 新增
last_review_quality INTEGER字段(上次复习的 SM-2 quality 值) - ✅ 用途:2-Button(Hard/Easy)序列推导——根据上次评分 + 本次按钮推导出 4 级质量分
- ✅ 默认值:NULL(首次复习按无历史处理,退化为 q=1 或 q=4)
v35 更新说明(2026-03-27):
- ✅ reading_sources 新增
source_ref TEXT字段(存储 URL/文件路径引用) - ✅ reading_sources.source_type 扩展支持
'web'(网页来源,RB 浏览器扩展同步) - ⚠️ 新建 word_contexts 表 — 已于 v44 删除(从未投入使用)
v34 更新说明(2026-03-21):
- ❌ 删除 9 个冗余/空壳字段:
definition,chinese_meaning,part_of_speech,difficulty_score,example_sentence,synonyms,antonyms,collocations,translations - ✅ 重命名:
cefr_level→primary_cefr_level(明确表示"所有词性中最低的 CEFR") - ✅ pos_definitions 升级:每个 POS 块新增
translations字段(从顶层下沉到 POS 级别) - ✅ 保留 word_family:单词级别数据,后续从 Kaikki 提取填充
- ✅ VocabularyEntity 计算属性:
definition/chineseMeaning/exampleSentence/difficultyScore从 pos_definitions 派生,向后兼容
3层回填流程:
- 本地查询 vocabulary_items(90%+ 命中)
- Supabase 云端查询(8% 命中)→ 回填本地
- Edge Function API(2% 命中)→ 回填Supabase → 回填本地
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| word | TEXT | UK NOT NULL | 单词(唯一索引) |
| ipa_pronunciation | TEXT | IPA音标 | |
| pronunciation_url | TEXT | 发音URL(v13新增)。⚠️ 2026-08-03 起 app 不再读取此列:RB 把查词上游换成 Wiktionary 后新回填词恒为 null,发音改走 Azure TTS 现合成(lib/core/services/tts_service.dart)。列保留不删(可空无害,删列的三端协调成本远超收益),但不要再拿它做「有没有发音」的判据 | |
| primary_cefr_level | TEXT | 主CEFR等级(A1-C2),所有词性中最低(v34重命名) | |
| pos_definitions | TEXT | 多词性结构化数据(JSON格式,v16新增,v34升级含translations) | |
| frequency_rank | INTEGER | 词频排名 | |
| source | TEXT | NOT NULL DEFAULT 'preinstalled' | 来源标记 ('preinstalled' / 'backfilled') |
| last_accessed_at | TEXT | 最后访问时间(用于LRU淘汰) | |
| etymology | TEXT | 词源描述(v32新增,来自 Kaikki/Wiktionary) | |
| word_forms | TEXT | 词形变化 JSON(v32新增,如 {"plural":"books"}) | |
| audio_local_path | TEXT | 本地音频文件路径(v32新增,A1-B1 级别) | |
| word_family | TEXT | 词族(JSON数组,v13新增,当前为空,后续填充) | |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
索引:
idx_vocabulary_word(word)idx_vocabulary_primary_cefr(primary_cefr_level)idx_vocabulary_frequency(frequency_rank)idx_vocabulary_source(source)idx_vocabulary_last_accessed(last_accessed_at)
2. notebook_entries(生词本)
用途: 用户添加的生词记录(v8: 合并SM-2算法字段,v18: 删除冗余字段)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| vocabulary_id | TEXT | FK NOT NULL | 关联词汇库ID(v18改为NOT NULL) |
| mastery_level | TEXT | NOT NULL DEFAULT 'level0' | 掌握等级(level0-level5) |
| easy_factor | REAL | NOT NULL DEFAULT 2.5 | SM-2容易度因子(1.3+) |
| interval | INTEGER | NOT NULL DEFAULT 0 | 复习间隔(天) |
| repetitions | INTEGER | NOT NULL DEFAULT 0 | 重复次数 |
| correct_count | INTEGER | DEFAULT 0 | 正确次数 |
| incorrect_count | INTEGER | DEFAULT 0 | 错误次数 |
| average_response_time | REAL | 平均响应时间(秒) | |
| last_review_quality | INTEGER | 上次复习质量(SM-2 quality 0-5,v36新增,2-Button序列推导) | |
| deleted_at | TEXT | soft-delete 时间戳,NULL=活跃(v38新增) | |
| added_at | TEXT | NOT NULL | 添加时间 |
| first_reviewed_at | TEXT | 首次复习时间 | |
| last_review_date | TEXT | 最后复习日期 | |
| next_review_date | TEXT | 下次复习日期 | |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
历史变更:
- v8: 合并 word_mastery_info 表,添加 SM-2 算法字段
- v18: 删除冗余字段(word, phonetic, definition, chinese_meaning, pronunciation_url),vocabulary_id 改为 NOT NULL
- v19: 外键约束从 SET NULL 改为 CASCADE(修复 NOT NULL + SET NULL 矛盾)
- v33: 删除 example_sentence(冗余,词库例句统一从 vocabulary_items.pos_definitions 获取)
- v36: 新增 last_review_quality(SM-2 quality 0-5,用于 2-Button 序列推导)
- v38: 新增 deleted_at(soft-delete 时间戳,同步传播删除操作)
外键:
- vocabulary_id → vocabulary_items(id) ON DELETE CASCADE
索引:
idx_notebook_next_review(next_review_date)idx_notebook_mastery_level(mastery_level)idx_notebook_added_at(added_at)
3. learning_records(学习记录) ❌ v27删除
v27变更:该表已删除。学习记录功能已合并到
notebook_entries表的last_review_date字段。
3. books(书籍)
用途: 管理用户的书籍信息,支持三层架构 Book → ReadingSource → Word(v7新增)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| title | TEXT | NOT NULL | 书名 |
| author | TEXT | 作者 | |
| isbn | TEXT | ISBN号 | |
| publisher | TEXT | 出版社 | |
| cover_image_path | TEXT | 封面图片路径 | |
| description | TEXT | 简介 | |
| total_pages | INTEGER | 总页数 | |
| reading_progress | REAL | DEFAULT 0.0 | 阅读进度(0.0-1.0) |
| language | TEXT | DEFAULT 'en' | 语言 |
| note_type | TEXT | DEFAULT 'physical' | 笔记类型(physical/webArticle/newsArticle/courseMaterial) |
| full_text_path | TEXT | 电子书原文文件路径(v25新增) | |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
索引:
idx_books_title(title)idx_books_created(created_at)
5. reading_sources(阅读来源)
用途: 记录用户拍照识别的书籍/文章来源 + 电子书文本导入
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| book_id | TEXT | FK NOT NULL | 关联书籍ID(v18改为NOT NULL) |
| name | TEXT | NOT NULL | 来源名称 |
| source_type | TEXT | NOT NULL DEFAULT 'image' | 来源类型('image'/'text'/'web',v23新增,v35扩展) |
| source_ref | TEXT | 外部引用(v35新增,source_type='web'时为URL) | |
| image_path | TEXT | 图片路径(仅source_type='image'时使用,v23改为可空) | |
| linked_at | TEXT | NOT NULL | 关联时间 |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
| deleted_at | TEXT | 软删时间戳(v55新增,RB「移除来源页」墓碑,RVH pull-only 消费) |
历史变更:
- v7: 添加 book_id 关联书籍
- v18: 删除 type/page_number/chapter_name 字段,book_id 改为 NOT NULL
- v23: 添加 source_type 字段支持双输入源,image_path 改为可空
- v25: 简化文本导入流程,删除 text_content/excerpt_start/excerpt_end
- v35: 新增 source_ref 字段(URL/文件路径引用),source_type 扩展支持 'web'
- v55: 新增 deleted_at 字段(软删,镜像 RB 笔记四级删除闭环 commit 6fa0185)
外键:
- book_id → books(id) ON DELETE CASCADE
索引:
idx_reading_sources_book_id(book_id)idx_reading_sources_linked_at(linked_at)
6. word_source_relations(单词-来源关联)
用途: 关联生词与其阅读来源(多对多中间表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| notebook_entry_id | TEXT | FK NOT NULL | 关联生词本ID |
| reading_source_id | TEXT | FK NOT NULL | 关联阅读来源ID |
| lemma | TEXT | NOT NULL DEFAULT '' | 词根(v24新增,用于词形还原匹配) |
| created_at | TEXT | NOT NULL | 创建时间 |
| deleted_at | TEXT | 软删时间戳(v55新增,RB「词级解除关联」墓碑,RVH pull-only 消费) |
历史变更:
- v10: 删除 context_sentence, position_in_text(功能效果不理想)
- v24: 新增 lemma 字段支持词形还原匹配
- v55: 新增 deleted_at 字段(软删,镜像 RB 笔记四级删除闭环 commit 6fa0185)
外键:
- notebook_entry_id → notebook_entries(id) ON DELETE CASCADE
- reading_source_id → reading_sources(id) ON DELETE CASCADE
唯一约束:
- UNIQUE(notebook_entry_id, reading_source_id)
索引:
idx_word_source_notebook(notebook_entry_id)idx_word_source_reading(reading_source_id)
7. ocr_word_positions(OCR单词位置)
用途: 存储OCR识别出的单词在原图中的精确位置
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| reading_source_id | TEXT | FK NOT NULL | 关联阅读来源ID |
| word | TEXT | NOT NULL | 单词(OCR原词) |
| lemma | TEXT | NOT NULL | 词根(v11新增,用于查询) |
| line_text | TEXT | NOT NULL | 所在行的完整文本 |
| word_x | INTEGER | NOT NULL | 单词X坐标(px) |
| word_y | INTEGER | NOT NULL | 单词Y坐标(px) |
| word_width | INTEGER | NOT NULL | 单词宽度(px) |
| word_height | INTEGER | NOT NULL | 单词高度(px) |
| line_x | INTEGER | NOT NULL | 行X坐标(px) |
| line_y | INTEGER | NOT NULL | 行Y坐标(px) |
| line_width | INTEGER | NOT NULL | 行宽度(px) |
| line_height | INTEGER | NOT NULL | 行高度(px) |
| confidence | REAL | NOT NULL | OCR识别置信度(0.0-1.0) |
| created_at | TEXT | NOT NULL | 创建时间 |
外键:
- reading_source_id → reading_sources(id) ON DELETE CASCADE
索引:
idx_ocr_word_positions_lookup(reading_source_id, word)- 复合索引idx_ocr_word_positions_source(reading_source_id)
8. excluded_words(排除词库)
用途: 存储用户明确标记为"不学习"的单词(v22 新增,v42 重构为 per-user)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID / 推荐词为 rec:{rec_id}:{user_id} 确定性 id |
| word | TEXT | NOT NULL | 单词(小写) |
| source | TEXT | NOT NULL | 来源类型('recommended'/'user');v42: 'system' → 'recommended' |
| vocabulary_id | TEXT | FK | 关联词汇库ID(用户排除词有此字段) |
| reason | TEXT | 排除原因(用户备注,可选) | |
| added_at | TEXT | NOT NULL | 添加时间 |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
| synced_at | TEXT | 同步时间戳(v40) | |
| deleted_at | TEXT | 软删除时间戳;跨端 tombstone 传播(v40) | |
| user_id | TEXT | 用户 ID;匿名态为 NULL(v40) |
索引:
idx_excluded_words_user_word— UNIQUE(user_id, word COLLATE NOCASE)(v42 新增,取代全局 UNIQUE(word))idx_excluded_words_word— 非唯一 word 索引(供 isExcluded / OCR 过滤不带 user_id 的查询)
说明(v42 重构,对齐 RB 阶段 F):
- 推荐停用词(source='recommended'):由 Supabase 公共表
recommended_excluded_words作为单一权威源;登录后 fetch + merge 到当前用户(确定性 id 保证跨端幂等) - 用户排除词(source='user'):OCR 后用户主动排除的词
- 所有读写走
WHERE user_id IS ? AND deleted_at IS NULL;推荐词也可被用户软删, tombstone 通过user_excluded_words跨设备传播 merge用synced_at=now标记未触碰的推荐词,避免每用户复制 211 行回推 Supabase
外键:
- vocabulary_id → vocabulary(id) ON DELETE CASCADE
8b. recommended_excluded_words(推荐排除词缓存)
用途: Supabase 公共推荐停用词的本地缓存(v42 新增)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | Supabase 的推荐词 id |
| word | TEXT | UK NOT NULL | 单词(小写) |
| sort_order | INTEGER | DEFAULT 0 | 展示排序 |
说明:
- 仅 3 列精简缓存,不维护 deleted_at / synced_at(每次 fetch 全量 DELETE + INSERT)
- 登录态下 fetch 后立即 merge 进当前用户的 excluded_words
- 匿名态只刷新缓存不 merge
9. reference_words(参考词库)
用途: 存储"识别但不学习"的单词(专有名词、缩写等),v30新增
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| word | TEXT | UK NOT NULL | 单词(小写,唯一索引) |
| category | TEXT | NOT NULL | 分类('properNoun'/'abbreviation'/'other') |
| chinese_meaning | TEXT | 中文释义 | |
| english_meaning | TEXT | 英文释义(全称/说明) | |
| source | TEXT | NOT NULL DEFAULT 'system' | 来源类型('system'/'user') |
| added_at | TEXT | NOT NULL | 添加时间 |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
说明:
- 系统预装(source='system'):~160个常见专有名词和缩写(国家、城市、组织、媒体等)
- 用户添加(source='user'):用户自定义的参考词
- OCR管道中,在排除词之后、词汇查找之前拦截参考词
- 参考词在Harvest Station中显示为REF分组,带中文释义,不可选中
- 防止模糊匹配误修正(如
iran→cran)
10. user_settings(用户设置)
用途: 用户偏好配置
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| user_id | TEXT | UK NOT NULL | 用户ID(唯一) |
| theme_mode | TEXT | 主题模式(system/light/dark) | |
| language | TEXT | 界面语言 | |
| notifications_enabled | INTEGER | 启用通知(0/1) | |
| sound_enabled | INTEGER | 启用声音(0/1) | |
| vibration_enabled | INTEGER | 启用震动(0/1) | |
| daily_reminder | INTEGER | 每日提醒(0/1) | |
| daily_reminder_time | TEXT | 提醒时间(HH:mm) | |
| auto_sync | INTEGER | 自动同步(0/1) | |
| wifi_only_sync | INTEGER | 仅WiFi同步(0/1) | |
| selected_cefr_levels | TEXT | 选中CEFR级别(逗号分隔) | |
| daily_learning_goal | INTEGER | 每日学习目标 | |
| space_repetition_enabled | INTEGER | 启用间隔重复(0/1) | |
| space_repetition_days | INTEGER | 间隔重复天数 | |
| notebook_group_sort | TEXT | 分组排序规则 | |
| show_pronunciation | INTEGER | 显示发音(0/1) | |
| show_example | INTEGER | 显示例句(0/1) | |
| page_size | INTEGER | 分页大小 | |
| debug_mode | INTEGER | 调试模式(0/1) | |
| ocr_mode | TEXT | OCR模式(local/cloud) | |
| duplicate_similarity_threshold | INTEGER | 照片去重阈值(0-100) | |
| auto_document_crop | INTEGER | 文档自动裁剪(0/1) | |
| image_enhancement | INTEGER | 图像增强(0/1) | |
| edge_detection_sensitivity | INTEGER | 边缘检测灵敏度(1-10) | |
| preprocess_quality | TEXT | 预处理质量(fast/balanced/quality) | |
| auto_orientation_correction | INTEGER | 自动方向矫正(0/1) | |
| orientation_detection_sensitivity | INTEGER | 方向检测灵敏度(1-10) | |
| auto_add_filtered_words | INTEGER | DEFAULT 0 | 自动添加筛选词(0/1,v23新增) |
| review_batch_size | INTEGER | DEFAULT 10 | 复习批量大小(v26新增) |
| show_crop_box_by_default | INTEGER | DEFAULT 1 | 默认显示裁剪框(0/1,v27新增) |
| crop_mode | TEXT | DEFAULT 'manual' | 裁剪模式(none/manual/smart,v31新增) |
| enable_reading_notes | INTEGER | DEFAULT 0 | 启用读书笔记模式(0/1,v31新增) |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
10. app_metadata(应用元数据)
用途: 系统级全局配置
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | TEXT | PK | UUID主键 |
| key | TEXT | UK NOT NULL | 配置键名(唯一) |
| value | TEXT | NOT NULL | 配置值 |
| created_at | TEXT | NOT NULL | 创建时间 |
| updated_at | TEXT | NOT NULL | 更新时间 |
🔗 表关系详解
核心关系
vocabulary_items → notebook_entries (1 : 多)
- 一个词汇可被多个生词本条目引用
- 外键:
notebook_entries.vocabulary_id(NOT NULL,v18起强制关联)
notebook_entries → learning_records (1 : 多)❌ v27删除books → reading_sources (1 : 多)
- 一本书包含多个阅读来源(多页/多次拍照)
- 外键:
reading_sources.book_id
notebook_entries ↔ reading_sources (多 : 多)
- 中间表:
word_source_relations - 同一单词可来自多个来源
- 同一来源可包含多个单词
- 中间表:
reading_sources → ocr_word_positions (1 : 多)
- 一个来源有多个单词位置记录
- 外键:
ocr_word_positions.reading_source_id
vocabulary_items → excluded_words (1 : 0..1)
- 用户排除词可选关联词汇库
- 外键:
excluded_words.vocabulary_id
级联删除规则
| 操作 | 影响 |
|---|---|
删除 vocabulary_items | CASCADE → notebook_entries, excluded_words |
删除 notebook_entries | CASCADE → word_source_relations |
删除 books | CASCADE → reading_sources |
删除 reading_sources | CASCADE → word_source_relations, ocr_word_positions |
🔄 数据流转说明
流程1: 添加生词(拍照识词)
1. 用户OCR拍照
↓
2. 创建 reading_sources(保存图片路径,source_type='image')
↓
3. 批量创建 ocr_word_positions(每个单词的坐标和词根)
↓
4. 用户选择单词添加到生词本
↓
5. 查询 vocabulary_items(3层回填)
├─ 本地命中 → 使用本地数据
├─ 本地未命中 → 查询Supabase → 回填本地
└─ Supabase未命中 → Edge Function API → 回填Supabase → 回填本地
↓
6. 创建 notebook_entries
├─ 关联 vocabulary_id(NOT NULL)
└─ 初始化 SM-2 算法字段(mastery_level=level0, easy_factor=2.5)
↓
7. 创建 word_source_relations(关联单词和来源,记录lemma)流程2: 添加生词(电子书文本导入)
1. 用户导入电子书文本
↓
2. 创建 books(保存书籍信息,full_text_path指向原文文件)
↓
3. 创建 reading_sources(source_type='text',book级别)
↓
4. 提取词汇 → 查询 vocabulary_items(3层回填)
↓
5. 创建 notebook_entries + word_source_relations流程3: 学习复习
1. 查询 notebook_entries
WHERE next_review_date <= TODAY
↓
2. LEFT JOIN vocabulary_items
获取完整词汇信息
↓
3. 用户完成学习
↓
4. 更新 notebook_entries(SM-2字段)
├─ 使用SM-2算法计算新的 interval
├─ 更新 easy_factor, repetitions
├─ 更新 correct_count 或 incorrect_count
├─ 计算 next_review_date
├─ 更新 mastery_level
└─ 更新 last_review_date流程4: 查看原图高亮
1. 用户点击单词查看原图
↓
2. 通过 word_source_relations
找到 reading_source_id
↓
3. 读取 reading_sources.image_path
↓
4. 查询 ocr_word_positions
WHERE reading_source_id = ? AND lemma = ?
↓
5. 在原图上绘制高亮框
使用 (word_x, word_y, word_width, word_height)⚠️ 重要约束
唯一约束
vocabulary_items.wordexcluded_words.worduser_settings.user_idapp_metadata.keyword_source_relations(notebook_entry_id, reading_source_id)- 复合唯一
JSON字段处理
需要应用层序列化:
vocabulary_items:pos_definitions,word_family
时间格式
所有时间字段使用 ISO 8601 格式:
dart
DateTime.now().toIso8601String()
// 例如: "2026-01-01T12:00:00.000Z"完整SQL脚本:
- 01_create_tables.sql - DDL
- 02_create_indexes.sql - 索引
- 03_init_data.sql - 初始化数据