Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 11s
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>
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 | 来源已删除 |
|