duoqi-api/docs/api-reference.md
Wang Zhuoxuan 7682bb2ae7
Some checks failed
CI/CD Pipeline / Code Quality (push) Failing after 18s
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Build & Deploy Test (push) Has been skipped
CI/CD Pipeline / Build & Deploy Production (push) Has been skipped
refactor: 融合认证 schemeCode 按平台拆分为独立环境变量
不同端(Android/iOS/Harmony)在阿里云控制台使用不同的方案 Code,
单一 ALIYUN_FUSION_SCHEME_CODE 无法满足多端需求。改为按平台映射
三个独立环境变量,providers 端点也按平台精确判断可用性。

同步更新 API 文档:补充 fusion/token 请求参数表和 Harmony 平台支持。
2026-05-28 23:43:21 +08:00

1638 lines
33 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 更新。
>
> 最近一次代码审计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 <jwt_token>` | `/v1/auth/link` |
| 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
}
```
#### 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` 判断。