ios-projects/docs/system-map/capabilities/02-学习信息收集.md

27 KiB
Raw Permalink Blame History

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_sessionRust 创建 V2 自动阅读会话,生成唯一的 client_session_id。iOS 调用 Rust push_material_opened事件进入 Rust 内存缓冲区Pending

  2. 阅读过程中:

    • 用户滚动时iOS 以 2 秒防抖调用 Rust push_position_changed。
    • iOS 创建并管理 15 秒心跳定时器。仅在阅读会话为 active 且 App 位于前台时触发。每次 TickiOS 调用 Rust heartbeat 接口Rust 根据 ActiveTimeTracker 计算 activeSecondsDelta创建 heartbeat 事件并写入缓冲区。
    • Rust 不创建也不管理 iOS 的系统定时器。
  3. 用户点击"标记已读"iOS 本地更新 MarkedReadStore同时调用 Rust push_marked_as_read。

  4. 用户离开资料iOS 调用 Rust close_reading_sessionRust 计算最终活跃时长增量,推送 material_closed 事件。

3.2 Rust 内存缓冲与交接阶段Rust → iOS 本地队列)

  1. 所有事件暂存在 Rust 内存缓冲区中,状态为 Pending。

  2. iOS 周期性地调用 Rust export_pending_events_v2 导出事件(事件状态变为 ExportedRust 缓冲区不是网络上传队列——导出仅完成从 Rust 到 iOS 的内存交接。

  3. iOS 将导出的原始事件映射为 UploadItem补充 knowledgeBaseId、platform、appVersion、timezone 等元数据)。

  4. iOS 将 UploadItem 成功写入持久化 ReadingEventUploadQueueJSON 文件)后,立即调用 Rust ack_events_v2。Rust 从内存缓冲区删除已完成交接的事件。

  5. 后续 API 上传完全由 iOS 本地持久化队列负责。API 上传失败时,只更新本地队列条目的 failed/dead/retry 状态,不再调用 Rust markFailed。

3.3 上传阶段iOS 本地持久化队列 → API

  1. iOS 上传队列定期调用 flush从本地持久化队列中取出事件批量 POST /learning/reading-events/batch每批最多 100 条)。
  2. 上传中的条目状态为 uploading。API 上传成功后,从 iOS 本地队列删除该条目记录。
  3. 上传失败时:
    • 401未授权暂停队列保留 pending等待 Token 刷新
    • 403/404禁止/不存在标记条目为永久失败dead
    • 其他错误:标记条目为 failed进入指数退避重试5s × 2^n最多 3 次,最长 5 分钟)
    • 重试耗尽:条目进入 dead 状态

3.4 崩溃恢复的三种场景

  1. export 后、本地写入前崩溃: Rust reload_stale_events_v2 将所有 Exported 未 Ack 事件重置为 Pending。下次启动重新导出。
  2. 本地写入成功、Ack 前崩溃: 重新启动后从 Rust 重新导出(含已持久化但未 Ack 的事件。API 端通过 eventId 去重。iOS 再次 Ack 完成交接。
  3. Ack 后、API 上传前崩溃: 事件已安全在 iOS 本地持久化队列中Rust 不再参与。重启后本地队列继续上传。

3.5 服务端处理阶段API

  1. 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 查询)

  1. 用户在分析页和学习主页看到的结果:
    • 继续学习: GET /learning/continue → 最近未读完的资料。
    • 学习摘要: GET /learning/summary → 今日/本周/总计分钟、活跃天数。
    • 阅读趋势: GET /learning/trend?days=7 → 每日阅读时长序列。
    • 连续天数: GET /activity/streak → 当前连续、最长连续。
    • 活动摘要: GET /activity/summary → 总分钟、复习卡片、活跃天、日均。
    • 学习记录: GET /learning/records → 分页学习记录列表。

4. 项目参与关系

iOSAIStudyApp— 主链路参与者

职责:

  • 驱动 Rust Document Runtime 的自动阅读会话和事件采集
  • 创建并管理 15 秒心跳定时器(仅在 session active 且前台时触发,每次 Tick 调用 Rust heartbeat 接口)
  • 管理阅读位置本地缓存和标记已读本地缓存
  • 管理本地上传队列(独立于 Rust 缓冲区的网络上传队列事件映射、JSON 持久化、重试策略、从队列删除已上传条目
  • Rust → iOS 事件交接export → UploadItem 映射 → 持久化写入 → 立即 Ack(不等待 API 上传)
  • 提供网络恢复自动上传NetworkMonitor 监听)
  • 崩溃恢复App 重启时调用 RuntimeRecoveryService
  • 提供专注学习会话页的本地计时
  • 查询并展示学习分析数据

Rust Document Runtimezhixi-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_v2iOS 本地持久化写入成功后立即调用Rust 删除已交接事件
  • 崩溃恢复stale event reloadExported → Pending、stale session cleanup
  • 缓冲区容量管理(最大 1000 条,溢出时按优先级淘汰)
  • 阅读位置模型

不负责: 网络请求、API 调用、事件上传、iOS 系统定时器创建和管理。

主 APIapi-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 — 上游支撑(不直接参与)

不直接参与学习信息收集。 负责文档导入后的预处理(解析→分块→嵌入→索引),产生的知识库结构化数据是阅读的前提,但不参与阅读行为的采集、传输和聚合。

Adminadmin-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 本地队列写入成功后立即 AckRust 删除已交接事件
崩溃恢复 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 → DBLearningSession Upsert by clientSessionId
进度聚合 Event Processor → DBMaterialReadingProgress Upsert by userId+materialId
每日活动聚合 Event Processor → DBDailyLearningActivity Upsert by userId+localDate
学习记录创建 Event Processor → DBLearningRecord 里程碑事件插入
事件重放 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 自动阅读会话LearningSessionAPI 聚合自 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 是否调用专注学习会话 APILearningSessionService持久化仍标记为"暂未确认"。
  • 关键区别: 自动阅读会话随进入/离开资料自动产生;专注学习会话由用户主动开始/结束。自动阅读会话已通过事件链路上报至 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 → Exportedexport/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_readAPI 是否支持待确认
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 的代码