From 2dd5f18822e6d36815671b396d29f14d455df27d Mon Sep 17 00:00:00 2001 From: Wang Zhuoxuan Date: Sat, 11 Apr 2026 12:01:53 +0800 Subject: [PATCH] docs: add API reference and environment variable guide - docs/api-reference.md: comprehensive API documentation for client and admin endpoints - docs/env-secrets-guide.md: guide for generating secure keys and tokens --- docs/api-reference.md | 1274 +++++++++++++++++++++++++++++++++++++ docs/env-secrets-guide.md | 77 +++ 2 files changed, 1351 insertions(+) create mode 100644 docs/api-reference.md create mode 100644 docs/env-secrets-guide.md diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..5c512b9 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,1274 @@ +# Duoqi API Reference + +> 多奇服务端 API 接口文档 +> Base URL: `http://:3000/v1` + +## 目录 + +- [通用约定](#通用约定) +- [客户端 API](#客户端-api) + - [健康检查](#健康检查) + - [认证](#认证) + - [答题](#答题) + - [进度](#进度) + - [游戏化](#游戏化) + - [支付](#支付) +- [管理端 API](#管理端-api) + - [管理端认证](#管理端认证) + - [题目管理](#题目管理) + - [分类管理](#分类管理) + - [知识点卡片](#知识点卡片) + - [技能树管理](#技能树管理) + - [用户管理](#用户管理) + - [统计数据](#统计数据) + - [反馈管理](#反馈管理) + +--- + +## 通用约定 + +### 认证方式 + +| 类型 | Header | 适用路径 | +|------|--------|----------| +| 无需认证 | - | `/v1/health`, `/v1/auth/*` | +| JWT | `Authorization: Bearer ` | 大多数客户端 API | +| Admin Token | `Authorization: Bearer ` | `/v1/admin/*` | + +### 统一响应格式 + +```typescript +// 成功响应 +{ + "success": true, + "data": , + "error": null +} + +// 错误响应 +{ + "success": false, + "data": null, + "error": { + "code": "ERROR_CODE", + "message": "错误描述" + } +} + +// 分页响应(额外包含) +{ + "success": true, + "data": [...], + "pagination": { + "total": 100, + "page": 1, + "limit": 20 + }, + "error": null +} +``` + +### 状态码 + +| 状态码 | 含义 | +|--------|------| +| 200 | 请求成功 | +| 400 | 请求参数验证失败 | +| 401 | 未认证 / 认证失败 | +| 404 | 资源不存在 | +| 501 | 功能未实现 | + +--- + +## 客户端 API + +### 健康检查 + +#### GET /health + +健康检查端点,用于服务可用性探测。 + +**认证**: 无 + +**请求**: 无 + +**响应**: +```json +{ + "success": true, + "data": { + "status": "ok", + "timestamp": "2026-04-10T12:00:00.000Z" + }, + "error": null +} +``` + +--- + +### 认证 + +#### POST /auth/guest + +游客登录,通过设备 ID 创建或获取用户账号。 + +**认证**: 无 +**限流**: 10 次/分钟 + +**请求体**: +```json +{ + "deviceId": "string (必填)" +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "user": { + "id": "uuid", + "nickname": null, + "avatarUrl": null, + "tier": "free" + }, + "tokens": { + "accessToken": "jwt_token", + "refreshToken": "jwt_token" + } + }, + "error": null +} +``` + +--- + +#### POST /auth/huawei + +华为账号登录。 + +**认证**: 无 +**限流**: 10 次/分钟 + +**请求体**: +```json +{ + "authorizationCode": "string (必填)" +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "user": { + "id": "uuid", + "nickname": "用户昵称", + "avatarUrl": "头像URL", + "tier": "free" + }, + "tokens": { + "accessToken": "jwt_token", + "refreshToken": "jwt_token" + } + }, + "error": null +} +``` + +--- + +#### POST /auth/phone + +手机号登录(未实现)。 + +**认证**: 无 +**状态**: 501 Not Implemented + +--- + +#### POST /auth/refresh + +刷新访问令牌。 + +**认证**: 无 +**限流**: 10 次/分钟 + +**请求体**: +```json +{ + "refreshToken": "string (必填)" +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "accessToken": "new_jwt_token", + "refreshToken": "new_refresh_token" + }, + "error": null +} +``` + +--- + +#### GET /auth/me + +获取当前用户信息。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": { + "id": "uuid", + "nickname": "用户昵称", + "avatarUrl": "头像URL", + "tier": "free | pro | proplus", + "xpTotal": 150, + "streakDays": 3, + "heartsRemaining": 5, + "dailyXpEarned": 20, + "dailyXpGoal": 50 + }, + "error": null +} +``` + +--- + +### 答题 + +#### GET /quiz/categories + +获取所有题目分类列表。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "name": "历史", + "slug": "history", + "parentId": null + } + ], + "error": null +} +``` + +--- + +#### GET /quiz/categories/:id/chapters + +获取指定分类下的章节列表。 + +**认证**: JWT + +**路径参数**: +- `id`: 分类 ID + +**响应**: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "categoryId": "uuid", + "title": "第一章", + "parentId": null, + "sortOrder": 1 + } + ], + "error": null +} +``` + +--- + +#### GET /quiz/chapters/:id/questions + +获取章节下的题目(包含用户答题状态)。 + +**认证**: JWT + +**路径参数**: +- `id`: 章节 ID + +**响应**: +```json +{ + "success": true, + "data": { + "chapterId": "uuid", + "title": "第一章", + "questionsRequired": 5, + "passThreshold": 3, + "questions": [ + { + "id": "uuid", + "stem": { "text": "题目内容" }, + "contentType": "text", + "options": ["A", "B", "C", "D"], + "answered": false, + "isCorrect": null + } + ] + }, + "error": null +} +``` + +--- + +#### POST /quiz/answer + +提交答案。 + +**认证**: JWT + +**请求体**: +```json +{ + "questionId": "uuid (必填)", + "selectedAnswer": "string (必填)", + "timeMs": 1500 +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "correct": true, + "correctAnswer": "B", + "xpEarned": 10, + "streakBonus": 0, + "chapterCompleted": false + }, + "error": null +} +``` + +--- + +#### POST /quiz/rate + +评价题目质量。 + +**认证**: JWT + +**请求体**: +```json +{ + "questionId": "uuid (必填)", + "rating": "good | bad" +} +``` + +**响应**: +```json +{ + "success": true, + "data": null, + "error": null +} +``` + +--- + +### 进度 + +#### GET /progress/dashboard + +获取用户进度概览。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": { + "xpTotal": 150, + "streakDays": 3, + "heartsRemaining": 5, + "dailyXpEarned": 20, + "dailyXpGoal": 50, + "categoriesCompleted": 1, + "totalCategories": 5 + }, + "error": null +} +``` + +--- + +#### GET /progress/streak + +获取连胜信息。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": { + "currentStreak": 3, + "longestStreak": 7, + "lastActiveDate": "2026-04-10" + }, + "error": null +} +``` + +--- + +#### GET /progress/hearts + +获取红心信息。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": { + "remaining": 5, + "max": 5, + "nextRestoreAt": "2026-04-10T13:00:00.000Z" + }, + "error": null +} +``` + +--- + +#### POST /progress/hearts/restore + +恢复红心。 + +**认证**: JWT + +**请求体**: +```json +{ + "method": "ad | wait | upgrade" +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "remaining": 5, + "restored": 1 + }, + "error": null +} +``` + +--- + +#### GET /progress/chapters + +获取所有章节进度。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": [ + { + "chapterId": "uuid", + "title": "第一章", + "completedQuestions": 3, + "totalQuestions": 5, + "passed": false, + "passedAt": null + } + ], + "error": null +} +``` + +--- + +#### POST /feedback + +提交用户反馈。 + +**认证**: JWT + +**请求体**: +```json +{ + "content": "string (必填, 1-2000字符)", + "contact": "string (可选, 最多255字符)", + "pageContext": "string (可选, 最多200字符)" +} +``` + +**响应**: +```json +{ + "success": true, + "data": null, + "error": null +} +``` + +--- + +### 游戏化 + +#### GET /leaderboard + +获取排行榜。 + +**认证**: JWT + +**查询参数**: +- `tier`: "free" | "pro" | "proplus" (可选) +- `page`: 页码 (默认: 1) +- `limit`: 每页数量 (默认: 20) + +**响应**: +```json +{ + "success": true, + "data": [ + { + "rank": 1, + "userId": "uuid", + "nickname": "玩家昵称", + "avatarUrl": "头像URL", + "xpTotal": 5000 + } + ], + "pagination": { + "total": 100, + "page": 1, + "limit": 20 + }, + "error": null +} +``` + +--- + +#### GET /leaderboard/me + +获取当前用户排名。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": { + "rank": 15, + "xpTotal": 1500 + }, + "error": null +} +``` + +--- + +#### GET /achievements + +获取成就列表。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "code": "first_win", + "name": "初出茅庐", + "description": "完成第一道题", + "iconUrl": "图标URL", + "unlocked": true, + "unlockedAt": "2026-04-10T10:00:00.000Z" + } + ], + "error": null +} +``` + +--- + +#### POST /achievements/check + +检查并解锁新成就。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": { + "newlyUnlocked": [ + { + "id": "uuid", + "code": "streak_7", + "name": "连胜达人", + "description": "连续7天活跃" + } + ] + }, + "error": null +} +``` + +--- + +### 支付 + +#### POST /payment/verify-huawei + +验证华为 IAP 收据并激活订阅。 + +**认证**: JWT + +**请求体**: +```json +{ + "purchaseToken": "string (必填)", + "productId": "string (必填)", + "tier": "pro | proplus" +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "tier": "pro", + "provider": "huawei", + "active": true, + "expiresAt": "2026-05-10T00:00:00.000Z" + }, + "error": null +} +``` + +--- + +#### GET /payment/subscription + +获取当前订阅状态。 + +**认证**: JWT + +**响应**: +```json +{ + "success": true, + "data": { + "tier": "pro", + "provider": "huawei", + "active": true, + "expiresAt": "2026-05-10T00:00:00.000Z", + "autoRenew": true + }, + "error": null +} +``` + +--- + +## 管理端 API + +### 管理端认证 + +#### POST /admin/auth + +管理端认证。 + +**认证**: 无 + +**请求体**: +```json +{ + "token": "string (必填)" +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "authenticated": true + }, + "error": null +} +``` + +**错误 (401)**: +```json +{ + "success": false, + "data": null, + "error": { + "code": "UNAUTHORIZED", + "message": "Invalid admin token" + } +} +``` + +--- + +### 题目管理 + +#### GET /admin/questions + +获取题目列表。 + +**认证**: Admin Token + +**查询参数**: +- `page`: 页码 (默认: 1) +- `limit`: 每页数量 (默认: 20) +- `status`: draft | reviewing | published | archived (可选) +- `categoryId`: 分类 ID (可选) + +**响应**: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "stem": { "text": "题目内容" }, + "contentType": "text", + "correctAnswer": "B", + "distractors": ["A", "C", "D"], + "categoryId": "uuid", + "difficulty": 3, + "status": "published" + } + ], + "pagination": { + "total": 100, + "page": 1, + "limit": 20 + }, + "error": null +} +``` + +--- + +#### GET /admin/questions/:id + +获取题目详情。 + +**认证**: Admin Token + +**路径参数**: +- `id`: 题目 ID + +**响应**: +```json +{ + "success": true, + "data": { + "id": "uuid", + "stem": { "text": "题目内容" }, + "contentType": "text", + "correctAnswer": "B", + "distractors": ["A", "C", "D"], + "categoryId": "uuid", + "difficulty": 3, + "status": "published", + "knowledgeCard": { + "id": "uuid", + "summary": "知识点摘要", + "deepDive": "深入解析", + "sourceRef": "来源引用" + } + }, + "error": null +} +``` + +--- + +#### POST /admin/questions + +创建新题目。 + +**认证**: Admin Token + +**请求体**: +```json +{ + "stem": { "text": "题目内容" }, + "contentType": "text | image | video | audio", + "correctAnswer": "B (必填)", + "distractors": ["A", "C", "D"], + "categoryId": "uuid (必填)", + "difficulty": 3, + "knowledgeCard": { + "summary": "知识点摘要 (必填)", + "deepDive": "深入解析", + "sourceRef": "来源引用" + } +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "id": "uuid", + "status": "draft" + }, + "error": null +} +``` + +--- + +#### PUT /admin/questions/:id + +更新题目。 + +**认证**: Admin Token + +**路径参数**: +- `id`: 题目 ID + +**请求体**: +```json +{ + "stem": { "text": "题目内容" }, + "contentType": "text | image | video | audio", + "correctAnswer": "B", + "distractors": ["A", "C", "D"], + "categoryId": "uuid", + "difficulty": 3, + "status": "draft | reviewing | published | archived" +} +``` + +--- + +#### DELETE /admin/questions/:id + +归档题目。 + +**认证**: Admin Token + +**路径参数**: +- `id`: 题目 ID + +**响应**: +```json +{ + "success": true, + "data": null, + "error": null +} +``` + +--- + +#### POST /admin/questions/batch-publish + +批量发布题目。 + +**认证**: Admin Token + +**请求体**: +```json +{ + "ids": ["uuid1", "uuid2"] +} +``` + +**响应**: +```json +{ + "success": true, + "data": null, + "error": null +} +``` + +--- + +### 分类管理 + +#### GET /admin/categories + +获取所有分类。 + +**认证**: Admin Token + +**响应**: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "name": "历史", + "slug": "history", + "parentId": null, + "sortOrder": 1, + "status": "active" + } + ], + "error": null +} +``` + +--- + +#### POST /admin/categories + +创建分类。 + +**认证**: Admin Token + +**请求体**: +```json +{ + "id": "uuid (必填)", + "name": "分类名称 (必填)", + "slug": "分类slug (必填)", + "parentId": "uuid", + "sortOrder": 1 +} +``` + +--- + +#### PUT /admin/categories/:id + +更新分类。 + +**认证**: Admin Token + +**请求体**: +```json +{ + "name": "分类名称", + "slug": "分类slug", + "parentId": "uuid | null", + "sortOrder": 1, + "status": "active | inactive" +} +``` + +--- + +#### DELETE /admin/categories/:id + +归档分类。 + +**认证**: Admin Token + +--- + +### 知识点卡片 + +#### GET /admin/knowledge-cards + +获取知识点卡片列表。 + +**认证**: Admin Token + +**查询参数**: +- `page`: 页码 (默认: 1) +- `limit`: 每页数量 (默认: 20) + +--- + +#### GET /admin/knowledge-cards/by-question/:questionId + +根据题目 ID 获取知识点卡片。 + +**认证**: Admin Token + +--- + +#### PUT /admin/knowledge-cards/:id + +更新知识点卡片。 + +**认证**: Admin Token + +**请求体**: +```json +{ + "summary": "摘要 (必填)", + "deepDive": "深入解析", + "sourceRef": "来源引用" +} +``` + +--- + +### 技能树管理 + +#### GET /admin/skill-tree + +获取章节列表。 + +**认证**: Admin Token + +**查询参数**: +- `categoryId`: 分类 ID (可选) + +--- + +#### POST /admin/skill-tree + +创建章节。 + +**认证**: Admin Token + +**请求体**: +```json +{ + "categoryId": "uuid (必填)", + "title": "章节标题 (必填)", + "parentId": "uuid", + "sortOrder": 1, + "questionsRequired": 5, + "passThreshold": 3 +} +``` + +--- + +#### PUT /admin/skill-tree/:id + +更新章节。 + +**认证**: Admin Token + +**请求体**: +```json +{ + "title": "章节标题", + "parentId": "uuid | null", + "sortOrder": 1, + "questionsRequired": 5, + "passThreshold": 3 +} +``` + +--- + +#### DELETE /admin/skill-tree/:id + +删除章节。 + +**认证**: Admin Token + +--- + +### 用户管理 + +#### GET /admin/users + +获取用户列表。 + +**认证**: Admin Token + +**查询参数**: +- `page`: 页码 (默认: 1) +- `limit`: 每页数量 (默认: 20) +- `search`: 搜索关键词 (昵称/ID) + +**响应**: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "nickname": "玩家昵称", + "avatarUrl": "头像URL", + "tier": "free", + "xpTotal": 150, + "streakDays": 3, + "banned": false + } + ], + "pagination": { ... }, + "error": null +} +``` + +--- + +#### GET /admin/users/:id + +获取用户详情。 + +**认证**: Admin Token + +--- + +#### PUT /admin/users/:id/ban + +封禁用户。 + +**认证**: Admin Token + +--- + +#### PUT /admin/users/:id/unban + +解封用户。 + +**认证**: Admin Token + +--- + +### 统计数据 + +#### GET /admin/stats + +获取仪表盘统计数据。 + +**认证**: Admin Token + +**响应**: +```json +{ + "success": true, + "data": { + "totalUsers": 1000, + "activeUsers": 150, + "totalQuestions": 500, + "publishedQuestions": 450, + "averageXp": 200 + }, + "error": null +} +``` + +--- + +### 反馈管理 + +#### GET /admin/feedback + +获取用户反馈列表。 + +**认证**: Admin Token + +**查询参数**: +- `page`: 页码 (默认: 1) +- `limit`: 每页数量 (默认: 20) + +**响应**: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "userId": "uuid", + "content": "反馈内容", + "contact": "联系方式", + "pageContext": "页面上下文", + "createdAt": "2026-04-10T10:00:00.000Z" + } + ], + "pagination": { ... }, + "error": null +} +``` + +--- + +## 附录 + +### 错误代码 + +| 代码 | 说明 | +|------|------| +| VALIDATION_ERROR | 请求参数验证失败 | +| UNAUTHORIZED | 未认证或认证失败 | +| NOT_FOUND | 资源不存在 | +| INVALID_RECEIPT | 支付收据验证失败 | +| NOT_IMPLEMENTED | 功能未实现 | +| INTERNAL_ERROR | 服务器内部错误 | + +### 数据模型 + +#### User (用户) +```typescript +{ + id: string; // UUID + nickname: string | null; // 昵称 + avatarUrl: string | null; // 头像URL + tier: 'free' | 'pro' | 'proplus'; // 会员等级 + xpTotal: number; // 总经验值 + streakDays: number; // 连续天数 + heartsRemaining: number; // 剩余红心 + banned: boolean; // 是否封禁 +} +``` + +#### Question (题目) +```typescript +{ + id: string; // UUID + stem: Record; // 题目内容(支持多语言) + contentType: 'text' | 'image' | 'video' | 'audio'; + correctAnswer: string; // 正确答案 + distractors: string[]; // 干扰项 + categoryId: string; // 分类ID + difficulty: 1-5; // 难度等级 + status: 'draft' | 'reviewing' | 'published' | 'archived'; +} +``` + +#### Chapter (章节) +```typescript +{ + id: string; // UUID + categoryId: string; // 分类ID + title: string; // 章节标题 + parentId: string | null; // 父章节ID + sortOrder: number; // 排序 + questionsRequired: number; // 需要答题数 + passThreshold: number; // 通过阈值 +} +``` diff --git a/docs/env-secrets-guide.md b/docs/env-secrets-guide.md new file mode 100644 index 0000000..cc235f4 --- /dev/null +++ b/docs/env-secrets-guide.md @@ -0,0 +1,77 @@ +--- +title: 环境变量密钥生成指南 +date: 2026-04-10 +tags: + - duoqi-api + - devops + - security +--- + +# 环境变量密钥生成指南 + +duoqi-api 的 `.env` 中有多个需要生成的密钥/Token,本文档记录各字段的生成方式与注意事项。 + +## JWT_SECRET + +用于 JWT 签名与验证(`@fastify/jwt`),==必须==使用密码学安全的随机值。 + +```bash +# 推荐:openssl +openssl rand -base64 32 + +# 或用 Node.js +node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" + +# 或 hex 格式 +openssl rand -hex 32 +``` + +> [!warning] 安全要点 +> - 32 字节(256 位)是常见选择 +> - 泄露后可伪造任意用户 token +> - 更换后所有已颁发的 token 失效(用户全部掉线) +> - ==不要==提交到 Git,仅存在于 `.env` 文件 + +## ADMIN_TOKEN + +管理端鉴权 Token,`src/middleware/admin-auth.ts` 中直接字符串比对。 + +```bash +openssl rand -hex 16 +``` + +也可以自定义强密码,只要够长且不可预测。 + +> [!tip] 生产环境建议 +> 当前实现是明文比对,生产环境可考虑改为哈希比对(如 `bcrypt`),避免 `.env` 泄露时直接暴露 Token。 + +## HUAWEI_CLIENT_SECRET + +华为开发者平台提供,不可自行生成。在 [华为开发者联盟](https://developer.huawei.com/) 应用详情页获取。 + +## OSS_ACCESS_KEY_SECRET + +阿里云控制台提供。在 [RAM 访问控制](https://ram.console.aliyun.com/) 中创建 AccessKey 时获取。 + +> [!danger] 严禁 +> - 将 AccessKey 提交到 Git +> - 使用主账号 AccessKey(应创建 RAM 子账号并最小授权) + +## 快速生成所有密钥 + +一键生成 `JWT_SECRET` 和 `ADMIN_TOKEN`: + +```bash +echo "JWT_SECRET=$(openssl rand -base64 32)" +echo "ADMIN_TOKEN=$(openssl rand -hex 16)" +``` + +将输出复制到 `.env` 文件即可。 + +--- + +相关文件: +- `.env.example` — 环境变量模板 +- `src/utils/config.ts` — Zod 校验与启动 fail-fast +- `src/middleware/auth.ts` — JWT 验证逻辑 +- `src/middleware/admin-auth.ts` — Admin Token 验证逻辑