新增 wiki/ 知识库 (遵循 HMS wiki-methodology.md 5 节结构): - index.md (84 行) — 症状导航 13 条 + 模块索引 + 系统数据流 - architecture.md (120 行) — 基座剥离 7 耦合点 + Feature Flag + PIPL 合规 - handwriting-engine.md (124 行) — 双层 Canvas + O(1) 点缓冲 + 光栅化缓存 - data-layer.md (127 行) — Isar + SyncEngine 离线同步 + 踩坑记录 - frontend.md (118 行) — 16 模块地图 + BLoC 注入链 + 设计系统 - erp-diary.md (101 行) — 15 Entity / 10 Service / 8 Handler + API 端点 新增 docs/: - tech-debt-board.md (110 行) — 10 条技术债 + 偿还优先级排名 其他更新: - .gitignore: 添加 .understand-anything/ (待初始化) - CLAUDE.md §9: 添加 wiki 参考文档链接
4.7 KiB
4.7 KiB
title, updated, status, tags
| title | updated | status | tags | ||||
|---|---|---|---|---|---|---|---|
| 数据层 | 2026-06-01 | active |
|
数据层 — 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 扩展方法需显式 import — import '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 | 待做 | journalId 非空时未从 Isar 读取 |
历史教训
- Isar
findAll()编译报"未定义" — 原因是扩展方法不随 import 传播,需显式import 'package:isar/isar.dart' - Isar 3.x 查询链必须通过
.filter()过渡才能调用.findAll()
5. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-06-01 | Isar 集成完成:3 Collection + Repository + SyncEngine 持久化 (2481c8f) |
| 2026-06-01 | 初始创建 — 数据层架构、Isar 踩坑记录 |