学习信息收集 API Contract
M8 | 版本 v1.0 | 2026-06-08
所有响应 shape、错误码以本文档为准。
设计逻辑参见 学习信息收集总设计。
上传协议详见 阅读事件上传协议。
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
// 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
// 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
// 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
{
"todaySeconds": 300,
"weekSeconds": 1800,
"totalSeconds": 7200,
"activeDays": 12,
"sessionsCount": 20,
"materialsReadCount": 5,
"markedReadCount": 2,
"dailyAverageSeconds": 600
}
7. 阅读趋势
GET /learning/trend?days=7
{
"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
{
"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 过滤 |
{
"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 |
来源已删除 |