# 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

---

# 后续迁移：第三方登录平台（IAuthPlatform）

## [Overview]

将 Flutter `IAuthPlatform`（`/Users/chase/Documents/cozsweet/lib/data/services/auth/`）迁移为 Next.js 16 项目的纯 TypeScript 服务层。SDK 选用 `@react-oauth/google`（Google Identity Services React 包装器）+ `@greatsumini/react-facebook-login`（`react-facebook` 的活跃 fork，兼容 React 19）。

**Flutter 端的 `AuthRepository` 已经完整迁移**（`src/data/repositories/auth_repository.ts`，含 `googleLogin`/`facebookLogin`/`facebookIdLogin`/`appleLogin`），本步只补齐"生产第三方 identity token"那一段——平台层产 token，调用方拿 token 去调 `authRepository`（或 `authApi`）完成登录。

**范围限定**：
- 客户端 SDK 接入层，**不**写 React Context/useAuth，**不**写 UI
- 跳过 Apple（Flutter 端本来就是 stub）
- 平台**只产 token**，不调 `authRepository` / `authApi`
- 保留 Mock（`NEXT_PUBLIC_USE_MOCK_AUTH=true` 切换，对齐 Dart `kDebugMode`）

## [Architecture: event-bridge 模式]

`IAuthPlatform` 必须是 plain TypeScript（不依赖 React），但 React 包装器 SDK（`useGoogleOAuth`、`FacebookLoginClient`）只能在 React 组件里用。解决方案是 **plain-TS 平台类 ↔ React Bridge 组件通过 DOM `CustomEvent` 通信**：

```
WebAuthPlatform (plain TS)              AuthPlatformBridge ("use client")
┌──────────────────────┐               ┌──────────────────────────┐
│ googleSignIn()       │               │                          │
│  ├─ new Promise      │ dispatchEvent │  useGoogleOAuth()        │
│  │  store requestId  │ ────────────▶ │  window.google.accounts  │
│  │   → resolver      │               │    .id.initialize({...}) │
│  └─ return Promise   │               │  on auth event:          │
│                      │ resolveGoogle │    window.google.accounts│
│  resolveGoogle(      │ ◀──────────── │      .id.prompt()        │
│    requestId, result)│               │  GIS callback:           │
└──────────────────────┘               │    resolve the promise   │
                                       └──────────────────────────┘
```

- `WebAuthPlatform` 暴露公开方法 `resolveGoogle(requestId, result)` / `resolveFacebook(...)` 给 Bridge 回写 Promise
- `AuthPlatformBridge` 监听 `cozsweet:auth:google:start` / `cozsweet:auth:facebook:start` 事件，触发对应 SDK 流程
- **v1 限制**：单 popup 语义——一次只追踪一个待处理 Google / Facebook 请求；并发调用会被覆盖

## [Files]

### 新建（9 个文件）

| 路径 | 作用 |
|---|---|
| `src/data/services/auth/auth_platform.ts` | `IAuthPlatform` 接口 + `AuthPlatformFactory` + `getAuthPlatform()` 单例 + 命名类型 `GoogleCredential` / `FacebookLoginResult` |
| `src/data/services/auth/web_auth_platform.ts` | 真机平台类（plain TS），通过 DOM 事件与 Bridge 通信；含 `resolveGoogle()` / `resolveFacebook()` 公开方法 |
| `src/data/services/auth/mock_auth_platform.ts` | 调试用 Mock（返回固定 token + 250ms 模拟延迟） |
| `src/data/services/auth/auth_event_bridge.ts` | 跨 plain-TS / React 边界的 DOM 事件通道（无 React 依赖；导出事件名常量 + dispatch / subscribe 助手） |
| `src/data/services/auth/auth_bridge_component.tsx` | `"use client"` 组件，承载 `useGoogleOAuth` + `FacebookLoginClient`，监听事件并 resolve Promise |
| `src/data/services/auth/initialize_google_auth.ts` | Google GIS 预热辅助（占位） |
| `src/data/services/auth/initialize_facebook_auth.ts` | Facebook SDK 预热辅助（占位） |
| `src/data/schemas/auth/facebook_user_data.ts` | Zod schema + `Input` / `Data` 类型 |
| `src/data/dto/auth/facebook_user_data.ts` | `FacebookUserData` DTO 类（`from` / `fromJson` / `toJson`） |
| `src/data/services/auth/global.d.ts` | `window.google.accounts.id.*` 的全局类型声明 |

### 修改

| 路径 | 变更 |
|---|---|
| `package.json` | 新增 `@react-oauth/google@0.13.5`、`@greatsumini/react-facebook-login@3.4.0` |
| `.env.example` | 新增 `NEXT_PUBLIC_GOOGLE_CLIENT_ID`、`NEXT_PUBLIC_FACEBOOK_APP_ID`、`NEXT_PUBLIC_FACEBOOK_API_VERSION=v25.0`、`NEXT_PUBLIC_USE_MOCK_AUTH=false` |
| `barrelsby.json` | 修整：删除已不存在的 `./src/data/models` 残留；新增 `./src/data/services` + `./src/data/services/auth` |

### 删除

| 路径 | 原因 |
|---|---|
| `src/data/storage/index.ts` | barrelsby 自动生成的 barrel，把同步 `AuthStorage`（`./auth_storage`）和异步 `AuthStorage`（`./auth/auth_storage`）同时 re-export，触发 TS2308 类型冲突。该 barrel 在仓库内**无消费者**（所有 import 都走直接路径 `@/data/storage/auth_storage` 或 `@/data/storage/auth/auth_storage`），可安全删除；后续若需要 barrel 须先解决命名冲突 |

## [Google Flow 决策（重要）]

`@react-oauth/google@0.13.5` 的 `useGoogleLogin(options)` 只支持两种 flow：
- `flow: 'implicit'` → 回调 `TokenResponse`，含 `access_token`（**不是** id_token）
- `flow: 'auth-code'` → 回调 `CodeResponse`，含 `code`（授权码，需后端交换）

**但** 后端 `GoogleLoginRequest.idToken: z.string()` 期望的是 JWT id_token（与 Flutter `GoogleSignInAccount.authentication.idToken` 字段同名同义），所以 `useGoogleLogin` 两种 flow 都不适配。

**采用方案**：直接调用 GIS 全局 API（在 Bridge 的 `useEffect` 中），由 `GoogleOAuthProvider` 加载 GIS 脚本后：
- `window.google.accounts.id.initialize({ client_id, callback })` —— 注册 callback
- `window.google.accounts.id.prompt()` —— 触发弹窗
- callback 收到 `{ credential: '<JWT>' }`，`credential` 即 id_token

JWT payload 通过 `atob(parts[1])` 解析拿到 `email` / `name` / `picture`，填充 `GoogleCredential` 接口字段。

## [调用方契约（参考实现，**不在本步交付**）]

```ts
import { getAuthPlatform } from "@/data/services/auth";
import { authRepository } from "@/data/repositories";
import { AuthStorage } from "@/data/storage/auth/auth_storage";

const platform = getAuthPlatform({
  googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
  facebookAppId: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID!,
});

async function googleLogin() {
  const r = await platform.googleSignIn();
  if (r.kind === "failure") return handleError(r.error);
  const guestId = (await AuthStorage.getInstance().getDeviceId()).value ?? "";
  await authRepository.googleLogin({ idToken: r.value.idToken, guestId });
  // token 持久化由 authRepository._saveLoginData 自动完成
}

async function facebookLogin() {
  const r = await platform.facebookSignIn();
  if (r.kind === "failure") return handleError(r.error);
  await authRepository.facebookLogin({ accessToken: r.value.accessToken, guestId: "" });
  // 拉头像 + by-id 登录（与 Flutter AuthBloc._uploadFacebookAvatar 一致）
  const ud = await platform.getFacebookUserData();
  if (ud.kind === "success" && ud.value.id && ud.value.pictureUrl) {
    await AuthStorage.getInstance().setFacebookId(ud.value.id);
    await authRepository.facebookIdLogin({ fbId: ud.value.id, avatarUrl: ud.value.pictureUrl });
  }
}
```

## [下一步：UI 挂载方式（留给下一轮迁移）]

本步**不**挂载到 `src/app/layout.tsx`。下一轮 UI 接入时按如下方式挂载：

```tsx
// src/app/layout.tsx
import { GoogleOAuthProvider } from "@react-oauth/google";
import { getAuthPlatform, AuthPlatformBridge } from "@/data/services/auth";

const platform = getAuthPlatform({
  googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
  facebookAppId: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID!,
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}>
          <AuthPlatformBridge platform={platform}>
            {children}
          </AuthPlatformBridge>
        </GoogleOAuthProvider>
      </body>
    </html>
  );
}
```

## [Implementation Order]

1. **安装**：`pnpm add @react-oauth/google @greatsumini/react-facebook-login`
2. **追加 env** 到 `.env.example`
3. **修整 `barrelsby.json`**：删除 `./src/data/models` 残留、新增 `./src/data/services` 与 `./src/data/services/auth`
4. **删除** `src/data/storage/index.ts`（命名冲突）
5. **新建 Zod schema** + **DTO 类** for `FacebookUserData`
6. **生成 barrel**：`pnpm generate-barrels`
7. **新建 global.d.ts**：声明 `window.google.accounts.id.*` 类型
8. **依次新建** `auth_event_bridge.ts`、`initialize_*.ts`、`auth_platform.ts`、`web_auth_platform.ts`、`mock_auth_platform.ts`、`auth_bridge_component.tsx`
9. **验证**：`npx tsc --noEmit`、`pnpm lint`、`pnpm build`
