Files
nj/wiki/data-layer.md

4.9 KiB
Raw Blame History

title, updated, status, tags
title updated status tags
数据层 2026-06-07 active
isar
offline-first
sync
repository-pattern

数据层 — Isar 本地存储 + SyncEngine 离线同步

index 导航。关联: handwriting-engine frontend erp-diary

1. 设计决策

Q: 为什么离线优先?

小学生使用场景中网络不稳定(校园 WiFi 信号差、家庭网络波动)。所有功能必须离线可用,联网后自动同步。

Q: 为什么选 Isar 不用 SQLite

Isar 提供 FTS 全文搜索、内置加密、零配置 Flutter 原生支持、类型安全的查询 API。对 Flutter 项目比 SQLite+drift 更轻量。

Q: 为什么用版本号冲突检测?

离线编辑 → 联网同步场景下同一条数据可能在多端修改。version 字段做乐观锁Phase 1 使用"本地优先"策略本地版本覆盖远端Phase 2 提供 UI 手动解决。

Q: 为什么 Repository 抽象接口?

JournalRepository 抽象接口 → IsarJournalRepository(本地)+ RemoteJournalRepository远程。BLoC 只依赖抽象,切换实现不影响业务逻辑。

2. 关键文件 + 数据流

核心文件

文件 行数 职责
data/local/isar_database.dart 72 单例管理3 Schema 注册
data/repositories/isar_journal_repository.dart 333 CRUD + 乐观锁 + 软删除
data/repositories/remote_journal_repository.dart 114 Dio HTTP API 代理
data/repositories/journal_repository.dart 184 抽象接口 + InMemory 测试实现
data/services/sync_engine.dart 338 WiFi 增量同步 + Isar 队列持久化
data/local/collections/ 3+3 文件 Isar Collection 定义 + 生成 Schema

数据流

UI 操作
  │
  ▼
EditorBloc.onSave (2s debounce)
  │
  ├─→ IsarJournalRepository.createJournal()  [首次]
  ├─→ IsarJournalRepository.updateJournal()  [后续]
  │     └─ 乐观锁: version 匹配 → +1 → 写入
  │     └─ 不匹配 → throw StateError('版本冲突')
  │
  └─→ SyncEngine.enqueue()  [待同步操作入队]
        │ WiFi 可用时
        ▼
      trySync() → RemoteJournalRepository → API → PostgreSQL
        │ 成功
        ▼
      persistPendingQueue()  [更新 Isar 队列]

集成契约

方向 组件 接口 触发时机
被调用 ← EditorBloc onSave(EditorState) 笔画/元素变更 2s 后
调用 → IsarJournalRepository createJournal/updateJournal 首次/后续保存
调用 → SyncEngine enqueue/trySync 数据变更/网络恢复
启动时 → SyncEngine restorePendingQueue() app.dart 创建时

3. 代码逻辑

不变量

Isar 扩展方法需显式 importimport 'package:isar/isar.dart' 不会随传递 import 传播,必须在使用 findAll()/findFirst() 的文件中显式导入

笔画序列化固定 ID${journalId}_strokes 保证每次覆盖而非重复创建

SyncEngine 双写 — 操作同时存在于内存 Queue 和 Isar PendingOperationCollection

乐观锁 — updateJournal 比对 version不匹配抛 StateError

软删除级联 — 删除日记时关联元素的 isDeleted 也设为 true

3 个 Isar Collection

Collection Isar 主键 业务 ID 用途
JournalEntryCollection autoIncrement id (indexed) 日记条目
JournalElementCollection autoIncrement id (indexed) 日记元素
PendingOperationCollection autoIncrement id (indexed) 同步操作队列

初始化链

// main.dart
WidgetsFlutterBinding.ensureInitialized();
await IsarDatabase.init();  // 注册 3 个 Schema

// app.dart
final syncEngine = SyncEngine(apiClient: apiClient);
syncEngine.restorePendingQueue();  // fire-and-forget 恢复队列

4. 活跃问题 + 陷阱

问题 级别 状态 说明
authorId 硬编码 'local' HIGH 待修 EditorPage 未接入 AuthBloc 获取真实用户
SyncEngine 仅 WiFi MEDIUM Phase 2 蜂窝数据同步未实现
版本冲突静默覆盖 MEDIUM Phase 2 "本地优先"策略,需 UI 手动解决
编辑器未加载已有数据 MEDIUM 已修复 _loadExistingJournal 读取日记 + 元素 + 笔画

历史教训

  • Isar findAll() 编译报"未定义" — 原因是扩展方法不随 import 传播,需显式 import 'package:isar/isar.dart'
  • Isar 3.x 查询链必须通过 .filter() 过渡才能调用 .findAll()

5. 变更记录

日期 变更
2026-06-07 编辑器加载已有数据已修复、打开已有日记默认查看模式
2026-06-01 Isar 集成完成3 Collection + Repository + SyncEngine 持久化 (2481c8f)
2026-06-01 初始创建 — 数据层架构、Isar 踩坑记录