445 lines
27 KiB
Markdown
445 lines
27 KiB
Markdown
# 02 学习信息收集 — 跨项目完整能力分析
|
||
|
||
> 试点能力二:从 C 端业务能力"学习信息收集"出发,跨项目完整整理。
|
||
> 项目角色分类:主链路(iOS、Rust Document Runtime、主 API、数据库)、上游支撑(无直接上游)、下游消费(Heavy Runtime)、运维可观测(Admin)。
|
||
|
||
---
|
||
|
||
## 1. 能力目标
|
||
|
||
自动采集用户在学习过程中的所有行为数据(阅读行为、学习时长、阅读位置、标记已读等),经过 Rust 内存缓冲、iOS 本地持久化队列、批量上传、服务端去重验证、聚合计算后,为学习分析、继续学习推荐提供数据基础。
|
||
|
||
---
|
||
|
||
## 2. C 端入口
|
||
|
||
学习信息收集是一个**用户不可见的自动能力**。用户不需要主动触发,系统在以下场景自动采集:
|
||
|
||
| 触发场景 | 收集的数据 | iOS 页面 | 驱动方式 |
|
||
|---------|-----------|---------|---------|
|
||
| 用户打开一份资料 | material_opened 事件 | 资料阅读页 | iOS 调用 Rust push |
|
||
| 用户滚动阅读内容 | position_changed 事件(2 秒防抖) | 资料阅读页 | iOS 调用 Rust push |
|
||
| iOS 15 秒定时器触发 | heartbeat 事件 | 资料阅读页(自动) | iOS Timer → Rust heartbeat 接口 |
|
||
| 用户关闭/离开资料 | material_closed 事件 | 资料阅读页(自动) | iOS 调用 Rust close → push |
|
||
| 用户点击"标记已读" | marked_as_read 事件 | 资料阅读页 | iOS 调用 Rust push |
|
||
| 用户开始专注学习 | 学习会话计时开始 | 学习会话页 | iOS 本地计时器(用户主动触发) |
|
||
|
||
用户在分析页和学习主页**消费**这些数据的结果,但数据采集本身是透明的。
|
||
|
||
---
|
||
|
||
## 3. 用户主流程
|
||
|
||
### 3.1 数据采集阶段(iOS + Rust Document Runtime)
|
||
|
||
1. 用户打开资料进入阅读页。iOS 调用 Rust start_reading_session,Rust 创建 V2 自动阅读会话,生成唯一的 client_session_id。iOS 调用 Rust push_material_opened,事件进入 Rust 内存缓冲区(Pending)。
|
||
|
||
2. 阅读过程中:
|
||
- 用户滚动时,iOS 以 2 秒防抖调用 Rust push_position_changed。
|
||
- iOS 创建并管理 15 秒心跳定时器。仅在阅读会话为 active 且 App 位于前台时触发。每次 Tick,iOS 调用 Rust heartbeat 接口,Rust 根据 ActiveTimeTracker 计算 activeSecondsDelta,创建 heartbeat 事件并写入缓冲区。
|
||
- Rust 不创建也不管理 iOS 的系统定时器。
|
||
|
||
3. 用户点击"标记已读",iOS 本地更新 MarkedReadStore,同时调用 Rust push_marked_as_read。
|
||
|
||
4. 用户离开资料,iOS 调用 Rust close_reading_session,Rust 计算最终活跃时长增量,推送 material_closed 事件。
|
||
|
||
### 3.2 Rust 内存缓冲与交接阶段(Rust → iOS 本地队列)
|
||
|
||
5. 所有事件暂存在 Rust 内存缓冲区中,状态为 Pending。
|
||
|
||
6. iOS 周期性地调用 Rust export_pending_events_v2 导出事件(事件状态变为 Exported)。**Rust 缓冲区不是网络上传队列**——导出仅完成从 Rust 到 iOS 的内存交接。
|
||
|
||
7. iOS 将导出的原始事件映射为 UploadItem(补充 knowledgeBaseId、platform、appVersion、timezone 等元数据)。
|
||
|
||
8. iOS 将 UploadItem 成功写入持久化 ReadingEventUploadQueue(JSON 文件)后,**立即**调用 Rust ack_events_v2。Rust 从内存缓冲区删除已完成交接的事件。
|
||
|
||
9. 后续 API 上传完全由 iOS 本地持久化队列负责。API 上传失败时,只更新本地队列条目的 failed/dead/retry 状态,不再调用 Rust markFailed。
|
||
|
||
### 3.3 上传阶段(iOS 本地持久化队列 → API)
|
||
|
||
10. iOS 上传队列定期调用 flush,从本地持久化队列中取出事件,批量 POST /learning/reading-events/batch(每批最多 100 条)。
|
||
11. 上传中的条目状态为 uploading。API 上传成功后,从 iOS 本地队列删除该条目记录。
|
||
12. 上传失败时:
|
||
- 401(未授权):暂停队列(保留 pending),等待 Token 刷新
|
||
- 403/404(禁止/不存在):标记条目为永久失败(dead)
|
||
- 其他错误:标记条目为 failed,进入指数退避重试(5s × 2^n,最多 3 次,最长 5 分钟)
|
||
- 重试耗尽:条目进入 dead 状态
|
||
|
||
### 3.4 崩溃恢复的三种场景
|
||
|
||
13. **export 后、本地写入前崩溃:** Rust reload_stale_events_v2 将所有 Exported 未 Ack 事件重置为 Pending。下次启动重新导出。
|
||
14. **本地写入成功、Ack 前崩溃:** 重新启动后从 Rust 重新导出(含已持久化但未 Ack 的事件)。API 端通过 eventId 去重。iOS 再次 Ack 完成交接。
|
||
15. **Ack 后、API 上传前崩溃:** 事件已安全在 iOS 本地持久化队列中,Rust 不再参与。重启后本地队列继续上传。
|
||
|
||
### 3.5 服务端处理阶段(API)
|
||
|
||
16. API 收到事件批次后,逐条处理每个事件。单条事件处理在一个数据库事务中完成:
|
||
- **验证:** 校检事件类型、目标类型、活跃时长(上限 300 秒)、时间戳。
|
||
- **去重:** 检查 userId + eventId 是否已处理,已处理的标记为 duplicate。
|
||
- **权限校验:** 验证材料存在、属于当前用户、未删除、未过期。
|
||
- **事件入库:** 插入 ReadingEvent 行。
|
||
- **自动阅读会话聚合:** Upsert LearningSession(按 clientSessionId,增量累加 totalActiveSeconds)。
|
||
- **进度聚合:** Upsert MaterialReadingProgress(按 userId+materialId)。
|
||
- **每日活动聚合:** Upsert DailyLearningActivity(按 userId+localDate,时区感知)。
|
||
- **学习记录:** 为里程碑事件(opened/closed/marked_as_read)插入 LearningRecord。
|
||
- **标记完成:** 事件 status 更新为 processed。
|
||
|
||
### 3.6 数据消费阶段(iOS 查询)
|
||
|
||
17. 用户在分析页和学习主页看到的结果:
|
||
- **继续学习:** GET /learning/continue → 最近未读完的资料。
|
||
- **学习摘要:** GET /learning/summary → 今日/本周/总计分钟、活跃天数。
|
||
- **阅读趋势:** GET /learning/trend?days=7 → 每日阅读时长序列。
|
||
- **连续天数:** GET /activity/streak → 当前连续、最长连续。
|
||
- **活动摘要:** GET /activity/summary → 总分钟、复习卡片、活跃天、日均。
|
||
- **学习记录:** GET /learning/records → 分页学习记录列表。
|
||
|
||
---
|
||
|
||
## 4. 项目参与关系
|
||
|
||
### iOS(AIStudyApp)— 主链路参与者
|
||
|
||
**职责:**
|
||
- 驱动 Rust Document Runtime 的自动阅读会话和事件采集
|
||
- 创建并管理 15 秒心跳定时器(仅在 session active 且前台时触发,每次 Tick 调用 Rust heartbeat 接口)
|
||
- 管理阅读位置本地缓存和标记已读本地缓存
|
||
- 管理本地上传队列(独立于 Rust 缓冲区的网络上传队列):事件映射、JSON 持久化、重试策略、从队列删除已上传条目
|
||
- Rust → iOS 事件交接:export → UploadItem 映射 → 持久化写入 → **立即 Ack**(不等待 API 上传)
|
||
- 提供网络恢复自动上传(NetworkMonitor 监听)
|
||
- 崩溃恢复:App 重启时调用 RuntimeRecoveryService
|
||
- 提供专注学习会话页的本地计时
|
||
- 查询并展示学习分析数据
|
||
|
||
### Rust Document Runtime(zhixi-document-runtime)— 主链路参与者
|
||
|
||
**职责:**
|
||
- V2 自动阅读会话管理(start/pause/resume/close/interrupt)
|
||
- 活跃时间跟踪器(ActiveTimeTracker),由 iOS heartbeat tick 驱动计算 activeSecondsDelta
|
||
- 5 种阅读事件类型的创建和推送(opened/closed/position_changed/heartbeat/marked_as_read)
|
||
- 事件内存缓冲区管理(Pending → Exported 状态机)
|
||
- 事件导出(export_pending_events_v2):将事件从内存交接给 iOS
|
||
- 事件确认(ack_events_v2):iOS 本地持久化写入成功后立即调用,Rust 删除已交接事件
|
||
- 崩溃恢复:stale event reload(Exported → Pending)、stale session cleanup
|
||
- 缓冲区容量管理(最大 1000 条,溢出时按优先级淘汰)
|
||
- 阅读位置模型
|
||
|
||
**不负责:** 网络请求、API 调用、事件上传、iOS 系统定时器创建和管理。
|
||
|
||
### 主 API(api-server)— 主链路参与者
|
||
|
||
**职责:**
|
||
- 接收阅读事件批量上传(POST /learning/reading-events/batch,每批最多 100 条)
|
||
- 单条事件处理管线(验证→去重→权限→入库→聚合→记录→标记)
|
||
- 事件去重(userId + eventId 唯一约束)
|
||
- 自动阅读会话聚合(按 clientSessionId upsert,自动处理 completed/interrupted 状态)
|
||
- 资料阅读进度聚合(按 userId + materialId upsert)
|
||
- 每日学习活动聚合(按 userId + localDate upsert,时区感知)
|
||
- 学习记录创建(里程碑事件记录)
|
||
- 异常检测和警告代码
|
||
- 提供学习数据查询端点(/learning/*, /activity/*)
|
||
- 提供内部管理端点(事件重放、失败事件重处理)
|
||
|
||
### Heavy Runtime — 下游数据消费者
|
||
|
||
**不直接参与学习信息收集。** 消费已聚合的学习数据,生成 AI 分析结果(掌握度评估、薄弱点识别、学习建议、测验题目、闪卡)。位于数据采集链的下游,不属于学习信息采集链路。
|
||
|
||
### Python RAG Worker — 上游支撑(不直接参与)
|
||
|
||
**不直接参与学习信息收集。** 负责文档导入后的预处理(解析→分块→嵌入→索引),产生的知识库结构化数据是阅读的前提,但不参与阅读行为的采集、传输和聚合。
|
||
|
||
### Admin(admin-projects)— 运维与可观测
|
||
|
||
**不属于学习信息采集链路的同步环节。** 职责包括:
|
||
- 学习数据总览仪表板
|
||
- 阅读事件/会话/进度/活动/记录查询
|
||
- 用户时间线与诊断
|
||
- 异常数据页(失败/警告/重复事件)
|
||
- 事件重放(单条 + 批量)
|
||
- 学习数据重算
|
||
- 数据导出
|
||
|
||
---
|
||
|
||
## 5. 项目间调用关系
|
||
|
||
### 5.1 数据采集链路(iOS → Rust Document Runtime)
|
||
|
||
| 调用目的 | 方向 | 业务结果 |
|
||
|---------|------|---------|
|
||
| 开启自动阅读会话 | iOS → Rust | 创建 V2 ReadingSession,返回 client_session_id |
|
||
| 推送阅读事件(opened/closed/position_changed/marked_as_read) | iOS → Rust | 事件进入内存缓冲区(Pending) |
|
||
| 心跳 Tick | iOS → Rust | Rust 计算 activeSecondsDelta,创建 heartbeat 事件写入缓冲区 |
|
||
| 活跃时间累计 | iOS → Rust | ActiveTimeTracker 由 iOS heartbeat tick 驱动,暂停时停止 |
|
||
| 导出待交接事件 | iOS → Rust | 返回 Exported 状态的事件列表 |
|
||
| 确认事件交接完成 | iOS → Rust | iOS 本地队列写入成功后立即 Ack,Rust 删除已交接事件 |
|
||
| 崩溃恢复 | iOS → Rust | Exported 未 Ack 事件重置为 Pending;清理超时 24h 的过期会话 |
|
||
| 暂停/恢复/关闭会话 | iOS → Rust | 会话状态切换,时间跟踪同步 |
|
||
|
||
### 5.2 数据上传链路(iOS 本地持久化队列 → API)
|
||
|
||
| 调用目的 | 方向 | 业务结果 |
|
||
|---------|------|---------|
|
||
| 批量上传阅读事件 | iOS 本地队列 → API | POST /learning/reading-events/batch,返回 processed/duplicate/failed |
|
||
| 查询阅读进度 | iOS → API | GET /materials/:id/reading-progress |
|
||
| 查询继续学习 | iOS → API | GET /learning/continue |
|
||
| 查询学习摘要 | iOS → API | GET /learning/summary |
|
||
| 查询学习趋势 | iOS → API | GET /learning/trend?days=N |
|
||
| 查询学习记录 | iOS → API | GET /learning/records |
|
||
| 查询活动摘要/连续天数/推荐 | iOS → API | GET /activity/summary, /activity/streak, /activity/recommendations |
|
||
|
||
### 5.3 数据处理链路(API 内部)
|
||
|
||
| 调用目的 | 方向 | 业务结果 |
|
||
|---------|------|---------|
|
||
| 事件批处理 | API Controller → Event Processor | 逐条处理,单条一个 DB 事务 |
|
||
| 事件去重 | Event Processor → DB | 检查 userId+eventId 唯一性 |
|
||
| 材料权限校验 | Event Processor → DB | 验证存在性、所有权、状态 |
|
||
| 自动阅读会话聚合 | Event Processor → DB(LearningSession) | Upsert by clientSessionId |
|
||
| 进度聚合 | Event Processor → DB(MaterialReadingProgress) | Upsert by userId+materialId |
|
||
| 每日活动聚合 | Event Processor → DB(DailyLearningActivity) | Upsert by userId+localDate |
|
||
| 学习记录创建 | Event Processor → DB(LearningRecord) | 里程碑事件插入 |
|
||
| 事件重放 | API → Event Processor | 重新处理失败事件 |
|
||
| 数据重算 | API → LearningActivityService | 用户或全局学习数据重新计算 |
|
||
|
||
### 5.4 Admin 查询链路(Admin → API,运维可观测)
|
||
|
||
| 调用目的 | 方向 | 业务结果 |
|
||
|---------|------|---------|
|
||
| 学习仪表板 | Admin → API | 总览 KPI |
|
||
| 阅读事件/会话/进度/活动/记录列表 | Admin → API | 分页列表(多条件筛选+排序) |
|
||
| 用户时间线与诊断 | Admin → API | 时序视图+完整诊断 |
|
||
| 异常检测 | Admin → API | 失败/警告/重复事件列表 |
|
||
| 事件重放/数据重算/导出 | Admin → API | 运维操作 |
|
||
|
||
---
|
||
|
||
## 6. 核心业务数据
|
||
|
||
### 6.1 阅读事件(ReadingEvent)
|
||
|
||
- **谁创建:** Rust Document Runtime(在 iOS 端内存中创建),API 入库(MySQL)。
|
||
- **谁更新:** API 更新处理状态(processing → processed/failed/duplicate)和警告代码。
|
||
- **谁查询:** API 在批处理时查重;Admin 在事件列表和异常检测页查询。
|
||
- **用于什么功能:** 所有学习分析的原始数据源。每条事件包含 eventId、clientSessionId、materialId、eventType、position、activeSecondsDelta、timestampMs、sequence、platform、appVersion、timezoneOffset。
|
||
|
||
### 6.2 自动阅读会话(LearningSession,API 聚合自 ReadingEvent)
|
||
|
||
- **谁创建:** API 在收到第一个 material_opened 事件时通过 upsert 创建(按 clientSessionId)。
|
||
- **谁更新:** API 在每次事件处理中 upsert(增量 totalActiveSeconds、更新 lastPosition、更新 lastEventAt)。material_closed 事件触发状态变为 completed。30 分钟无新事件自动标记为 interrupted。
|
||
- **谁查询:** iOS 学习主页(通过继续学习 API 间接使用);Admin 在学习会话页和用户诊断页查询。
|
||
- **用于什么功能:** 自动阅读会话的服务端聚合记录。注意:这是 API 根据 Rust V2 ReadingSession 产生的事件聚合而来的服务端数据对象,与客户端 Rust 内存中的 ReadingSession 是不同的存储层对象。
|
||
|
||
### 6.3 专注学习会话(LearningSessionView 本地计时)
|
||
|
||
- **谁创建:** 用户主动点击"开始学习"按钮后,iOS 创建本地计时器。
|
||
- **谁更新:** 用户操作暂停/继续/结束(含确认弹窗),计时器以 1 秒精度运行。
|
||
- **谁查询:** iOS 学习会话页展示当前进度。
|
||
- **用于什么功能:** 用户主动发起的专注学习时段计时。**与自动阅读会话是不同的业务对象。** 当前 iOS 是否调用专注学习会话 API(LearningSessionService)持久化仍标记为"暂未确认"。
|
||
- **关键区别:** 自动阅读会话随进入/离开资料自动产生;专注学习会话由用户主动开始/结束。自动阅读会话已通过事件链路上报至 API;专注学习会话的 API 持久化调用链待确认。
|
||
|
||
### 6.4 资料阅读进度(MaterialReadingProgress)
|
||
|
||
- **谁创建:** API 在收到某资料第一个事件时 upsert 创建。
|
||
- **谁更新:** API 每次事件处理中 upsert。累计 totalActiveSeconds、sessionCount。更新 lastPosition、lastProgress。marked_as_read 时设置 isMarkedRead=true、status=read。
|
||
- **谁查询:** iOS 在进入阅读页时查询;Admin 在阅读进度页查询。
|
||
- **用于什么功能:** 阅读位置恢复、"继续学习"推荐、阅读进度展示。
|
||
|
||
### 6.5 每日学习活动(DailyLearningActivity)
|
||
|
||
- **谁创建:** API 在收到某用户某日第一个事件时 upsert 创建。
|
||
- **谁更新:** API 每次事件处理中 upsert。增量累加 durationSeconds、readingSeconds、materialsReadCount、markedReadCount。日期按用户时区(clientTimezoneOffsetMinutes)计算。
|
||
- **谁查询:** API 在学习摘要/趋势/活跃热力图/连续天数计算中查询;Admin 在每日活动页查询。
|
||
- **用于什么功能:** 日级别的学习活动聚合、趋势分析、连续天数计算、分析页数据。
|
||
|
||
### 6.6 学习记录(LearningRecord)
|
||
|
||
- **谁创建:** API 为里程碑事件(material_opened/closed/marked_as_read)创建。
|
||
- **谁更新:** 创建后不可修改。
|
||
- **谁查询:** iOS 在分析页查询(最近 5 条);Admin 在学习记录页查询。
|
||
- **用于什么功能:** 用户学习时间线、分析页"最近学习记录"展示。
|
||
|
||
### 6.7 继续学习状态(ContinueLearning)
|
||
|
||
- **谁创建:** API 实时计算(查询最近阅读的未完成资料)。
|
||
- **谁更新:** 不持久化(每次查询实时计算)。
|
||
- **谁查询:** iOS 学习主页(优先级算法首选项)。
|
||
- **用于什么功能:** 用户快速回到上次阅读位置继续学习。
|
||
|
||
### 6.8 学习摘要(LearningSummary)
|
||
|
||
- **谁创建:** API 实时聚合(查询 DailyLearningActivity)。
|
||
- **谁更新:** 不持久化(每次查询实时聚合)。
|
||
- **谁查询:** iOS 学习主页和学习分析页。
|
||
- **用于什么功能:** 今日/本周/总计的阅读分钟、活跃天数等汇总展示。
|
||
|
||
### 6.9 学习趋势(LearningTrend)
|
||
|
||
- **谁创建:** API 实时计算(过去 N 天每日阅读时长序列)。
|
||
- **谁更新:** 不持久化。
|
||
- **谁查询:** iOS 分析页阅读时长趋势图。
|
||
- **用于什么功能:** 展示用户阅读时长的日间变化趋势。
|
||
|
||
---
|
||
|
||
## 7. 查询入口
|
||
|
||
### 用户在哪里查看
|
||
|
||
- **继续学习:** iOS 学习主页主操作卡片。
|
||
- **每周摘要:** iOS 学习主页每周摘要卡片。
|
||
- **学习分析:** iOS 学习分析页(掌握度/总分钟/复习卡片/活跃天/阅读统计/趋势图/连续统计/学习记录/推荐/AI 综合分析)。
|
||
- **资料阅读进度:** iOS 资料详情页(阅读进度卡片)。
|
||
|
||
### Admin 在哪里查看(运维可观测)
|
||
|
||
- **学习仪表板:** Admin 学习数据→总览(KPI 汇总)。
|
||
- **阅读事件列表:** Admin 学习数据→阅读事件页(按用户/材料/状态/类型筛选)。
|
||
- **学习会话列表:** Admin 学习数据→学习会话页。
|
||
- **阅读进度/每日活动/学习记录列表:** Admin 学习数据→对应页面。
|
||
- **用户时间线:** Admin 学习数据→用户时间线。
|
||
- **用户诊断:** Admin 学习数据→用户诊断。
|
||
- **异常数据:** Admin 学习数据→异常数据(失败/警告/重复三 Tab)。
|
||
- **数据导出:** Admin 学习数据→导出。
|
||
|
||
### 系统内部在哪里使用
|
||
|
||
- **活动趋势 AI 分析:** API GET /activity/trend → Heavy Runtime 生成学习趋势洞察(Heavy Runtime 是下游消费者)。
|
||
- **推荐系统:** API GET /activity/recommendations → 学习主页优先级算法。
|
||
- **学习连续天数:** API GET /activity/streak → 连续天数计算。
|
||
- **活跃热力图:** API GET /activity/heatmap → 每日阅读时长热力图。
|
||
|
||
---
|
||
|
||
## 8. 修改入口
|
||
|
||
### 用户主动修改
|
||
|
||
- **标记已读:** iOS 资料阅读页工具栏按钮。本地 MarkedReadStore 即时生效,marked_as_read 事件通过采集链路上报至 API。
|
||
- **取消标记已读:** 本地 MarkedReadStore 即时生效。服务端同步方式暂未确认(当前事件模型只有 marked_as_read,没有 unmarked_as_read 事件类型)。
|
||
- **开始专注学习会话:** iOS 资料阅读页工具栏"开始学习"按钮。
|
||
- **结束专注学习会话:** iOS 学习会话页结束按钮(含确认弹窗)。
|
||
|
||
### Admin 人工修改(运维可观测)
|
||
|
||
- **事件重放:** Admin 单条或批量重放失败事件。
|
||
- **事件重处理:** API 内部端点批量重处理失败事件。
|
||
- **数据重算:** Admin 触发用户或全局学习数据重新计算。
|
||
- **数据导出:** Admin 导出原始事件或会话数据。
|
||
|
||
### 系统自动修改
|
||
|
||
- **事件去重:** API 自动检测并标记重复事件为 duplicate。
|
||
- **活跃时长封顶:** API 将超过 300 秒的 activeSecondsDelta 截断为 300 秒,添加警告。
|
||
- **时间戳偏差检测:** API 检测客户端时间戳与服务器时间的偏差(>5 分钟),添加警告。
|
||
- **乱序事件检测:** API 检测 sequence 不连续的事件,添加警告。
|
||
- **会话自动关闭:** API 在事件处理前自动将 30 分钟无新事件的活跃会话标记为 interrupted。
|
||
- **过期材料自动检测:** API 在事件处理时自动检测临时材料的过期状态。
|
||
- **崩溃恢复:** Rust + iOS 在 App 重启时自动恢复(Rust reload + iOS 队列 reloadOnLaunch)。
|
||
|
||
### 系统配置修改
|
||
|
||
- **批次大小上限:** API 配置(每批最多 100 条)。
|
||
- **活跃时长上限:** API 硬编码 300 秒。
|
||
- **时间戳偏差阈值:** API 硬编码 5 分钟。
|
||
- **会话中断超时:** API 硬编码 30 分钟。
|
||
- **事件缓冲区上限:** Rust Document Runtime 硬编码 1000 条。
|
||
- **上传重试策略:** iOS 硬编码(指数退避 5s×2^n,最多 3 次,最长 5 分钟)。
|
||
|
||
---
|
||
|
||
## 9. 异常与恢复
|
||
|
||
### 9.1 App 崩溃 / 系统杀死恢复
|
||
|
||
- **Rust 侧:** App 重启时调用 reload_stale_events_v2() → 所有 Exported 未 Ack 事件重置为 Pending。调用 cleanup_stale_sessions_v2(now_ms, 24h) → 清理超过 24 小时的过期会话。
|
||
- **iOS 侧:** RuntimeRecoveryService.run() 在 App 启动时执行。处理三种场景:
|
||
1. export 后、本地写入前崩溃 → Rust reload 恢复 Pending,重新导出
|
||
2. 本地写入成功、Ack 前崩溃 → 重新导出后 API 通过 eventId 去重,再 Ack
|
||
3. Ack 后、API 上传前崩溃 → 本地持久化队列重启后继续上传
|
||
- **API 侧:** 下次事件处理时清理超过 30 分钟无新事件的活跃会话(标记为 interrupted)。
|
||
|
||
### 9.2 网络失败重试
|
||
|
||
- **iOS 本地队列:** 401 → 暂停(pending),等待 Token 刷新。403/404 → 永久失败(dead)。其他错误 → failed + 指数退避重试。重试耗尽 → dead。
|
||
- **NetworkMonitor:** 检测到网络恢复时,自动触发本地队列 flush。
|
||
- **API 鉴权:** Token 过期时 APIClient 自动尝试 refresh,成功后重试原请求一次。
|
||
|
||
### 9.3 事件去重
|
||
|
||
- **API 处理:** userId + eventId 唯一约束。重复事件跳过处理、不更新聚合数据,标记为 duplicate。
|
||
- **客户端保障:** Rust V2 每个事件有 UUID v4 的 eventId,崩溃恢复时 eventId 不变,确保重传不会产生重复聚合。
|
||
|
||
### 9.4 事件乱序
|
||
|
||
- **API 处理:** 检测 sequence 不连续 → 添加 OUT_OF_ORDER_EVENT 警告。事件仍然正常处理(不拒绝)。通常由崩溃恢复后重传较早事件引起,不影响聚合结果(增量累加而非绝对值替换)。
|
||
|
||
### 9.5 活跃时长异常
|
||
|
||
- **API 处理:** activeSecondsDelta > 300 秒 → 截断为 300 秒,添加 ACTIVE_SECONDS_CAPPED 警告。Admin 异常页可筛选复核。
|
||
|
||
### 9.6 时间戳偏差
|
||
|
||
- **API 处理:** 客户端时间戳与服务器时差 > 5 分钟 → 添加 CLIENT_TIMESTAMP_SKEWED 警告,事件正常处理。
|
||
|
||
### 9.7 材料被删除或失去权限
|
||
|
||
- **API 处理:** 标记为 FATAL_MATERIAL_NOT_FOUND / FATAL_ACCESS_DENIED / FATAL_TEMPORARY_MATERIAL_EXPIRED。
|
||
- **Admin 恢复:** 无法恢复已删除材料的关联事件。
|
||
|
||
### 9.8 用户切换隔离
|
||
|
||
- **实现方式:** 所有 API 操作基于 JWT Token 中的 userId 进行用户隔离。
|
||
- **阅读事件:** 每个事件在 API 处理时使用当前登录用户的 userId 关联。
|
||
- **所有聚合数据:** 按 userId 分区存储和查询,用户只能看到自己的数据。
|
||
- **Admin:** 可通过 userId 筛选查看任一用户的数据,需 admin 权限。
|
||
|
||
---
|
||
|
||
## 10. 当前完成情况
|
||
|
||
统一使用以下口径:
|
||
- **已完成:** 跨项目链路已接通,用户可以获得最终结果。
|
||
- **部分完成:** 只有部分项目、部分页面或部分场景完成。
|
||
- **占位实现:** 存在结构,但使用 Mock、硬编码或静态数据。
|
||
- **暂未确认:** 无法确认完整调用链。
|
||
|
||
| 组件 | 状态 | 说明 |
|
||
|------|------|------|
|
||
| Rust Document Runtime V2 自动阅读会话 | 已完成 | start/pause/resume/close/interrupt 完整生命周期 |
|
||
| Rust Document Runtime V2 事件采集(5 种) | 已完成 | material_opened/closed/position_changed/heartbeat/marked_as_read |
|
||
| Rust 活跃时间跟踪器 | 已完成 | ActiveTimeTracker,由 iOS heartbeat tick 驱动 |
|
||
| Rust 事件缓冲区管理 | 已完成 | Pending → Exported,export/ack/reload |
|
||
| iOS 15 秒心跳定时器 | 已完成 | iOS 创建 Timer,仅在 session active 且前台时触发,Tick 调用 Rust heartbeat 接口 |
|
||
| Rust → iOS 事件交接链路 | 已完成 | export → 本地队列写入 → 立即 Ack(交接在本地完成,不依赖网络) |
|
||
| iOS 本地上传队列 | 已完成 | JSON 持久化,独立于 Rust 缓冲区的网络上传队列 |
|
||
| iOS 上传重试与网络恢复 | 已完成 | 指数退避 + NetworkMonitor 自动恢复 |
|
||
| iOS 阅读位置本地缓存 | 已完成 | ReadingPositionStore |
|
||
| iOS 标记已读本地缓存 | 已完成 | MarkedReadStore(本地即时生效) |
|
||
| iOS 标记已读服务端同步 | 已完成 | marked_as_read 事件 → 上传链路 → API 聚合 |
|
||
| iOS 取消标记已读服务端同步 | 暂未确认 | 当前事件模型无 unmarked_as_read,API 是否支持待确认 |
|
||
| iOS 崩溃恢复 | 已完成 | RuntimeRecoveryService + 本地队列 reloadOnLaunch |
|
||
| Rust 崩溃恢复 | 已完成 | stale event reload + stale session cleanup |
|
||
| API 事件批处理端点 | 已完成 | POST /learning/reading-events/batch |
|
||
| API 事件去重 | 已完成 | userId+eventId 唯一约束 |
|
||
| API 事件验证 | 已完成 | 类型校验 + 时长上限 + 时间戳检测 + 乱序检测 |
|
||
| API 权限校验 | 已完成 | 材料存在性、归属、状态、过期校验 |
|
||
| API 自动阅读会话聚合 | 已完成 | 按 clientSessionId upsert,自动完成/中断 |
|
||
| API 资料阅读进度聚合 | 已完成 | 按 userId+materialId upsert |
|
||
| API 每日学习活动聚合 | 已完成 | 按 userId+localDate upsert,时区感知 |
|
||
| API 学习记录创建 | 已完成 | 里程碑事件自动创建 |
|
||
| API 学习摘要/趋势/连续天数 | 已完成 | GET /learning/*, GET /activity/* |
|
||
| API 继续学习推荐 | 已完成 | GET /learning/continue |
|
||
| API 活动推荐 | 已完成 | GET /activity/recommendations |
|
||
| API 活跃时长封顶 | 已完成 | 超 300s 截断 + 警告码 |
|
||
| API 时间戳偏差检测 | 已完成 | >5 分钟偏差警告 |
|
||
| 用户切换隔离 | 已完成 | JWT userId + 所有查询按 userId 分区 |
|
||
| Admin 学习仪表板 | 已完成 | KPI 总览(运维可观测) |
|
||
| Admin 事件/会话/进度/活动/记录查看 | 已完成 | 列表+筛选+详情(运维可观测) |
|
||
| Admin 用户时间线与诊断 | 已完成 | 时序视图+完整诊断(运维可观测) |
|
||
| Admin 异常检测 | 已完成 | 失败/警告/重复三视图(运维可观测) |
|
||
| Admin 事件重放 | 已完成 | 单条+批量(运维可观测) |
|
||
| Admin 数据重算 | 已完成 | 触发重算(运维可观测) |
|
||
| Admin 数据导出 | 已完成 | CSV/JSON 导出(运维可观测) |
|
||
| iOS 专注学习会话 API 持久化 | 暂未确认 | 本地计时已完成,代码中未发现调用 LearningSessionService API 的代码 |
|