duoqi-api/docs/api-reference.md

1106 lines
19 KiB
Markdown
Raw 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.

# Duoqi API Reference
> 多奇服务端 API 接口文档。本文按当前 Fastify 路由和 TypeScript DTO 更新。
## Base URL
| 环境 | Base URL |
|------|----------|
| 生产 | `https://api.duoqi.me/v1` |
| 本地开发 | `http://localhost:3000/v1` |
健康检查是唯一不带 `/v1` 前缀的客户端端点:`GET /health`。
## 通用约定
### 认证
| 类型 | Header | 适用路径 |
|------|--------|----------|
| 无需认证 | - | `/health`, `/v1/auth/guest`, `/v1/auth/huawei`, `/v1/auth/refresh`, `/v1/admin/login` |
| JWT | `Authorization: Bearer <jwt_token>` | 大多数客户端 API |
| Admin JWT | `Authorization: Bearer <admin_jwt_token>` | `/v1/admin/*` |
### 统一响应
```json
{
"success": true,
"data": {},
"error": null
}
```
```json
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input"
}
}
```
分页响应会额外包含:
```json
{
"pagination": {
"total": 100,
"page": 1,
"limit": 20
}
}
```
## 客户端 API
### 健康检查
#### GET /health
认证:无
响应:
```json
{
"success": true,
"data": {
"status": "ok",
"timestamp": "2026-05-05T12:00:00.000Z"
},
"error": null
}
```
### 认证
#### POST /auth/guest
认证:无
限流10 次/分钟
请求:
```json
{
"deviceId": "device-id"
}
```
响应:
```json
{
"success": true,
"data": {
"user": {
"id": "uuid",
"nickname": null,
"avatarUrl": null,
"tier": "free"
},
"tokens": {
"accessToken": "jwt",
"refreshToken": "jwt"
}
},
"error": null
}
```
#### POST /auth/huawei
认证:无
限流10 次/分钟
请求:
```json
{
"authorizationCode": "authorization-code"
}
```
响应同 `/auth/guest`
#### POST /auth/refresh
认证:无
限流10 次/分钟
请求:
```json
{
"refreshToken": "jwt"
}
```
响应:
```json
{
"success": true,
"data": {
"accessToken": "jwt",
"refreshToken": "jwt"
},
"error": null
}
```
#### GET /auth/me
认证JWT
响应:
```json
{
"success": true,
"data": {
"id": "uuid",
"nickname": "用户昵称",
"avatarUrl": "https://example.com/avatar.png",
"tier": "free",
"xpTotal": 150,
"streakDays": 3,
"heartsRemaining": 5,
"dailyXpEarned": 20,
"dailyXpGoal": 50
},
"error": null
}
```
### Flutter 客户端聚合 API
这组接口对应 Flutter 原型使用的主题路线、挑战、资源摘要、商店和订阅合同。
#### GET /app/bootstrap
认证JWT
响应:
```json
{
"success": true,
"data": {
"user": {
"id": "uuid",
"nickname": "知识探险家",
"avatarUrl": null,
"tier": "free",
"level": 1
},
"progress": {
"hearts": 5,
"maxHearts": 5,
"nextHeartRestoreAt": null,
"dailyAttemptsLeft": 5,
"dailyAttemptsMax": 5,
"nextAttemptResetAt": "2026-05-06T00:00:00.000Z",
"xp": 0,
"level": 1,
"xpToNextLevel": 400,
"streakDays": 0,
"checkInDays": 0,
"streakProtectedUntil": null,
"activeTrackId": null,
"isSubscribed": false
},
"tracks": [],
"shopBenefits": [],
"subscription": {
"status": "none",
"tier": "free",
"expiresAt": null,
"autoRenew": false
}
},
"error": null
}
```
#### GET /tracks
认证JWT
响应:
```json
{
"success": true,
"data": [
{
"id": "history",
"name": "历史",
"icon": "🏛",
"progress": 25,
"nodes": [
{
"id": "uuid",
"title": "第一章",
"status": "current",
"reward": "+40 XP",
"questionCount": 4
}
]
}
],
"error": null
}
```
`nodes[].status` 取值:`done`, `current`, `locked`, `chest`
#### GET /tracks/:trackId
认证JWT
路径参数:`trackId` 可以是分类 `id``slug`
响应:单个 `ThemeTrackDto`,不存在时 `data``null`
#### GET /challenges/next
认证JWT
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `trackId` | string | 是 | 分类 `id``slug` |
响应:
```json
{
"success": true,
"data": {
"challengeId": "challenge-session-uuid",
"trackId": "history",
"nodeId": "chapter-uuid",
"totalQuestions": 5,
"questions": [
{
"challengeId": "challenge-session-uuid",
"trackId": "history",
"nodeId": "chapter-uuid",
"question": {
"id": "question-uuid-1",
"prompt": "题目文本",
"options": [
{ "id": "a", "text": "选项 A" },
{ "id": "b", "text": "选项 B" },
{ "id": "c", "text": "选项 C" }
]
}
}
]
},
"error": null
}
```
服务端会创建挑战组会话并一次返回 5 题,题目选项不包含正确答案标记。题库不足 5 题或没有可用题目时 `data``null`
#### POST /challenges/answer
认证JWT
请求:
```json
{
"challengeId": "challenge-session-uuid",
"questionId": "question-uuid",
"selectedOptionId": "a",
"timeMs": 1500,
"comboCount": 0,
"submitRequestId": "client-submit-id"
}
```
`submitRequestId` 可选;未传时服务端会使用 `challengeId + questionId` 作为默认幂等 key。同一挑战组内重复提交同一道题会返回第一次裁决结果不会重复扣资源或重复发放奖励。
响应:
```json
{
"success": true,
"data": {
"challengeId": "challenge-session-uuid",
"answerState": "correct",
"correctOptionId": "a",
"xpDelta": 10,
"progress": {
"hearts": 5,
"dailyAttemptsLeft": 5,
"xp": 10,
"streakDays": 0
},
"knowledgeCard": {
"id": "card-uuid",
"title": "知识点摘要",
"summary": "知识点摘要",
"fact": "深入解析"
},
"rewards": [
{ "type": "xp", "amount": 10, "title": "+10 XP" }
]
},
"error": null
}
```
`answerState` 取值:`correct`, `wrong`
#### GET /progress/summary
认证JWT
响应:`ProgressSummaryDto`,字段同 `/app/bootstrap` 中的 `progress`
#### PATCH /progress/preferences
认证JWT
请求:
```json
{
"activeTrackId": "history"
}
```
响应:更新后的 `ProgressSummaryDto`
#### POST /progress/check-in
认证JWT
请求:无
响应:更新后的 `ProgressSummaryDto`
#### GET /leaderboards
认证JWT
查询参数:
| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `scope` | `region``topic` | `region` | 排行榜范围 |
| `trackId` | string | - | `scope=topic` 时可传 |
| `page` | number | 1 | 页码 |
| `limit` | number | 20 | 1-100 |
响应:
```json
{
"success": true,
"data": [
{
"rank": 1,
"userId": "uuid",
"displayName": "玩家昵称",
"avatarUrl": null,
"xp": 5000,
"badge": "王者",
"isMe": false
}
],
"pagination": {
"total": 100,
"page": 1,
"limit": 20
},
"error": null
}
```
#### GET /leaderboards/me
认证JWT
查询参数同 `/leaderboards`
响应:
```json
{
"success": true,
"data": {
"rank": 15,
"userId": "uuid",
"displayName": "我",
"avatarUrl": null,
"xp": 1500,
"badge": "新秀",
"isMe": true
},
"error": null
}
```
#### GET /shop
认证JWT
响应:
```json
{
"success": true,
"data": {
"benefits": [
{
"id": "restore-hearts",
"type": "hearts",
"title": "恢复满血",
"description": "血量不足时继续挑战",
"enabled": true,
"requiresAd": true
}
],
"products": [
{
"id": "hint-feather",
"type": "item",
"itemId": "hint_feather",
"title": "提示羽毛",
"description": "答题时排除 1 个错误选项",
"priceCoins": 80,
"quantity": 1,
"enabled": true
}
]
},
"error": null
}
```
说明:`requiresAd=true` 的权益应通过 `/rewards/ad-recovery/session``/rewards/ad-recovery/complete` 完成资格检查和结算。
可购买商品价格:提示羽毛 80 金币,爱心补给 150 金币,双倍 XP 药水 250 金币,连胜护盾 400 金币,第一版吉祥物装扮 800 金币。
#### POST /shop/purchase
认证JWT
请求:
```json
{
"productId": "hint-feather",
"clientRequestId": "request-uuid"
}
```
`productId` 取值:`hint-feather`, `heart-supply`, `double-xp-potion`, `streak-shield`, `mascot-outfit-starter`
响应:
```json
{
"success": true,
"data": {
"product": {
"id": "hint-feather",
"type": "item",
"itemId": "hint_feather",
"title": "提示羽毛",
"description": "答题时排除 1 个错误选项",
"priceCoins": 80,
"quantity": 1,
"enabled": true
},
"coinsSpent": 80,
"coinsBalance": 220,
"item": {
"itemId": "hint_feather",
"quantity": 3,
"activeUntil": null,
"metadata": null
},
"rewards": [
{
"type": "item",
"source": "inventory",
"itemId": "hint_feather",
"quantity": 1,
"title": "提示羽毛 x1"
}
]
},
"error": null
}
```
购买使用 `clientRequestId` 作为幂等边界;金币不足时返回统一错误格式,`error.code` 为 `VALIDATION_ERROR`
#### GET /subscription
认证JWT
响应:
```json
{
"success": true,
"data": {
"status": "active",
"tier": "pro",
"expiresAt": "2026-06-05T00:00:00.000Z",
"autoRenew": true
},
"error": null
}
```
`status` 取值:`none`, `active`, `expired`, `cancelled`。`tier` 取值:`free`, `pro`, `proplus`
#### POST /subscription/verify
认证JWT
请求:
```json
{
"platform": "huawei",
"purchaseToken": "purchase-token",
"productId": "duoqi_plus_monthly",
"tier": "pro"
}
```
响应:更新后的订阅 DTO。当前仅支持 `platform=huawei`,其他平台返回 `UNSUPPORTED_PLATFORM`
### 激励广告恢复
#### POST /rewards/ad-recovery/session
认证JWT
用途:展示广告前创建恢复会话,服务端返回会话 ID、广告位和资格状态。
请求:
```json
{
"type": "hearts",
"clientRequestId": "uuid-from-client",
"platform": "ios",
"adProvider": "mock"
}
```
`type` 取值:`hearts`, `bonusAttempts`, `streakProtection`
`platform` 取值:`ios`, `android`, `harmony`, `web`
符合资格响应:
```json
{
"success": true,
"data": {
"sessionId": "uuid",
"eligible": true,
"type": "hearts",
"adPlacementId": "duoqi_restore_hearts_ios",
"remainingToday": 2,
"expiresAt": "2026-05-05T12:30:00.000Z"
},
"error": null
}
```
不符合资格响应:
```json
{
"success": true,
"data": {
"sessionId": null,
"eligible": false,
"reason": "daily_limit_reached",
"nextAvailableAt": "2026-05-06T00:00:00.000Z"
},
"error": null
}
```
#### POST /rewards/ad-recovery/complete
认证JWT
用途:广告 SDK 返回完整播放后提交凭证,由服务端幂等结算奖励。`mock` provider 用于测试;真实 provider 需要提交 `providerRewardToken`
请求:
```json
{
"sessionId": "uuid",
"clientRequestId": "uuid-from-client",
"adProvider": "admob",
"providerRewardToken": "opaque-provider-token",
"completedAt": "2026-05-05T12:03:00.000Z"
}
```
成功响应:
```json
{
"success": true,
"data": {
"status": "completed",
"type": "hearts",
"reward": {
"heartsDelta": 3,
"dailyAttemptsDelta": 0,
"streakProtectionGranted": false
},
"progress": {
"hearts": 5,
"maxHearts": 5,
"dailyAttemptsLeft": 2,
"dailyAttemptsMax": 5,
"streakDays": 21,
"streakProtectedUntil": null
},
"limits": {
"remainingHeartsRecoveriesToday": 2,
"remainingAttemptRecoveriesToday": 3,
"nextStreakProtectionAvailableAt": "2026-05-12T00:00:00.000Z"
}
},
"error": null
}
```
失败响应:
```json
{
"success": true,
"data": {
"status": "failed",
"reason": "provider_verification_failed",
"message": "广告未完整播放,未发放奖励。",
"progress": {
"hearts": 2,
"maxHearts": 5,
"dailyAttemptsLeft": 1,
"dailyAttemptsMax": 5
}
},
"error": null
}
```
`reason` 取值:`ad_not_completed`, `provider_verification_failed`, `session_expired`, `daily_limit_reached`, `cooldown_active`, `already_subscribed`, `invalid_type`
## 管理端 API
管理端路由统一带 `/v1/admin` 前缀。
### 管理端认证
#### POST /admin/login
认证:无
请求:
```json
{
"username": "admin",
"password": "password123"
}
```
响应:
```json
{
"success": true,
"data": {
"accessToken": "jwt",
"refreshToken": "jwt",
"admin": {
"id": "uuid",
"username": "admin",
"role": "super_admin"
}
},
"error": null
}
```
#### PUT /admin/change-password
认证Admin JWT
请求:
```json
{
"currentPassword": "old-password",
"newPassword": "new-password"
}
```
响应:
```json
{
"success": true,
"data": {
"message": "Password changed successfully"
},
"error": null
}
```
### 管理员管理
#### GET /admin/admins
认证Admin JWT
查询参数:`page`, `limit`, `role`, `isActive`
响应:管理员数组 + `pagination`
#### GET /admin/admins/:id
认证Admin JWT
#### POST /admin/admins
认证Admin JWT`role=super_admin`
请求:
```json
{
"username": "newadmin",
"password": "password123",
"role": "admin"
}
```
#### PUT /admin/admins/:id
认证Admin JWT`role=super_admin`
请求:
```json
{
"username": "updated",
"role": "admin",
"isActive": 1
}
```
#### DELETE /admin/admins/:id
认证Admin JWT`role=super_admin`。软删除管理员。
#### POST /admin/admins/:id/reset-password
认证Admin JWT`role=super_admin`。响应包含一次性明文密码 `plainPassword`
### 题目管理
#### GET /admin/questions
认证Admin JWT
查询参数:`page`, `limit`, `status`, `categoryId`, `keyword`, `difficulty`, `source`, `sortBy`, `sortOrder`
`sortBy` 取值:`createdAt`, `updatedAt`, `difficulty`。`sortOrder` 取值:`asc`, `desc`
#### GET /admin/questions/:id
认证Admin JWT
#### POST /admin/questions
认证Admin JWT
请求:
```json
{
"stem": { "text": "题目内容" },
"contentType": "text",
"correctAnswer": "正确答案",
"distractors": ["干扰项1", "干扰项2"],
"categoryId": "history",
"difficulty": 3,
"knowledgeCard": {
"summary": "知识点摘要",
"deepDive": "深入解析",
"sourceRef": "来源"
}
}
```
`contentType` 取值:`text`, `image`, `video`, `audio`
#### PUT /admin/questions/:id
认证Admin JWT。请求字段同创建接口均可选额外支持 `status`
#### PATCH /admin/questions/:id/status
认证Admin JWT
请求:
```json
{
"status": "published"
}
```
`status` 取值:`draft`, `reviewing`, `published`, `archived`。服务会校验状态流转。
#### DELETE /admin/questions/:id
认证Admin JWT。归档题目。
#### POST /admin/questions/batch-publish
认证Admin JWT
请求:
```json
{
"ids": ["uuid"]
}
```
#### POST /admin/questions/batch-archive
认证Admin JWT。请求同 `batch-publish`
#### POST /admin/questions/batch-delete
认证Admin JWT。软删除等同批量归档。
#### POST /admin/questions/import
认证Admin JWT
请求:
```json
{
"questions": [
{
"stem": { "text": "题目内容" },
"contentType": "text",
"correctAnswer": "正确答案",
"distractors": ["干扰项1", "干扰项2"],
"categoryId": "history",
"difficulty": 3,
"knowledgeCard": {
"summary": "知识点摘要",
"deepDive": "深入解析",
"sourceRef": "来源"
}
}
]
}
```
单次 1-200 条,全有或全无。
#### POST /admin/questions/import-csv
认证Admin JWT
`Content-Type`: `text/plain`
CSV 表头:
```csv
categoryId,contentType,difficulty,stemText,correctAnswer,distractor1,distractor2,distractor3,distractor4,distractor5,cardSummary,cardDeepDive,cardSourceRef
```
### 分类管理
#### GET /admin/categories
认证Admin JWT
查询参数:`page`, `limit`
响应:分类数组 + `pagination`
#### POST /admin/categories
认证Admin JWT
请求:
```json
{
"id": "history",
"name": "历史",
"slug": "history",
"parentId": null,
"sortOrder": 1
}
```
#### PUT /admin/categories/:id
认证Admin JWT
请求:
```json
{
"name": "历史",
"slug": "history",
"parentId": null,
"sortOrder": 1,
"status": "active"
}
```
#### DELETE /admin/categories/:id
认证Admin JWT。归档分类。
### 知识点卡片
#### GET /admin/knowledge-cards
认证Admin JWT
查询参数:`page`, `limit`
响应:知识点卡片数组 + `pagination`
#### GET /admin/knowledge-cards/by-question/:questionId
认证Admin JWT
#### PUT /admin/knowledge-cards/:id
认证Admin JWT
请求:
```json
{
"summary": "摘要",
"deepDive": "深入解析",
"sourceRef": "来源"
}
```
所有字段均可选,但至少应传一个需要更新的字段。
### 技能树管理
#### GET /admin/skill-tree
认证Admin JWT
查询参数:`categoryId`。
#### POST /admin/skill-tree
认证Admin JWT
请求:
```json
{
"categoryId": "history",
"title": "第一章",
"parentId": null,
"sortOrder": 1,
"questionsRequired": 4,
"passThreshold": 2
}
```
#### PUT /admin/skill-tree/:id
认证Admin JWT。请求字段同创建接口均可选。
#### DELETE /admin/skill-tree/:id
认证Admin JWT。
### 用户管理
#### GET /admin/users
认证Admin JWT
查询参数:`page`, `limit`, `search`
响应:用户数组 + `pagination`
#### GET /admin/users/:id
认证Admin JWT
#### PUT /admin/users/:id/ban
认证Admin JWT
#### PUT /admin/users/:id/unban
认证Admin JWT
### 统计数据
#### GET /admin/stats
认证Admin JWT
响应:
```json
{
"success": true,
"data": {
"totalUsers": 1000,
"activeUsers": 150,
"totalQuestions": 500,
"publishedQuestions": 450,
"averageXp": 200
},
"error": null
}
```
### 反馈管理
#### GET /admin/feedback
认证Admin JWT
查询参数:`page`, `limit`
响应:反馈数组 + `pagination`
## 错误代码
| 代码 | 说明 |
|------|------|
| `VALIDATION_ERROR` | 请求参数验证失败 |
| `VALIDATION_FAILED` | 批量导入中部分题目校验失败 |
| `CSV_PARSE_ERROR` | CSV 解析失败 |
| `UNAUTHORIZED` | 未认证或认证失败 |
| `FORBIDDEN` | 权限不足 |
| `NOT_FOUND` | 资源不存在 |
| `INVALID_STATUS_TRANSITION` | 题目状态流转不合法 |
| `INVALID_RECEIPT` | 支付收据验证失败 |
| `UNSUPPORTED_PLATFORM` | 订阅平台暂不支持 |
| `INTERNAL_ERROR` | 服务器内部错误 |