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