# 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 的代码 |