Models: ReadingMaterialContext, ReadingEventUploadItem, AppContext Runtime: ReadingRuntimeAdapter, SessionManager, RecoveryService Reader: MaterialReaderView lifecycle, Heartbeat Timer, Position Adapter Export: EventMapper, UploadQueue, export→queue→ack flow Tests: 16 test files covering models, runtime, lifecycle, upload Docs: ios-learning-info-architecture.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13 KiB
iOS 学习信息采集架构设计
IOS-INFO-000 | v1.0 | 2026-06-12
本文档定义 iOS 学习信息采集系统的架构决策、模块职责边界与核心链路。 具体类型定义、API 签名、代码示例见
ios-learning-info-design.md。
1. 三方职责边界
┌──────────────────────────────────────────────────────────────┐
│ iOS App │
│ │
│ ① 生命周期触发 ② ReadingMaterialContext 管理 │
│ ③ readingTargetType 补充 ④ 本地上传队列 │
│ ⑤ ack/reload ⑥ API reading-progress 查询 │
│ ⑦ 继续学习 UI ⑧ 分析页数据展示 │
│ │ │
│ ReadingRuntimeAdapter (FFI) │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Rust Document Runtime (zhixi-document-runtime) │ │
│ │ │ │
│ │ ① 事件生成 (UUID/sequence/delta) │ │
│ │ ② position normalize │ │
│ │ ③ session 管理 + buffer 状态机 │ │
│ │ ④ 只存 materialId,不存 readingTargetType/userId │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
POST /learning/reading-events/batch
GET /materials/:id/reading-progress
GET /learning/continue
GET /learning/summary | /learning/trend | /activity/heatmap
│
▼
┌───────────────────────────────┐
│ API Server │
│ ① 事件落库 + 二次校验 │
│ ② reading-progress 权威数据源 │
│ ③ 继续学习 / 分析 / 历史 │
│ ④ 幂等处理 (duplicate event) │
└───────────────────────────────┘
| 层 | 负责 | 不负责 |
|---|---|---|
| Rust | 事件生成、session、buffer 状态机、position normalize | readingTargetType/userId、上传 API、UI |
| iOS | 生命周期触发、ReadingMaterialContext、上传队列、ack/reload、UI 联动 | 事件生成、聚合分析、直接写业务主表 |
| API | 事件落库、reading-progress 权威数据、继续学习、分析聚合 | 事件生成、本地 buffer 管理 |
2. 核心链路:export → queue → ack
MaterialReader Lifecycle
│
▼
ReadingRuntimeSessionManager
│
▼
ReadingRuntimeAdapter (FFI → Rust)
│
▼
Rust document runtime
① push*.V2 → buffer
② exportPendingEventsV2 → [ReadingEventV2]
│
▼
iOS export + supplement
③ 映射 ReadingEventV2 → ReadingEventUploadItem
④ 补充 readingTargetType, platform, appVersion, timezone
│
▼
ReadingEventUploadQueue
⑤ enqueue (写入本地持久化)
│
▼
ackEventsV2 (告诉 Rust 删除已导出的事件)
⚠️ ack 在写入本地队列成功后立即执行,不等 API 上传
│
▼
UploadScheduler (定时或前台/网络恢复触发)
⑥ POST /learning/reading-events/batch
│
┌────┴────┐
▼ ▼
HTTP 200 HTTP !200 / 网络错误
│ │
▼ ▼
从队列移除 markFailed → keep → 下次 retry
关键时序约束:
- Rust export → iOS 写入本地队列 → Rust ack
- ack 不等待 API 上传(ack 在 export 后立即执行)
- API 上传失败不影响下一轮 export
3. 九条核心原则
原则 1:Rust 只接 materialId
Rust document runtime 的 ReadingMaterialRef V2 仅包含 materialId。
readingTargetType、userId、platform 等由 iOS 在 UploadItem 层补充。
原则 2:iOS 负责 ReadingMaterialContext
ReadingMaterialContext 由 iOS 构造,包含 materialId、readingTargetType、title、knowledgeBaseId、Rust 层的 MaterialType 和 PreviewMode。
原则 3:iOS 上传时补充 iOS 专属字段
ReadingEventUploadItem 中补充 readingTargetType、platform: "ios"、appVersion、timezoneOffsetMinutes。
原则 4:ack 不等 API 上传
Rust exportPendingEventsV2 → iOS 写入 ReadingEventUploadQueue 成功 → Rust ackEventsV2。
ack 在 API 上传之前完成。
原则 5:ReadingPositionStore 只做兜底
位置恢复优先 API GET /materials/:id/reading-progress。
ReadingPositionStore(UserDefaults)仅在 API 不可用或返回 not_started 时使用。
原则 6:API reading-progress 是主数据源
跨设备同步、首次阅读、重新阅读等场景以 API 的 reading-progress 为准。
原则 7:首页继续学习替换硬编码
替换当前硬编码的"推荐继续阅读",替换为 GET /learning/continue 动态数据。
API 返回 type=none 时不显示。
原则 8:iOS 不直接判断聚合结果
iOS 只负责采集、上传、展示 API 结果。 分析判断(薄弱点、下一步建议等)由 API / AI Runtime 产出,iOS 只展示。
原则 9:debug 面板仅开发模式
Debug 面板(日志、buffer 状态、上传队列状态)仅在 #if DEBUG 下编译。
4. 模块结构
Core/
Reading/
ReadingMaterialContext.swift // 上下文类型
ReadingRuntimeAdapter.swift // FFI 适配器协议
ReadingRuntimeSessionManager.swift // 生命周期 + 心跳管理
ReadingPositionStore.swift // 本地位置缓存(兜底)
MaterialReaderView.swift // 阅读页生命周期
HeartbeatTimer.swift // 心跳定时器
Upload/
ReadingEventUploadItem.swift // 上传条目 DTO
ReadingEventUploadQueue.swift // 本地上传队列 + 持久化
UploadScheduler.swift // 定时 + 前后台触发 flush
Networking/
LearningInfoAPIClient.swift // API 客户端
Recovery/
ReadingPositionRestoreService.swift // 位置恢复(API 优先 + 本地兜底)
Home/
ContinueLearningService.swift // 继续学习查询
Analysis/
AnalysisSummaryService.swift // 分析页数据接入
LearningHistoryService.swift // 学习历史
Debug/
ReadingEventDebugPanel.swift // Debug 面板(#if DEBUG)
Tests/
ReadingInfoUploadTests/ // 单元测试 + 集成测试
5. ReadingMaterialContext
ReadingMaterialContext
├── materialId: String
├── readingTargetType: ReadingTargetType (.knowledgeSource | .temporaryFile)
├── title: String?
├── knowledgeBaseId: String?
├── materialType: MaterialType // Rust 侧类型
└── previewMode: PreviewMode // Rust 侧类型
iOS 在 openMaterial(ctx) 时构造并向 Rust 传递 materialId,其余字段在 upload 时补充到 ReadingEventUploadItem。
6. ReadingRuntimeAdapter 协议
protocol ReadingRuntimeAdapter {
// Session
func startSession(material: ReadingMaterialRef, timestampMs: Int64) throws -> String
func closeSession(_ sessionId: String) throws
func pauseSession(_ sessionId: String) throws
func resumeSession(_ sessionId: String) throws
// Events
func pushOpened(...) throws -> ReadingEventV2
func pushClosed(...) throws -> ReadingEventV2
func pushPositionChanged(...) throws -> ReadingEventV2
func pushHeartbeat(...) throws -> ReadingEventV2
func pushMarkedAsRead(...) throws -> ReadingEventV2
// Buffer
func exportEvents(limit: UInt32, timestampMs: Int64) -> [ReadingEventV2]
func ackEvents(_ eventIds: [String]) -> UInt32
func markFailed(_ eventIds: [String]) -> UInt32
func reloadStale() -> UInt32
func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32
}
7. SessionManager 生命周期
MaterialReader.onAppear
→ openMaterial(ctx)
→ startSession(material)
→ pushOpened(sessionId, materialId, ts)
→ startHeartbeat(interval: 15s)
Heartbeat Timer (every 15s)
→ pushHeartbeat(sessionId, materialId, delta, position, ts)
Scroll/Page Change (debounced 500ms)
→ updatePosition(pos)
→ pushPositionChanged(sessionId, materialId, position, ts)
Tap "Mark as Read"
→ markAsRead()
→ pushMarkedAsRead(sessionId, materialId, ts)
MaterialReader.onDisappear
→ stopHeartbeat()
→ pushClosed(sessionId, materialId, delta, ts)
→ closeSession(sessionId)
App → Background
→ exportEvents → enqueue → ackEvents
→ stopHeartbeat()
→ UploadScheduler.flush()
App → Foreground
→ reloadStaleEventsV2
→ cleanupStaleSessions
→ UploadScheduler.flush()
→ startHeartbeat() if reader is active
8. 上传队列
ReadingEventUploadQueue
final class ReadingEventUploadQueue {
private var items: [ReadingEventUploadItem] // 内存
private let persistenceURL: URL // 磁盘持久化
func enqueue(_ items: [ReadingEventUploadItem]) // 写入内存 + 磁盘
func flush() async -> FlushResult // POST batch → 成功则移除
func retryFailed() async -> FlushResult // 网络恢复重试
func reloadOnLaunch() // 启动时加载磁盘缓存 + reloadStale
}
UploadScheduler 触发时机
| 时机 | 动作 |
|---|---|
| App → Background | flush() |
| App → Foreground | flush() |
| Network recovery (NWPathMonitor) | retryFailed() |
| Timer (every 30s) | flush() |
| App launch | reloadOnLaunch() |
9. 阅读位置恢复:API 优先 + 本地兜底
ReadingPositionRestoreService.restore(materialId, readingTargetType)
│
├── ① GET /materials/:id/reading-progress
│ ├── 200 + lastPosition != nil → 使用 API 位置
│ └── 其他 → ②
│
└── ② ReadingPositionStore.shared.get(materialId)
└── 本地缓存兜底
10. API 端点
| 端点 | iOS 用途 |
|---|---|
POST /learning/reading-events/batch |
批量上传事件 |
GET /materials/:id/reading-progress |
位置恢复(主数据源) |
GET /learning/continue |
首页继续学习 |
GET /learning/summary |
分析页摘要 |
GET /learning/trend?days=7 |
趋势曲线 |
GET /activity/heatmap?days=365 |
热力图 |
GET /learning/history |
学习历史 |
11. 错误处理
| 错误类型 | 动作 |
|---|---|
MATERIAL_ACCESS_DENIED |
从队列移除,不重试,toast |
TEMPORARY_MATERIAL_EXPIRED |
从队列移除 |
DUPLICATE_EVENT |
直接 ack 移除 |
SOURCE_DELETED |
toast,从队列移除 |
INVALID_* |
从队列移除,记录日志 |
| 网络错误 | 保留队列,等 NWPathMonitor 通知后 retry |
| HTTP 5xx | 指数退避重试 (1s/2s/4s/8s, max 3 retry) |
12. 离线与登录状态
- 离线:事件正常写入 Rust buffer + 本地队列,export 正常执行,upload 跳过
- 网络恢复:
reloadStaleEventsV2()+flush() - 登录态变化(登出/切换账号):丢弃本地队列(不属于当前用户),
cleanupStaleSessions - 删除资料:API 返回
SOURCE_DELETED→ 从队列移除关联事件
13. 测试范围
| 层级 | 内容 |
|---|---|
| 单元测试 | ReadingMaterialContext 构造、UploadItem 映射、PositionRestore 优先级 |
| 集成测试 | Adapter FFI mock → SessionManager → UploadQueue → API mock |
| UI 测试 | MaterialReader 生命周期触发、Debug 面板 |
14. 实现指导
本文档中的架构决策和模块边界直接指导以下 Issue 的实现:
- IOS-INFO-001:ReadingMaterialContext 实现
- IOS-INFO-003:ReadingRuntimeAdapter 协议定义
- IOS-INFO-006:ReadingRuntimeSessionManager 实现
- IOS-INFO-011:ReadingEventUploadQueue 实现
- IOS-INFO-016:API Client 实现
详细类型定义和代码示例参见 ios-learning-info-design.md。