Skip to content

数据库架构设计

版本: 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/languagecover_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

两级后果:

  1. 删除永不上行:任一端删,对端笔记继续存在
  2. 删除端自己会被复活:云端活行 + 无条件 upsert。因红线 #5d 的 server_updated_at > watermark filter 而潜伏,触发条件为「对端 touch 该行 / server_updated_at_migration_v1 兜底全量 pull / 换设备重装」
  3. 连带抹掉子表墓碑PRAGMA foreign_keys = ON,硬删经 FK CASCADE 物理删除 reading_pages + word_page_links,把 v55 刚加的墓碑一并抹掉,子树同样永生

实现要点

  • 删除改软删事务deleteBook 同事务软删 笔记 + 其 reading_pages + 其 word_page_links不能依赖 FK CASCADE(只在硬删触发,且会抹掉子表墓碑)。 learning_entries 不动 —— 词仍在生词本,只失去这个出处(镜像 RB remove_reading_page 语义)。
  • 8 处读查询加 deleted_at IS NULLreading_note_datasource_impl 5 处 (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_entrieslearning_entriesnotebook_entry_idlearning_entry_id
reading_sourcesreading_pagesreading_source_idreading_page_id(含 ocr_word_positions 的同名列,表名不变)
word_sourcesword_page_links
excluded_wordsknown_words语义由"排除"翻转为"已认识",i18n 文案同步改写
recommended_excluded_wordsdefault_stopwords架构简化:删除 Supabase 拉取路径,改纯本地预装种子(03_init_data.sql 211 行确定性 sys-ew-* id),Supabase 公开表已 DROP

保持不变vocabularyreading_notesreference_wordsocr_word_positions(表名)、app_metadatauser_settings。RVH 无 sites 表,跳过 favorite_sites。

背景:镜像 RB 桌面端 + Supabase 已完成的统一改名,消除 notebook_entriesreading_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_sourcesdeleted_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 wholeas a whole 等)。base_forms.json 不变

来源 / 跨端契约(红线 #10):canonical_surface 由 RVH pipeline 单点产出generate_db._applyPhraseOverlayphrase_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:_pullReadingSources ON 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)FK 改 ON DELETE RESTRICT(原 CASCADE)2026-05-21 整体 DROP(见下)
user_excluded_words.word → vocabulary.word (Supabase)FK 改 ON DELETE RESTRICT(原 CASCADE)2026-05-21 整体 DROP(见下)

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 时清掉所有 sources
  • word_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_nounabbreviation 双标签场景,单字段 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_notescover_image_path TEXT;新增 cover_image_data BLOB + cover_image_mime TEXT + cover_source_url TEXT

跨端契约(CLAUDE.md 红线 #6c):

  • PostgREST bytea 走 \x<hex> transit(与 RB Rust format!("\\x{}", hex::encode(b)) / hex::decode 严格对齐)
  • MIME 始终 image/jpeg,双端都压 192×192 JPEG q85,100KB 硬上限
  • 三字段同进同出(data NULL → mime / source_url 也 NULL)
  • _pullReadingNotes ON CONFLICT DO UPDATE SET 子句必须包含三 cover 列
  • 父表禁 INSERT OR REPLACE(继承红线 #6b)

配套

  • lib/core/services/cover_image_processor.dart(Isolate.run 内 decode/resize/encodeJpg)
  • _bytesToPostgresHex / _postgresHexToBytes helper(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_entriesuser_id TEXTuser_id TEXT NOT NULL
reading_notesuser_id TEXTuser_id TEXT NOT NULL
reading_sourcesuser_id TEXTuser_id TEXT NOT NULL
word_sourcesuser_id TEXTuser_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.dart 4 个 _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_entriesUNIQUE(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 NOCASE
  • notebook_entriesvocabulary_idword(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_levelprimary_cefr_level(明确表示"所有词性中最低的 CEFR")
  • pos_definitions 升级:每个 POS 块新增 translations 字段(从顶层下沉到 POS 级别)
  • 保留 word_family:单词级别数据,后续从 Kaikki 提取填充
  • VocabularyEntity 计算属性definition/chineseMeaning/exampleSentence/difficultyScore 从 pos_definitions 派生,向后兼容

3层回填流程:

  1. 本地查询 vocabulary_items(90%+ 命中)
  2. Supabase 云端查询(8% 命中)→ 回填本地
  3. Edge Function API(2% 命中)→ 回填Supabase → 回填本地
字段类型约束说明
idTEXTPKUUID主键
wordTEXTUK NOT NULL单词(唯一索引)
ipa_pronunciationTEXTIPA音标
pronunciation_urlTEXT发音URL(v13新增)。⚠️ 2026-08-03 起 app 不再读取此列:RB 把查词上游换成 Wiktionary 后新回填词恒为 null,发音改走 Azure TTS 现合成(lib/core/services/tts_service.dart)。列保留不删(可空无害,删列的三端协调成本远超收益),但不要再拿它做「有没有发音」的判据
primary_cefr_levelTEXT主CEFR等级(A1-C2),所有词性中最低(v34重命名)
pos_definitionsTEXT多词性结构化数据(JSON格式,v16新增,v34升级含translations)
frequency_rankINTEGER词频排名
sourceTEXTNOT NULL DEFAULT 'preinstalled'来源标记 ('preinstalled' / 'backfilled')
last_accessed_atTEXT最后访问时间(用于LRU淘汰)
etymologyTEXT词源描述(v32新增,来自 Kaikki/Wiktionary)
word_formsTEXT词形变化 JSON(v32新增,如 {"plural":"books"})
audio_local_pathTEXT本地音频文件路径(v32新增,A1-B1 级别)
word_familyTEXT词族(JSON数组,v13新增,当前为空,后续填充)
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT 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: 删除冗余字段)

字段类型约束说明
idTEXTPKUUID主键
vocabulary_idTEXTFK NOT NULL关联词汇库ID(v18改为NOT NULL)
mastery_levelTEXTNOT NULL DEFAULT 'level0'掌握等级(level0-level5)
easy_factorREALNOT NULL DEFAULT 2.5SM-2容易度因子(1.3+)
intervalINTEGERNOT NULL DEFAULT 0复习间隔(天)
repetitionsINTEGERNOT NULL DEFAULT 0重复次数
correct_countINTEGERDEFAULT 0正确次数
incorrect_countINTEGERDEFAULT 0错误次数
average_response_timeREAL平均响应时间(秒)
last_review_qualityINTEGER上次复习质量(SM-2 quality 0-5,v36新增,2-Button序列推导)
deleted_atTEXTsoft-delete 时间戳,NULL=活跃(v38新增)
added_atTEXTNOT NULL添加时间
first_reviewed_atTEXT首次复习时间
last_review_dateTEXT最后复习日期
next_review_dateTEXT下次复习日期
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT 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新增)

字段类型约束说明
idTEXTPKUUID主键
titleTEXTNOT NULL书名
authorTEXT作者
isbnTEXTISBN号
publisherTEXT出版社
cover_image_pathTEXT封面图片路径
descriptionTEXT简介
total_pagesINTEGER总页数
reading_progressREALDEFAULT 0.0阅读进度(0.0-1.0)
languageTEXTDEFAULT 'en'语言
note_typeTEXTDEFAULT 'physical'笔记类型(physical/webArticle/newsArticle/courseMaterial)
full_text_pathTEXT电子书原文文件路径(v25新增)
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT NULL更新时间

索引:

  • idx_books_title(title)
  • idx_books_created(created_at)

5. reading_sources(阅读来源)

用途: 记录用户拍照识别的书籍/文章来源 + 电子书文本导入

字段类型约束说明
idTEXTPKUUID主键
book_idTEXTFK NOT NULL关联书籍ID(v18改为NOT NULL)
nameTEXTNOT NULL来源名称
source_typeTEXTNOT NULL DEFAULT 'image'来源类型('image'/'text'/'web',v23新增,v35扩展)
source_refTEXT外部引用(v35新增,source_type='web'时为URL)
image_pathTEXT图片路径(仅source_type='image'时使用,v23改为可空)
linked_atTEXTNOT NULL关联时间
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT NULL更新时间
deleted_atTEXT软删时间戳(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(单词-来源关联)

用途: 关联生词与其阅读来源(多对多中间表)

字段类型约束说明
idTEXTPKUUID主键
notebook_entry_idTEXTFK NOT NULL关联生词本ID
reading_source_idTEXTFK NOT NULL关联阅读来源ID
lemmaTEXTNOT NULL DEFAULT ''词根(v24新增,用于词形还原匹配)
created_atTEXTNOT NULL创建时间
deleted_atTEXT软删时间戳(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识别出的单词在原图中的精确位置

字段类型约束说明
idTEXTPKUUID主键
reading_source_idTEXTFK NOT NULL关联阅读来源ID
wordTEXTNOT NULL单词(OCR原词)
lemmaTEXTNOT NULL词根(v11新增,用于查询)
line_textTEXTNOT NULL所在行的完整文本
word_xINTEGERNOT NULL单词X坐标(px)
word_yINTEGERNOT NULL单词Y坐标(px)
word_widthINTEGERNOT NULL单词宽度(px)
word_heightINTEGERNOT NULL单词高度(px)
line_xINTEGERNOT NULL行X坐标(px)
line_yINTEGERNOT NULL行Y坐标(px)
line_widthINTEGERNOT NULL行宽度(px)
line_heightINTEGERNOT NULL行高度(px)
confidenceREALNOT NULLOCR识别置信度(0.0-1.0)
created_atTEXTNOT 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)

字段类型约束说明
idTEXTPKUUID / 推荐词为 rec:{rec_id}:{user_id} 确定性 id
wordTEXTNOT NULL单词(小写)
sourceTEXTNOT NULL来源类型('recommended'/'user');v42: 'system' → 'recommended'
vocabulary_idTEXTFK关联词汇库ID(用户排除词有此字段)
reasonTEXT排除原因(用户备注,可选)
added_atTEXTNOT NULL添加时间
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT NULL更新时间
synced_atTEXT同步时间戳(v40)
deleted_atTEXT软删除时间戳;跨端 tombstone 传播(v40)
user_idTEXT用户 ID;匿名态为 NULL(v40)

索引:

  • idx_excluded_words_user_wordUNIQUE(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 跨设备传播
  • mergesynced_at=now 标记未触碰的推荐词,避免每用户复制 211 行回推 Supabase

外键:

  • vocabulary_id → vocabulary(id) ON DELETE CASCADE

用途: Supabase 公共推荐停用词的本地缓存(v42 新增)

字段类型约束说明
idTEXTPKSupabase 的推荐词 id
wordTEXTUK NOT NULL单词(小写)
sort_orderINTEGERDEFAULT 0展示排序

说明

  • 仅 3 列精简缓存,不维护 deleted_at / synced_at(每次 fetch 全量 DELETE + INSERT)
  • 登录态下 fetch 后立即 merge 进当前用户的 excluded_words
  • 匿名态只刷新缓存不 merge

9. reference_words(参考词库)

用途: 存储"识别但不学习"的单词(专有名词、缩写等),v30新增

字段类型约束说明
idTEXTPKUUID主键
wordTEXTUK NOT NULL单词(小写,唯一索引)
categoryTEXTNOT NULL分类('properNoun'/'abbreviation'/'other')
chinese_meaningTEXT中文释义
english_meaningTEXT英文释义(全称/说明)
sourceTEXTNOT NULL DEFAULT 'system'来源类型('system'/'user')
added_atTEXTNOT NULL添加时间
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT NULL更新时间

说明

  • 系统预装(source='system'):~160个常见专有名词和缩写(国家、城市、组织、媒体等)
  • 用户添加(source='user'):用户自定义的参考词
  • OCR管道中,在排除词之后、词汇查找之前拦截参考词
  • 参考词在Harvest Station中显示为REF分组,带中文释义,不可选中
  • 防止模糊匹配误修正(如 iran→cran

10. user_settings(用户设置)

用途: 用户偏好配置

字段类型约束说明
idTEXTPKUUID主键
user_idTEXTUK NOT NULL用户ID(唯一)
theme_modeTEXT主题模式(system/light/dark)
languageTEXT界面语言
notifications_enabledINTEGER启用通知(0/1)
sound_enabledINTEGER启用声音(0/1)
vibration_enabledINTEGER启用震动(0/1)
daily_reminderINTEGER每日提醒(0/1)
daily_reminder_timeTEXT提醒时间(HH:mm)
auto_syncINTEGER自动同步(0/1)
wifi_only_syncINTEGER仅WiFi同步(0/1)
selected_cefr_levelsTEXT选中CEFR级别(逗号分隔)
daily_learning_goalINTEGER每日学习目标
space_repetition_enabledINTEGER启用间隔重复(0/1)
space_repetition_daysINTEGER间隔重复天数
notebook_group_sortTEXT分组排序规则
show_pronunciationINTEGER显示发音(0/1)
show_exampleINTEGER显示例句(0/1)
page_sizeINTEGER分页大小
debug_modeINTEGER调试模式(0/1)
ocr_modeTEXTOCR模式(local/cloud)
duplicate_similarity_thresholdINTEGER照片去重阈值(0-100)
auto_document_cropINTEGER文档自动裁剪(0/1)
image_enhancementINTEGER图像增强(0/1)
edge_detection_sensitivityINTEGER边缘检测灵敏度(1-10)
preprocess_qualityTEXT预处理质量(fast/balanced/quality)
auto_orientation_correctionINTEGER自动方向矫正(0/1)
orientation_detection_sensitivityINTEGER方向检测灵敏度(1-10)
auto_add_filtered_wordsINTEGERDEFAULT 0自动添加筛选词(0/1,v23新增)
review_batch_sizeINTEGERDEFAULT 10复习批量大小(v26新增)
show_crop_box_by_defaultINTEGERDEFAULT 1默认显示裁剪框(0/1,v27新增)
crop_modeTEXTDEFAULT 'manual'裁剪模式(none/manual/smart,v31新增)
enable_reading_notesINTEGERDEFAULT 0启用读书笔记模式(0/1,v31新增)
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT NULL更新时间

10. app_metadata(应用元数据)

用途: 系统级全局配置

字段类型约束说明
idTEXTPKUUID主键
keyTEXTUK NOT NULL配置键名(唯一)
valueTEXTNOT NULL配置值
created_atTEXTNOT NULL创建时间
updated_atTEXTNOT NULL更新时间

🔗 表关系详解

核心关系

  1. vocabulary_items → notebook_entries (1 : 多)

    • 一个词汇可被多个生词本条目引用
    • 外键:notebook_entries.vocabulary_id(NOT NULL,v18起强制关联)
  2. notebook_entries → learning_records (1 : 多) ❌ v27删除

  3. books → reading_sources (1 : 多)

    • 一本书包含多个阅读来源(多页/多次拍照)
    • 外键:reading_sources.book_id
  4. notebook_entries ↔ reading_sources (多 : 多)

    • 中间表:word_source_relations
    • 同一单词可来自多个来源
    • 同一来源可包含多个单词
  5. reading_sources → ocr_word_positions (1 : 多)

    • 一个来源有多个单词位置记录
    • 外键:ocr_word_positions.reading_source_id
  6. vocabulary_items → excluded_words (1 : 0..1)

    • 用户排除词可选关联词汇库
    • 外键:excluded_words.vocabulary_id

级联删除规则

操作影响
删除 vocabulary_itemsCASCADE → notebook_entries, excluded_words
删除 notebook_entriesCASCADE → word_source_relations
删除 booksCASCADE → reading_sources
删除 reading_sourcesCASCADE → 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.word
  • excluded_words.word
  • user_settings.user_id
  • app_metadata.key
  • word_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脚本: