Compare commits
No commits in common. "6507d9e9acd4c704dd3e3d7bdf6efbf52fe7b916" and "1116b9a2ecabe12f6fc0ea3a588f2a52db6c3e91" have entirely different histories.
6507d9e9ac
...
1116b9a2ec
66
CLAUDE.md
66
CLAUDE.md
@ -7,10 +7,6 @@
|
|||||||
|
|
||||||
多奇(Duoqi)是游戏化知识闯关学习平台。duoqi-api 是三端(HarmonyOS / Flutter / Web)共享的后端服务,从 Phase 1 起即为 HarmonyOS 客户端提供 API 支持。
|
多奇(Duoqi)是游戏化知识闯关学习平台。duoqi-api 是三端(HarmonyOS / Flutter / Web)共享的后端服务,从 Phase 1 起即为 HarmonyOS 客户端提供 API 支持。
|
||||||
|
|
||||||
## 相关项目
|
|
||||||
|
|
||||||
- **duoqi-flutter**: `../duoqi-flutter/`, Flutter 客户端代码库.
|
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
| 层面 | 选型 | 说明 |
|
| 层面 | 选型 | 说明 |
|
||||||
@ -106,65 +102,3 @@ db/seeds/index.ts # 幂等种子导入脚本
|
|||||||
- 认证:`Authorization: Bearer <jwt>`(公开端点:`/v1/auth/*`, `/v1/health`)
|
- 认证:`Authorization: Bearer <jwt>`(公开端点:`/v1/auth/*`, `/v1/health`)
|
||||||
- Admin 认证:`Authorization: Bearer <admin_token>`(`/v1/admin/*`)
|
- Admin 认证:`Authorization: Bearer <admin_token>`(`/v1/admin/*`)
|
||||||
- JWT 有效期:access_token 1h, refresh_token 30d
|
- 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.
|
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
| 类型 | Header | 适用路径 |
|
| 类型 | Header | 适用路径 |
|
||||||
|------|--------|----------|
|
|------|--------|----------|
|
||||||
| 无需认证 | - | `/health`, `/v1/auth/guest`, `/v1/auth/huawei`, `/v1/auth/refresh`, `/v1/admin/login` |
|
| 无需认证 | - | `/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 |
|
| JWT | `Authorization: Bearer <jwt_token>` | 大多数客户端 API |
|
||||||
| Admin JWT | `Authorization: Bearer <admin_jwt_token>` | `/v1/admin/*` |
|
| Admin JWT | `Authorization: Bearer <admin_jwt_token>` | `/v1/admin/*` |
|
||||||
|
|
||||||
@ -155,102 +154,6 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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
|
#### POST /auth/phone
|
||||||
|
|
||||||
认证:无
|
认证:无
|
||||||
@ -1494,7 +1397,6 @@ categoryId,contentType,difficulty,stemText,correctAnswer,distractor1,distractor2
|
|||||||
| `UNAUTHORIZED` | 未认证或认证失败 |
|
| `UNAUTHORIZED` | 未认证或认证失败 |
|
||||||
| `FORBIDDEN` | 权限不足 |
|
| `FORBIDDEN` | 权限不足 |
|
||||||
| `NOT_FOUND` | 资源不存在 |
|
| `NOT_FOUND` | 资源不存在 |
|
||||||
| `CONFLICT` | 资源冲突(如重复迁移、已关联账号) |
|
|
||||||
| `INVALID_STATUS_TRANSITION` | 题目状态流转不合法 |
|
| `INVALID_STATUS_TRANSITION` | 题目状态流转不合法 |
|
||||||
| `INVALID_RECEIPT` | 支付收据验证失败 |
|
| `INVALID_RECEIPT` | 支付收据验证失败 |
|
||||||
| `UNSUPPORTED_PLATFORM` | 订阅平台暂不支持 |
|
| `UNSUPPORTED_PLATFORM` | 订阅平台暂不支持 |
|
||||||
|
|||||||
@ -18,15 +18,15 @@ export function buildLoginResponse(
|
|||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
): LoginResponse {
|
): LoginResponse {
|
||||||
return {
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
nickname: user.nickname ?? null,
|
nickname: user.nickname ?? null,
|
||||||
avatarUrl: user.avatarUrl ?? null,
|
avatarUrl: user.avatarUrl ?? null,
|
||||||
tier: user.tier ?? 'free',
|
tier: user.tier ?? 'free',
|
||||||
},
|
xpTotal: user.xpTotal ?? 0,
|
||||||
tokens: {
|
streakDays: user.streakDays ?? 0,
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -90,7 +90,13 @@ export async function findOrCreateHuawei(
|
|||||||
return buildLoginResponse(user, tokens.accessToken, tokens.refreshToken);
|
return buildLoginResponse(user, tokens.accessToken, tokens.refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshJwt(app: FastifyInstance, refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
|
export async function refreshJwt(app: FastifyInstance, refreshToken: string): Promise<{ accessToken: string }> {
|
||||||
const decoded = app.jwt.verify<JwtPayload>(refreshToken);
|
const decoded = app.jwt.verify<JwtPayload>(refreshToken);
|
||||||
return signTokens(app, decoded.userId, decoded.authType, decoded.tier ?? 'free');
|
const payload: JwtPayload = {
|
||||||
|
userId: decoded.userId,
|
||||||
|
authType: decoded.authType,
|
||||||
|
tier: decoded.tier,
|
||||||
|
};
|
||||||
|
const accessToken = app.jwt.sign(payload, { expiresIn: '1h' });
|
||||||
|
return { accessToken };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,15 +8,15 @@ export interface JwtPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
nickname: string | null;
|
nickname: string | null;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
tier: string;
|
tier: string;
|
||||||
};
|
xpTotal: number;
|
||||||
tokens: {
|
streakDays: number;
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user