← API | 列表 | 同伴App_前端开发规范
提示信息
# 同伴 App · Flutter 生产级应用工程规范 v5

> 本规范面向中国大陆市场,禁止使用任何 Google 相关 SDK(Firebase、Google Maps、Google Analytics 等)。  
> 项目由独立开发者维护,目标平台为 **iOS + Android**,不涉及 Flutter Web。  
> **v5 更新**:依赖版本全面升级至 2025 最新稳定版,Riverpod 3.x、freezed 3.x、go_router 17.x breaking changes 已同步。

---

## 目录

1. [工程总体原则](#一工程总体原则强制)
2. [平台与环境兼容](#二平台与环境兼容强制)
3. [第三方 SDK 接入规范](#三第三方-sdk-接入规范强制)
4. [依赖版本基线](#四依赖版本基线强制)
5. [环境切换规范](#五环境切换规范强制)
6. [乐观操作规范](#六乐观操作optimistic-ui规范强制)
7. [目录结构](#七目录结构feature-first强制)
8. [主题系统](#八主题系统强制)
9. [敏捷布局规范](#九敏捷布局规范velocityx强制)
10. [UI 基础组件](#十ui-基础组件规范强制)
11. [图标体系](#十一图标体系强制)
12. [启动流程与启动广告](#十二启动流程与启动广告强制)
13. [国际化规范](#十三国际化规范强制)
14. [网络与配置规范](#十四网络与配置规范强制)
15. [路由规范](#十五路由规范强制)
16. [状态管理规范](#十六状态管理规范强制)
17. [代码生成规范](#十七代码生成规范强制)
18. [开发钩子规范](#十八开发钩子规范强制)
19. [动画规范](#十九动画规范强制)
20. [性能与稳定性](#二十性能与稳定性强制)
21. [安全与密钥保护](#二十一安全与密钥保护强制)
22. [隐私合规](#二十二隐私合规强制)
23. [运行环境安全检测](#二十三运行环境安全检测强制)
24. [日志与调试](#二十四日志与调试强制)
25. [代码质量约束](#二十五代码质量约束强制)
26. [引用管理与 Linter 规范](#二十六引用管理与-linter-规范强制)
27. [SDK 深度联动架构](#二十七sdk-深度联动架构强制)
28. [禁止行为清单](#二十八禁止行为清单强制)
29. [AI Agent 开发行为约束](#二十九ai-agent-开发行为约束强制)
30. [API 协议与后端对接规范](#三十api-协议与后端对接规范强制)
31. [错误码处理策略](#三十一错误码处理策略强制)

---

## 一、工程总体原则(强制)

- 所有代码必须满足生产级质量标准,禁止 Demo 写法、临时代码、一次性实现
- 保证模块低耦合、高内聚
- 所有能力必须可替换、可测试
- 任何公共能力必须沉淀为基础设施层
- 任何业务能力必须基于基础设施组合,禁止跨层直调
- 项目为独立开发者维护,**敏捷优先**:在满足生产质量的前提下,优先选择更简洁的写法和工具组合(VelocityX、flutter_hooks、flutter_animate),降低维护成本

---

## 二、平台与环境兼容(强制)

- 目标平台:**iOS 13.0+ 和 Android(minSdkVersion 21+)**
- 不涉及 Flutter Web,**无需任何 Web 兼容处理**,禁止引入仅为 Web 降级设计的冗余代码
- 所有平台差异(推送、支付、广告等)通过 Adapter 层隔离,业务层感知不到平台差异
- 不允许使用任何 Google 相关 SDK
- **Dart SDK 要求**:`>=3.8.0 <4.0.0`(flutter_riverpod 3.x 要求)

---

## 三、第三方 SDK 接入规范(强制)

### 3.1 Adapter 层要求

所有第三方 SDK 必须封装 Adapter 层,业务层禁止直接 import 或调用任何 SDK。

每个 Adapter 必须提供:

| 实现类型 | 说明 |
|--------|------|
| 接口抽象层 | 定义业务侧契约,与具体 SDK 解耦 |
| 默认实现 | 对接真实 SDK |

> 不再要求 Mock 实现与 Web 降级实现,测试使用本地测试环境(见第五节)。

SDK 初始化必须集中在 `AdapterRegistry` 中统一管理,支持运行时替换与依赖注入。

### 3.2 核心 SDK 清单(v5 更新版本)

| 能力 | SDK | 版本 | 接入方式 |
|-----|-----|-----|--------|
| 状态管理 | flutter_riverpod | ^3.3.1 | Flutter 插件 |
| 路由 | go_router | ^17.1.0 | Flutter 插件 |
| 本地存储 | mmkv | ^2.0.1 | Flutter 插件 |
| 网络 | dio | ^5.4.0 | Flutter 插件 |
| 即时通讯 | flutter_openim_sdk | ^3.8.3 | Flutter 插件 |
| 微信登录/支付/分享 | fluwx | ^5.7.5 | Flutter 插件 + 原生配置 |
| 支付宝支付 | tobias | ^5.3.4 | Flutter 插件 + 原生配置 |
| 应用内购 | in_app_purchase | ^3.2.3 | Flutter 插件 |
| 阿里云推送 | AlicloudPush ~>3 | 原生 | 纯原生 + MethodChannel |
| 阿里云反馈 | AlicloudFeedback ~>3.4.2 | 原生 | 纯原生 |

<!-- 友盟统计(已暂停接入)
| 友盟统计 | umeng_common_sdk | ^1.3.0 | Flutter 插件 + 原生配置 |
-->
| 穿山甲广告 | Ads-CN | 原生 | 纯原生 + MethodChannel |
| 腾讯广告 | GDTMobSDK | 原生 | 纯原生 + MethodChannel |
| WebView | flutter_inappwebview | ^6.1.5 | Flutter 插件 |
| 震动反馈 | 平台 API via Adapter | — | Adapter 封装 |
| 安全检测 | freerasp | ^7.5.0 | Flutter 插件 |

### 3.3 目录结构

```
lib/core/adapters/
├── analytics_adapter.dart       # 友盟统计
├── push_adapter.dart            # 阿里云推送
├── payment_adapter.dart         # 微信/支付宝/IAP
├── im_adapter.dart              # OpenIM
├── ads_adapter.dart             # 穿山甲/腾讯广告/自营广告
├── webview_adapter.dart         # WebView(含隐私政策展示)
├── security_adapter.dart        # 安全检测
├── haptic_adapter.dart          # 震动反馈
└── adapter_registry.dart        # 统一初始化入口
```

### 3.4 关键 SDK 接入注意事项

**MMKV(本地存储)**

- `await MMKV.initialize()` 必须是 `WidgetsFlutterBinding.ensureInitialized()` 之后的第一个 `await`
- 不适合存储超过 128KB 的大型数据,大文件改用 SQLite / 磁盘文件

**OpenIM(即时通讯)**

- `platformID` 必须固定为 `5`(自定义端),不可动态检测,否则不同环境消息互不可见
- Token 过期错误码 `10004` / `10005`:回调 `onUserTokenExpired` → 刷新 imToken → 重新调用 `login()`
- 网络恢复时若 `!_isLoggedIn` 则触发重试登录

<!-- 友盟统计合规说明(已暂停接入)
**友盟统计(合规)**

- **绝对不能在隐私协议弹窗前初始化**,必须等用户主动同意后才可调用 `UmengCommonSdk.initCommon()`
- 使用 `setPageCollectionModeManual()` 手动上报,避免自动采集触发合规风险
-->

**穿山甲 / 腾讯广告**

- AppId 和 SlotId 必须从后端 `/app/start` 接口动态下发,保存到 MMKV,下次启动生效
- 首次冷启动无缓存时跳过广告,进入后台请求更新配置
- iOS 14+ 的 ATT 授权弹窗必须在展示广告前调用,且只能弹一次

**阿里云推送**

- iOS Podfile 中 `source 'https://github.com/aliyun/aliyun-specs.git'` 必须位于 CocoaPods 官方 source 之后
- 需在 `post_install` 中中和含 `Strip` / `archs` 的 shell script,否则 CI 打包失败
- 设置 `ENABLE_BITCODE = NO`


---

## 四、依赖版本基线(强制)

> ⚠️ **v5 重大更新**:riverpod 升至 3.x,freezed 升至 3.x,go_router 升至 17.x,envied 升至 1.x。所有包版本以此为准,禁止混用旧版。

```yaml
environment:
  sdk: '>=3.8.0 <4.0.0'

dependencies:
  # 核心架构(三件套必须版本一致)
  flutter_riverpod: ^3.3.1
  hooks_riverpod: ^3.3.1
  riverpod_annotation: ^4.0.2
  flutter_hooks: ^0.21.3
  go_router: ^17.1.0
  dio: ^5.4.0
  mmkv: ^2.0.1

  # UI 与动画
  shimmer: ^3.0.0
  easy_refresh: ^3.3.4
  flutter_animate: ^4.5.0
  cached_network_image: ^3.4.1
  flutter_cache_manager: any
  flutter_svg: ^2.0.9
  qr_flutter: ^4.1.0
  flutter_widget_from_html: ^0.17.1
  lottie: ^3.0.0

  # 图标
  remixicon: ^1.0.0

  # 主题与敏捷布局
  flex_color_scheme: ^8.4.0
  velocity_x: ^4.0.0

  # 数据模型
  freezed_annotation: ^3.1.0
  json_annotation: ^4.11.0

  # 工具
  connectivity_plus: ^7.0.0
  path_provider: ^2.1.2
  sqflite: ^2.3.0
  path: ^1.9.0
  permission_handler: ^12.0.1
  intl: ^0.20.2
  uuid: ^4.3.3
  crypto: ^3.0.7
  url_launcher: ^6.3.2
  screen_protector: ^1.5.1
  image_gallery_saver: ^2.0.3

  # 媒体
  wechat_assets_picker: ^10.1.1
  wechat_camera_picker: ^4.4.0
  image_cropper: ^11.0.0
  flutter_image_compress: ^2.4.0
  record: ^6.2.0
  just_audio: ^0.10.5

  # 业务 SDK
  flutter_openim_sdk: ^3.8.3
  fluwx: ^5.7.5          # 6.0 还在 preview,稳定版保持 5.x
  tobias: ^5.3.4         # 6.0 还在 preview,稳定版保持 5.x
  in_app_purchase: ^3.2.3
  # umeng_common_sdk: ^1.3.0   # 已暂停接入,待合规评估后重新启用

  # 安全
  freerasp: ^7.5.0
  device_info_plus: ^12.3.0
  flutter_udid: ^4.1.2
  envied: ^1.3.3

  # 调试
  talker_flutter: ^5.1.15
  talker_dio_logger: ^5.1.15
  talker_riverpod_logger: ^5.1.14

  # WebView
  flutter_inappwebview: ^6.1.5

  # 国际化
  flutter_localizations:
    sdk: flutter

  # 隐私
  app_tracking_transparency: ^2.0.6+1

dev_dependencies:
  flutter_test:
    sdk: flutter
  riverpod_generator: ^4.0.3   # 必须与 riverpod_annotation 版本配套
  build_runner: ^2.12.2
  freezed: ^3.2.5
  json_serializable: ^6.11.2
  envied_generator: ^1.3.3
  flutter_launcher_icons: ^0.14.4
  flutter_lints: ^6.0.0
```

> **三件套升级规则**:`riverpod_annotation`、`hooks_riverpod`、`riverpod_generator` 版本必须严格对应,升级时一起升。当前锁定:annotation `^4.0.2`、generator `^4.0.3`、riverpod `^3.3.1`。

---

## 五、环境切换规范(强制)

项目仅维护两套环境:**本地测试版(dev)** 和 **线上正式版(prod)**。

### 5.1 切换方式

通过 `--dart-define-from-file` 注入环境配置:

```bash
# 本地测试
flutter run --dart-define-from-file=env.dev.json

# 正式构建
flutter build ipa --dart-define-from-file=env.prod.json
flutter build apk --dart-define-from-file=env.prod.json
```

### 5.2 AppConfig 环境配置

```dart
// lib/core/config/app_config.dart
class AppConfig {
  static const bool isProd = bool.fromEnvironment('isProd', defaultValue: false);

  static String get apiBaseUrl => isProd
      ? const String.fromEnvironment('API_BASE_URL_PROD')
      : const String.fromEnvironment('API_BASE_URL_DEV');

  static String get appKeyIos => isProd
      ? const String.fromEnvironment('APP_KEY_IOS_PROD')
      : const String.fromEnvironment('APP_KEY_IOS_DEV');

  static String get appKeyAndroid => isProd
      ? const String.fromEnvironment('APP_KEY_ANDROID_PROD')
      : const String.fromEnvironment('APP_KEY_ANDROID_DEV');
}
```

### 5.3 数据层规范

- 禁止复杂 Mock 体系,**数据流严格遵循**:`Page → Controller → Service → Repository → RealApi`
- 本地测试环境对接测试服务器真实接口,不使用静态 Mock 数据
- 如需前端独立调试某个接口,可在 Repository 层临时注释切换,**调试代码不得合并到主分支**
- `env.dev.json` 与 `env.prod.json` 均加入 `.gitignore`,不提交到代码仓库

---

## 六、乐观操作(Optimistic UI)规范(强制)

- 用户操作必须优先更新 UI,网络失败必须支持自动回滚
- 必须统一封装 `OptimisticActionManager`,页面禁止自行实现乐观逻辑
- 必须支持并发冲突处理

---

## 七、目录结构(Feature First,强制)

```
lib/
├── main.dart
├── app.dart
├── env.dart                      # envied 密钥抽象类(env.g.dart 加入 .gitignore)
├── features/
│   └── {feature_name}/
│       ├── {feature}_page.dart
│       ├── {feature}_controller.dart
│       ├── {feature}_service.dart
│       ├── {feature}_repository.dart
│       ├── api/
│       │   ├── {feature}_api.dart
│       │   └── {feature}_real_api.dart
│       └── widgets/
├── core/
│   ├── adapters/
│   ├── optimistic/
│   ├── cache/
│   ├── privacy/
│   ├── preload/
│   ├── migration/
│   ├── network/
│   ├── models/
│   ├── storage/
│   ├── config/
│   └── utils/
├── theme/
├── routes/
├── widgets/
├── icons/
├── l10n/
└── shared.dart
```

---

## 八、主题系统(强制)

- 必须支持:浅色模式、深色模式、跟随系统、用户手动切换,`ThemeMode` 必须持久化存储
- 主题体系基于 `flex_color_scheme` 构建,利用 M3 种子色算法自动生成完整调色板
- 所有颜色来自 `AppColors`,所有字体样式来自 `AppTextStyles`
- 所有间距来自 `AppSpacing`,所有圆角来自 `AppRadius`
- 页面禁止直接使用 `Colors.xxx`、`EdgeInsets`、`TextStyle` 字面量

---

## 九、敏捷布局规范 · VelocityX(强制)

> **使用示例和速查 → 粘贴 `docs/skill/frontend_ui.md`**

本项目用 VelocityX 链式扩展语法替代传统嵌套 Widget,**禁止在新代码中使用传统多层嵌套实现可用 VelocityX 一行覆盖的布局**。

| 场景 | 写法示例 |
|------|---------|
| 文本样式 | `'标题'.text.bold.xl2.gray900.make()` |
| 水平排列 | `[icon, text].hStack(spacing: 8)` |
| 垂直排列 | `[title, subtitle].vStack(crossAlignment: CrossAxisAlignment.start)` |
| 内边距 | `.p16()` / `.px16().py8()` |
| 圆角容器 | `.box.rounded12.white.shadow.make()` |
| 颜色填充 | `.box.color(AppColors.primary).make()` |
| 点击事件 | `widget.onTap(() => ...)` |
| 全宽 | `.wFull(context)` |

**强制规则**:颜色用 `AppColors`,间距用 `AppSpacing`,字体用 `AppTextStyles`,禁止字面量。

---

## 十、UI 基础组件规范(强制)

### 10.1 骨架屏(Skeleton)

页面骨架、列表骨架、卡片骨架,支持 shimmer 动效开关,页面禁止自行绘制占位结构。

### 10.2 Loading

全屏 Loading、局部 Loading、按钮 Loading,支持 Overlay 管理,页面禁止自行实现 Loading 动画。

### 10.3 空状态(AppEmptyState)

```dart
class AppEmptyState extends StatelessWidget {
  const AppEmptyState({
    super.key,
    required this.type,
    this.animationAsset,
    this.icon,
    this.title,
    this.subtitle,
    this.actionLabel,
    this.onAction,
    this.secondaryActionLabel,
    this.onSecondaryAction,
  });
}
```

| 类型 | 默认动画/图标 | 默认标题 | 典型场景 |
|-----|------------|--------|--------|
| `noData` | lottie: empty_box.json | 暂无内容 | 列表为空 |
| `networkError` | lottie: network_error.json | 网络开小差了 | 断网/超时 |
| `searchEmpty` | lottie: search_empty.json | 没找到相关内容 | 搜索无结果 |
| `noMessage` | lottie: no_message.json | 还没有消息 | 聊天列表空 |
| `noNotification` | lottie: no_notification.json | 暂无通知 | 通知列表空 |
| `permissionDenied` | lottie: lock.json | 需要权限 | 权限被拒绝 |
| `serverError` | lottie: server_error.json | 服务器出错了 | 5xx 错误 |
| `custom` | 由调用方传入 | 由调用方传入 | 任意自定义 |

### 10.4 列表组件

下拉刷新、上拉加载更多(基于 `easy_refresh`),自动处理 loading / error / empty / noMore,禁止页面自行实现分页逻辑。

### 10.5 Toast / Dialog

必须统一封装为 `AppToast`,带"重叠排队清除防抖机制",强制调用 `clearSnackBars()`。

### 10.6 防抖与防重复点击

所有涉及网络请求的按钮必须维护 Loading / Disabled 状态,请求完成前禁止再次触发。

### 10.7 语音消息组件

完整四态:`idle` / `loading` / `playing` / `paused`。`dispose` 时必须 `await player.dispose()`(just_audio ^0.10.5)。

### 10.8 图片处理

- 图片选择器:`wechat_assets_picker ^10.1.1` + `wechat_camera_picker ^4.4.0`
- 裁剪:`image_cropper ^11.0.0`
- 压缩:文件 >200KB 时 quality=80,输出 JPEG
- 临时文件存 `getTemporaryDirectory()`,上传后删除

---

## 十一、图标体系(强制)

- 使用 RemixIcon 字体图标(`remixicon ^1.0.0`)
- 必须封装 `AppIcons` 语义层,页面禁止直接使用 `IconData` 或字体编码

---

## 十二、启动流程与启动广告(强制)

**状态机**:`Splash → 初始化 → 广告加载 → 登录态检查 → 首页`

启动广告支持三类来源:`self`(自营)/ `csj`(穿山甲)/ `gdt`(腾讯广告),通过 `AdWaterfallManager` 管理,AppId/SlotId 从 `/app/start` 动态下发,禁止硬编码。

禁止在 `main()` 中堆叠启动逻辑,必须使用状态机管理。

---

## 十三、国际化规范(强制)

```yaml
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
```

所有文案禁止硬编码,支持动态语言切换。

---

## 十四、网络与配置规范(强制)

- 使用 Dio 统一封装网络层,集成 `talker_dio_logger ^5.1.15`
- 支持:请求/响应拦截器、错误统一处理、Token 自动刷新
- **OSS 预签名 URL 下载必须使用纯净 `Dio()`**,不得携带 `Authorization` 头

---

## 十五、路由规范(强制)

- 所有路由集中管理,路由名称定义在 `AppRoutes`
- 路由跳转必须通过 `NavigationService`,禁止直接使用 `Navigator.push / pop`
- Tab 页使用 `StatefulShellRoute.indexedStack` 保持各 Tab 状态
- 集成 `TalkerRouteObserver` 自动记录页面跳转日志

### ⚠️ go_router 17.x breaking change

`redirect` 回调签名升级为异步,必须使用新签名:

```dart
// ✅ go_router 17.x 正确写法
@riverpod
GoRouter router(Ref ref) {   // ← Ref 不再有独立 XxxRef 类型
  return GoRouter(
    redirect: (BuildContext context, GoRouterState state) async {
      // 异步操作可直接 await
      final agreed = AppStorage.getBool(StorageKeys.privacyAgreed) ?? false;
      if (!agreed && state.matchedLocation != AppRoutes.privacy) {
        return AppRoutes.privacy;
      }
      return null;
    },
    routes: [...],
  );
}
```

---

## 十六、状态管理规范(强制)

### ⚠️ Riverpod 3.x breaking changes

**1. `Ref` 类型统一,废除独立 `XxxRef`**

```dart
// ❌ riverpod 2.x 旧写法
Dio mainDio(MainDioRef ref) { ... }

// ✅ riverpod 3.x 新写法
Dio mainDio(Ref ref) { ... }
```

**2. `@riverpod` 注解类语法不变,函数式 provider 的 `Ref` 参数类型改为通用 `Ref`**

```dart
// ✅ 正确
@riverpod
GoRouter router(Ref ref) { ... }

@riverpod
AppStartApi appStartApi(Ref ref) { ... }
```

**3. `AsyncNotifier` 与 `Notifier` 写法不变,仍使用 `@riverpod class`**

```dart
@riverpod
class XxxController extends _$XxxController {
  @override
  Future<List<XxxModel>> build() async {
    return ref.watch(xxxRepositoryProvider).fetchList();
  }
}
```

- 页面只负责 UI 渲染,所有业务状态在 Provider / Notifier 中维护
- 在 `ProviderScope` 中挂载 `TalkerRiverpodObserver` 监控全局状态流转

---

## 十七、代码生成规范(强制)

### ⚠️ freezed 3.x breaking changes

freezed 3.x 的 `@freezed` class 语法与 2.x 基本兼容,但生成的 mixin 结构有变化,**升级后必须重新跑 build_runner**:

```bash
flutter pub run build_runner build --delete-conflicting-outputs
```

freezed 3.x 数据模型写法(与 2.x 语法兼容):

```dart
@freezed
class UserState with _$UserState {
  const factory UserState({
    required String id,
    @Default(false) bool isLoading,
    String? nickname,
  }) = _UserState;

  factory UserState.fromJson(Map<String, dynamic> json) =>
      _$UserStateFromJson(json);
}
```

### envied 1.x 写法变化

envied 升至 1.x 后,`@Envied` 注解 `path` 参数改为读取 JSON 文件,写法同旧版兼容,无需改动。

```dart
@Envied(path: 'env.dev.json', obfuscate: true)
abstract class Env {
  @EnviedField(varName: 'APP_SECRET_DEV')
  static final String appSecret = _Env.appSecret;
}
```

---

## 十八、开发钩子规范(强制)

使用 `flutter_hooks ^0.21.3`(`HookConsumerWidget`)管理 Controller 生命周期:

```dart
class LoginScreen extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final phoneCtrl = useTextEditingController();
    final scrollCtrl = useScrollController();
    return TextField(controller: phoneCtrl);
  }
}
```

- `useMemoized` / `useCallback` 必须正确传入依赖项,否则会导致 reload loop

---

## 十九、动画规范(强制)

使用 `flutter_animate ^4.5.0` 替代手动 `AnimationController`:

```dart
'Hello'.text.make()
  .animate()
  .fadeIn(duration: 500.ms)
  .slideY(begin: 0.2, end: 0);

[...items].animate(interval: 100.ms).fade().slideX()
```

---

## 二十、性能与稳定性(强制)

- 避免无效 rebuild(合理使用 `select`、`Consumer` 精细订阅)
- 列表必须懒加载,图片必须缓存(`cached_network_image ^3.4.1`)
- Skeleton 优先于 Loading Spinner 展示
- 异步状态必须明确可追踪(`AsyncValue` 三态全处理)

---

## 二十一、安全与密钥保护(强制)

### 21.1 密钥注入规范

所有 SDK Key 通过 `--dart-define-from-file=env.{env}.json` 注入,`env.*.json` 加入 `.gitignore`。

### 21.2 envied 混淆加固(1.x)

```dart
// lib/env.dart
@Envied(path: 'env.dev.json', obfuscate: true)
abstract class Env {
  @EnviedField(varName: 'APP_SECRET_DEV')
  static final String appSecret = _Env.appSecret;

  @EnviedField(varName: 'WECHAT_APP_ID')
  static final String wechatAppId = _Env.wechatAppId;
}
```

- `.env.dev.json`、`.env.prod.json` 与生成的 `lib/env.g.dart` 均加入 `.gitignore`

### 21.3 Native 层密钥保护

- 尽量通过 MethodChannel 将 Dart 层 `Env` 密钥传递给原生层
- `build.gradle` / `AppDelegate.swift` 中禁止硬编码生产密钥

### 21.4 本地存储安全

- 本地存储使用 MMKV 加密模式
- 异常必须统一捕获与上报,禁止 silent failure

---

## 二十二、隐私合规(强制)

### 22.1 采集 SDK 合规

- **必须在用户同意隐私协议后**才能初始化友盟、广告 SDK 等采集类 SDK
- 隐私政策展示必须通过 WebView(`flutter_inappwebview ^6.1.5`)加载

### 22.2 iOS Privacy Manifest(2024 合规)

Runner 目录下必须包含 `PrivacyInfo.xcprivacy`,声明 Collected Data Types 和 Accessed API Types(至少包含 `FileTimestamp` 和 `UserDefaults`)。

### 22.3 iOS 后台模式(IM / 推送必需)

Xcode → Signing & Capabilities → Background Modes,必须勾选 **Background fetch** 和 **Remote notifications**。

### 22.4 权限声明(permission_handler)

iOS Podfile 中只声明**实际使用**的权限,声明未使用的权限会导致 App Store 审核被拒。

---

## 二十三、运行环境安全检测(强制)

- `freerasp ^7.5.0`:检测 Root、越狱、模拟器、代码注入、重打包
- `device_info_plus ^12.3.0`:基础硬件特征排查
- `flutter_udid ^4.1.2`:设备唯一硬件 ID,用于设备级拉黑

**风控处理原则:禁止本地弹窗拦截**,改为静默标记:

- Dio 请求 Header 携带 `X-Device-Risk-Flag: 1`
- 后端收到标记后对该账号降权处理

⚠️ **freerasp 7.x 启动阻塞问题**:`Talsec.instance.start()` 在某些机型下可能永久阻塞,必须加超时保护:

```dart
// security_adapter.dart
await Talsec.instance.start(config).timeout(
  const Duration(seconds: 5),
  onTimeout: () => talker.warning('freerasp start timeout, continuing'),
);
```

---

## 二十四、日志与调试(强制)

- 使用 `talker_flutter ^5.1.15` 作为全局日志系统
- `talker_dio_logger ^5.1.15` 挂载到 Dio 拦截器
- `talker_riverpod_logger ^5.1.14` 挂载到 ProviderScope
- Debug 模式提供悬浮按钮呼出日志控制台(`TalkerScreen`)
- Release 自动关闭 verbose / debug 级别日志

---

## 二十五、代码质量约束(强制)

- 单文件不超过 300 行,单 Widget 不超过 150 行
- 发布前必须强制运行 `flutter analyze`,消除所有 warning
- `environment: sdk: '>=3.8.0 <4.0.0'`(json_serializable 6.11+ 要求 >=3.8.0)

---

## 二十六、引用管理与 Linter 规范(强制)

- **统一相对路径**:`lib/` 目录下所有内部文件互相引用,必须使用相对路径
- **Barrel File 模式**:核心组件已在 `lib/shared.dart` 中统一导出
- **导包排序**:① `dart:` → ② `package:` → ③ 相对路径,组内按字母排序

---

## 二十七、SDK 深度联动架构(强制)

```dart
// main.dart
runApp(
  ProviderScope(
    observers: [TalkerRiverpodObserver(talker)],
    child: MyApp(),
  ),
);

// app_router.dart
@riverpod
GoRouter router(Ref ref) {   // Riverpod 3.x: 使用通用 Ref
  return GoRouter(
    observers: [TalkerRouteObserver(talker)],
    routes: [...],
  );
}
```

---

## 二十八、禁止行为清单(强制)

| 禁止行为 | 说明 |
|--------|------|
| 传统嵌套布局 | 可用 VelocityX 覆盖的布局禁止使用多层嵌套 Widget 实现 |
| 样式硬编码 | 禁止 `Colors.xxx`、`EdgeInsets.all(16)`、`TextStyle(...)` 直接使用 |
| 写死数据 | 禁止在 Page / Widget / Controller 中写死任何业务数据 |
| 调试代码合并主分支 | 临时注释/切换代码不得合并到主分支 |
| 页面直接调用 SDK | 业务层只能通过 Adapter 访问 SDK |
| 页面直接请求接口 | 必须经过 Service → Repository 层 |
| 页面直接跳转 | 禁止 `Navigator.push`,必须通过 `NavigationService` |
| 使用 Google SDK | 项目面向中国大陆市场,全面禁止 |
| 重复组件 | 禁止重复实现已有基础组件 |
| 隐私协议前初始化采集 SDK | 违规,可能导致应用下架 |
| 本地弹出风控提示 | 会暴露防御逻辑,改用静默标记 |
| 密钥硬编码入代码仓库 | 使用 envied + gitignore 管理 |
| silent failure | 所有异常必须捕获并上报 |
| VelocityX 内置色替代主题色 | 必须从 `AppColors` 取色 |
| 使用旧版 XxxRef | Riverpod 3.x 已废除,统一使用 `Ref` |

---

## 二十九、AI Agent 开发行为约束(强制)

> **AI 开发时必须粘贴以下 Skill 文件:**
> - 每次新对话:`docs/skill/frontend.md`
> - 编写 UI 页面时额外粘贴:`docs/skill/frontend_ui.md`

- **先思考后执行**:创建新 Feature 前,先读 `lib/shared.dart` 和现有基础组件目录,优先复用
- **禁止幻觉**:缺少 `AppColors` 颜色等基础元素,必须先在 `theme/` 里补全后再引用
- **严格遵循 Linter**:代码生成后确保符合 `flutter analyze`
- **不伪造 SDK 版本**:遇到 SDK API 不确定,查阅 pub.dev,禁止推测
- **遵守 Adapter 约束**:任何 SDK 接入必须先创建 Adapter,不得直接调用 SDK API
- **Riverpod 3.x**:所有 provider 函数的 `Ref` 参数必须使用通用 `Ref`,禁止使用 `XxxRef`
- **go_router 17.x**:`redirect` 回调必须是 `FutureOr<String?>` 异步签名

---

## 附录:常见编译错误速查

| 错误 | 原因 | 解决方案 |
|-----|------|--------|
| `'UMCommon/UMConfigure.h' file not found` | 友盟头文件路径未注入 | Podfile 加 `:modular_headers => true` + 搜索路径注入 |
| `Command PhaseScriptExecution failed (AlicloudUtils)` | 阿里云 Strip 脚本失败 | `post_install` 中 neutralize 含 Strip 的 shell script |
| `OpenIM Login Error 10004/10005` | IM Token 过期 | 刷新 imToken,重新调用 `login()` |
| `OSS 签名 URL 请求 403` | 用了带 Auth 头的 Dio 实例 | 使用纯净 `Dio()` 不带 Authorization 头 |
| `GoRouter redirect loop` | 路由守卫对 splash/privacy 路由放行条件错误 | 检查 `redirect` 中对 splash 和 privacy 路由的放行逻辑 |
| `第二次启动 App 黑屏` | `Talsec.instance.start()` 在某些机型下永久阻塞 | 加 `.timeout(Duration(seconds: 5))` + try-catch 兜底 |
| `MMKV crash on first launch` | `initialize()` 未最先调用 | 确保在任何 MMKV 读写前 `await MMKV.initialize()` |
| `flutter_hooks reload loop` | `useMemoized` 忘记传 keys | 确保 `useMemoized` / `useCallback` 正确传入依赖项 |
| `OpenIM platformID 错误导致消息不可见` | platformID 被动态检测 | 固定写 `platformID: 5` |
| `envied 密钥被逆向` | 使用了 `dart-define` 而未加 obfuscate | 改用 `@Envied(obfuscate: true)` |
| `Lottie 动画不显示` | asset 未在 pubspec 注册 | 确认 `assets/lottie/` 已在 `pubspec.yaml` 的 `flutter.assets` 中声明 |
| `riverpod 版本冲突` | 三包版本不一致 | `riverpod_annotation` / `hooks_riverpod` / `riverpod_generator` 一起升级 |
| `build_runner 生成文件冲突` | 旧生成文件残留 | 运行 `build_runner build --delete-conflicting-outputs` |
| `Undefined class 'XxxRef'` | Riverpod 3.x 废除了独立 Ref 类型 | 将所有 `XxxRef` 替换为通用 `Ref` |
| `sdk constraint mismatch (json_serializable)` | pubspec.yaml sdk 约束太低 | 改为 `sdk: '>=3.8.0 <4.0.0'` |
| `pod install Ruby 报错 (fluwx/tobias)` | fluwx/tobias 自带 Ruby setup 脚本失败 | Podfile 顶部加 `ENV['FLUWX_SKIP_SETUP']='true'` 和 `ENV['TOBIAS_SKIP_SETUP']='true'` |

---

## 三十、API 协议与后端对接规范(强制)

### 30.1 响应格式约定

```json
{
  "code":        0,
  "msg":         "success",
  "data":        {},
  "server_time": 1710000000
}
```

- `code = 0`:成功,消费 `data`
- `code ≠ 0`:失败,见错误码参考文档

### 30.2 请求签名、认证与 Token 管理 (强制)

本项目的安全机制(包括 Header 要求、签名算法、Token 刷新逻辑、风控标记等)遵循统一的 **《API 认证与签名规范》**。

[**API 认证与签名规范.md**](./API认证与签名规范.md)

### 30.3 启动必调接口:`/app/start`

### 30.4 启动必调接口:`/app/start`

App 启动后第一个接口,白名单(无需签名),返回广告配置、SDK 配置、Feature Flag、强制更新信息。返回数据存入 MMKV。

### 30.4 OSS 上传流程

1. 调用 `POST /upload/oss/token` 获取 STS 临时凭证,或 `POST /upload/oss/sign` 获取预签名 URL
2. 前端直接用凭证上传 OSS,不经过后端中转
3. 上传成功后将 OSS object_key 传入业务接口

**注意**: OSS 下载逻辑详见 [**API 认证与签名规范.md**](./API认证与签名规范.md)。

---

## 三十一、错误码处理策略(强制)

> 完整错误码含义、触发场景、extraData 结构 → **《同伴 App — 错误码参考 V2.1》**(`docs/错误码参考.md`)。  
> 本章只定义前端处理架构,不重复描述各 code 含义。

### 31.1 Dio 拦截器统一处理

下列 code 在 `ApiInterceptor` 层拦截,业务层无需感知:

```dart
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
  final body = response.data as Map<String, dynamic>;
  final code = body['code'] as int? ?? -1;
  final msg  = body['msg']  as String? ?? '未知错误';
  final data = body['data'];

  if (code == 0) { handler.next(response); return; }

  // 1005: Access Token 过期 → 静默刷新后重试
  if (code == 1005) { _handleTokenRefresh(response, handler); return; }

  // 1001 / 1006: Token 无效 / Refresh Token 过期 → 强制退出
  if (code == 1001 || code == 1006) { AuthRepository.forceLogout(); return; }

  // 1008 普通封禁 / 1013 涉嫌诈骗封禁 → 跳封禁页,BannedPage 按 code 区分样式
  if (code == 1008 || code == 1013) {
    NavigationService.go(
      AppRoutes.banned,
      extra: {'code': code, 'ban_info': (data as Map?)?['ban_info']},
    );
    return;
  }

  // 1012: 强制更新
  if (code == 1012) { NavigationService.go(AppRoutes.forceUpdate); return; }

  // 其余交给业务层 catch
  handler.reject(DioException(
    requestOptions: response.requestOptions,
    response:       response,
    error:          AppApiError(code: code, message: msg, data: data),
  ));
}
```

网络层错误(断网 / 超时)在 `onError` 中统一包装为 `AppApiError(code: -1)`。

### 31.2 错误处理矩阵

| code | 处理层级 | 处理方式 |
|------|----------|----------|
| `0` | 拦截器透传 | 正常消费 data |
| `-1` | 拦截器包装 | Toast "网络异常,请检查网络连接" |
| `1001` / `1006` | 拦截器 | `forceLogout()` → 登录页 |
| `1005` | 拦截器 | 静默刷新 Token + 原请求重试 |
| `1008` / `1013` | 拦截器 | 跳封禁页(按 code 渲染普通 / 诈骗两种样式) |
| `1012` | 拦截器 | 跳强制更新页 |
| `1002` / `1003` / `1004` | 业务层 catch | Toast `msg` |
| `1009` / `1010` | 业务层 catch | Toast `msg`;`1010` 额外读 `data.retry_after` 倒计时 |
| `2xxx` | 业务层 catch | Toast `msg`;部分 code 需消费 extraData(详见错误码参考)|
| `3006` | 业务层 catch | 展示 `AppEmptyType.noData` 空状态,不 Toast |
| `3007` | 业务层 catch | 弹广告解锁弹窗,消费 `data.ad_config` |
| `3008` | 业务层 catch | Toast `msg`,不展示广告引导 |
| `3xxx` 其余 | 业务层 catch | Toast `msg` |
| `4xxx` | 业务层 catch | Toast `msg`;乐观操作需回滚 |
| `5xxx` / `6xxx` | 业务层 catch | Toast `msg`;`5001` / `6001` 静默上报日志 |

### 31.3 AppApiError 模型

```dart
// lib/core/models/app_api_error.dart
@freezed
class AppApiError with _$AppApiError implements Exception {
  const factory AppApiError({
    required int    code,
    required String message,
    Object?         data,
  }) = _AppApiError;

  const AppApiError._();

  // ── 拦截器层 ────────────────────────────────────────────
  bool get isUnauthorized   => code == 1001;
  bool get isTokenExpired   => code == 1005;
  bool get isRefreshExpired => code == 1006;
  bool get isBanned         => code == 1008 || code == 1013;
  bool get isBannedNormal   => code == 1008;  // 普通封禁
  bool get isBannedFraud    => code == 1013;  // 涉嫌诈骗
  bool get needForceUpdate  => code == 1012;

  // ── 业务层 ──────────────────────────────────────────────
  bool get isNetworkError    => code == -1;
  bool get isParamError      => code == 1002;
  bool get isNotFound        => code == 1004;
  bool get isServerError     => code == 1009;
  bool get isRateLimited     => code == 1010;
  bool get isSignalEmpty     => code == 3006;
  bool get canWatchAd        => code == 3007;
  bool get isSignalLimitVip  => code == 3008;
  bool get isCodeTooFrequent => code == 2002;
}
```

---

*规范版本:v5 · 基于 Flutter 3.41.x / Dart 3.11.x / Riverpod 3.x / freezed 3.x / go_router 17.x · 错误码定义见《同伴 App — 错误码参考 V2.1》*