diff --git a/.gitignore b/.gitignore index 18420e1..e5b14dd 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,9 @@ trace-*.json # Graphify knowledge graph (regenerated locally) graphify-out/ +# Understand-Anything knowledge graph (local dev tool) +.understand-anything/ + # Native miniprogram (separate project) apps/mp-native/ diff --git a/CLAUDE.md b/CLAUDE.md index 23ca6ad..254e999 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -414,6 +414,13 @@ chore(docker): 添加 PostgreSQL 16 + Redis 7 开发环境 | 文档 | 位置 | |------|------| +| **知识库首页** | `wiki/index.md` — 症状导航 + 模块索引 | +| 架构决策 | `wiki/architecture.md` — 基座剥离 + Feature Flag + 多租户 | +| 手写引擎 | `wiki/handwriting-engine.md` — 双层 Canvas + 光栅化缓存 | +| 数据层 | `wiki/data-layer.md` — Isar + SyncEngine 离线同步 | +| Flutter 前端 | `wiki/frontend.md` — 16 模块 + BLoC + 设计系统 | +| 后端模块 | `wiki/erp-diary.md` — Entity/Service/Handler 清单 | +| 技术债看板 | `docs/tech-debt-board.md` — 10 条待偿还债务 | | 产品设计规格 v1.2 | `docs/superpowers/specs/2026-05-31-nuanji-warm-notes-design.md` | | 实施规划 v2.1 | `plans/hazy-petting-lampson.md` | | 头脑风暴文档 (8 份) | `.superpowers/brainstorm/734-1780218658/` | diff --git a/docs/tech-debt-board.md b/docs/tech-debt-board.md new file mode 100644 index 0000000..1d5586c --- /dev/null +++ b/docs/tech-debt-board.md @@ -0,0 +1,110 @@ +# 暖记技术债看板 + +> 最后更新: 2026-06-01 + +## 指标概览 + +| 指标 | 当前值 | 目标 | 状态 | +|------|--------|------|------| +| flutter analyze 错误 | 0 | 0 | 🟢 健康 | +| flutter analyze 警告 | 1 | 0 | 🟡 需改进 | +| flutter analyze info | 18 | ≤10 | 🟡 需改进 | +| 后端测试 | ~50 | 80%+ 覆盖 | 🟡 需改进 | +| 前端测试 | 0 | 80%+ 覆盖 | 🔴 缺失 | +| CI/CD | 无 | 全自动化 | 🔴 缺失 | +| Docker 部署 | 未验证 | 可运行 | 🔴 缺失 | + +## 高息技术债(按优先级排序) + +### TD-1: editor_page.dart authorId 硬编码 [利息: HIGH] + +- **现状**: `JournalEntry.create(authorId: 'local')` 硬编码 +- **影响**: 无法关联真实用户,同步后数据混乱 +- **修复成本**: 0.5 天 +- **计划**: 接入 AuthBloc 获取当前用户 ID +- **文件**: `app/lib/features/editor/views/editor_page.dart:96` + +### TD-2: CI/CD 未建立 [利息: HIGH] + +- **现状**: 手动 `cargo check` + `flutter analyze` +- **影响**: 无法防止回归,无自动部署 +- **修复成本**: 1 天 +- **计划**: GitHub Actions / Gitea CI + 基础流水线 + +### TD-3: 后端 Docker 部署未验证 [利息: HIGH] + +- **现状**: `docker/` 目录有 compose 文件但从未实际运行 +- **影响**: 无法确认后端可正常启动 +- **修复成本**: 0.5 天 +- **计划**: docker-compose up + 健康检查验证 + +### TD-4: 笔画 toImage() 同步阻塞 [利息: MEDIUM] + +- **现状**: `stroke_cache.dart` 中 `toImage()` 在主线程执行 +- **影响**: 大笔画可能导致 UI 卡顿 +- **修复成本**: 1 天 +- **计划**: compute() isolate 异步光栅化 + +### TD-5: 前端测试为零 [利息: MEDIUM] + +- **现状**: `app/test/` 目录空 +- **影响**: 无回归保护,重构风险高 +- **修复成本**: 3 天 +- **计划**: Repository 单元测试 → BLoC 测试 → Widget 测试 + +### TD-6: 画布尺寸变化缓存过渡 [利息: MEDIUM] + +- **现状**: 屏幕旋转时 ui.Image 缓存失效,需要重新光栅化 +- **影响**: 旋转瞬间可能卡顿或空白 +- **修复成本**: 1 天 +- **计划**: 监听尺寸变化,异步重建缓存 + +### TD-7: SecureStorage token 未持久化 [利息: MEDIUM] + +- **现状**: app.dart 中 token 设置为 TODO 注释 +- **影响**: 每次重启需要重新登录 +- **修复成本**: 0.5 天 +- **计划**: flutter_secure_storage 存取 JWT + +### TD-8: 编辑器不加载已有日记 [利息: MEDIUM] + +- **现状**: journalId 非空时未从 Isar 读取数据 +- **影响**: 编辑已有日记时显示空白 +- **修复成本**: 1 天 +- **计划**: EditorBloc 添加 LoadJournal event + Isar 查询 + +### TD-9: Isar FTS 搜索未实现 [利息: LOW] + +- **现状**: search 模块是空壳 +- **影响**: 无法搜索日记内容 +- **修复成本**: 1 天 +- **计划**: Isar FTS 索引 + 搜索结果页 + +### TD-10: SyncEngine 蜂窝数据未支持 [利息: LOW] + +- **现状**: 仅 WiFi 自动同步 +- **影响**: 无 WiFi 时无法同步 +- **修复成本**: 0.5 天 +- **计划**: 添加用户设置允许蜂窝数据同步 + +## 已偿还债务 + +| 债务 | 还清日期 | 投入 | +|------|---------|------| +| 手写引擎单层 Canvas 卡顿 | 2026-06-01 | 双层架构 + 光栅化缓存 | +| 编辑器 onSave 仅 debugPrint | 2026-06-01 | IsarJournalRepository 持久化 | +| SyncEngine 队列纯内存 | 2026-06-01 | Isar PendingOperation 持久化 | +| Isar 未初始化 | 2026-06-01 | main.dart init + Schema 注册 | +| 深色模式适配问题 | 2026-05-31 | F11 修复 + B7 测试套件 | + +## 偿还优先级排名 + +| 优先级 | 债务 | 预估投入 | 预期效果 | +|--------|------|---------|---------| +| P0 | TD-1 authorId 硬编码 | 0.5 天 | 数据可关联真实用户 | +| P0 | TD-3 Docker 部署 | 0.5 天 | 后端可运行验证 | +| P1 | TD-7 token 持久化 | 0.5 天 | 免重复登录 | +| P1 | TD-8 加载已有日记 | 1 天 | 编辑器完整可用 | +| P1 | TD-4 toImage 异步 | 1 天 | 消除大笔画卡顿 | +| P2 | TD-2 CI/CD | 1 天 | 自动化质量守卫 | +| P2 | TD-5 前端测试 | 3 天 | 回归保护 | diff --git a/wiki/architecture.md b/wiki/architecture.md new file mode 100644 index 0000000..c9450da --- /dev/null +++ b/wiki/architecture.md @@ -0,0 +1,120 @@ +--- +title: 架构决策 +updated: 2026-06-01 +status: active +tags: [architecture, base, multi-tenant, security] +--- + +# 架构决策 + +> 从 [[index]] 导航。关联: [[data-layer]] [[erp-diary]] [[frontend]] + +## 1. 设计决策 + +### Q: 为什么从 HMS 剥离而不是 fork? + +独立 `base.git` 仓库(https://git.stableeasy.com/iven/base.git)作为通用 ERP 基座,多个项目可独立克隆后添加业务模块。fork 会将两个项目的 git 历史绑定,无法独立演进。 + +### Q: 为什么用 Feature Flag 组装模块? + +`cargo build --features diary` 按需引入暖记模块。基座 crate(auth/config/message/workflow/plugin)不需要 diary 功能时零开销。 + +### Q: 为什么选 Flutter 而不是原生? + +Android + iOS 跨平台首发。CustomPainter + Listener 手写性能满足 <16ms 目标,一套代码双端覆盖。 + +### Q: 为什么选 BLoC 不是 Provider/Riverpod? + +编辑器、日历、同步引擎等复杂交互需要 Event/State 显式建模。BLoC 的单向数据流在复杂场景下比 Provider 更可控。 + +### 基座剥离 — 7 个耦合点 + +从 HMS 剥离到 base.git 时遇到的耦合及解决方案: + +| 耦合点 | 问题 | 解决 | +|--------|------|------| +| main.rs 业务初始化 | 硬编码 health 模块注册 | 改为 Feature Flag 动态注册 | +| AppState 业务字段 | 嵌入 HealthState 等 | 改为 trait object / AnyMap | +| 迁移文件混合 | 基座 + 业务迁移在同一目录 | 按 prefix 分类,业务迁移独立 | +| 路由注册 | 业务路由写死在基座 | Module trait 的 routes() 方法 | +| 事件定义 | 业务事件在基座 crate | 各模块自定义 event.rs | +| 权限 seed | 业务权限码在基座迁移 | 各模块自带 seed 迁移 | +| Cargo.toml | 业务依赖在 workspace 根 | 各业务 crate 独立管理 | + +## 2. 关键文件 + 数据流 + +### 仓库拓扑 + +``` +HMS (G:\hms) [只读] + └─复制→ base.git (https://git.stableeasy.com/iven/base.git) + └─克隆→ nj.git (暖记 = 基座 + erp-diary + Flutter) +``` + +### Cargo Workspace + +``` +nj/crates/ +├── erp-core/ # 基座 — 模块系统/事件/加密/错误 +├── erp-auth/ # 基座 — JWT/RBAC/用户/角色 +├── erp-config/ # 基座 — 字典/菜单/设置 +├── erp-message/ # 基座 — 消息/通知/SSE +├── erp-workflow/ # 基座 — BPMN 工作流 +├── erp-plugin/ # 基座 — WASM 插件运行时 +├── erp-server/ # Axum 入口 + 迁移 + 路由组装 +└── erp-diary/ # 暖记业务模块 (~5,500 行新增) +``` + +### 集成契约 + +| 方向 | 模块 | 接口 | 触发时机 | +|------|------|------|---------| +| 被调用 ← | erp-server | `DiaryModule::new()` | 启动时 feature=diary | +| 调用 → | erp-core | `EventBus` | diary.created 等事件 | +| 调用 → | erp-auth | `require_permission()` | handler 层权限守卫 | +| 提供 → | erp-server | `routes()` + `entities()` | Axum 路由注册 | + +## 3. 代码逻辑 + +### 不变量 + +⚡ **G:\hms 只读** — 绝不修改 HMS 源码,所有操作在 nj 仓库进行 + +⚡ **业务 crate 间无直接依赖** — erp-diary 不 import erp-health,只通过 EventBus 通信 + +⚡ **所有查询带 tenant_id** — 多租户中间件自动注入,API 路径不含 tenant_id + +⚡ **软删除不硬删** — 所有 delete 操作设置 deleted_at,不做 DELETE FROM + +⚡ **version 乐观锁** — 所有 Entity 的 version 字段用于同步冲突检测 + +### 多租户隔离策略 + +中间件从 JWT 提取 tenant_id → 注入请求扩展 → handler/service 层自动过滤。API 路径不含 tenant_id(`/api/v1/diary/...` 而非 `/{tenant}/diary/...`)。 + +### PIPL 合规 + +- 未满 14 岁必须家长授权 +- 最小必要数据(昵称 + 年级,无需真实姓名) +- AES-256-GCM 加密 + TLS 传输 +- 30 天内注销删除所有关联数据 +- 班级码:6 位混合码,5 次错误锁定 30 分钟 + +## 4. 活跃问题 + 陷阱 + +| 问题 | 级别 | 状态 | 说明 | +|------|------|------|------| +| 上下文窗口耗尽 | HIGH | 已缓解 | CLAUDE.md §8 会话交接机制 | +| Windows Defender 锁定 exe | MEDIUM | 需手动 | 排除 target/ 目录 | +| Docker 部署未验证 | MEDIUM | 待做 | docker/ 目录存在但未测试 | + +### 历史教训 + +- 基座剥离耗时比预期长(7 个耦合点需逐一解耦) +- Isar 3.x 扩展方法不随传递 import 传播,必须显式 import + +## 5. 变更记录 + +| 日期 | 变更 | +|------|------| +| 2026-06-01 | 初始创建 — 架构决策、基座剥离记录、集成契约 | diff --git a/wiki/data-layer.md b/wiki/data-layer.md new file mode 100644 index 0000000..f22c78f --- /dev/null +++ b/wiki/data-layer.md @@ -0,0 +1,127 @@ +--- +title: 数据层 +updated: 2026-06-01 +status: active +tags: [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 扩展方法需显式 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) | 同步操作队列 | + +### 初始化链 + +```dart +// 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 踩坑记录 | diff --git a/wiki/erp-diary.md b/wiki/erp-diary.md new file mode 100644 index 0000000..43ca667 --- /dev/null +++ b/wiki/erp-diary.md @@ -0,0 +1,101 @@ +--- +title: erp-diary 后端模块 +updated: 2026-06-01 +status: active +tags: [rust, axum, seaorm, diary, api] +--- + +# erp-diary — 暖记后端业务模块 + +> 从 [[index]] 导航。关联: [[architecture]] [[data-layer]] + +## 1. 设计决策 + +### Q: 为什么独立 erp-diary crate? + +基座解耦原则:erp-diary 通过 Feature Flag (`cargo build --features diary`) 按需引入。基座 crate 不依赖业务模块,其他项目可复用基座而不引入日记功能。 + +### Q: DiaryError 设计? + +15 种变体枚举(NotFound / VersionConflict / Unauthorized / ContentUnsafe 等),实现 `Into` 转换为 HTTP 状态码映射。集成测试验证每个错误码的正确 HTTP 响应。 + +### Q: 为什么内容安全过滤在 Service 层? + +日记标题、文字元素的文本需要敏感词检查(含谐音/拼音变体)。放在 Service 层而非 Handler 层,确保即使有新的入口点(事件消费、管理 API)也不会绕过检查。 + +## 2. 关键文件 + 数据流 + +### 模块结构 + +``` +crates/erp-diary/src/ +├── lib.rs (206 行) — DiaryModule 实现 + Feature Flag 注册 +├── dto.rs (569 行) — 请求/响应 DTO + Validate 注解 +├── error.rs (193 行) — DiaryError 15 种变体 → HTTP 状态码 +├── event.rs (61 行) — 事件定义 (diary.created 等) +├── state.rs (13 行) — DiaryState (DiaryModule 专用状态) +├── entity/ (15 文件) — SeaORM Entity +├── service/ (10 文件) — 业务逻辑 +└── handler/ (8 文件) — HTTP Handler + utoipa 注解 +``` + +### Entity 清单 (15 个) + +achievement, class_member, comment, handwriting_stroke, journal_element, journal_entry, parent_child_binding, school_class, sticker, sticker_pack, teacher_profile, template, topic_assignment, user_achievement, user_settings + +### Service 清单 (10 个) + +journal, class, comment, content_safety, achievement, mood_stats, notification, sticker, sync, topic + +### API 端点 + +| 端点前缀 | Handler | 主要操作 | +|---------|---------|---------| +| `/api/v1/diary/journals` | journal_handler | CRUD + 列表过滤 | +| `/api/v1/diary/journals/:id/elements` | journal_handler (同) | 元素 CRUD | +| `/api/v1/diary/classes` | class_handler | 班级 CRUD + 班级码 | +| `/api/v1/diary/comments` | comment_handler | 评论 CRUD | +| `/api/v1/diary/topics` | topic_handler | 主题布置 | +| `/api/v1/diary/achievements` | achievement_handler | 成就系统 | +| `/api/v1/diary/stickers` | sticker_handler | 贴纸管理 | +| `/api/v1/diary/stats` | stats_handler | 心情/写作统计 | +| `/api/v1/diary/sync` | sync_handler | 增量同步 API | + +### 集成契约 + +| 方向 | 模块 | 接口 | 触发时机 | +|------|------|------|---------| +| 提供 → | erp-server | `DiaryModule::routes()` | 启动 feature=diary | +| 调用 → | erp-core | `EventBus::publish()` | 日记创建/更新/删除 | +| 调用 → | erp-auth | `require_permission()` | 每个 handler 入口 | +| 调用 → | erp-message | 通知服务 | 评论/班级事件 | + +## 3. 代码逻辑 + +### 不变量 + +⚡ **所有 Entity 含标准字段** — id / tenant_id / created_at / updated_at / created_by / updated_by / deleted_at / version + +⚡ **软删除** — 查询带 `deleted_at IS NULL` 过滤,不做硬删除 + +⚡ **多租户隔离** — 中间件注入 tenant_id,handler 不从 API 路径获取 + +⚡ **权限守卫** — 每个 handler 方法第一行 `require_permission("diary.xxx")` + +⚡ **输入验证** — DTO 使用 `#[derive(Validate)]` + handler 层调 `.validate()` + +⚡ **内容安全** — 日记标题/文字通过 ContentSafetyService 检查 + +## 4. 活跃问题 + 陷阱 + +| 问题 | 级别 | 状态 | 说明 | +|------|------|------|------| +| Docker 部署未验证 | HIGH | 待做 | docker/ 目录存在但未实际运行 | +| CI/CD 未建立 | MEDIUM | 待做 | 无自动化构建/测试/部署 | +| 文件上传未实现 | MEDIUM | 待做 | 照片/贴纸文件上传参考健康模块 | + +## 5. 变更记录 + +| 日期 | 变更 | +|------|------| +| 2026-06-01 | 初始创建 — Entity/Service/Handler 清单、API 端点、集成契约 | diff --git a/wiki/frontend.md b/wiki/frontend.md new file mode 100644 index 0000000..bb1b83e --- /dev/null +++ b/wiki/frontend.md @@ -0,0 +1,118 @@ +--- +title: Flutter 前端 +updated: 2026-06-01 +status: active +tags: [flutter, bloc, design-system, responsive] +--- + +# Flutter 前端 + +> 从 [[index]] 导航。关联: [[handwriting-engine]] [[data-layer]] + +## 1. 设计决策 + +### Q: 为什么 BLoC 不是 Provider/Riverpod? + +编辑器(strokes + elements + undo/redo + autoSave)、同步引擎(pending queue + network status)、日历(date range + mood filter)等复杂交互需要 Event/State 显式建模。BLoC 的单向数据流比 Provider 的 notifyListeners() 更可控。 + +### Q: 为什么 go_router? + +声明式路由 + 深链接支持 + 路由守卫(auth guard 重定向未登录用户)。比 Navigator 2.0 更简洁。 + +### Q: 设计系统 7 色双模主题? + +暖记定位"温暖治愈",浅色模式奶油白(#FFF8F0) + 珊瑚色(#E07A5F) + 鼠尾草绿(#81B29A);深色模式自动映射为暖色调暗色。面向小学生,所有颜色避免冷硬感。 + +### Q: 为什么手写模型不用 freezed? + +Stroke/StrokePoint 是热路径高频创建对象,freezed 生成的代码有额外开销。手写实现 copyWith + toJson/fromJson 更轻量。 + +## 2. 关键文件 + 数据流 + +### 16 个功能模块 + +| 模块 | BLoC | 职责 | +|------|------|------| +| editor | EditorBloc | 手写 + 元素 + 撤销重做 + 自动保存 | +| auth | AuthBloc | 登录/注册/角色选择/班级码加入 | +| home | HomeBloc | 首页日记列表 + 搜索 | +| calendar | CalendarBloc | 日历视图 + 日期过滤 | +| mood | MoodBloc | 心情统计 + 趋势图 | +| class_ | ClassBloc | 班级管理 + 成员列表 | +| achievement | AchievementBloc | 成就徽章系统 | +| stickers | StickerBloc | 贴纸库浏览 + 选择 | +| templates | TemplateBloc | 模板画廊 | +| profile | SettingsBloc | 主题切换 + 个人设置 | +| search | — | 日记搜索 (Isar FTS 待实现) | +| teacher | — | 老师主题发布 + 批改 | +| parent | — | 家长监护 + 数据管理 | +| settings | — | 设置页面 UI | + +### 注入链 (app.dart) + +``` +MultiRepositoryProvider + ├─ ApiClient + ├─ AuthRepository + ├─ JournalRepository (= IsarJournalRepository, 离线优先) + ├─ RemoteJournalRepository (供 SyncEngine) + ├─ SyncEngine + ├─ ClassRepository + └─ SettingsBloc (ChangeNotifier) + └─ BlocProvider + └─ MaterialApp.router (ListenableBuilder 监听主题) +``` + +### 路由表 + +`app_router.dart` (269 行) 定义完整路由: +- `/` → 首页(auth guard 重定向) +- `/login`, `/role-selection`, `/class-code-join` +- `/editor/:id?` → 编辑器 +- `/calendar`, `/mood`, `/class`, `/achievements` +- `/stickers`, `/templates`, `/search` +- `/teacher/*`, `/parent/*` +- `/settings`, `/profile` + +## 3. 代码逻辑 + +### 不变量 + +⚡ **响应式断点** — 手机 <600px 底部 TabBar 单列 / 平板 600-1024px 侧边双栏 / 桌面 >1024px 三栏 + +⚡ **触摸目标 ≥ 44px** — 面向小学生,所有可交互元素不小于 44px + +⚡ **设计 Token 统一管理** — `core/constants/design_tokens.dart` 定义间距/圆角/阴影 + +⚡ **SettingsBloc 用 ChangeNotifier** — 不用 BLoC 因为设置是全局状态,ChangeNotifier + ListenableBuilder 更轻量 + +### 主题系统 + +``` +AppTheme.light() / AppTheme.dark() + ├─ 7 色 × 2 模式 (bg/accent/secondary/tertiary/fg/surface/rose) + ├─ 3 套字体 (Noto Sans SC / Caveat / JetBrains Mono) + ├─ 4 级圆角 (10/16/22/28/pill) + └─ 动画曲线 cubic-bezier(0.34, 1.56, 0.64, 1) — 弹性过冲 +``` + +## 4. 活跃问题 + 陷阱 + +| 问题 | 级别 | 状态 | 说明 | +|------|------|------|------| +| 编辑器不加载已有数据 | HIGH | 待做 | journalId 非空时需从 Isar 读取 | +| 搜索功能空壳 | MEDIUM | 待做 | Isar FTS 未实现 | +| 深色模式细节 | LOW | 持续 | 部分组件深色适配需检查 | + +### 历史教训 + +- F11 深色模式修复需要 bloat bloc 测试套件同步更新 (05317d5) +- NuanjiApp 是 StatelessWidget,build() 可被调用多次 → 全局依赖应在 build() 中创建单例 + +## 5. 变更记录 + +| 日期 | 变更 | +|------|------| +| 2026-06-01 | IsarJournalRepository 注入为主 JournalRepository (2481c8f) | +| 2026-06-01 | 设置页 UI + Mood/成就/贴纸 BLoC (8331db6) | +| 2026-06-01 | 初始创建 — 16 模块地图、注入链、设计系统 | diff --git a/wiki/handwriting-engine.md b/wiki/handwriting-engine.md new file mode 100644 index 0000000..2ba5825 --- /dev/null +++ b/wiki/handwriting-engine.md @@ -0,0 +1,124 @@ +--- +title: 手写引擎 +updated: 2026-06-01 +status: active +tags: [handwriting, performance, canvas, perfect-freehand] +--- + +# 手写引擎 + +> 从 [[index]] 导航。关联: [[data-layer]] [[frontend]] + +暖记的**核心价值** — 保留用户真实笔迹。目标:<16ms 延迟(p99),在 Samsung Tab A9 / iPad 10th 上流畅运行。 + +## 1. 设计决策 + +### Q: 为什么用 Listener 不用 GestureDetector? + +GestureDetector 内部有手势竞技场(gesture arena),额外延迟约 5-8ms。Listener 直接接收 PointerDownEvent/PointerMoveEvent,跳过竞技场,延迟最低。 + +### Q: 为什么双层 Canvas 架构? + +单层 Canvas 每帧需要重绘全部 N 条笔画,复杂度 O(N×P)。双层架构将已完成笔画光栅化为 ui.Image 位图,每帧只绘制当前活跃笔画,复杂度降至 O(P_current)。 + +### Q: 为什么选 perfect_freehand? + +支持压力感知、速度相关的变宽笔画,开箱即用的高质量笔迹渲染。4 种画笔(钢笔/铅笔/马克笔/橡皮擦)通过不同参数配置实现。 + +### Q: 笔画存储策略? + +HandwritingStroke 作为独立 SeaORM Entity,大字段(points JSON)隔离,日记列表查询时延迟加载。前端 Isar 中笔画序列化为 handwriting_ref 元素,固定 ID `${journalId}_strokes`。 + +## 2. 关键文件 + 数据流 + +### 核心文件 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `features/editor/widgets/handwriting_canvas.dart` | 307 | Listener + 双层 Stack + 掌心抑制 | +| `features/editor/widgets/stroke_cache.dart` | 303 | StrokeRasterCache 光栅化 + 合成 | +| `features/editor/widgets/active_stroke_painter.dart` | 35 | 实时绘制当前笔画 | +| `features/editor/widgets/cached_strokes_painter.dart` | 35 | drawImage 绘制缓存位图 | +| `features/editor/widgets/stroke_renderer.dart` | ~120 | 4 种画笔渲染(pen/pencil/marker/eraser) | +| `features/editor/widgets/stroke_model.dart` | 112 | Stroke/StrokePoint 数据模型 | + +### 数据流 + +``` +PointerDown/Move/Up Events + │ (Listener, not GestureDetector) + ▼ +StrokePoint (x, y, pressure, timestamp) + │ accumulate + ▼ +Stroke (id, points[], brushType, color, width) + │ ValueNotifier → 触发重绘 + ▼ +ActiveStrokePainter ──── 实时绘制当前笔画 + │ onStrokeCompleted + ▼ +StrokeRasterCache.addStroke() + │ toImage() 光栅化 + ▼ +compositeImage (ui.Image) ──── 所有完成笔画合成位图 + │ + ▼ +CachedStrokesPainter ──── drawImage 绘制缓存 +``` + +## 3. 代码逻辑 + +### O(1) 点缓冲 + +```dart +// 旧方案:O(N²) — 每次 rebuild 都 spread 整个列表 +final points = [..._allPoints, newPoint]; + +// 新方案:O(1) — 可变缓冲区 + ValueNotifier +_currentPoints.add(newPoint); +_pointsNotifier.value = _currentPoints; +``` + +### 橡皮擦实现 + +``` +saveLayer() // 保存当前画布状态 + drawPath(eraserPath, BlendMode.dstOut) // 用 dstOut 混合模式"擦除" +restore() // 恢复,不穿透背景 +``` + +### 模式切换 + +``` +旧方案:if (isDrawingMode) Canvas() else ElementLayer() — 销毁重建 +新方案:IgnorePointer(ignoring: !isDrawingMode) — 不销毁,只控制交互 +``` + +### 不变量 + +⚡ **shouldRepaint 守卫** — CachedStrokesPainter 仅在 compositeImage 引用变化时重绘 + +⚡ **_currentPoints 可变缓冲** — 不创建新 List,直接 add + ValueNotifier 通知 + +⚡ **掌心抑制** — 通过 PointerDeviceKind 过滤,仅处理 touch 事件 + +## 4. 活跃问题 + 陷阱 + +| 问题 | 级别 | 状态 | 说明 | +|------|------|------|------| +| toImage() 同步阻塞 | HIGH | 待修 | 光栅化在主线程,大笔画可能卡 UI | +| 画布尺寸变化缓存失效 | MEDIUM | 待修 | 屏幕旋转时需平滑过渡 | +| 橡皮擦视觉反馈 | LOW | 待优化 | 擦除区域无实时预览 | + +### 历史教训 + +- 初版用 GestureDetector,延迟约 20ms → 改用 Listener 降至 <16ms +- 初版 if/else 切换模式会销毁 Canvas → IgnorePointer 保持 Widget 树稳定 +- 单层 Canvas 100+ 笔画时明显卡顿 → 双层 + 光栅化缓存解决 + +## 5. 变更记录 + +| 日期 | 变更 | +|------|------| +| 2026-06-01 | 初始创建 — 双层架构、性能优化记录、活跃问题 | +| 2026-06-01 | 性能优化提交 (e07da7a):双层 Canvas + 光栅化缓存 + O(1) 点缓冲 | diff --git a/wiki/index.md b/wiki/index.md new file mode 100644 index 0000000..91f9e89 --- /dev/null +++ b/wiki/index.md @@ -0,0 +1,84 @@ +# 暖记 (Nuanji) — 知识库 + +> **温暖治愈风格的手写手账日记 App**,面向小学生首发,核心价值是保留真实笔迹。从 [[architecture]] 导航。 + +## 关键数字 + +> 最后更新: 2026-06-01 | 基线: main (2481c8f) + +| 指标 | 值 | +|------|-----| +| Rust crate | 8 个(6 基座 + 1 入口 + erp-diary 新增) | +| Rust 新增代码 | ~5,500 行(erp-diary) | +| Dart 文件 | 70 个(~18,200 行,含生成代码) | +| SeaORM Entity | 15 个(erp-diary) | +| 数据库迁移 | 15 个(diary 相关) | +| BLoC 模块 | 12 个 | +| Flutter features | 16 个 | +| Isar Collection | 3 个(JournalEntry / JournalElement / PendingOperation) | +| 后端测试 | ~50 个通过 | +| flutter analyze | 0 error | +| Git 提交 | 17 次 | + +## 系统数据流 + +``` +用户手写/涂鸦 + │ + ▼ +HandwritingCanvas (Listener → StrokePoint) + │ Stroke + ▼ +EditorBloc (strokes + elements) + │ onSave (2s debounce) + ▼ +IsarJournalRepository ──→ Isar 本地数据库 + │ │ + │ ▼ (启动恢复) + │ SyncEngine (pending queue) + │ │ WiFi 可用 + ▼ ▼ +JournalEntry RemoteJournalRepository ++ JournalElement │ + ▼ + API Client (Dio) + │ + ▼ + Axum → erp-diary → PostgreSQL +``` + +## 模块导航 + +- [[architecture]] — 仓库拓扑、基座继承、Feature Flag、多租户、安全合规 +- [[handwriting-engine]] — 双层 Canvas、光栅化缓存、perfect_freehand、4 种画笔 +- [[data-layer]] — Isar 本地存储、Repository 模式、SyncEngine 离线同步 +- [[frontend]] — Flutter BLoC、16 个功能模块、设计系统、响应式布局 +- [[erp-diary]] — Rust 后端业务模块、Entity/Service/Handler、API 端点、权限码 + +## 症状导航 + +| 症状 | 先查 | 再查 | 常见根因 | +|------|------|------|----------| +| 笔画卡顿 >16ms | [[handwriting-engine]] | 光栅化缓存 | shouldRepaint 守卫失效 / 未用 Listener | +| 编辑器保存失败 | [[data-layer]] | IsarDatabase | Isar 未初始化 | +| Isar `findAll` 未定义 | [[data-layer]] | 扩展方法 | 缺少 `import 'package:isar/isar.dart'` | +| 同步版本冲突 | [[data-layer]] | 乐观锁 | version 不匹配 | +| 深色模式颜色异常 | [[frontend]] | AppTheme | token 未适配深色值 | +| API 返回 403 | [[erp-diary]] | 权限守卫 | 权限码不匹配 | +| 迁移执行失败 | [[architecture]] | 多租户 | 表冲突 / 缺失迁移 | +| 上下文窗口耗尽 | CLAUDE.md §8 | 会话交接 | 长会话未及时交接 | +| 手写穿透背景 | [[handwriting-engine]] | 橡皮擦 | 未用 saveLayer + dstOut | +| 模式切换卡顿 | [[handwriting-engine]] | IgnorePointer | if/else 销毁重建 Widget | +| 热重载后 Isar 崩溃 | [[data-layer]] | 初始化 | 未 close 就 re-open | +| 编辑器加载空白 | [[frontend]] | EditorBloc | journalId 有值但未加载 Isar 数据 | +| SyncEngine 队列丢失 | [[data-layer]] | 持久化 | 退出时未调 persistPendingQueue | + +## 相关文档 + +| 文档 | 位置 | +|------|------| +| 产品设计规格 v1.2 | `docs/superpowers/specs/2026-05-31-nuanji-warm-notes-design.md` | +| 实施规划 v2.1 | `plans/hazy-petting-lampson.md` | +| 项目协作规则 | `CLAUDE.md` | +| 技术债看板 | `docs/tech-debt-board.md` | +| 基座仓库 | https://git.stableeasy.com/iven/base.git |