duoqi-api/docs/api-reference.md
Wang Zhuoxuan 0ee3522280 更新 API 参考文档对齐游戏化变更
- 排行榜响应新增 meta 字段含周信息/分组/奖励预览
- xp 改为本周 XP,说明排名基于 20-30 人分组
- 广告恢复 session 新增 Plus 用户 subscriptionBenefits 响应
- 标注旧恢复接口已废弃,指向 session+complete 两步流程
2026-05-13 22:11:52 +08:00

1219 lines
22 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": [],
"shop": {
"benefits": [],
"products": [
{
"id": "hint-feather",
"type": "item",
"itemId": "hint_feather",
"title": "提示羽毛",
"description": "答题时排除 1 个错误选项",
"priceCoins": 80,
"quantity": 1,
"enabled": true
}
]
},
"wallet": {
"coinsBalance": 260
},
"inventory": {
"items": [
{
"itemId": "hint_feather",
"quantity": 2,
"activeUntil": null,
"metadata": null
}
]
},
"subscription": {
"status": "none",
"tier": "free",
"expiresAt": null,
"autoRenew": false
}
},
"error": null
}
```
说明:`shopBenefits` 为兼容旧客户端保留,内容等同于 `shop.benefits`。新客户端应优先读取 `shop.products`、`wallet.coinsBalance` 和 `inventory.items` 来展示金币、背包和可购买商品。
#### 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": 500,
"badge": "榜首",
"isMe": false
}
],
"meta": {
"weekStart": "2026-05-11",
"weekEnd": "2026-05-17",
"nextRefreshAt": "2026-05-18",
"groupId": "week-2026-05-11-group-1",
"rewardPreview": [
{ "rank": 1, "coins": 300 },
{ "rank": 2, "coins": 150 },
{ "rank": 3, "coins": 50 }
]
},
"pagination": {
"total": 25,
"page": 1,
"limit": 20
},
"error": null
}
```
> `xp` 为本周累计 XP非全局累计排名基于用户所在 20-30 人分组内。`meta.rewardPreview` 展示各组前 3 名的金币奖励,激励用户冲榜。
#### GET /leaderboards/me
认证JWT
查询参数同 `/leaderboards`
响应:
```json
{
"success": true,
"data": {
"rank": 3,
"userId": "uuid",
"displayName": "我",
"avatarUrl": null,
"xp": 150,
"badge": "前三",
"isMe": true
},
"meta": {
"weekStart": "2026-05-11",
"weekEnd": "2026-05-17",
"nextRefreshAt": "2026-05-18",
"groupId": "week-2026-05-11-group-1",
"rank": 3,
"rewardPreview": [
{ "rank": 1, "coins": 300 },
{ "rank": 2, "coins": 150 },
{ "rank": 3, "coins": 50 }
]
},
"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`
#### POST /inventory/items/use
认证JWT
请求:
```json
{
"itemId": "hint_feather",
"clientRequestId": "request-uuid",
"questionId": "question-uuid"
}
```
`itemId` 取值:`heart_supply`, `double_xp_potion`, `hint_feather`, `streak_shield`。使用 `hint_feather` 时必须传 `questionId`
响应:
```json
{
"success": true,
"data": {
"itemId": "hint_feather",
"quantityRemaining": 2,
"effect": {
"type": "hint",
"excludedOptions": ["错误选项 A"]
}
},
"error": null
}
```
效果说明:`heart_supply` 恢复当前用户爱心到上限;`double_xp_potion` 返回 15 分钟有效期 `activeUntil``hint_feather` 返回可排除选项;`streak_shield` 返回 `streakProtectedUntil`。`clientRequestId` 用于道具消耗幂等。
#### 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
}
```
Plus 用户响应(无需看广告,返回订阅权益摘要):
```json
{
"success": true,
"data": {
"sessionId": null,
"eligible": false,
"reason": "already_subscribed",
"subscriptionBenefits": {
"tier": "pro",
"unlimitedHearts": true,
"dailyHighRewardSessions": null
}
},
"error": null
}
```
> `subscriptionBenefits.dailyHighRewardSessions` 为 `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`
> 广告恢复奖励通过统一奖励结算层(`rewardLedger`)发放,以 `ad_recovery:{sessionId}` 为幂等 key记录发放前后资源快照。旧接口 `POST /rewards/hearts/restore`、`POST /rewards/attempts/restore`、`POST /rewards/streak/protect`、`POST /progress/hearts/restore` 已废弃,请使用上述 session + complete 两步流程。
## 管理端 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` | 服务器内部错误 |