27 KiB
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)
-
用户打开资料进入阅读页。iOS 调用 Rust start_reading_session,Rust 创建 V2 自动阅读会话,生成唯一的 client_session_id。iOS 调用 Rust push_material_opened,事件进入 Rust 内存缓冲区(Pending)。
-
阅读过程中:
- 用户滚动时,iOS 以 2 秒防抖调用 Rust push_position_changed。
- iOS 创建并管理 15 秒心跳定时器。仅在阅读会话为 active 且 App 位于前台时触发。每次 Tick,iOS 调用 Rust heartbeat 接口,Rust 根据 ActiveTimeTracker 计算 activeSecondsDelta,创建 heartbeat 事件并写入缓冲区。
- Rust 不创建也不管理 iOS 的系统定时器。
-
用户点击"标记已读",iOS 本地更新 MarkedReadStore,同时调用 Rust push_marked_as_read。
-
用户离开资料,iOS 调用 Rust close_reading_session,Rust 计算最终活跃时长增量,推送 material_closed 事件。
3.2 Rust 内存缓冲与交接阶段(Rust → iOS 本地队列)
-
所有事件暂存在 Rust 内存缓冲区中,状态为 Pending。
-
iOS 周期性地调用 Rust export_pending_events_v2 导出事件(事件状态变为 Exported)。Rust 缓冲区不是网络上传队列——导出仅完成从 Rust 到 iOS 的内存交接。
-
iOS 将导出的原始事件映射为 UploadItem(补充 knowledgeBaseId、platform、appVersion、timezone 等元数据)。
-
iOS 将 UploadItem 成功写入持久化 ReadingEventUploadQueue(JSON 文件)后,立即调用 Rust ack_events_v2。Rust 从内存缓冲区删除已完成交接的事件。
-
后续 API 上传完全由 iOS 本地持久化队列负责。API 上传失败时,只更新本地队列条目的 failed/dead/retry 状态,不再调用 Rust markFailed。
3.3 上传阶段(iOS 本地持久化队列 → API)
- iOS 上传队列定期调用 flush,从本地持久化队列中取出事件,批量 POST /learning/reading-events/batch(每批最多 100 条)。
- 上传中的条目状态为 uploading。API 上传成功后,从 iOS 本地队列删除该条目记录。
- 上传失败时:
- 401(未授权):暂停队列(保留 pending),等待 Token 刷新
- 403/404(禁止/不存在):标记条目为永久失败(dead)
- 其他错误:标记条目为 failed,进入指数退避重试(5s × 2^n,最多 3 次,最长 5 分钟)
- 重试耗尽:条目进入 dead 状态
3.4 崩溃恢复的三种场景
- export 后、本地写入前崩溃: Rust reload_stale_events_v2 将所有 Exported 未 Ack 事件重置为 Pending。下次启动重新导出。
- 本地写入成功、Ack 前崩溃: 重新启动后从 Rust 重新导出(含已持久化但未 Ack 的事件)。API 端通过 eventId 去重。iOS 再次 Ack 完成交接。
- Ack 后、API 上传前崩溃: 事件已安全在 iOS 本地持久化队列中,Rust 不再参与。重启后本地队列继续上传。
3.5 服务端处理阶段(API)
- 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 查询)
- 用户在分析页和学习主页看到的结果:
- 继续学习: 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 启动时执行。处理三种场景:
- export 后、本地写入前崩溃 → Rust reload 恢复 Pending,重新导出
- 本地写入成功、Ack 前崩溃 → 重新导出后 API 通过 eventId 去重,再 Ack
- 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 的代码 |