Compare commits
2 Commits
1116b9a2ec
...
6507d9e9ac
| Author | SHA1 | Date | |
|---|---|---|---|
| 6507d9e9ac | |||
| b2e5b8f789 |
66
CLAUDE.md
66
CLAUDE.md
@ -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.
|
||||
|
||||
@ -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` | 订阅平台暂不支持 |
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user