docs: 项目 Wiki 知识库 — 7 文件覆盖架构/手写/数据/前端/后端/技术债

新增 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 参考文档链接
This commit is contained in:
iven
2026-06-01 15:08:21 +08:00
parent 2481c8fce6
commit d1a07229e2
9 changed files with 794 additions and 0 deletions

3
.gitignore vendored
View File

@@ -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/

View File

@@ -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/` |

110
docs/tech-debt-board.md Normal file
View File

@@ -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 天 | 回归保护 |

120
wiki/architecture.md Normal file
View File

@@ -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` 按需引入暖记模块。基座 crateauth/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 | 初始创建 — 架构决策、基座剥离记录、集成契约 |

127
wiki/data-layer.md Normal file
View File

@@ -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 踩坑记录 |

101
wiki/erp-diary.md Normal file
View File

@@ -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<AppError>` 转换为 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_idhandler 不从 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 端点、集成契约 |

118
wiki/frontend.md Normal file
View File

@@ -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<AuthBloc>
└─ 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 是 StatelessWidgetbuild() 可被调用多次 → 全局依赖应在 build() 中创建单例
## 5. 变更记录
| 日期 | 变更 |
|------|------|
| 2026-06-01 | IsarJournalRepository 注入为主 JournalRepository (2481c8f) |
| 2026-06-01 | 设置页 UI + Mood/成就/贴纸 BLoC (8331db6) |
| 2026-06-01 | 初始创建 — 16 模块地图、注入链、设计系统 |

124
wiki/handwriting-engine.md Normal file
View File

@@ -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) 点缓冲 |

84
wiki/index.md Normal file
View File

@@ -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 |