# 统一请求库封装计划（src/utils/request.ts 版）

## Summary

目标：把统一请求库集中封装在 `src/utils/request.ts`，类型集中在 `src/shared/`，实现：

- baseURL/timeout/取消请求（Axios Web）
- 默认 header 注入：Authorization（Bearer）、clientId、X-Latitude、X-Longitude、language
- 业务错误/网络错误/HTTP 错误的统一归一化（只抛错，不做 Toast/跳转）
- 输出说明文档到 `docs/request.md`

## Current State Analysis（以实际仓库为准）

已确认的事实：

- 依赖中已包含 `axios`，但 `src/` 内暂无 axios 的实际封装与使用。
- `src/utils/request.ts` 存在但为空文件，且命名疑似历史遗留。
- 代码中存在大量“缺失模块引用”（详见文末“历史遗留问题清单”），其中 `@/request/api/*` 属于最集中、最影响后续迁移的一类。

结论：

- 本次统一请求库规划要做到“新增即可用、可渐进替换”，不依赖现有（缺失的）API 模块结构。

## Assumptions & Decisions（你已确认）

- 底层实现：Axios（Web）
- baseURL：从 `import.meta.env` 的环境变量读取（本计划固定变量名为 `VITE_API_BASE_URL`）
- header 值来源：全局上下文注入（内存态），由登录/定位/语言切换时写入
- Authorization：`Bearer <token>`（token 为空则不带 Authorization）
- 错误处理：请求库只做错误标准化并抛错，不做 Toast/跳转
- header 字段：`Authorization`、`clientId`、`X-Latitude`、`X-Longitude`、`language`

## 为什么不拆得更细（回答你的问题）

可以都放在 `src/utils/request.ts` 里完成，且更适合你现在“先落地一个可用的统一入口”的诉求：

- 单文件落地成本低：少目录、少入口、少导出点，替换/回滚都更直接
- 不牺牲可维护性：在单文件里用“内部分区函数 + 类型从 shared 引入”的方式，同样能保持边界清晰
- 何时再拆分：当出现“单文件 > 300~500 行、多人同时改动频繁、需要独立单测/Mock”的情况，再把 errors/context/http 拆出去更合理

因此本计划采用：**实现集中在 request.ts，类型抽到 shared** 的折中方案。

## Proposed Changes（全新规划）

### 1) 新增类型目录：`src/shared/`

新增目录：`src/shared/`

新增文件（建议）：

- `src/shared/request-types.ts`

包含的类型定义（稳定、可复用）：

- `export interface ApiResponse<T> { code: number; message?: string; data: T }`
- `export interface RequestOptions { signal?: AbortSignal; headers?: Record<string, string>; skipAuth?: boolean }`
- `export type RequestContext = { token: string | null; clientId: string | null; latitude: number | null; longitude: number | null; language: string | null }`
- `export type NormalizedErrorKind = "business" | "http" | "network" | "unknown"`
- `export interface NormalizedError extends Error { kind: NormalizedErrorKind; code?: number; httpStatus?: number; response?: unknown }`

### 2) 统一请求实现：`src/utils/request.ts`

新增文件：`src/utils/request.ts`

职责（全部在一个文件内完成）：

#### 2.1 上下文容器（内存态）

提供以下导出函数（由业务在合适时机调用）：

- `setAuthToken(token: string | null): void`
- `setClientId(clientId: string | null): void`
- `setLocation(latitude: number | null, longitude: number | null): void`
- `setLanguage(language: string | null): void`
- `getRequestContext(): RequestContext`

默认值策略：

- `language`：如果未显式设置，尝试读取 `src/i18n/index.ts` 的 `getCurrentLocale()`（读取失败则不注入 language header）

#### 2.2 Axios 实例创建与默认配置

- `baseURL`：`import.meta.env.VITE_API_BASE_URL`
  - 若缺失：直接抛出清晰错误（提示需要配置 baseURL）
- `timeout`：可选 `import.meta.env.VITE_API_TIMEOUT_MS`，缺失则默认 15000

#### 2.3 请求拦截器（header 注入）

组装 headers 规则：

- 合并顺序：`默认headers < 上下文headers < 调用方options.headers`
- `Authorization`：仅当 token 存在时注入 `Bearer <token>`；当 `options.skipAuth === true` 时跳过
- `clientId`：存在则注入
- `X-Latitude` / `X-Longitude`：存在则注入（数值转字符串）
- `language`：存在则注入

#### 2.4 响应处理（业务码 + 错误归一化）

1. 正常响应：
   - 若响应体符合 `ApiResponse<T>` 且 `code === 0`：返回整个 `ApiResponse<T>`
   - 若响应体符合 `ApiResponse<T>` 且 `code !== 0`：抛出 `NormalizedError(kind="business", code, message, response)`
2. 异常响应（axios error）：
   - 有 `response.status`：抛 `NormalizedError(kind="http", httpStatus, response)`
   - 无 `response`：抛 `NormalizedError(kind="network")`

对外导出（建议最小集合）：

- `export async function request<T>(config, options?: RequestOptions): Promise<ApiResponse<T>>`
- 可选：`export async function requestData<T>(...): Promise<T>`（内部调用 request 并返回 `data`，用于未来更干净的调用风格）

### 3) 文档输出到 `docs/`

新增目录：`docs/`

新增文档：`docs/request.md`（必须包含）：

- 环境变量约定：`VITE_API_BASE_URL`、`VITE_API_TIMEOUT_MS`（可选）
- 上下文写入时机：
  - 登录成功：`setAuthToken(token)`
  - 应用初始化：`setClientId(clientId)`
  - 定位成功：`setLocation(lat, lng)`
  - 语言切换：`setLanguage(locale)`（或依赖默认读取 i18n）
- 使用方式：
  - 旧代码风格兼容：`const res = await request<T>(); res.data...`
  - 新代码推荐：`const data = await requestData<T>()`
- 错误处理示例：区分 business/http/network/unknown 并给出上层建议策略

## 历史遗留问题清单（独立梳理，方便逐个替换）

说明：以下均为“当前仓库扫描可证实”的问题点，按模块引用位置列出，便于你后续按优先级逐个替换/补齐。

### A. `@/request/api/*` 悬空引用（13 个文件）

- `src/pages/quick/index.vue`
- `src/components/Feedback/index.vue`
- `src/components/CreateServiceOrder/index.vue`
- `src/pages/service/order/components/OrderCard/index.vue`
- `src/pages/booking/components/FooterSection/index.vue`
- `src/pages/goods/index.vue`
- `src/pages/order/order/components/FooterSection/index.vue`
- `src/pages/quick/components/Tabs/index.vue`
- `src/pages/order/order/list.vue`
- `src/pages/service/order/index.vue`
- `src/pages/booking/index.vue`
- `src/pages/login/index.vue`
- `src/pages/order/order/detail.vue`

建议后续替换策略：

1. 先确定“这些 API 模块的真实来源”（是否在其它分支/其它仓库/尚未迁入）。
2. 如果确认不会迁回，则逐文件把 `@/request/api/*` 替换为基于 `src/utils/request.ts` 的真实调用（需要你提供具体 URL/参数约定）。

### B. `@/utils` 目录级导入缺失（6 个文件）

当前仓库没有 `src/utils/index.*`，但以下文件在导入 `@/utils`：

- `src/pages/quick/index.vue`
- `src/pages/booking/components/FooterSection/index.vue`
- `src/pages/goods/index.vue`
- `src/pages/order/order/components/FooterSection/index.vue`
- `src/pages/booking/index.vue`
- `src/pages/home/index.vue`

### C. `@/store` 与实际目录 `src/stores/` 命名不一致（6 个文件）

- `src/components/ImageSwiper/index.vue`
- `src/pages/quick/components/Card/index.vue`
- `src/pages/goods/album/index.vue`
- `src/pages/goods/index.vue`
- `src/pages/booking/index.vue`
- `src/pages/home/index.vue`

### D. `@/hooks/*` 缺失（3 个文件）

- `src/components/SwipeCards/index.vue`
- `src/pages/login/index.vue`
- `src/pages/home/index.vue`

### E. `@/constant/*` 缺失（6 个文件）

- `src/components/ModuleTitle/index.vue`
- `src/components/Feedback/index.vue`
- `src/components/SurveyQuestionnaire/index.vue`
- `src/components/CreateServiceOrder/index.vue`
- `src/pages/booking/index.vue`
- `src/pages/login/index.vue`

### F. `src/utils/request.ts`（空文件 + 疑似拼写问题）

- 该文件当前为空，且命名疑似应为 `requests` 或 `request`
- 建议后续统一策略：保留/替换/删除前先全仓搜索确认是否在其它分支被引用

## Verification（执行阶段你手动确认运行）

你在实现完成后手动执行：

- `yarn typecheck`
- `yarn build`
- 如有用例：`yarn test`

验收标准：

- `src/utils/request.ts` + `src/shared/request-types.ts` 编译通过
- 发起请求时能注入 headers（Authorization/clientId/X-Latitude/X-Longitude/language）
- `code !== 0` 时抛出稳定结构的错误对象（上层可根据 kind 做统一处理）
