19 KiB
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/* |
统一响应
{
"success": true,
"data": {},
"error": null
}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input"
}
}
分页响应会额外包含:
{
"pagination": {
"total": 100,
"page": 1,
"limit": 20
}
}
客户端 API
健康检查
GET /health
认证:无
响应:
{
"success": true,
"data": {
"status": "ok",
"timestamp": "2026-05-05T12:00:00.000Z"
},
"error": null
}
认证
POST /auth/guest
认证:无
限流:10 次/分钟
请求:
{
"deviceId": "device-id"
}
响应:
{
"success": true,
"data": {
"user": {
"id": "uuid",
"nickname": null,
"avatarUrl": null,
"tier": "free"
},
"tokens": {
"accessToken": "jwt",
"refreshToken": "jwt"
}
},
"error": null
}
POST /auth/huawei
认证:无
限流:10 次/分钟
请求:
{
"authorizationCode": "authorization-code"
}
响应同 /auth/guest。
POST /auth/refresh
认证:无
限流:10 次/分钟
请求:
{
"refreshToken": "jwt"
}
响应:
{
"success": true,
"data": {
"accessToken": "jwt",
"refreshToken": "jwt"
},
"error": null
}
GET /auth/me
认证:JWT
响应:
{
"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
响应:
{
"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
响应:
{
"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 |
响应:
{
"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
请求:
{
"challengeId": "challenge-session-uuid",
"questionId": "question-uuid",
"selectedOptionId": "a",
"timeMs": 1500,
"comboCount": 0,
"submitRequestId": "client-submit-id"
}
submitRequestId 可选;未传时服务端会使用 challengeId + questionId 作为默认幂等 key。同一挑战组内重复提交同一道题会返回第一次裁决结果,不会重复扣资源或重复发放奖励。
响应:
{
"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
请求:
{
"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 |
响应:
{
"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。
响应:
{
"success": true,
"data": {
"rank": 15,
"userId": "uuid",
"displayName": "我",
"avatarUrl": null,
"xp": 1500,
"badge": "新秀",
"isMe": true
},
"error": null
}
GET /shop
认证:JWT
响应:
{
"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
请求:
{
"productId": "hint-feather",
"clientRequestId": "request-uuid"
}
productId 取值:hint-feather, heart-supply, double-xp-potion, streak-shield, mascot-outfit-starter。
响应:
{
"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
请求:
{
"itemId": "hint_feather",
"clientRequestId": "request-uuid",
"questionId": "question-uuid"
}
itemId 取值:heart_supply, double_xp_potion, hint_feather, streak_shield。使用 hint_feather 时必须传 questionId。
响应:
{
"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
响应:
{
"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
请求:
{
"platform": "huawei",
"purchaseToken": "purchase-token",
"productId": "duoqi_plus_monthly",
"tier": "pro"
}
响应:更新后的订阅 DTO。当前仅支持 platform=huawei,其他平台返回 UNSUPPORTED_PLATFORM。
激励广告恢复
POST /rewards/ad-recovery/session
认证:JWT
用途:展示广告前创建恢复会话,服务端返回会话 ID、广告位和资格状态。
请求:
{
"type": "hearts",
"clientRequestId": "uuid-from-client",
"platform": "ios",
"adProvider": "mock"
}
type 取值:hearts, bonusAttempts, streakProtection。
platform 取值:ios, android, harmony, web。
符合资格响应:
{
"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
}
不符合资格响应:
{
"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。
请求:
{
"sessionId": "uuid",
"clientRequestId": "uuid-from-client",
"adProvider": "admob",
"providerRewardToken": "opaque-provider-token",
"completedAt": "2026-05-05T12:03:00.000Z"
}
成功响应:
{
"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
}
失败响应:
{
"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
认证:无
请求:
{
"username": "admin",
"password": "password123"
}
响应:
{
"success": true,
"data": {
"accessToken": "jwt",
"refreshToken": "jwt",
"admin": {
"id": "uuid",
"username": "admin",
"role": "super_admin"
}
},
"error": null
}
PUT /admin/change-password
认证:Admin JWT
请求:
{
"currentPassword": "old-password",
"newPassword": "new-password"
}
响应:
{
"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
请求:
{
"username": "newadmin",
"password": "password123",
"role": "admin"
}
PUT /admin/admins/:id
认证:Admin JWT,且 role=super_admin
请求:
{
"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
请求:
{
"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
请求:
{
"status": "published"
}
status 取值:draft, reviewing, published, archived。服务会校验状态流转。
DELETE /admin/questions/:id
认证:Admin JWT。归档题目。
POST /admin/questions/batch-publish
认证:Admin JWT
请求:
{
"ids": ["uuid"]
}
POST /admin/questions/batch-archive
认证:Admin JWT。请求同 batch-publish。
POST /admin/questions/batch-delete
认证:Admin JWT。软删除,等同批量归档。
POST /admin/questions/import
认证:Admin JWT
请求:
{
"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 表头:
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
请求:
{
"id": "history",
"name": "历史",
"slug": "history",
"parentId": null,
"sortOrder": 1
}
PUT /admin/categories/:id
认证:Admin JWT
请求:
{
"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
请求:
{
"summary": "摘要",
"deepDive": "深入解析",
"sourceRef": "来源"
}
所有字段均可选,但至少应传一个需要更新的字段。
技能树管理
GET /admin/skill-tree
认证:Admin JWT
查询参数:categoryId。
POST /admin/skill-tree
认证:Admin JWT
请求:
{
"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
响应:
{
"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 |
服务器内部错误 |