# 学习信息收集 API Contract > M8 | 版本 v1.0 | 2026-06-08 > > 所有响应 shape、错误码以本文档为准。 > 设计逻辑参见 [学习信息收集总设计](./learning-info-design.md)。 > 上传协议详见 [阅读事件上传协议](./reading-event-api-protocol.md)。 --- ## 1. 基础信息 | 项目 | 值 | |------|----| | Base Path | `/learning` / `/materials` / `/activity` | | Auth | Bearer JWT (所有端点需要) | | Content-Type (Request) | `application/json` | | Content-Type (Response) | `application/json` | | Batch Limit | 100 条/次 | --- ## 2. 端点总览 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/learning/reading-events/batch` | 批量上报阅读事件 | | GET | `/materials/:id/reading-progress` | 查询资料阅读进度 | | GET | `/learning/continue` | 首页继续学习 | | GET | `/learning/summary` | 学习摘要 | | GET | `/learning/trend?days=7` | 阅读趋势 | | GET | `/activity/heatmap?days=365` | 学习热力图 | | GET | `/learning/records?cursor=&limit=20&type=reading` | 学习历史记录 | | POST | `/internal/learning/reading-events/:id/reprocess` | 重处理单事件 | | POST | `/internal/learning/reading-events/reprocess-failed` | 批量重处理失败事件 | --- ## 3. 上报阅读事件 ### POST /learning/reading-events/batch ```json // Request { "events": [{ "eventId": "550e8400-e29b-41d4-a716-446655440001", "clientSessionId": "550e8400-e29b-41d4-a716-446655440000", "materialId": "cuid_mat_001", "readingTargetType": "knowledge_source", "eventType": "material_opened", "position": { "type": "Markdown", "blockId": "intro", "scrollProgress": 0.25 }, "activeSecondsDelta": 0, "clientTimestampMs": 1717800000000, "sequence": 1, "platform": "ios", "appVersion": "1.2.3", "clientTimezoneOffsetMinutes": -480 }] } // Response { "processed": 1, "duplicate": 0, "failed": 0, "warnings": [] } ``` ### 校验规则 | 字段 | 规则 | 失败处理 | |------|------|----------| | eventId | UUID v4, userId+eventId unique | DUPLICATE_EVENT | | activeSecondsDelta | 0 ✅, <0 ❌, >300 截断+warning | INVALID_ACTIVE_SECONDS | | readingTargetType | knowledge_source / temporary_file | INVALID_TARGET_TYPE | | eventType | 5 种之一 | INVALID_EVENT_TYPE | | materialId | knowledge_source: KnowledgeSource 存在+归属,temporary_file: 存在+归属+未过期 | MATERIAL_ACCESS_DENIED / SOURCE_DELETED | --- ## 4. 查询资料阅读进度 ### GET /materials/:id/reading-progress?readingTargetType=knowledge_source ```json // Response (有记录) { "status": "reading", "lastPosition": { "type": "Markdown", "blockId": "ch1", "scrollProgress": 0.5 }, "lastProgress": 0.5, "totalActiveSeconds": 120, "isMarkedRead": false, "firstOpenedAt": "2026-06-01T00:00:00Z", "lastReadAt": "2026-06-08T12:00:00Z" } // Response (无记录) { "status": "not_started", "lastPosition": null, "lastProgress": null, "totalActiveSeconds": 0, "isMarkedRead": false } // Response (权限拒绝) { "status": "not_started", "reason": "MATERIAL_ACCESS_DENIED" } ``` --- ## 5. 首页继续学习 ### GET /learning/continue ```json // Response (有数据) { "type": "knowledge_source", "materialId": "cuid_mat_001", "title": "Document Title", "lastPosition": { "type": "Pdf", "pageNumber": 3, "pageProgress": 0.5, "overallProgress": 0.32 }, "lastProgress": 0.32, "totalActiveSeconds": 1200, "lastReadAt": "2026-06-08T12:00:00Z" } // Response (无数据) { "type": "none" } ``` --- ## 6. 学习摘要 ### GET /learning/summary ```json { "todaySeconds": 300, "weekSeconds": 1800, "totalSeconds": 7200, "activeDays": 12, "sessionsCount": 20, "materialsReadCount": 5, "markedReadCount": 2, "dailyAverageSeconds": 600 } ``` --- ## 7. 阅读趋势 ### GET /learning/trend?days=7 | 参数 | 默认 | 最大 | |------|------|------| | days | 7 | 90 | ```json { "days": 7, "series": [ { "date": "2026-06-02", "value": 120 }, { "date": "2026-06-03", "value": 0 }, { "date": "2026-06-04", "value": 300 } ] } ``` --- ## 8. 学习热力图 ### GET /activity/heatmap?days=365 | 参数 | 默认 | 最大 | |------|------|------| | days | 365 | 365 | ```json { "2026-06-01": 120, "2026-06-02": 0, "2026-06-03": 300 } ``` --- ## 9. 学习历史记录 ### GET /learning/records?cursor=&limit=20&type=reading | 参数 | 默认 | 说明 | |------|------|------| | cursor | — | 分页游标(记录 id) | | limit | 20 | 最大 50 | | type | — | recordType 过滤 | ```json { "items": [{ "id": "cuid_rec_001", "recordType": "reading", "title": "Reading started", "description": null, "durationSeconds": 120, "occurredAt": "2026-06-08T12:00:00Z", "metadata": { "materialId": "cuid_mat_001", "readingTargetType": "knowledge_source", "knowledgeBaseId": "kb_001", "totalActiveSeconds": 120, "lastPosition": { "type": "progress", "progress": 0.5 } }, "createdAt": "2026-06-08T12:00:00Z" }], "nextCursor": "cuid_rec_021" } ``` --- ## 10. 重处理(Internal) ### POST /internal/learning/reading-events/:id/reprocess?force=true - failed/pending 事件可重处理 - processed 事件需 `?force=true` - 返回 `{ id, result: { outcome, warnings } }` ### POST /internal/learning/reading-events/reprocess-failed?limit=50 - 批量重处理 status=failed 事件 - limit 默认 50,最大 200 - 返回 `{ reprocessed: N, results: [{ id, outcome }] }` --- ## 11. 错误码 | 码 | 类型 | 含义 | |----|------|------| | MATERIAL_NOT_FOUND | error | knowledge_source 不存在 | | TEMPORARY_MATERIAL_NOT_FOUND | error | temporary_file 不存在 | | MATERIAL_ACCESS_DENIED | error | 不属于当前用户 | | TEMPORARY_MATERIAL_EXPIRED | error | 临时文件已过期 | | INVALID_TARGET_TYPE | error | 未知 readingTargetType | | INVALID_EVENT_TYPE | error | 未知 eventType | | INVALID_ACTIVE_SECONDS | error | delta < 0 | | BATCH_LIMIT_EXCEEDED | error | 超过 100 条 | | ACTIVE_SECONDS_CAPPED | warning | delta > 300 截断 | | CLIENT_TIMESTAMP_SKEWED | warning | 时钟偏差 > 5min | | POSITION_IGNORED | warning | position 无效 | | DUPLICATE_EVENT | warning | 幂等重放 | | SOURCE_DELETED | warning | 来源已删除 |