api-server/docs/learning-info-api.md

264 lines
6.2 KiB
Markdown
Raw Normal View History

# 学习信息收集 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 | 来源已删除 |