# Networking 模块业务逻辑

## 模块职责

Networking 模块是 App 的统一网络请求入口。业务模块不直接使用 `URLSession`，而是通过自己的 API 类创建 `APIRequest`，再交给 `APIClient.send` 发送。

这个模块负责：
- 维护不同环境的服务器地址。
- 描述一次接口请求的方法、路径、query、header、body 和响应类型。
- 把业务请求转换成 `URLRequest`。
- 自动注入公共 Header 和登录 token。
- 发送请求并处理网络错误。
- 校验 HTTP 状态码。
- 解包后端统一 Envelope。
- 把错误转换成统一的 `APIError`。

## 文件职责

- `APIEnvironment.swift`：定义生产环境、测试环境、HTTP baseURL 和 WebSocket 地址。
- `APIRequest.swift`：定义强类型请求模型，业务 API 通过它声明接口。
- `APIClient.swift`：统一网络客户端，负责真正构造、发送、解析请求。
- `APIEnvelope.swift`：定义后端统一响应结构。
- `APIError.swift`：定义网络层错误和登录失效判断。

## 请求调用链路

一次业务请求的大致流程是：

1. 业务 API 创建 `APIRequest<Response>`。
2. `APIClient.send` 接收这个请求。
3. `APIClient.makeURLRequest` 把 `APIRequest` 转成 `URLRequest`。
4. `APIClient` 注入公共 Header、业务 Header、token 和请求体。
5. `URLSessionProtocol.data(for:)` 发送请求。
6. `APIClient.validateHTTPResponse` 校验 HTTP 状态码。
7. `APIClient.decodeEnvelope` 解包后端 Envelope。
8. 成功时返回 `Response` 类型的业务数据。
9. 失败时抛出 `APIError`。

## APIRequest 的作用

`APIRequest<Response>` 是业务接口和网络底层之间的桥梁。

它包含：
- `method`：HTTP 方法，目前支持 GET、POST、PUT、DELETE。
- `path`：接口路径，例如 `/api/app/v9/login`。
- `queryItems`：URL query 参数。
- `headers`：接口额外 Header。
- `body`：请求体，内部通过 `AnyEncodable` 做类型擦除。
- `Response`：接口成功后期望返回的数据类型。

示例：

```swift
try await client.send(
    APIRequest(
        method: .post,
        path: "/api/app/v9/login",
        body: LoginRequest(username: username, password: password)
    )
)
```

业务 API 类应该负责创建 `APIRequest`，View 和 ViewModel 不应该直接拼 URL。

## URLRequest 构造规则

`APIClient.makeURLRequest` 会做这些事情：

1. 如果 `path` 没有 `/` 前缀，会自动补上。
2. 使用 `APIEnvironment.baseURL + path` 生成完整 URL。
3. 如果 `queryItems` 非空，则写入 URL query。
4. 设置 HTTP method。
5. 设置公共 Header：
   - `Content-Type: application/json`
   - `Accept: application/json`
   - `X-APP-VERSION`
   - `X-OS-TYPE`
6. 合并业务 API 传入的额外 Header。
7. 选择并注入 token。
8. 如果有 body，则使用 `JSONEncoder` 编码为 JSON。

## token 注入规则

token 有两个来源：

1. `tokenOverride`
2. `authTokenProvider`

优先级是：

```text
tokenOverride > authTokenProvider()
```

正常登录后的接口走 `authTokenProvider`。`RootView` 启动时会绑定：

```swift
apiClient.bindAuthTokenProvider { appSession.token }
```

登录流程里的 `set-user` 比较特殊，它需要使用登录接口返回的临时 token，所以会传入 `tokenOverride`。

token 最终会写入请求 Header：

```text
token: <token>
```

空 token 不会写入 Header。

## 环境选择

`APIEnvironment.current` 根据编译环境选择接口地址：

- Debug：`https://api-test.zhifly.cn`
- Release：`https://api.zhifly.cn`

WebSocket 地址也在 `APIEnvironment` 中定义，当前网络客户端主要使用 HTTP baseURL。

## App 版本 Header

`AppClientInfo.appVersion` 会从 `Info.plist` 读取：

- `CFBundleShortVersionString`
- `CFBundleVersion`

如果版本号已经有三段，则直接使用版本号。  
如果版本号只有两段，并且有 build 号，则拼成 `版本号.build`。  
如果读取失败，则兜底为 `1.0.0`。

这个值会作为 `X-APP-VERSION` Header 发送给后端。

系统类型固定为：

```text
X-OS-TYPE: iOS
```

## 后端 Envelope 约定

后端统一响应结构由 `APIEnvelope<T>` 表示：

```swift
struct APIEnvelope<T: Decodable>: Decodable {
    let data: T?
    let code: Int
    let msg: String?
}
```

当前成功业务码是：

```text
100000
```

只有 `code == 100000` 时才会继续解析 `data`。

如果接口成功但没有业务数据，使用 `EmptyPayload`：

```swift
let _: EmptyPayload = try await client.send(...)
```

## 错误处理规则

网络错误分为几层：

### 1. URL 构造错误

URL 拼接失败时抛出：

```swift
APIError.invalidURL
```

### 2. URLSession 错误

`URLError` 会被转换成中文提示：

- 超时：请求超时，请稍后重试
- 无网络：网络不可用，请检查网络连接
- 连接中断：网络连接中断，请重试
- 无法连接服务器：无法连接服务器，请稍后重试

取消请求会继续抛出 `CancellationError`，不会包装成业务错误。

### 3. HTTP 状态码错误

非 2xx 状态码会抛出：

```swift
APIError.httpStatus(statusCode, message)
```

错误文案优先从响应体里解析：

1. `msg`
2. `message`
3. `error`
4. plain text 响应体
5. 兜底文案“服务端返回错误”

### 4. Envelope 解码错误

响应体无法按 `APIEnvelope<Response>` 解码时抛出：

```swift
APIError.decodeFailed(message)
```

### 5. 后端业务码错误

HTTP 成功但 `code != 100000` 时抛出：

```swift
APIError.serverCode(code, msg)
```

### 6. 空数据错误

接口声明需要返回 `Response`，但 Envelope 里没有 `data` 时抛出：

```swift
APIError.emptyData
```

## 登录失效判断

`APIError.isAuthenticationExpired` 用于判断是否需要清空登录态。

会被视为登录失效的情况：
- HTTP 401
- HTTP 403
- 业务码 `200001`
- 错误文案包含：
  - `token`
  - `过期`
  - `登录失效`
  - `重新登录`
  - `unauthorized`
  - `验证失败`
  - `驗證失敗`

`SessionBootstrapper` 会用这个方法判断冷启动校验失败时是否要清空 token 和账号快照。

账号上下文相关接口集中在 `AccountContextAPI`：

- `rolePermissions()` 读取角色权限。
- `scenicListAll()` 读取景区列表。
- `storeAll()` 读取门店列表。
- `scenicSpotListAll(scenicId:)` 按景区读取景点/打卡点列表。

这些接口仍然只通过 `APIClient.send` 发起请求，token 由 `APIClient` 的 token provider 注入。

## Debug 日志

Debug 环境下，`APIClient` 会打印：

- 请求方法和 URL
- 响应状态码
- 格式化后的响应体
- 被取消的请求信息

Release 环境不会打印这些日志。

## 新增接口的推荐写法

新增业务接口时，优先按这个结构写：

```swift
@MainActor
@Observable
final class SomeFeatureAPI {
    @ObservationIgnored private let client: APIClient

    init(client: APIClient) {
        self.client = client
    }

    func loadData() async throws -> SomeResponse {
        try await client.send(
            APIRequest(
                method: .get,
                path: "/api/example/path"
            )
        )
    }
}
```

规则：
- API 类只负责接口封装，不处理 UI 状态。
- ViewModel 调用 API 类，不直接调用 `APIClient`。
- 请求体单独定义 `Encodable` Model。
- 响应体单独定义 `Decodable` Model。
- 接口成功无 data 时使用 `EmptyPayload`。
- 需要临时 token 的接口使用 `tokenOverride`。

## 当前注意点

- `APIClient` 是 `@MainActor @Observable`，当前用于方便通过 Environment 注入和共享。网络发送本身是 async，不会阻塞主线程等待网络返回。
- `URLSessionProtocol` 用于后续单元测试注入假 session。
- `APIEnvelope.isSuccess` 当前只认 `100000`，如果后端未来新增成功码，需要集中改这里。
- `APIEnvironment.current` 在 Debug 下默认测试环境，真机调试时需要注意接口环境。
