api-server/docs/learning-info-api.md
wangdl 38a8629e42
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 11s
feat: M8 学习信息收集系统完整实现
Phase 1-2: 设计文档 + 数据库 (ReadingEvent/MaterialReadingProgress/TemporaryReadingMaterial/LearningSession扩展/DailyLearningActivity扩展/LearningRecord)
Phase 3: 批量上报 + 校验去重 + ReadingEventProcessorService
Phase 4: 4表聚合管线 (LearningSession/MaterialReadingProgress/DailyLearningActivity/LearningRecord)
Phase 5: 查询接口 (progress/continue/summary/trend/heatmap/history/reprocess)
Phase 6: 权限校验 + session中断清理 + API文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 21:09:13 +08:00

264 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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