Compare commits

..

2 Commits

Author SHA1 Message Date
6507d9e9ac fix: 登录响应结构与 API 文档对齐
All checks were successful
CI/CD Pipeline / Code Quality (push) Successful in 20s
CI/CD Pipeline / Unit Tests (push) Successful in 14s
CI/CD Pipeline / Build & Deploy Test (push) Has been skipped
CI/CD Pipeline / Build & Deploy Production (push) Successful in 1m29s
- LoginResponse 的 tokens 嵌入 tokens 对象,移除平铺的 accessToken/refreshToken
- user 移除 xpTotal/streakDays(文档中未定义)
- refreshJwt 同时返回 accessToken 和 refreshToken,复用 signTokens
2026-05-25 00:37:12 +08:00
b2e5b8f789 docs: 补充 POST /v1/auth/link 接口文档与 CLAUDE.md 项目约定
- 新增 /auth/link 游客账号关联接口的完整 API 文档
  (认证表、请求/响应格式、场景说明、幂等保证、错误码 CONFLICT)
- CLAUDE.md 补充相关项目引用和编码行为约定
2026-05-24 00:41:39 +08:00
4 changed files with 174 additions and 16 deletions

View File

@ -7,6 +7,10 @@
多奇Duoqi是游戏化知识闯关学习平台。duoqi-api 是三端HarmonyOS / Flutter / Web共享的后端服务从 Phase 1 起即为 HarmonyOS 客户端提供 API 支持。
## 相关项目
- **duoqi-flutter**: `../duoqi-flutter/`, Flutter 客户端代码库.
## 技术栈
| 层面 | 选型 | 说明 |
@ -102,3 +106,65 @@ db/seeds/index.ts # 幂等种子导入脚本
- 认证:`Authorization: Bearer <jwt>`(公开端点:`/v1/auth/*`, `/v1/health`
- Admin 认证:`Authorization: Bearer <admin_token>``/v1/admin/*`
- JWT 有效期access_token 1h, refresh_token 30d
## 其他约定
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
### 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
### 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
### 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
### 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

View File

@ -20,6 +20,7 @@
| 类型 | Header | 适用路径 |
|------|--------|----------|
| 无需认证 | - | `/health`, `/v1/auth/guest`, `/v1/auth/huawei`, `/v1/auth/refresh`, `/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/*` |
@ -154,6 +155,102 @@
}
```
#### 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
认证:无
@ -1397,6 +1494,7 @@ categoryId,contentType,difficulty,stemText,correctAnswer,distractor1,distractor2
| `UNAUTHORIZED` | 未认证或认证失败 |
| `FORBIDDEN` | 权限不足 |
| `NOT_FOUND` | 资源不存在 |
| `CONFLICT` | 资源冲突(如重复迁移、已关联账号) |
| `INVALID_STATUS_TRANSITION` | 题目状态流转不合法 |
| `INVALID_RECEIPT` | 支付收据验证失败 |
| `UNSUPPORTED_PLATFORM` | 订阅平台暂不支持 |

View File

@ -18,15 +18,15 @@ export function buildLoginResponse(
refreshToken: string,
): LoginResponse {
return {
accessToken,
refreshToken,
user: {
id: user.id,
nickname: user.nickname ?? null,
avatarUrl: user.avatarUrl ?? null,
tier: user.tier ?? 'free',
xpTotal: user.xpTotal ?? 0,
streakDays: user.streakDays ?? 0,
},
tokens: {
accessToken,
refreshToken,
},
};
}
@ -90,13 +90,7 @@ export async function findOrCreateHuawei(
return buildLoginResponse(user, tokens.accessToken, tokens.refreshToken);
}
export async function refreshJwt(app: FastifyInstance, refreshToken: string): Promise<{ accessToken: string }> {
export async function refreshJwt(app: FastifyInstance, refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
const decoded = app.jwt.verify<JwtPayload>(refreshToken);
const payload: JwtPayload = {
userId: decoded.userId,
authType: decoded.authType,
tier: decoded.tier,
};
const accessToken = app.jwt.sign(payload, { expiresIn: '1h' });
return { accessToken };
return signTokens(app, decoded.userId, decoded.authType, decoded.tier ?? 'free');
}

View File

@ -8,15 +8,15 @@ export interface JwtPayload {
}
export interface LoginResponse {
accessToken: string;
refreshToken: string;
user: {
id: string;
nickname: string | null;
avatarUrl: string | null;
tier: string;
xpTotal: number;
streakDays: number;
};
tokens: {
accessToken: string;
refreshToken: string;
};
}