232 lines
8.1 KiB
Markdown
232 lines
8.1 KiB
Markdown
# Implementation Plan: HTTP API 层迁移
|
||
|
||
## [Overview]
|
||
|
||
将 Flutter 项目 `/lib/core/net/` 与 `/lib/data/services/api/` 下的 HTTP 网络层迁移至 Next.js 16,使用 **ofetch** 作为 HTTP 客户端(轻量、同构、拦截器钩子丰富),通过 `.env.local` 配置环境变量,复用已迁移的 Zod 数据模型进行响应验证。
|
||
|
||
## [Types]
|
||
|
||
### ApiResult 统一响应包装
|
||
|
||
```typescript
|
||
// src/data/api/api_result.ts
|
||
export type ApiResult<T> =
|
||
| { success: true; data: T }
|
||
| { success: false; error: ApiError };
|
||
|
||
export class ApiError extends Error {
|
||
readonly code: string;
|
||
readonly status?: number;
|
||
readonly details?: unknown;
|
||
constructor(code: string, message: string, status?: number, details?: unknown);
|
||
}
|
||
```
|
||
|
||
### 环境配置类型
|
||
|
||
```typescript
|
||
// src/data/api/config/api_config.ts
|
||
export type AppEnv = "development" | "test" | "production";
|
||
|
||
export interface ApiConfig {
|
||
baseUrl: string;
|
||
wsUrl: string;
|
||
connectTimeout: number; // ms
|
||
receiveTimeout: number; // ms
|
||
sendTimeout: number; // ms
|
||
}
|
||
```
|
||
|
||
### 拦截器钩子签名
|
||
|
||
```typescript
|
||
// ofetch 原生 hooks
|
||
onRequest?: (ctx: { request: Request; options: FetchOptions }) => void | Promise<void>;
|
||
onResponse?: (ctx: { request: Request; response: Response; options: FetchOptions }) => void | Promise<void>;
|
||
onResponseError?: (ctx: { request: Request; response: Response; options: FetchOptions; error: FetchError }) => void | Promise<void>;
|
||
onError?: (ctx: { request: Request; error: Error; options: FetchOptions }) => void | Promise<void>;
|
||
```
|
||
|
||
## [Files]
|
||
|
||
### 新增
|
||
|
||
| 路径 | 用途 |
|
||
|---|---|
|
||
| `src/data/api/api_path.ts` | 路径常量(40+ endpoints) |
|
||
| `src/data/api/api_result.ts` | 统一响应包装 `ApiResult<T>` + `ApiError` |
|
||
| `src/data/api/config/api_config.ts` | 环境 → baseUrl/超时 配置 |
|
||
| `src/data/api/storage/auth_storage.ts` | localStorage 包装(token 持久化) |
|
||
| `src/data/api/storage/device_storage.ts` | 设备 ID 持久化 |
|
||
| `src/data/api/http_client.ts` | ofetch 实例 + 拦截器装配 |
|
||
| `src/data/api/interceptor/token_interceptor.ts` | 注入 `Authorization: Bearer` |
|
||
| `src/data/api/interceptor/auth_refresh_interceptor.ts` | 401 → 自动刷新 token |
|
||
| `src/data/api/interceptor/logging_interceptor.ts` | 请求/响应日志(开发模式) |
|
||
| `src/data/api/auth_api.ts` | 11 个 auth 接口 |
|
||
| `src/data/api/chat_api.ts` | 5 个 chat 接口(含 multipart) |
|
||
| `src/data/api/user_api.ts` | 5 个 user 接口 |
|
||
| `src/data/api/metrics_api.ts` | 2 个 metrics 接口 |
|
||
| `src/data/api/index.ts` | barrel 导出 |
|
||
| `.env.local` | 本地环境变量(不提交) |
|
||
| `.env.example` | 环境变量模板(提交) |
|
||
|
||
### 修改
|
||
|
||
| 路径 | 修改 |
|
||
|---|---|
|
||
| `package.json` | 添加 `ofetch` 依赖 |
|
||
| `pnpm-lock.yaml` | `pnpm install` 自动更新 |
|
||
| `.gitignore` | 确保 `.env.local` 已被忽略(Next.js 默认) |
|
||
|
||
### 不迁移
|
||
|
||
- ❌ `websocket_handler.dart` / `websocket_manager.dart` — WebSocket 独立项
|
||
- ❌ `message_queue.dart` — 离线消息队列
|
||
- ❌ `service_manager.dart` 的 dio 特定部分(已被 ofetch 实例替代)
|
||
- ❌ `payment_api_client.dart` — 原始 Dart 也不存在客户端类
|
||
|
||
## [Functions]
|
||
|
||
### 新增
|
||
|
||
| 函数 | 签名 | 文件 | 用途 |
|
||
|---|---|---|---|
|
||
| `getAuthToken()` | `() => Promise<string \| null>` | `storage/auth_storage.ts` | 优先登录 token,回退游客 token |
|
||
| `setLoginToken(t)` | `(token: string) => Promise<void>` | 同上 | 持久化登录 token |
|
||
| `setGuestToken(t)` | `(token: string) => Promise<void>` | 同上 | 持久化游客 token |
|
||
| `clearAuthData()` | `() => Promise<void>` | 同上 | 清除所有 auth 数据 |
|
||
| `getDeviceId()` | `() => string` | `storage/device_storage.ts` | 读取/生成设备 ID |
|
||
| `getApiConfig()` | `() => ApiConfig` | `config/api_config.ts` | 当前环境配置 |
|
||
| `createHttpClient()` | `() => typeof ofetch` | `http_client.ts` | ofetch 实例工厂 |
|
||
|
||
### 删除/重写
|
||
|
||
| 函数 | 旧实现 | 新实现 |
|
||
|---|---|---|
|
||
| `BackendService` 单例 | Dio + 4 ApiClient 字段 | ofetch + 4 api 类模块级单例 |
|
||
| `getBaseUrl()` | ServiceType.getBaseUrl() | api_config.ts 静态方法 |
|
||
| `getServiceConfig()` | ServiceType 扩展 | api_config.ts 静态方法 |
|
||
|
||
## [Classes]
|
||
|
||
### 新增
|
||
|
||
| 类 | 文件 | 关键方法 |
|
||
|---|---|---|
|
||
| `ApiError` | `api_result.ts` | 构造、`toString()` |
|
||
| `AuthStorage` | `storage/auth_storage.ts` | `getLoginToken`/`setLoginToken`/`getRefreshToken`/`getGuestToken`/`setGuestToken`/`hasLoginToken`/`clearAuthData` |
|
||
| `DeviceStorage` | `storage/device_storage.ts` | `getDeviceId()` |
|
||
| `AuthApi` | `auth_api.ts` | `sendCode`/`emailLogin`/`register`/`appleLogin`/`googleLogin`/`facebookLogin`/`facebookIdLogin`/`logout`/`guestLogin`/`refreshToken`/`getCurrentUser` |
|
||
| `ChatApi` | `chat_api.ts` | `sendMessage`/`getHistory`/`speechToText(multipart)`/`syncMessages`/`uploadImage(multipart)` |
|
||
| `UserApi` | `user_api.ts` | `getUserStats`/`getCurrentUser`/`updateProfile`/`getCredits`/`getCreditsHistory` |
|
||
| `MetricsApi` | `metrics_api.ts` | `reportPwaEvent`/`reportUserInfo` |
|
||
|
||
### 修改
|
||
|
||
无(无现有 class 需要修改)
|
||
|
||
### 拦截器实现(函数式,非 class)
|
||
|
||
由于 ofetch 钩子是函数式 API,拦截器以函数形式实现:
|
||
|
||
```typescript
|
||
// interceptor/token_interceptor.ts
|
||
export const tokenInterceptor: FetchHook = async (ctx) => {
|
||
const path = new URL(ctx.request.url).pathname;
|
||
if (!needsToken(path)) return;
|
||
const token = await getAuthToken();
|
||
if (token) {
|
||
ctx.request.headers.set("Authorization", `Bearer ${token}`);
|
||
}
|
||
};
|
||
|
||
// interceptor/auth_refresh_interceptor.ts
|
||
export const authRefreshInterceptor: FetchHook = async (ctx) => {
|
||
if (ctx.response?.status !== 401) return;
|
||
// 401 处理 + token 刷新 + 请求重试
|
||
};
|
||
|
||
// http_client.ts
|
||
const client = ofetch.create({
|
||
baseURL: getApiConfig().baseUrl,
|
||
timeout: getApiConfig().receiveTimeout,
|
||
hooks: {
|
||
onRequest: [loggingInterceptor, tokenInterceptor],
|
||
onResponse: [loggingInterceptor],
|
||
onResponseError: [authRefreshInterceptor, loggingInterceptor],
|
||
},
|
||
});
|
||
```
|
||
|
||
## [Dependencies]
|
||
|
||
### 新增
|
||
|
||
| 包 | 版本 | 用途 |
|
||
|---|---|---|
|
||
| `ofetch` | `^1.5.0` | 同构 HTTP 客户端(浏览器 + Node.js) |
|
||
|
||
### 安装
|
||
|
||
```bash
|
||
pnpm add ofetch
|
||
```
|
||
|
||
### 版本选择
|
||
|
||
选择 ofetch v1.5+,原因:
|
||
- 完整同构支持(unjs/h3 生态)
|
||
- 内置拦截器钩子
|
||
- 体积小(~5KB gzipped)
|
||
- TypeScript 优先
|
||
|
||
## [Testing]
|
||
|
||
### 验证策略
|
||
|
||
每个批次完成后:
|
||
- `npx tsc --noEmit` — 0 错误
|
||
- `pnpm lint` — 通过
|
||
|
||
最终验证:
|
||
- 模块导入测试(`import { authApi, chatApi } from '@/data/api'`)
|
||
- 拦截器单元测试(mock localStorage 与 fetch)
|
||
- 端到端测试(启动 dev server,触发实际请求)
|
||
|
||
### 测试要点
|
||
|
||
1. **环境变量加载**:`process.env.NEXT_PUBLIC_API_BASE_URL` 正确解析
|
||
2. **Token 注入**:`getCurrentUser` 自动携带 Authorization 头
|
||
3. **401 刷新**:模拟 401 响应,验证 token 刷新队列与重试
|
||
4. **Multipart 上传**:`uploadImage` 正确发送 FormData
|
||
5. **超时控制**:30s/60s 配置生效
|
||
|
||
## [Implementation Order]
|
||
|
||
按依赖关系,**9 个步骤**:
|
||
|
||
1. **安装 ofetch**:`pnpm add ofetch`(修改 `package.json` + `pnpm-lock.yaml`)
|
||
2. **创建 `.env.example` + `.env.local`**:环境变量模板
|
||
3. **批次 A:基础设施**(5 个文件)
|
||
- `api_path.ts`(路径常量)
|
||
- `api_result.ts`(响应包装)
|
||
- `config/api_config.ts`(环境配置)
|
||
- `storage/auth_storage.ts`(localStorage 包装)
|
||
- `storage/device_storage.ts`(设备 ID)
|
||
4. **批次 B:拦截器 + http_client**(4 个文件)
|
||
- `interceptor/token_interceptor.ts`
|
||
- `interceptor/auth_refresh_interceptor.ts`
|
||
- `interceptor/logging_interceptor.ts`
|
||
- `http_client.ts`(ofetch 实例装配)
|
||
5. **批次 C:API clients**(4 个文件,独立)
|
||
- `auth_api.ts`(11 个方法)
|
||
- `chat_api.ts`(5 个方法)
|
||
- `user_api.ts`(5 个方法)
|
||
- `metrics_api.ts`(2 个方法)
|
||
6. **批次 D:barrel 导出**(1 个文件)
|
||
- `index.ts`
|
||
7. **最终验证**:
|
||
- `npx tsc --noEmit`
|
||
- `pnpm lint`
|
||
- 端到端 smoke test
|