# Duoqi API Reference > 多奇服务端 API 接口文档。本文按当前 Fastify 路由和 TypeScript DTO 更新。 > > 最近一次代码审计:2026-05-18,来源为 `src/index.ts` 注册的路由、`src/routes/**/*.ts`、`src/types/app-api.ts` 和相关 service 返回值。 ## 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/auth/fusion/token`, `/v1/auth/fusion/verify`, `/v1/auth/providers`, `/v1/admin/login` | | JWT(游客) | `Authorization: Bearer ` | `/v1/auth/link` | | 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 } ``` #### POST /auth/link 认证:JWT(游客 Token) 限流:10 次/分钟 用途:将当前登录的游客账号关联到第三方正式账号(当前支持 Apple Sign In)。支持两种场景: - **场景 A(新用户)**:该 Apple ID 未注册过,游客行原地升级为 Apple 账号,无需数据迁移。 - **场景 B(老用户)**:该 Apple ID 已有正式账号,在事务内将游客的答题记录、奖励流水等数据合并到正式账号,不覆盖老账号的订阅、余额、库存、连续学习等核心资产。 请求: ```json { "provider": "apple", "credential": { "identityToken": "apple-identity-token", "authorizationCode": "optional-authorization-code" }, "mergePolicy": "server_account_first", "clientMigrationId": "client-generated-uuid" } ``` - `provider`:当前仅支持 `apple`。 - `credential.identityToken`:Apple Sign In 返回的 identity token(必填)。 - `credential.authorizationCode`:Apple 授权码(可选)。 - `mergePolicy`:当前固定为 `server_account_first`,以服务端已有数据为主。 - `clientMigrationId`:客户端生成的唯一 ID,用于幂等保证,防止网络重试导致重复合并。 成功响应: ```json { "success": true, "data": { "session": { "user": { "id": "uuid", "nickname": null, "avatarUrl": null, "tier": "free" }, "tokens": { "accessToken": "jwt", "refreshToken": "jwt" } }, "migrationSummary": { "policy": "server_account_first", "imported": { "challengeAttempts": 5 }, "skipped": { "subscriptions": 1 }, "conflicts": [] }, "bootstrap": null }, "error": null } ``` - `session.tokens`:关联成功后返回正式账号的新 JWT,客户端应替换本地存储的 token。 - `migrationSummary.imported`:从游客账号导入的数据类型和数量。 - `migrationSummary.skipped`:因合并策略跳过的数据(不覆盖老账号已有的数据)。 - `migrationSummary.conflicts`:合并过程中发现的冲突项列表,通常为空。 - `bootstrap`:当前固定为 `null`,客户端需调用 `GET /app/bootstrap` 获取最新状态。 错误响应: ```json { "success": false, "data": null, "error": { "code": "CONFLICT", "message": "Migration already completed" } } ``` ```json { "success": false, "data": null, "error": { "code": "VALIDATION_ERROR", "message": "Invalid Apple identity token" } } ``` > 重复提交相同 `clientMigrationId` 时返回幂等结果,不会触发重复合并。游客账号只能关联一次,已关联后再次调用返回 `CONFLICT`。 #### POST /auth/phone 认证:无 限流:10 次/分钟 当前已注册但未实现,固定返回 HTTP 501。手机号登录请使用融合认证接口(`/auth/fusion/token` + `/auth/fusion/verify`)。 ```json { "success": false, "data": null, "error": { "code": "NOT_IMPLEMENTED", "message": "Phone login not implemented yet" } } ``` #### POST /auth/fusion/token 认证:无 限流:10 次/分钟 用途:获取阿里云融合认证 SDK 鉴权 Token。客户端在初始化 SDK 前调用此接口。`schemeCode` 和 `durationSeconds` 由服务端配置,不暴露给客户端。 请求: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `platform` | `"Android"` \| `"iOS"` \| `"Harmony"` | 是 | 客户端平台 | | `packageName` | string | Android 必填 | App 包名 | | `packageSign` | string | Android 必填 | App 包签名(MD5,去冒号小写) | | `bundleId` | string | iOS 必填 | App Bundle ID | ```json { "platform": "Android", "packageName": "com.example.app", "packageSign": "a1b2c3d4e5f6..." } ``` 响应: ```json { "success": true, "data": { "fusionAuthToken": "FKcksloqk***********jalEc+" }, "error": null } ``` 错误响应(服务未配置): ```json { "success": false, "data": null, "error": { "code": "SERVICE_UNAVAILABLE", "message": "Fusion auth is not configured on the server" } } ``` #### POST /auth/fusion/verify 认证:无 限流:10 次/分钟 用途:客户端融合认证 SDK 完成认证后,将运营商返回的 verifyToken 发送到此端点。服务端用 verifyToken 向阿里云换取手机号,完成登录或自动注册。一键登录和短信验证码登录均使用此端点。 请求: ```json { "verifyToken": "LD108enNdlsl*******sFLKCks1==" } ``` 成功响应(同 `/auth/guest`): ```json { "success": true, "data": { "user": { "id": "uuid", "nickname": null, "avatarUrl": null, "tier": "free" }, "tokens": { "accessToken": "jwt", "refreshToken": "jwt" } }, "error": null } ``` 错误响应: ```json { "success": false, "data": null, "error": { "code": "UNAUTHORIZED", "message": "Phone verification failed" } } ``` #### GET /auth/providers 认证:无 用途:返回指定平台可用的登录方式列表。客户端根据此接口决定登录页面显示哪些登录按钮。第三方登录方式的开关可通过 Admin API 热更新,无需重启服务器。 查询参数: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `platform` | `'ios' \| 'android' \| 'harmony'` | 是 | 客户端平台 | 平台过滤规则: - 一键登录 / 短信验证码:全平台 - 华为账号:仅 Android、Harmony - Apple 账号:仅 iOS - 微信 / QQ:全平台,enabled 由数据库配置控制 响应: ```json { "success": true, "data": { "providers": [ { "id": "fusion", "name": "一键登录", "type": "primary", "enabled": true }, { "id": "phone_sms", "name": "短信验证码登录", "type": "secondary", "enabled": true }, { "id": "apple", "name": "通过 Apple 登录", "type": "third_party", "iconKey": "apple", "enabled": true }, { "id": "wechat", "name": "通过微信登录", "type": "third_party", "iconKey": "wechat", "enabled": false } ] }, "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", "highRewardSessionsLeft": 3, "highRewardSessionsMax": 3, "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, "highRewardEligible": true, "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 题,题目选项不包含正确答案标记。`highRewardEligible=false` 表示今日高奖励挑战次数已用尽,本轮 XP 和宝箱掉落按降级规则结算。题库不足 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, "highRewardSessionsLeft": 2, "highRewardSessionsMax": 3, "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`,包含 `highRewardSessionsLeft` 和 `highRewardSessionsMax`。 #### 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, "nextAttemptResetAt": "2026-05-06T00:00:00.000Z", "highRewardSessionsLeft": 3, "highRewardSessionsMax": 3, "xp": 1200, "level": 4, "xpToNextLevel": 200, "streakDays": 21, "checkInDays": 8, "streakProtectedUntil": null, "activeTrackId": "history", "isSubscribed": false }, "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, "nextAttemptResetAt": "2026-05-06T00:00:00.000Z", "highRewardSessionsLeft": 0, "highRewardSessionsMax": 3, "xp": 1200, "level": 4, "xpToNextLevel": 200, "streakDays": 21, "checkInDays": 8, "streakProtectedUntil": null, "activeTrackId": "history", "isSubscribed": false } }, "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 } ``` #### POST /admin/auth 认证:无 兼容旧管理端的 `ADMIN_TOKEN` 验证接口。新管理端应使用 `/admin/login` 获取 Admin JWT。 请求: ```json { "token": "admin-token" } ``` 响应: ```json { "success": true, "data": { "authenticated": true }, "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`。 ### 游戏化审计 #### GET /admin/gamification/users/:id/wallet 认证:Admin JWT 响应:指定用户金币钱包,未创建钱包时 `data` 为 `null`。 #### GET /admin/gamification/users/:id/inventory 认证:Admin JWT 响应:指定用户道具库存数组。 #### GET /admin/gamification/users/:id/rewards 认证:Admin JWT 查询参数:`page`, `limit`。 响应:指定用户奖励流水数组 + `pagination`。 #### GET /admin/gamification/users/:id/ad-recovery 认证:Admin JWT 查询参数:`page`, `limit`。 响应:指定用户广告恢复会话数组 + `pagination`。 #### GET /admin/gamification/users/:id/transactions 认证:Admin JWT 查询参数:`page`, `limit`。 响应:指定用户资源变更流水数组 + `pagination`。 ### 定时任务 #### GET /admin/jobs 认证:Admin JWT 响应: ```json { "success": true, "data": [ { "name": "weekly-settlement", "description": "周榜结算:按组快照上周排名,给每组前 3 名发金币奖励", "schedule": "每周一 UTC 00:30" }, { "name": "expire-subscriptions", "description": "订阅过期检查:检查并过期到期的订阅", "schedule": "每日 UTC 01:00" } ], "error": null } ``` #### POST /admin/jobs/trigger 认证:Admin JWT 请求: ```json { "job": "weekly-settlement", "dryRun": true } ``` `job` 取值:`weekly-settlement`, `expire-subscriptions`。`dryRun` 可选,默认 `false`。 响应: ```json { "success": true, "data": { "job": "weekly-settlement", "dryRun": true, "executedAt": "2026-05-18T00:30:00.000Z", "result": {} }, "error": null } ``` ## 已注册兼容接口 以下接口仍在 `src/index.ts` 中注册,但不是新客户端的推荐主合同。新客户端优先使用上文的 Flutter 客户端聚合 API、订阅 API 和广告恢复两步流程。 | 路径 | 状态 | 推荐替代 | |------|------|----------| | `GET /quiz/categories` | 兼容旧出题接口 | `GET /tracks` | | `GET /quiz/categories/:id/chapters` | 兼容旧出题接口 | `GET /tracks/:trackId` | | `GET /quiz/chapters/:id/questions` | 兼容旧出题接口 | `GET /challenges/next` | | `POST /quiz/answer` | 兼容旧出题接口 | `POST /challenges/answer` | | `POST /quiz/rate` | 兼容旧反馈接口 | 仍可用于题目评分 | | `GET /progress/dashboard` | 兼容旧进度接口 | `GET /progress/summary` | | `GET /progress/streak` | 兼容旧进度接口 | `GET /progress/summary` | | `GET /progress/hearts` | 兼容旧进度接口 | `GET /progress/summary` | | `POST /progress/hearts/restore` | 已废弃恢复接口 | `POST /rewards/ad-recovery/session` + `POST /rewards/ad-recovery/complete` | | `GET /progress/chapters` | 兼容旧进度接口 | `GET /tracks/:trackId` | | `POST /feedback` | 兼容旧反馈接口 | 仍可用于用户反馈 | | `GET /leaderboard` | 兼容旧排行榜接口 | `GET /leaderboards` | | `GET /leaderboard/me` | 兼容旧排行榜接口 | `GET /leaderboards/me` | | `GET /achievements` | 兼容旧成就接口 | 当前无聚合替代 | | `POST /achievements/check` | 兼容旧成就接口 | 当前无聚合替代 | | `POST /payment/verify-huawei` | 兼容旧支付接口 | `POST /subscription/verify` | | `GET /payment/subscription` | 兼容旧订阅接口 | `GET /subscription` | | `POST /rewards/hearts/restore` | 已废弃恢复接口 | `POST /rewards/ad-recovery/session` + `POST /rewards/ad-recovery/complete` | | `POST /rewards/attempts/restore` | 已废弃恢复接口 | `POST /rewards/ad-recovery/session` + `POST /rewards/ad-recovery/complete` | | `POST /rewards/streak/protect` | 已废弃恢复接口 | `POST /rewards/ad-recovery/session` + `POST /rewards/ad-recovery/complete` | ## 错误代码 | 代码 | 说明 | |------|------| | `VALIDATION_ERROR` | 请求参数验证失败 | | `VALIDATION_FAILED` | 批量导入中部分题目校验失败 | | `CSV_PARSE_ERROR` | CSV 解析失败 | | `UNAUTHORIZED` | 未认证或认证失败 | | `FORBIDDEN` | 权限不足 | | `NOT_FOUND` | 资源不存在 | | `CONFLICT` | 资源冲突(如重复迁移、已关联账号) | | `INVALID_STATUS_TRANSITION` | 题目状态流转不合法 | | `INVALID_RECEIPT` | 支付收据验证失败 | | `UNSUPPORTED_PLATFORM` | 订阅平台暂不支持 | | `NOT_IMPLEMENTED` | 路由已注册但功能未实现 | | `INTERNAL_ERROR` | 服务器内部错误 | 客户端应把 `error.code` 当作稳定的机器分支键,`error.message` 只用于调试或兜底展示。广告恢复等业务失败会以 `success=true` 返回,并把状态放在 `data.status` / `data.reason` 中;这类流程不要只依赖 `error.code` 判断。