161 lines
4.8 KiB
Markdown
161 lines
4.8 KiB
Markdown
# 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<ReadingPosition>, // 最后位置
|
||
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<ReadingPosition>, // 阅读位置(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<ReadingEventV2>
|
||
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()
|
||
```
|