← API | 列表 | 阶段开发注意事项
提示信息
# 同伴 App · 各阶段开发注意事项

> 本文档记录各阶段 AI 生成代码后实际 Check 中发现的高频问题与修复经验,作为后续阶段的避坑参考。
> 每条均标注"**已修复**"或"**待处理**",以及涉及文件。

---

## 阶段三:网络层 + Storage

### 🔴 必须手动补的逻辑

**1. TokenInterceptor 并发排队需要同时存 RequestOptions**
AI 常只存 `Completer<Response>`,刷新成功后无法重试原请求,全部并发请求会被 reject。
正确写法:
```dart
final _pendingRequests =
    <({Completer<Response> completer, RequestOptions options})>[];
```
> 已修复:`lib/core/network/token_interceptor.dart`

**2. code=1006 必须先清 Token 再导航**
AI 常只调 `NavigationService.go(AppRoutes.login)`,忘记清 Token,导致下次启动误判已登录。
```dart
AppStorage.remove(StorageKeys.accessToken);
AppStorage.remove(StorageKeys.refreshToken);
NavigationService.go(AppRoutes.login);
```
> 已修复:`lib/core/network/api_interceptor.dart`

### 🟡 常见遗漏

**3. appSecret 不能硬编码**
AI 会写 `appSecret: 'your_dev_secret'`,应使用 `AppConfig.apiSalt`(或 envied)。
> 已修复:`lib/core/network/auth_interceptor.dart`

**4. Nonce 不要用 DateTime 拼接**
`microsecondsSinceEpoch + millisecond` 在高频调用下可重复且可预测,改用 `uuid.v4()`:
```dart
const _uuid = Uuid();
String _generateNonce() => _uuid.v4().replaceAll('-', '');
```
> 已修复:`lib/core/network/auth_interceptor.dart`

**5. X-App-Version 不要硬编码 '1.0.0'**
改用 `AppConfig.appVersion`(通过 `--dart-define=APP_VERSION=x.x.x` 注入),发版由 CI 管理。
> 已修复:`lib/core/config/app_config.dart` + `auth_interceptor.dart`

**6. Token 刷新端点需与后端约定一致**
计划要求 `POST /auth/token/refresh`,AI 容易写成 `/app/auth/refresh`。
> 已修复:`lib/core/network/token_interceptor.dart`

**7. TalkerDioLogger 必须传入全局 talker 实例**
不传则 Dio 日志与路由日志各自独立,无法统一查看。
全局 talker 放在专属文件 `lib/core/logger/app_logger.dart`(不放 main.dart,避免循环 import)。
> 已修复:`lib/core/network/dio_client.dart`

**8. forceLogout 双处重复(待 Phase 6 统一)**
`api_interceptor.dart`(1006)和 `token_interceptor.dart`(刷新失败)各自有 forceLogout 逻辑。
Phase 6 实现 `AuthRepository.forceLogout()` 静态方法后,两处均有 `TODO(Phase6)` 标记须替换。

---

## 阶段四:Adapter 层骨架

### 🔴 安全 / 合规红线

**9. freeRASP 在非 prod 环境必须跳过**
freeRASP 的 native 库在 iOS 开发环境(iOS 26 beta 等)第二次启动时会触发 XPC native crash,进程在 Flutter 引擎启动前崩溃,表现为黑屏。
```dart
if (!AppConfig.isProd) {
  debugPrint('[Security] 非 prod 环境,跳过 freeRASP 初始化');
  return;
}
```
同时,`Talsec.instance.start()` 要加 5s 超时,防止卡死黑屏:
```dart
await Talsec.instance.start(config).timeout(
  const Duration(seconds: 5),
  onTimeout: () => debugPrint('[Security] Talsec.start() 超时,已跳过'),
);
```
> 已修复:`lib/core/adapters/security_adapter.dart`

**10. freeRASP isProd 不能硬编码 true**
AI 常写 `isProd: true`,开发期被误判高风险,影响所有请求 Header。
应改为 `isProd: AppConfig.isProd`。
> 已修复:`lib/core/adapters/security_adapter.dart`

### 🟡 接口设计

**11. PaymentAdapter 接口须与 Phase 8 计划对齐**
AI 生成的骨架返回 `bool`,Phase 8 要求返回专属结果类型。提前用正确接口可避免届时破坏性变更:
- `payWithWechat(WechatPayParams)` → `Future<PaymentResult>`
- `payWithAlipay(String)` → `Future<PaymentResult>`
- `purchaseIap(String)` → `Future<PurchaseResult>`(不是 `payWithAppleIap`)
- 还需要 `getIapProducts(List<String>)` 和 `restorePurchases()`

> 已修复:`lib/core/adapters/payment_adapter.dart`

**12. WebViewAdapter URL 不要用 example.com 占位**
隐私政策 / 用户协议 URL 放 `AppConfig`,隐私页直接用,避免上线前返工:
```dart
static const String privacyPolicyUrl = 'https://www.tongban.wang/privacy';
static const String userAgreementUrl  = 'https://www.tongban.wang/agreement';
```
> 已修复:`lib/core/config/app_config.dart` + `webview_adapter.dart`

**13. PushAdapter StreamController 需要 dispose(Phase 9 前处理)**
当前 `RealPushAdapter._messageController` 无关闭机制。Phase 9 正式接入推送时须补 `dispose()` 方法,否则内存泄漏。
> 待处理:`lib/core/adapters/push_adapter.dart`

**14. ImAdapter 骨架方法远不够(Phase 7 前处理)**
当前只有 `initialize / login / logout`,Phase 7 还需要:
- `Stream<Message> onNewMessage`
- `Stream<List<ConversationInfo>> onConversationChanged`
- `Stream<UserOnlineStatus> onUserOnlineStatus`
- `platformID` 固定为 5(禁止动态检测)
- Token 过期回调(10004/10005)
- connectivity_plus 网络恢复自动重连

> 待处理(Phase 7):`lib/core/adapters/im_adapter.dart`

---

## 阶段五:启动流程 + 隐私合规

### 🔴 合规红线

**15. 广告 SlotId / AppId 禁止硬编码**
必须由 `/app/start` 接口下发,存入 MMKV,从 MMKV 读取后再传给 SDK 和 `showSplashAd`。
AI 常写 `AdConfig(slotId: '123')`,直接导致合规问题。
> 已修复:`lib/features/splash/splash_page.dart` + `lib/core/adapters/ads_adapter.dart`

**16. iOS 14+ 必须在展示广告前请求 ATT**
`showAd` 状态最开始加:
```dart
if (Platform.isIOS) {
  final status = await AppTrackingTransparency.trackingAuthorizationStatus;
  if (status == TrackingStatus.notDetermined) {
    await AppTrackingTransparency.requestTrackingAuthorization();
  }
}
```
> 已修复:`lib/features/splash/splash_page.dart`

**17. AnalyticsAdapter.init() 只能在隐私协议同意后调用**
`AdapterRegistry.initialize()` 中只实例化,不调用 `init()`。
`init()` 由 `PrivacyService.markAgreed()` 触发。
PushAdapter 的 `initialize()` 不在 `markAgreed()` 里,应在登录成功后调用。
> 已修复:`lib/core/privacy/privacy_service.dart`

### 🟡 常见遗漏

**18. `/app/start` 返回结果必须存 MMKV**
AI 常把 MMKV 存储代码注释掉做占位,导致离线兜底和 `checkUpdate` 无法工作。
> 已修复:`lib/features/splash/api/app_start_real_api.dart`

**19. `checkUpdate` 要读 MMKV 的 `force_update` 字段并真正阻断**
AI 常只写注释,不实现逻辑。检测到 `force_update: true` 时要 `return` 挂起状态机。
> 已修复:`lib/features/splash/splash_page.dart`

**20. `sdk_config` 解析和广告 SDK AppId 初始化不要漏**
启动序列第⑦步:拿到 `/app/start` 后解析 `sdk_config`,调 `AdapterRegistry.ads.initialize(csjAppId, gdtAppId)`,才能在第⑨步展示广告。
> 已修复:`lib/features/splash/splash_page.dart`

**21. 颜色禁止用 `Colors.xxx` 字面量**
`privacy_page.dart` 出现过 `Colors.black12`,必须改为 `AppColors.xxx`。
> 已修复:`lib/features/privacy/privacy_page.dart`

---

## 通用规则(跨阶段)

| 规则 | 说明 |
|------|------|
| 全局 `talker` 放 `app_logger.dart` | 不放 `main.dart`,避免其他文件反向 import |
| 正确包名:`talker_riverpod_logger` | 不是 `talker_riverpod`(两个不同包) |
| `AppConfig.isProd` 控制所有环境差异 | 不要在业务代码里写 `kReleaseMode` 或硬编码 `true/false` |
| SDK 结果不可信 | 支付/IAP 结果只负责唤起,最终由后端 `/order/status` 或推送确认 |
| forceLogout 统一入口 | Phase 6 后所有登出逻辑走 `AuthRepository.forceLogout()`,不要散落各处 |
| `StorageKeys` 常量集中管理 | 禁止在业务代码中散落字符串 key |

---

## Phase 6 前必须完成的 TODO

- [ ] `api_interceptor.dart` `TODO(Phase6)` → 替换为 `AuthRepository.forceLogout()`
- [ ] `token_interceptor.dart` `TODO(Phase6)` → 替换为 `AuthRepository.forceLogout()`
- [ ] `AuthRepository.forceLogout()` 设计为 **静态方法**,才能被 Dio 拦截器调用(无法访问 `ref`)

## Phase 7 前必须完成的 TODO

- [ ] `im_adapter.dart` 接口扩充(见条目 14)
- [ ] `platformID` 固定为 `5`,加代码注释说明

## Phase 9 前必须完成的 TODO

- [ ] `push_adapter.dart` 补 `dispose()` 方法关闭 `StreamController`