# 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 ``` **关键时序约束:** 1. Rust export → iOS 写入本地队列 → Rust ack 2. ack 不等待 API 上传(ack 在 export 后立即执行) 3. 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`。