# ReadingEvent V2 协议 ## 概述 V2 阅读事件协议定义了一套完整的学习行为采集框架:**阅读会话 → 事件生成 → buffer 暂存 → export 导出 → ack 确认**。 与 V1 的区别: - V1:无 session,无 ack,无 id 追踪 - V2:session 管理 + 全局事件 buffer + export/ack 确认 + crash recovery ## 核心概念 ### ReadingSessionV2 一次阅读会话。iOS App 一次打开资料即创建一个 session。 ```rust pub struct ReadingSessionV2 { pub client_session_id: String, // UUID v4,会话标识 pub material: ReadingMaterialRef, // 被阅读的资料 pub started_at_ms: i64, // 会话开始时间 pub last_event_at_ms: i64, // 最后一次事件时间 pub next_sequence: u64, // 下一个事件序号(从 1 开始) pub total_active_seconds: u32, // 累计活跃阅读秒数 pub last_position: Option, // 最后位置 pub status: ReadingSessionStatus, // Active/Paused/Closed } ``` #### 生命周期 ``` start → Active → (pause → Paused → resume → Active)* → close → Closed ``` - **Active**:可推送任意类型事件 - **Paused**:仅可推送 delta=0 事件(PositionChanged/MarkedAsRead) - **Closed**:不可推送事件,不可重新打开 ### ReadingMaterialRef ```rust pub struct ReadingMaterialRef { pub material_id: String, } ``` Rust 不存储 `readingTargetType`(如 knowledge/material/course/note),由 iOS 上传时补充。 ### ReadingEventV2 ```rust pub struct ReadingEventV2 { pub event_id: String, // UUID v4,全局唯一事件 ID pub client_session_id: String, // 关联的会话 ID pub material_id: String, // 资料 ID pub event_type: ReadingEventTypeV2, // 事件类型 pub position: Option, // 阅读位置(camelCase JSON) pub active_seconds_delta: u32, // 距上次事件的活跃秒数 pub timestamp_ms: i64, // 客户端时间戳 pub sequence: u64, // session 内递增序号(1-based) } ``` #### eventType 取值 | Rust | API JSON | |------|----------| | `MaterialOpened` | `material_opened` | | `MaterialClosed` | `material_closed` | | `PositionChanged` | `position_changed` | | `Heartbeat` | `heartbeat` | | `MarkedAsRead` | `marked_as_read` | #### activeSecondsDelta 规则 | 事件 | delta | |------|-------| | MaterialOpened | 0 | | PositionChanged | 0 | | MarkedAsRead | 0 | | Heartbeat | ActiveTimeTracker.tick() 返回值 | | MaterialClosed | ActiveTimeTracker.close() 返回值 | ### ActiveTimeTracker iOS 控制 tick 节奏,Rust 计算 delta: ``` start(ts) → tick(ts+15s) → tick(ts+30s) → close(ts+43s) delta=15 delta=15 delta=13 ``` - 暂停时不累计时间 - 时间倒退返回 0 - 余数毫秒累积到下次 tick ### EventBuffer 状态机 ``` push │ ▼ ┌─ Pending ──┐ │ │ export() reload_stale() │ │ ▼ │ Exported ───────┘ │ ┌─────┴─────┐ │ │ ack() mark_failed() │ │ ▼ ▼ (removed) Failed │ export() ──→ 重试 ``` - **Pending**:新事件,等待导出 - **Exported**:已导出,等待 ack。crash 后 `reload_stale_events()` 恢复为 Pending - **Failed**:上传失败,下次 export 重试 - **Ack 后删除**:从 buffer 移除 ### 溢出驱逐 Buffer 容量:1000 条。满时驱逐顺序: 1. Failed(最早失败的) 2. Exported(最早导出的) 3. 最早 Pending ## API 上传字段映射 | Rust ReadingEventV2 | API ReadingEventUploadItem | 来源 | |---------------------|---------------------------|------| | event_id | eventId | Rust UUID | | client_session_id | clientSessionId | Rust UUID | | material_id | materialId | Rust 保存 | | event_type | eventType | Rust→snake_case | | position | position | Rust(camelCase JSON,clamped) | | active_seconds_delta | activeSecondsDelta | ActiveTimeTracker | | timestamp_ms | clientTimestampMs | Rust | | sequence | sequence | session 内递增 | | — | readingTargetType | **iOS 补充** | | — | platform | **iOS 补充** | | — | appVersion | **iOS 补充** | | — | clientTimezoneOffsetMinutes | **iOS 补充** | ## iOS 上传流程 ``` 1. Rust export_pending_events_v2(limit, timestamp) → Vec 2. iOS 遍历事件,补充 readingTargetType/platform/appVersion/timezone 3. iOS 构造 API 请求体 4. POST /reading/events 5. 成功 → Rust ack_events_v2(eventIds) 6. 失败 → Rust mark_events_failed_v2(eventIds) 7. App 启动 → Rust reload_stale_events_v2() ```