修复旅拍相册 API 未注入共享 APIClient 导致未登录错误。

在 RootView 注入 travelAlbumAPI,并将 travel_album 纳入首页常用功能;补充 App/Networking 依赖注入说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 10:07:32 +08:00
parent 258c438f9a
commit 6769be60e4
8 changed files with 66 additions and 6 deletions

View File

@ -23,6 +23,59 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
- `AuthSessionCoordinator`:统一处理登录完成、退出登录、偏好读取和账号快照刷新。
- `SessionBootstrapper`:冷启动时读取本地 token 和账号快照,并向服务端校验登录态。
- `AccountContextLoader`:统一同步用户资料、角色权限、景区和门店。
- `AppServiceEnvironment.swift`:为无 UI 状态的服务定义 SwiftUI `EnvironmentKey`,供 View 通过 `@Environment(\.xxxAPI)` 读取。
## 依赖注入:为何 API 走 Environment 而非单例
SwiftUI 的 `Environment` 在本项目中是**依赖注入DI通道**不等于「UI 状态容器」。注入对象分两类:
| 类型 | 机制 | 代表 |
|------|------|------|
| 会驱动 UI 刷新的状态 | `environmentObject` + `ObservableObject` | `AppSession``ToastCenter` |
| 无 UI 状态的服务 | 自定义 `EnvironmentKey` | `OrdersAPI``ProfileAPI` |
### 数据流
1. `RootView.init()` 创建**一个** `APIClient`,再传给所有 `*API(client:)`
2. `RootView.body` 通过 `.environment(\.ordersAPI, ordersAPI)` 等下发到整棵视图树。
3. `task``apiClient.bindAuthTokenProvider { appSession.token }` 绑定 token。
4. View 用 `@Environment(\.ordersAPI)` 读取,调用 ViewModel 时作为参数传入,例如 `await viewModel.reload(api: ordersAPI, ...)`
5. ViewModel 不持有 API方法签名接收 `XxxServing` 协议,单元测试直接传 Mock。
网络层细节见 [Networking 模块](../Core/Networking/Networking.md)。
### 为何不用「每个 API 一个单例」
**核心原因:需要共享的是 `APIClient`,不是 API 类本身。**
`AuthAPI``OrdersAPI` 等只是 `APIClient` 上的薄封装,本身无状态。全 App 必须只有一个 `APIClient`,才能保证 token provider 只绑定一次、登录后 token 对所有请求一致、`tokenOverride`(如 `set-user`)走同一客户端。若每个 API 各自 `static let shared = XxxAPI(client: APIClient())`,容易在不经意间创建多个 client导致 token 不同步。
`RootView` 作为**组合根**集中创建并装配依赖,例如:
- `OSSUploadService(configService: uploadAPI)` 依赖 `UploadAPI`
- `SessionBootstrapper``AuthSessionCoordinator` 在 init 时注入具体 API
- `PushNotificationManager.shared.configure(api: pushAPI, ...)` 需要与 RootView 相同的 `pushAPI` 实例
### Environment 的其他收益
- **Preview / 局部替换**:可在子树 `.environment(\.ordersAPI, mockAPI)` 替换,不影响全局;`AppServiceEnvironment``defaultValue` 供 Preview 兜底。
- **测试**ViewModel 测试靠 `XxxServing` 协议 + 方法参数注入 Mock不依赖 EnvironmentEnvironment 主要给 View 层省掉层层传参。
- **历史选择**iOS 17 迁移到 iOS 16 时UI 状态统一 `environmentObject`,服务统一自定义 `EnvironmentKey`(见根目录 `iOS16兼容迁移记录.md`)。
### 单例是否可行
可以,但应有纪律。项目里已有单例先例:`PushNotificationManager.shared``PaymentVoiceSpeaker.shared``CloudTransferStore.shared`——特点是跨模块、与 UI 树无关、需在 AppDelegate 或后台回调中访问。
若全面改单例,推荐收敛为一个 `AppServices` 容器共享同一 `client`,而不是 20 个 `OrdersAPI.shared`。运行时行为可与当前方案等价,差别在可维护性和团队习惯。
### 当前方案的代价
1. `AppServiceEnvironment.swift` 约 25 组 Key`RootView` 大量 `@State` / `.environment` 行,样板代码多。
2. `Environment` 命名易误导为 UI 绑定,实际是 service locator。
3. Key 的 `defaultValue``new` 独立 `APIClient()`,仅未从 `RootView` 注入时使用Preview 需注意是否拿到真实实例。
4. View 用 `@Environment`ViewModel 用方法参数View 充当桥接层。
若将来觉得 Key 过多,可收敛为一个 `AppServices` 对象注入一次,属于简化写法,不是架构对错。
## 启动流程