# 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 ` | 大多数客户端 API | | Admin JWT | `Authorization: Bearer ` | `/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` | 服务器内部错误 |