← API | 列表 | 同伴App_plan开发计划
提示信息
# 同伴 App · Flutter 完整开发计划 v2.1

> 本文档面向独立开发者,基于《同伴 App 前端开发规范 v5》整理。  
> 涵盖:项目初始化 → SDK 接入顺序 → 基础设施搭建 → 业务开发 → 发布准备。  
> 每步均附 **与 AI 交流的提示词模板**,可直接复制使用。  
> **v2 更新**:全面同步新依赖版本,含 Riverpod 3.x、freezed 3.x、go_router 17.x、freerasp 7.x、envied 1.x、talker 5.x 等 breaking changes。  
> **v2.1 更新**:`remixicon ^1.0.0` → `^1.4.0`;`image_gallery_saver ^2.0.3` 替换为 `image_gallery_saver_plus ^4.0.1`(原包已停止维护)。

---

## 目录

1. [如何与 AI 交流](#一如何与-ai-交流)
2. [阶段一:项目初始化](#阶段一项目初始化-day-1)
3. [阶段二:核心 UI 体系](#阶段二核心-ui-体系-day-2)
4. [阶段三:网络层 + Storage](#阶段三网络层--storage-day-3)
5. [阶段四:Adapter 层骨架](#阶段四adapter-层骨架-day-4)
6. [阶段五:启动流程 + 隐私合规](#阶段五启动流程--隐私合规-day-5)
7. [阶段六:推送原生层](#阶段六推送原生层-day-6)
8. [阶段七:认证体系](#阶段七认证体系-day-7)
9. [阶段八:支付体系](#阶段八支付体系-day-8)
10. [阶段九:IM 接入](#阶段九im-接入-day-9)
11. [阶段十:业务 Feature 开发范式](#阶段十业务-feature-开发范式-day-10)
12. [阶段十一:发布准备](#阶段十一发布准备)
13. [iOS Podfile 通用完整模板](#ios-podfile-通用完整模板)
14. [SDK 接入顺序总表](#sdk-接入顺序总表)
15. [常见编译错误速查](#常见编译错误速查)

---

## 一、如何与 AI 交流

### 1.1 每次新对话必须粘贴的上下文

**新对话开头永远先粘贴 `frontend.md` 全文**,写 UI 页面时再额外粘贴 `frontend_ui.md` 全文。没有这两个文件,AI 不知道你的规范,会产生大量不符合要求的代码。

### 1.2 标准提问模板

```
【规范】(粘贴 frontend.md 全文)
(写 UI 时额外粘贴 frontend_ui.md 全文)

【需求】实现 [feature 名称] 的 [具体功能]

【接口】
- 路径:POST /xxx/yyy
- 请求:{ field1: string, field2: int }
- 响应 data:{ id: string, name: string }

【UI 描述】
(简要描述页面结构、交互逻辑)

【特殊要求】
(如:需要乐观更新 / 特殊错误码处理 / 某个组件的四态)
```

### 1.3 常用追问指令

| 场景 | 追问方式 |
|------|----------|
| 代码不符合规范 | "请检查是否符合规范第 XX 节,重新生成" |
| Riverpod Ref 类型错误 | "请检查所有 provider 函数参数,将 XxxRef 改为通用 Ref(Riverpod 3.x)" |
| go_router redirect 报错 | "请更新 redirect 回调为 async FutureOr<String?> 签名(go_router 17.x)" |
| 缺少某个文件 | "还需要帮我生成 `xxx_api.dart` 和 `xxx_real_api.dart`" |
| 需要补全 | "请补全 AsyncValue 的三态处理" |
| 生成 build_runner 代码后 | "帮我检查生成的代码是否可以通过 flutter analyze" |

### 1.4 强制规则提醒 AI

每次涉及以下场景,明确告知 AI:

- **颜色** → "颜色必须从 AppColors 取,禁止 Colors.xxx 字面量"
- **路由跳转** → "跳转必须通过 NavigationService,禁止 Navigator.push"
- **SDK 调用** → "必须通过 Adapter 层,禁止页面直接 import SDK"
- **新建 Feature** → "先告诉我 shared.dart 里有哪些可复用的基础组件"
- **Riverpod provider** → "所有 provider 函数参数类型必须是 `Ref`,不是 `XxxRef`"
- **go_router redirect** → "必须是异步签名 `FutureOr<String?>`"

---

## 阶段一:项目初始化(Day 1)

### Step 1.1 创建项目

```bash
flutter create --platforms=ios,android tongban_app
cd tongban_app
```

删除 `lib/main.dart` 中的默认计数器代码,只保留入口结构。

### Step 1.2 写入全量依赖

打开 `pubspec.yaml`,将以下内容完整替换 `environment`、`dependencies` 和 `dev_dependencies` 三节:

```yaml
environment:
  sdk: '>=3.8.0 <4.0.0'   # ⚠️ 必须 >=3.8.0,json_serializable 6.11+ 要求

dependencies:
  flutter:
    sdk: flutter

  # 核心架构(三件套必须版本一致)
  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.4.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_plus: ^4.0.1

  # 媒体
  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

flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/images/icon.png"
  adaptive_icon_background: "#FFFFFF"
  adaptive_icon_foreground: "assets/images/icon_foreground.png"
  web:
    generate: false
  windows:
    generate: false

flutter:
  uses-material-design: true
  generate: true   # 启用 flutter gen-l10n
  assets:
    - assets/lottie/
    - assets/images/
    - assets/icons/

tobias:
  url_scheme: com.xiaopai.match
```

然后执行:

```bash
flutter pub get
```

> **注意**:`riverpod_annotation ^4.0.2`、`hooks_riverpod ^3.3.1`、`riverpod_generator ^4.0.3` 三包版本必须严格对应,升级时一起升。

### Step 1.3 创建完整目录结构

```bash
mkdir -p lib/core/adapters
mkdir -p lib/core/network
mkdir -p lib/core/storage
mkdir -p lib/core/config
mkdir -p lib/core/utils
mkdir -p lib/core/models
mkdir -p lib/core/optimistic
mkdir -p lib/core/cache
mkdir -p lib/core/privacy
mkdir -p lib/core/preload
mkdir -p lib/core/migration
mkdir -p lib/features
mkdir -p lib/theme
mkdir -p lib/routes
mkdir -p lib/widgets
mkdir -p lib/icons
mkdir -p lib/l10n
mkdir -p assets/lottie
mkdir -p assets/images
mkdir -p assets/icons
touch lib/shared.dart
touch lib/app.dart
touch lib/env.dart
```

### Step 1.4 配置多环境与密钥管理

在根目录创建两个环境文件(**必须立即加入 `.gitignore`**):

**`env.dev.json`**(示例结构,填入真实测试环境值):
```json
{
  "isProd": "false",
  "API_BASE_URL_DEV": "https://api-dev.yourapp.com",
  "APP_KEY_IOS_DEV": "your_ios_dev_key",
  "APP_KEY_ANDROID_DEV": "your_android_dev_key",
  "APP_SECRET_DEV": "your_dev_secret",
  "WECHAT_APP_ID": "wx_your_wechat_id",
  "WECHAT_UNIVERSAL_LINK": "https://www.yourapp.com/app/",
  "ALIPAY_URL_SCHEME": "com.yourcompany.app",
  "ALICLOUD_PUSH_APP_KEY_IOS": "your_push_key_ios",
  "ALICLOUD_PUSH_APP_SECRET_IOS": "your_push_secret_ios"
  // "UMENG_APP_KEY_IOS": "your_umeng_key_ios",
  // "UMENG_APP_KEY_ANDROID": "your_umeng_key_android"
}
```

**`env.prod.json`**(填入正式环境值,结构相同,值换正式的):
```json
{
  "isProd": "true",
  "API_BASE_URL_PROD": "https://api.yourapp.com",
  "APP_KEY_IOS_PROD": "your_ios_prod_key",
  "APP_KEY_ANDROID_PROD": "your_android_prod_key",
  "APP_SECRET_PROD": "your_prod_secret",
  "WECHAT_APP_ID": "wx_your_wechat_id",
  "WECHAT_UNIVERSAL_LINK": "https://www.yourapp.com/app/",
  "ALIPAY_URL_SCHEME": "com.yourcompany.app",
  "ALICLOUD_PUSH_APP_KEY_IOS": "your_push_key_ios",
  "ALICLOUD_PUSH_APP_SECRET_IOS": "your_push_secret_ios"
  // "UMENG_APP_KEY_IOS": "your_umeng_key_ios",
  // "UMENG_APP_KEY_ANDROID": "your_umeng_key_android"
}
```

> 应用配置与系统状态由 `/app/start` 接口动态下发存入 MMKV,禁止提前硬编码。

在 `.gitignore` 中追加:
```
env.dev.json
env.prod.json
lib/env.g.dart
.env
```

**`lib/env.dart`**(envied 1.x 写法,`obfuscate: true` 是核心):
```dart
import 'package:envied/envied.dart';
part 'env.g.dart';

// ⚠️ envied 1.x:path 直接指向 JSON 配置文件
@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;
}
```

执行代码生成(⚠️ `flutter pub run` 已废弃,改用 `dart run`):
```bash
dart run build_runner build --delete-conflicting-outputs
```

**l10n 配置**,在根目录创建 `l10n.yaml`:
```yaml
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
```

在 `lib/l10n/` 下创建 `app_zh.arb`:
```json
{
  "@@locale": "zh",
  "appName": "同伴"
}
```

---

## 阶段二:核心 UI 体系(Day 2)

### Step 2.1 主题系统

创建 `lib/theme/` 下的四个文件:

**`app_colors.dart`** — 所有颜色常量,基于设计稿提取
```dart
import 'package:flutter/material.dart';

class AppColors {
  AppColors._();

  static const Color primary = Color(0xFF6366F1);
  static const Color primaryLight = Color(0xFFEEF2FF);
  static const Color secondary = Color(0xFF8B5CF6);
  static const Color error = Color(0xFFEF4444);
  static const Color warning = Color(0xFFF59E0B);
  static const Color success = Color(0xFF10B981);

  static const Color textPrimary = Color(0xFF111827);
  static const Color textSecondary = Color(0xFF6B7280);
  static const Color textHint = Color(0xFF9CA3AF);

  static const Color bgPrimary = Color(0xFFFFFFFF);
  static const Color bgSecondary = Color(0xFFF9FAFB);
  static const Color divider = Color(0xFFE5E7EB);

  // 语音消息固定用中性色
  static const Color voiceMessage = Color(0xFF6B7280);
}
```

**`app_text_styles.dart`**、**`app_spacing.dart`**、**`app_radius.dart`** — 同理,从设计稿提取所有字号、间距、圆角常量。

**`app_theme.dart`** — 基于 flex_color_scheme 构建亮/暗主题:
```dart
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'app_colors.dart';

class AppTheme {
  static ThemeData get light => FlexThemeData.light(
    colors: FlexSchemeColor.from(primary: AppColors.primary),
    useMaterial3: true,
  );

  static ThemeData get dark => FlexThemeData.dark(
    colors: FlexSchemeColor.from(primary: AppColors.primary),
    useMaterial3: true,
  );
}
```

### Step 2.2 图标语义层

创建 `lib/icons/app_icons.dart`:

```dart
import 'package:remixicon/remixicon.dart';

/// 所有页面必须通过 AppIcons 引用图标,禁止直接使用 Remix 或其他 IconData
class AppIcons {
  AppIcons._();

  // Tab Bar
  static const home = Remix.home_4_line;
  static const homeActive = Remix.home_4_fill;
  static const message = Remix.message_3_line;
  static const messageActive = Remix.message_3_fill;
  static const discover = Remix.compass_3_line;
  static const discoverActive = Remix.compass_3_fill;
  static const profile = Remix.user_3_line;
  static const profileActive = Remix.user_3_fill;

  // 通用操作
  static const back = Remix.arrow_left_line;
  static const close = Remix.close_line;
  static const more = Remix.more_2_line;
  static const add = Remix.add_line;
  static const search = Remix.search_2_line;
  static const send = Remix.send_plane_fill;
  static const camera = Remix.camera_line;
  static const image = Remix.image_line;
  static const voice = Remix.mic_line;
  static const voiceActive = Remix.mic_fill;
  static const settings = Remix.settings_3_line;
  static const notification = Remix.notification_3_line;
  static const like = Remix.heart_line;
  static const likeActive = Remix.heart_fill;
  static const share = Remix.share_line;
  static const edit = Remix.edit_line;
  static const delete = Remix.delete_bin_line;
  static const check = Remix.check_line;
  static const warning = Remix.error_warning_line;
}
```

### Step 2.3 基础 Widget 组件库

> 📖 完整组件规范与用法见:[交互组件整理报告.md](./交互组件整理报告.md)
> 🎮 组件演示:Debug 模式登录页点击「组件展示」,或 `context.push(AppRoutes.showcase)`

所有组件已实现并可用,通过 `import 'package:tongban_app/shared.dart';` 统一引入:

| 文件 | 组件 | 说明 |
|---|---|---|
| `widgets/primary_action_button.dart` | `PrimaryActionButton` | 主操作按钮,高度 48dp,内置 Loading 防抖 |
| `widgets/app_capsule_button.dart` | `AppCapsuleButton` | 社交胶囊按钮(关注/已关注/回关) |
| `widgets/app_dialog.dart` | `AppDialog` | 确认弹窗 + 底部表单(`show` / `showSheet`) |
| `widgets/app_toast.dart` | `AppToast` | Overlay 式 Toast,4 种类型,顶部滑入 |
| `widgets/app_empty_state.dart` | `AppEmptyState` | 空状态,8 种预设类型 + custom |
| `widgets/app_skeleton.dart` | `AppSkeleton` | 骨架屏;**推荐 `AppSkeleton.wrap(enabled:,child:)`** 自动生成(基于 skeletonizer ^1.4.2) |
| `widgets/app_loading.dart` | `AppLoading` | 局部/全屏 Loading |
| `widgets/app_list_view.dart` | `AppListView<T>` | 下拉刷新 + 上拉加载一站式列表 |
| `widgets/custom_avatar.dart` | `CustomAvatar` | 头像(5 种尺寸,自动失败占位) |
| `widgets/app_tab_bar.dart` | `AppTabBar` | 标签页(选中 12sp/粗体) |
| `widgets/nav/app_nav_bar.dart` | `AppNavBar` | 通用导航栏(44pt 高,iOS HIG) |
| `widgets/nav/app_transparent_nav_bar.dart` | `AppTransparentNavBar` | 透明渐变导航栏 |
| `widgets/nav/app_search_nav_bar.dart` | `AppSearchNavBar` | 搜索导航栏 |
| `widgets/nav/app_chat_nav_bar.dart` | `AppChatNavBar` | 聊天页导航栏 |
| `widgets/nav/app_bottom_nav_bar.dart` | `AppBottomNavBar` | 底部 TabBar(Material 3,64dp) |

**主路由架构**:登录后导向 `/main` → `MainShellPage`(三 Tab:首页 / 消息 / 我的)

### Step 2.4 初始化 `app.dart`

```dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'theme/app_theme.dart';
import 'routes/app_router.dart';

class App extends ConsumerWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final router = ref.watch(routerProvider);
    return MaterialApp.router(
      title: '同伴',
      theme: AppTheme.light,
      darkTheme: AppTheme.dark,
      routerConfig: router,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
    );
  }
}
```

### Step 2.5 配置路由骨架

创建 `lib/routes/app_routes.dart`(路由名常量)和 `lib/routes/app_router.dart`(go_router 配置骨架):

```dart
// app_routes.dart
class AppRoutes {
  static const splash = '/';
  static const privacy = '/privacy';
  static const login = '/login';
  static const home = '/home';
  static const banned = '/banned';
}
```

```dart
// app_router.dart
// ⚠️ go_router 17.x + Riverpod 3.x 新写法
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:talker_flutter/talker_flutter.dart';
part 'app_router.g.dart';

@riverpod
GoRouter router(Ref ref) {   // ← Riverpod 3.x: 通用 Ref,不是 RouterRef
  return GoRouter(
    initialLocation: AppRoutes.splash,
    observers: [TalkerRouteObserver(talker)],
    redirect: (BuildContext context, GoRouterState state) async {
      // ← go_router 17.x: redirect 必须是 async / FutureOr<String?>
      // 路由守卫逻辑在阶段五完善
      return null;
    },
    routes: [
      // 路由在各阶段逐步添加
    ],
  );
}
```

创建 `lib/routes/navigation_service.dart`(全局跳转封装):
```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class NavigationService {
  static final GlobalKey<NavigatorState> navigatorKey =
      GlobalKey<NavigatorState>();

  static BuildContext get context => navigatorKey.currentContext!;

  static void go(String route, {Object? extra}) =>
      context.go(route, extra: extra);

  static void push(String route, {Object? extra}) =>
      context.push(route, extra: extra);

  static void pop<T>([T? result]) => context.pop(result);
}
```

### Step 2.6 初始化 `main.dart`

```dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mmkv/mmkv.dart';
import 'package:talker_flutter/talker_flutter.dart';
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
import 'app.dart';

final talker = TalkerFlutter.init();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await MMKV.initialize();           // 必须是第一个 await

  runApp(
    ProviderScope(
      // ⚠️ talker 5.x: 参数改为命名参数 talker:
      observers: [TalkerRiverpodObserver(talker: talker)],
      child: const App(),
    ),
  );
}
```

---

## 阶段三:网络层 + Storage(Day 3)

### Step 3.1 Storage 层

**`lib/core/storage/storage_keys.dart`** — 所有 MMKV key 常量,禁止在代码中散落字符串:
```dart
class StorageKeys {
  StorageKeys._();

  static const String accessToken = 'auth:access_token';
  static const String refreshToken = 'auth:refresh_token';
  static const String userId = 'auth:user_id';
  static const String privacyAgreed = 'privacy:agreed';
  static const String themeMode = 'theme:mode';
  static const String appStartConfig = 'app:start_config';
  static const String adConfig = 'app:ad_config';
  static const String sdkConfig = 'app:sdk_config';
  static const String deviceRiskFlag = 'security:risk_flag';
  static const String attRequested = 'att:requested'; // iOS ATT
}
```

**`lib/core/storage/app_storage.dart`** — 加密模式 MMKV 封装:
```dart
import 'package:mmkv/mmkv.dart';

class AppStorage {
  static late MMKV _mmkv;

  static Future<void> init() async {
    _mmkv = MMKV('tongban_secure', cryptKey: 'your_encrypt_key');
  }

  static String? getString(String key) => _mmkv.decodeString(key);
  static void setString(String key, String value) =>
      _mmkv.encodeString(key, value);
  static bool? getBool(String key) => _mmkv.decodeBool(key);
  static void setBool(String key, bool value) =>
      _mmkv.encodeBool(key, value);
  static void remove(String key) => _mmkv.removeValue(key);
}
```

> **注意**:`AppStorage.init()` 在 `main()` 中 `MMKV.initialize()` 之后立即调用。

### Step 3.2 AppConfig

**`lib/core/config/app_config.dart`**:
```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');
}
```

### Step 3.3 网络层

创建以下文件(**按顺序**):

**`lib/core/models/app_api_error.dart`**(freezed 3.x,语法与 2.x 兼容):
```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'app_api_error.freezed.dart';

@freezed
class AppApiError with _$AppApiError implements Exception {
  const factory AppApiError({
    required int code,
    required String message,
    Object? data,
  }) = _AppApiError;

  const AppApiError._();

  bool get isNetworkError => code == -1;
  bool get canWatchAd => code == 3007;
  bool get isSignalLimit => code == 3008;
  bool get isTokenExpired => code == 1005;
  bool get isForceLogout => code == 1006;
  bool get isBanned => code == 1008;
}
```

**`lib/core/network/api_interceptor.dart`** — 响应拦截器,统一处理全部错误码:

```dart
// 处理规范 31.1 的完整错误码矩阵
// 1005 → 静默刷新 Token 后重试(需配合 TokenRefreshInterceptor)
// 1006 → 清除 Token + 强退登录页
// 1008 → 跳封禁页,传 data.ban_info
// 其余 → handler.reject(AppApiError)
```

**`lib/core/network/auth_interceptor.dart`** — 请求拦截器,统一注入所有必须 Header:
```
X-App-Key / X-Timestamp / X-Nonce / X-Sign / X-Device-Id
X-OS / X-App-Version / X-Language
(安全检测状态:X-Device-Risk-Flag: ["safe"] / ["unverified"] / ["risk_ids"...])
```

签名算法:`MD5(appKey + timestamp + nonce + sortedParams + appSecret)`

**`lib/core/network/token_interceptor.dart`** — Token 刷新逻辑单独拆出,防止循环调用。刷新成功后自动重试原请求,刷新失败则触发 forceLogout。

**`lib/core/network/dio_client.dart`** — 组装主 Dio 实例:
```dart
// 主 Dio:挂 AuthInterceptor + ApiInterceptor + TokenInterceptor + talker_dio_logger
// 纯净 Dio:专用于 OSS 预签名 URL 下载,不带任何自定义 Header
```

**`lib/core/network/network_provider.dart`** — ⚠️ Riverpod 3.x:用通用 `Ref` 暴露两个 Dio 实例:

```dart
import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'network_provider.g.dart';

@riverpod
Dio mainDio(Ref ref) {   // ← Riverpod 3.x: 通用 Ref,不是 MainDioRef
  final dio = Dio(BaseOptions(baseUrl: AppConfig.apiBaseUrl));
  dio.interceptors.addAll([
    AuthInterceptor(ref),
    ApiInterceptor(ref),
    TokenInterceptor(ref),
    TalkerDioLogger(talker: talker),
  ]);
  return dio;
}

@riverpod
Dio pureDio(Ref ref) {   // ← 通用 Ref,不是 PureDioRef
  // 纯净 Dio,专用于 OSS 预签名 URL,不带 Authorization 头
  return Dio();
}
```

---

**与 AI 交流此步的提示词:**

```
【规范】(粘贴 frontend.md)

帮我实现 lib/core/network/api_interceptor.dart。

要求:
1. 处理规范 31.1 的完整错误码矩阵
2. code=1005 触发静默 Token 刷新并重试原请求,刷新期间并发请求需排队等待
3. code=1006 调用 AuthRepository.forceLogout()
4. code=1008 调用 NavigationService.go(AppRoutes.banned, extra: body['data'])
5. 所有后端错误统一 reject AppApiError 异常
6. 其余非 0 code 一律 reject AppApiError(code, msg, data)
7. 使用 AppApiError freezed 模型(freezed 3.x)
8. 所有 @riverpod 函数参数使用通用 Ref(Riverpod 3.x 要求)
```

---

## 阶段四:Adapter 层骨架(Day 4)

所有 Adapter 遵循统一结构:接口抽象类 + 默认实现类,业务层只依赖接口。

### Step 4.1 按复杂度从低到高逐个实现

**1. `haptic_adapter.dart`**(最简单,先练手)

```dart
abstract class HapticAdapter {
  Future<void> lightImpact();
  Future<void> mediumImpact();
  Future<void> heavyImpact();
  Future<void> vibrate();
}

class RealHapticAdapter implements HapticAdapter {
  // 使用 HapticFeedback(平台原生 API,无需额外 SDK)
}
```

**2. `webview_adapter.dart`**(隐私政策展示依赖它)

```dart
abstract class WebViewAdapter {
  Future<void> openUrl(BuildContext context, String url, {String? title});
  Future<void> openPrivacyPolicy(BuildContext context);
  Future<void> openUserAgreement(BuildContext context);
}
// 实现基于 flutter_inappwebview ^6.1.5
```

**3. `security_adapter.dart`**(影响所有请求 Header)

```dart
abstract class SecurityAdapter {
  Future<void> initialize();
  bool get isHighRisk;
  bool get isRooted;
  bool get isJailbroken;
  bool get isEmulator;
}

// ⚠️ freerasp 7.x:start() 必须加超时兜底,防止启动阻塞导致黑屏
class RealSecurityAdapter implements SecurityAdapter {
  @override
  Future<void> initialize() async {
    try {
      await Talsec.instance.start(config).timeout(
        const Duration(seconds: 5),
        onTimeout: () => talker.warning('freerasp start timeout'),
      );
    } catch (e, st) {
      talker.error('freerasp init error', e, st);
    }
  }
  // 检测结果写入 AppStorage(StorageKeys.deviceRiskFlag)
  // 禁止本地弹窗,只静默标记
}
```

**4. `analytics_adapter.dart`**(友盟,注意合规)

<!-- 友盟统计 AnalyticsAdapter (已暂停接入)
```dart
abstract class AnalyticsAdapter {
  // 注意:此 init() 只能在用户同意隐私协议后才能调用
  Future<void> init();
  void logEvent(String event, {Map<String, dynamic>? params});
  void setPageManual(); // setPageCollectionModeManual()
}
// 实现基于 umeng_common_sdk ^1.3.0
```
-->

**5. `push_adapter.dart`**(阿里云推送,Dart 侧 MethodChannel)

```dart
abstract class PushAdapter {
  Future<void> initialize();
  Future<String?> getRegistrationId();
  Stream<Map<String, dynamic>> get onMessageReceived;
  Future<void> bindAccount(String userId);
  Future<void> unbindAccount();
}
```

**6. `push_adapter.dart`**(仅保留推送适配)

```dart
abstract class AdsAdapter {
  Future<void> initialize({required String csjAppId, required String gdtAppId});
  Future<void> showSplashAd({required AdConfig config, required VoidCallback onDismiss});
}
// AppId/SlotId 必须从 MMKV 读(/app/start 下发),禁止硬编码
```

**7. `im_adapter.dart`**(OpenIM,最复杂,见阶段七)

**8. `payment_adapter.dart`**(微信/支付宝/IAP,见阶段八)

**9. `adapter_registry.dart`** — 统一初始化入口:

```dart
class AdapterRegistry {
  static late HapticAdapter haptic;
  static late WebViewAdapter webView;
  static late SecurityAdapter security;
  static late AnalyticsAdapter analytics;
  static late PushAdapter push;
  static late AdsAdapter ads;
  static late ImAdapter im;
  static late PaymentAdapter payment;

  /// 在 main() 中调用,顺序固定
  static Future<void> initialize() async {
    haptic = RealHapticAdapter();
    webView = RealWebViewAdapter();
    security = RealSecurityAdapter();
    await security.initialize();   // freerasp 最先初始化(内部有 5s 超时兜底)

    // analytics / ads 不在这里 init,等用户同意隐私协议后
    analytics = RealAnalyticsAdapter();
    push = RealPushAdapter();
    ads = RealAdsAdapter();
    im = RealImAdapter();
    payment = RealPaymentAdapter();
  }
}
```

---

**与 AI 交流此步的提示词:**

```
【规范】(粘贴 frontend.md)

帮我实现 lib/core/adapters/security_adapter.dart,包含接口抽象类和 RealSecurityAdapter 实现。

要求:
1. 抽象接口:isHighRisk / isRooted / isJailbroken / isEmulator 四个 getter,initialize() 方法
2. 实现基于 freerasp ^7.5.0
3. start() 必须加 .timeout(Duration(seconds: 5)) + try-catch 兜底,防止启动阻塞
4. 检测结果写入 AppStorage(StorageKeys.deviceRiskFlag),供 AuthInterceptor 读取注入 X-Device-Risk-Flag Header
5. 禁止本地弹窗或任何 UI 提示,纯静默处理
6. app 生命周期内持续监听(freerasp 支持持续检测)
```

---

## 阶段五:启动流程 + 隐私合规(Day 5)

这是合规红线,顺序一旦错了可能导致下架。

### Step 5.1 隐私合规墙(必须最先)

**`lib/core/privacy/privacy_service.dart`**:
```dart
class PrivacyService {
  static bool get hasAgreed =>
      AppStorage.getBool(StorageKeys.privacyAgreed) ?? false;

  static void markAgreed() {
    AppStorage.setBool(StorageKeys.privacyAgreed, true);
    // 触发采集 SDK 初始化(用户同意后才可调用)
    AdapterRegistry.analytics.init();
  }
}
```

**`lib/features/privacy/privacy_page.dart`**:
- 首次启动检测 MMKV 是否有同意记录
- 没有则全屏展示隐私协议弹窗
- "隐私政策"和"用户协议"链接通过 WebViewAdapter 打开
- 用户点「同意并继续」后调用 `PrivacyService.markAgreed()`,继续启动流程
- 用户点「不同意」直接退出 App(`SystemNavigator.pop()`)

### Step 5.2 `/app/start` 接口

**`lib/features/splash/api/app_start_api.dart`** + **`app_start_real_api.dart`**

⚠️ Riverpod 3.x:provider 函数参数改为通用 `Ref`:

```dart
@riverpod
AppStartApi appStartApi(Ref ref) {   // ← 通用 Ref,不是 AppStartApiRef
  return RealAppStartApi(ref.watch(mainDioProvider));
}
```

返回数据包含:
- `app_config`:系统业务配置(存 MMKV)
- `feature_flags`:Feature Flag
- `min_version` + `force_update`:强制更新检测

### Step 5.3 AdWaterfallManager

**`lib/core/adapters/ads_adapter.dart`** 内实现瀑布流逻辑:

1. 读 MMKV 缓存的 `ad_config`,按 `priority` 排序
2. 依次尝试 `self` → `csj` → `gdt`
3. 某源超时(>3s)/ 失败 → 自动降级到下一源
4. 全部失败 → 调用 `onDismiss`,跳过广告进入 App
5. 首次冷启动无缓存 → 直接跳过广告,后台异步更新配置

### Step 5.4 Splash 状态机

**`lib/features/splash/splash_page.dart`**:

```
启动状态机(禁止在 main() 堆叠逻辑):

① WidgetsFlutterBinding.ensureInitialized()
② MMKV.initialize()
③ AppStorage.init()
④ AdapterRegistry.initialize()(其中 freerasp 最先初始化,含 5s 超时保护)
⑤ 检查隐私协议 → 未同意则展示隐私弹窗
⑥ 调用 /app/start 接口(有缓存时直接用缓存,并发请求更新)
⑦ 解析 sdk_config → 初始化广告 SDK AppId(还不展示广告)
⑧ 检查 force_update → 如需强制更新则跳转更新页
⑨ 展示启动广告(AdWaterfallManager,iOS 14+ 需先请求 ATT)
⑩ 检查登录态 → 未登录跳 login / 已登录跳 home
```

### Step 5.5 完善路由守卫(⚠️ go_router 17.x 新签名)

在 `app_router.dart` 的 `redirect` 中(必须 async):

```dart
@riverpod
GoRouter router(Ref ref) {   // ← Riverpod 3.x: 通用 Ref
  return GoRouter(
    initialLocation: AppRoutes.splash,
    observers: [TalkerRouteObserver(talker)],
    redirect: (BuildContext context, GoRouterState state) async {
      // ← go_router 17.x: 必须 async
      final location = state.matchedLocation;

      // 放行这两个路由,避免 redirect loop
      if (location == AppRoutes.splash || location == AppRoutes.privacy) {
        return null;
      }

      final agreed = AppStorage.getBool(StorageKeys.privacyAgreed) ?? false;
      if (!agreed) return AppRoutes.privacy;

      final token = AppStorage.getString(StorageKeys.accessToken);
      if (token == null) return AppRoutes.login;

      return null;
    },
    routes: [
      GoRoute(path: AppRoutes.splash,  builder: (_, __) => const SplashPage()),
      GoRoute(path: AppRoutes.privacy, builder: (_, __) => const PrivacyPage()),
      GoRoute(path: AppRoutes.login,   builder: (_, __) => const LoginPage()),
      GoRoute(path: AppRoutes.home,    builder: (_, __) => const HomePage()),
      GoRoute(path: AppRoutes.banned,  builder: (_, state) =>
          BannedPage(banInfo: state.extra)),
    ],
  );
}
```

---

**与 AI 交流此步的提示词:**

```
【规范】(粘贴 frontend.md)

帮我实现 Splash 启动状态机(lib/features/splash/splash_page.dart)。

要求:
1. 状态机枚举:loading / checkingPrivacy / loadingConfig / showingAd / checkingAuth / done
2. 严格按以下顺序执行:MMKV初始化 → 隐私检查 → /app/start接口 → 广告展示 → 登录态检查
3. 首次冷启动无 ad_config 缓存时跳过广告
4. /app/start 请求失败时使用 MMKV 缓存降级,无缓存则直接进入 App
5. 所有步骤用 Riverpod AsyncValue 管理,失败有兜底逻辑不能卡死
6. HookConsumerWidget,使用 flutter_animate 添加品牌 Logo 淡入动画
7. 注意:provider 函数参数使用通用 Ref(Riverpod 3.x),redirect 使用 async 签名(go_router 17.x)
```

---

## 阶段六:推送原生层(Day 6)

### Step 6.1 阿里云推送(纯原生 SDK)

MethodChannel 名称:`com.xiaopaix.app/push`

**iOS AppDelegate.swift**(原生初始化):
```swift
// didFinishLaunchingWithOptions 中
// ⚠️ 正确参数名:withAppkey / appSecret(非 withAccessKey / secretKey)
CloudPushSDK.start(withAppkey: "YOUR_KEY", appSecret: "YOUR_SECRET") { result in
    // result 非 optional,直接访问 result.success
}
CloudPushSDK.setPushTokenUploadHandler { token in /* 上传 token 到业务后端 */ }
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { ... }
```

**Android `app/build.gradle`**:
```groovy
android {
  defaultConfig {
    manifestPlaceholders = [
      PUSH_APPKEY   : "YOUR_KEY",
      PUSH_APPSECRET: "YOUR_SECRET"
    ]
  }
}
```

**Dart 侧获取推送 DeviceId**(上传后端关联用户,实现定向推送):
```dart
final deviceId = await _channel.invokeMethod('getDeviceId');
```

**Xcode → Signing & Capabilities** 必须勾选:
- Background fetch(后台唤醒拉取 IM 消息)
- Remote notifications(接收静默推送)

### Step 6.2 阿里云反馈(AlicloudFeedback)

与推送共享同一 AppKey/Secret,iOS Podfile 中已包含(见 Podfile 模板)。同样存在 `PhaseScriptExecution` 失败问题,用同样的 `post_install` 脚本 neutralize 处理。

### Step 6.3 穿山甲广告(Pangle/CSJ)+ 优量汇(GDT)

### Step 6.2 [DEPRECATED] 广告原生层移除
已按照最新要求,移除穿山甲(Pangle/CSJ)和优量汇(GDT)的所有业务逻辑及配置。

### Step 6.4 其他 SDK 原生实现状态

> 以下 SDK 均已在原生侧完整实现,无需额外操作。

| SDK | iOS 实现位置 | Android 实现位置 | 状态 |
|-----|------------|-----------------|------|
<!-- | 友盟统计 (Umeng) | `AppDelegate.didFinishLaunching`:`UMConfigure.initWithAppkey` | `MainActivity.configureFlutterEngine`:`UMConfigure.init` | ✅ 完成 | -->
| 阿里云推送 | `AppDelegate.didFinishLaunching`:`CloudPushSDK.start` | `MainApplication.onCreate`:`PushServiceFactory.init` | ✅ 完成 |
| 穿山甲/优量汇广告 | (已移除) | (已移除) | — |

### Step 6.4 [DEPRECATED] 广告瀑布流移除
已由后端 `/app/start`接口关闭广告开关并清空策略配置。


---

## 阶段七:认证体系(Day 7)

### Step 7.1 设备信息服务

**`lib/core/utils/device_info_service.dart`**:
```dart
// 整合 device_info_plus ^12.3.0 + flutter_udid ^4.1.2
// 提供:deviceId / osName / appVersion
// 结果缓存 MMKV,供 AuthInterceptor 读取
```

在 `AdapterRegistry.initialize()` 中添加设备信息初始化。

### Step 7.2 Auth 功能完整实现

**文件清单**:
- `lib/features/auth/api/auth_api.dart`
- `lib/features/auth/api/auth_real_api.dart`
- `lib/features/auth/auth_repository.dart`
- `lib/features/auth/auth_service.dart`
- `lib/features/auth/auth_controller.dart`
- `lib/features/auth/login_page.dart`

**AuthRepository 核心方法**:
```dart
Future<void> login(String phone, String code);
Future<void> register(String phone, String code, ...);
Future<void> logout();
Future<void> refreshToken();
static Future<void> forceLogout(); // 供 Dio 拦截器调用
```

**Token 存储**:
- Access Token(2h)→ `AppStorage.setString(StorageKeys.accessToken, token)`
- Refresh Token(30d)→ `AppStorage.setString(StorageKeys.refreshToken, token)`
- 登出时调用 `/auth/logout`,清除所有本地 Token

---

**与 AI 交流此步的提示词:**

```
【规范】(粘贴 frontend.md)

帮我实现认证模块(features/auth/),包含完整五层。

接口:
- POST /auth/login  请求:{phone, code}  响应data:{access_token, refresh_token, user_id}
- POST /auth/logout 无参数
- POST /auth/token/refresh 请求:{refresh_token} 响应data:{access_token, refresh_token}

要求:
1. AuthRepository 提供 forceLogout() 静态方法,可被 Dio 拦截器调用
2. Token 存入 AppStorage 加密 MMKV,key 使用 StorageKeys 常量
3. AuthController 用 @riverpod class,AsyncValue 三态全处理
4. 登录成功后 NavigationService.go(AppRoutes.main)
5. forceLogout 清除所有 Token 后 NavigationService.go(AppRoutes.login)
6. 所有 @riverpod 函数参数使用通用 Ref(Riverpod 3.x 要求)
```

---

## 阶段八:支付体系(Day 8)

### Step 8.1 payment_adapter.dart

三路支付统一接口:

```dart
abstract class PaymentAdapter {
  Future<PaymentResult> payWithWechat(WechatPayParams params);
  Future<PaymentResult> payWithAlipay(String orderInfo);
  Future<PurchaseResult> purchaseIap(String productId);
  Future<List<ProductDetails>> getIapProducts(List<String> productIds);
  Future<void> restorePurchases();
}
```

**各路支付关键注意点**:

- **微信支付**(fluwx ^5.7.5):需原生配置 URL Scheme(iOS Info.plist + Android AndroidManifest)
- **支付宝**(tobias ^5.3.4):Android 需在 MainActivity 中配置 Activity 回调
- **IAP 应用内购**(in_app_purchase ^3.2.3):iOS 必须接入,否则 App Store 审核被拒;支付结果不信任客户端,必须后端验票

**重要原则**:支付结果不依赖客户端回调,只负责唤起支付页。结果通过后端轮询 `/order/status` 或服务端推送 + 推送通知确认。

### Step 8.2 微信 fluwx 原生配置

**`pubspec.yaml`** 添加 fluwx 配置节:
```yaml
fluwx:
  app_id: "wx_your_wechat_id"
  universal_link: "https://www.yourapp.com/app/"
```

**iOS `Info.plist`**:
```xml
<key>LSApplicationQueriesSchemes</key>
<array>
  <string>weixin</string>
  <string>weixinULAPI</string>
  <string>alipay</string>
  <string>alipays</string>
</array>
<!-- URL Types 中添加 Scheme = 微信 AppId -->
```

**iOS `Info.plist` Associated Domains**:添加 `applinks:www.yourapp.com`(Universal Link 必须配置,且服务器 `.well-known/apple-app-site-association` 必须可访问)

**iOS Podfile** 必须加这两行环境变量,否则 pod install 会报 Ruby 错误:
```ruby
ENV['FLUWX_SKIP_SETUP'] = 'true'
ENV['TOBIAS_SKIP_SETUP'] = 'true'
```

**Android**:新建 `android/app/src/main/java/[包名]/wxapi/WXEntryActivity.java`,继承 `WXCallbackActivity`;`AndroidManifest.xml` 注册该 Activity。

### Step 8.3 支付宝 tobias 原生配置

**`pubspec.yaml`** 添加 tobias 配置节(已在全局 pubspec 中配置):
```yaml
tobias:
  url_scheme: "com.xiaopai.match"
```

**iOS `Info.plist`** 的 `URL Types` 中添加 Scheme = `com.xiaopai.match`。

支付完成回调通过 `listenAliPayResult()` 监听,**注意在页面 `dispose` 时取消订阅**,否则内存泄漏。

### Step 8.4 IAP 应用内购原生配置

**Xcode → Signing & Capabilities → 添加 `In-App Purchase`**(必须,缺少则 StoreKit 无法使用)。

`purchaseStream` 监听推荐在 `ProviderScope` 级别维护,保持整个应用生命周期活跃。

沙盒测试需在 App Store Connect 创建沙盒账号;本地调试可用 `StoreKitConfiguration` 文件,不依赖 App Store Connect。

---

**与 AI 交流此步的提示词:**

```
【规范】(粘贴 frontend.md)

帮我实现 lib/core/adapters/payment_adapter.dart,包含 PaymentAdapter 接口和 RealPaymentAdapter 实现。

要求:
1. 三路支付:微信(fluwx ^5.7.5)/ 支付宝(tobias ^5.3.4)/ IAP(in_app_purchase ^3.2.3)
2. 统一返回 PaymentResult(freezed 3.x 模型),包含 success / orderId / error 字段
3. 支付结果不负责轮询订单,只负责唤起并返回客户端结果码
4. IAP purchaseStream 在整个应用生命周期保持活跃
5. 支付宝 listenAliPayResult 订阅需在调用方 dispose 时取消
```

---

## 阶段九:IM 接入(Day 9)

### Step 9.1 完整实现 im_adapter.dart

基于 `flutter_openim_sdk ^3.8.3`,**关键点**:

```dart
// ⚠️ platformID 必须固定为 5(自定义端),禁止动态检测
const int _platformId = 5;
```

**Token 刷新逻辑**:
```dart
// Token 过期错误码 10004 / 10005
// onUserTokenExpired 回调 → 调用传入的 refreshImToken 闭包 → 重新 login()
```

**网络恢复重连**:
```dart
// 监听 connectivity_plus ^7.0.0
// 网络恢复时检查 !_isLoggedIn → 触发重试登录
```

**对外暴露的 Stream**:
```dart
Stream<Message> get onNewMessage;
Stream<List<ConversationInfo>> get onConversationChanged;
Stream<UserOnlineStatus> get onUserOnlineStatus;
```

---

**与 AI 交流此步的提示词:**

```
【规范】(粘贴 frontend.md)

帮我实现 lib/core/adapters/im_adapter.dart,包含 ImAdapter 接口 and RealImAdapter 实现。

要求:
1. platformID 固定写 5,代码注释说明原因,禁止任何动态检测逻辑
2. Token 过期(10004/10005)触发 onUserTokenExpired 回调,参数是 Future<String> Function() refreshImToken 闭包,刷新后重新调用 login()
3. 监听 connectivity_plus ^7.0.0,网络恢复且 !_isLoggedIn 时自动重试登录
4. 暴露 Stream<Message> onNewMessage 和 Stream<List<ConversationInfo>> onConversationChanged 供业务层订阅
5. login() / logout() 与 AuthController 状态联动(通过传入回调解耦)
6. RealImAdapter 通过 ImAdapter 接口抽象,AdapterRegistry 只持有 ImAdapter 引用
7. 基于 flutter_openim_sdk ^3.8.3
```


---

## 阶段十:业务 Feature 开发范式(Day 10+)

到这里基础设施全部就绪,开始正式写业务。

### 每个 Feature 的标准开发流程

**Step 1:先查 shared.dart**

告诉 AI:"先列出 `lib/shared.dart` 和 `lib/widgets/` 里有哪些可以复用的组件,再开始实现"。

**Step 2:建目录和文件**

```
lib/features/{feature_name}/
├── {feature}_page.dart          # UI,HookConsumerWidget
├── {feature}_controller.dart    # 状态,@riverpod class
├── {feature}_service.dart       # 业务规则
├── {feature}_repository.dart    # 数据访问,catch AppApiError
├── api/
│   ├── {feature}_api.dart       # 接口抽象
│   └── {feature}_real_api.dart  # 真实实现(⚠️ provider 函数参数用通用 Ref)
└── widgets/                     # 该 Feature 私有组件
```

**Step 3:建 freezed 数据模型**(freezed 3.x,语法与 2.x 兼容)

```dart
@freezed
class XxxModel with _$XxxModel {
  const factory XxxModel({
    required String id,
    required String name,
    @Default(false) bool isLoading,
  }) = _XxxModel;

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

跑:`dart run build_runner build --delete-conflicting-outputs`

**Step 4:Repository 层错误处理模板**

```dart
Future<void> doSomething() async {
  try {
    final res = await _api.doSomething();
    // 消费 res.data
  } on AppApiError catch (e) {
    if (e.canWatchAd) {
      AdWaterfallManager.showUnlockAd(config: e.data?['ad_config']);
    } else {
      AppToast.show(e.message);
    }
  } on DioException catch (_) {
    AppToast.show('网络异常,请检查网络连接');
  }
}
```

**Step 5:Controller 三态模板**(Riverpod 3.x,`@riverpod class` 写法不变)

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

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() =>
        ref.read(xxxRepositoryProvider).fetchList());
  }
}
```

**Step 6:Page 模板**

```dart
class XxxPage extends HookConsumerWidget {
  const XxxPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(xxxControllerProvider);

    return Scaffold(
      appBar: AppBar(title: 'xxx'.text.make()),
      body: state.when(
        data: (list) => AppListView(
          items: list,
          itemBuilder: (item) => XxxCard(item: item),
        ),
        loading: () => const SkeletonWidget(),
        error: (err, stack) => AppEmptyState(
          type: err is AppApiError && err.isNetworkError
              ? AppEmptyType.networkError
              : AppEmptyType.serverError,
          onAction: () => ref.refresh(xxxControllerProvider),
        ),
      ),
    );
  }
}
```

### 标准提问模板(每次业务开发都用这个)

```
【规范】(粘贴 frontend.md 全文)
【UI 规范】(粘贴 frontend_ui.md 全文)

【需求】实现 [feature 名] 的 [具体页面/功能]

【接口】
- POST /xxx/yyy
- 请求:{ field1: type, field2: type }
- 响应 data:{ field1: type, field2: type }

【UI 描述】
[描述页面结构、卡片样式、交互行为]

【特殊逻辑】
- 需要乐观更新(点赞/关注)
- 特殊错误码处理(code=3007 弹广告)
- 某组件四态(语音消息 idle/loading/playing/paused)

请生成完整五层文件(api / repository / service / controller / page),
以及需要的 freezed 数据模型。

注意:所有 @riverpod 函数参数使用通用 Ref(Riverpod 3.x 要求)。
```

---

## 阶段十一:发布准备

### Step 11.1 图标配置

在 `pubspec.yaml` 中配置(已在全量依赖中含 `flutter_launcher_icons` 节):
```yaml
flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/images/icon.png"   # 1024x1024 png
  adaptive_icon_background: "#010005"
  adaptive_icon_foreground: "assets/images/icon.png"
  web:
    generate: false
  windows:
    generate: false
```

执行:`dart run flutter_launcher_icons`

### Step 11.2 iOS 合规配置

**PrivacyInfo.xcprivacy**(Runner 目录下必须有此文件):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyCollectedDataTypes</key>
  <array></array>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>C617.1</string></array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>CA92.1</string></array>
    </dict>
  </array>
</dict>
</plist>
```

**Background Modes**(Xcode → Signing & Capabilities):
- 勾选 Background fetch(IM 后台拉消息)
- 勾选 Remote notifications(远程推送)

### Step 11.3 权限声明审查

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

```ruby
# 只保留实际用到的:
# 相机:NSCameraUsageDescription
# 麦克风:NSMicrophoneUsageDescription
# 相册:NSPhotoLibraryUsageDescription
# 推送:已通过 Remote Notifications 申请,不需要 Info.plist key
```

### Step 11.4 发布前必跑检查项

```bash
# 1. 静态分析,消除所有 warning
flutter analyze

# 2. 代码生成确认无遗漏(⚠️ 使用 dart run,flutter pub run 已废弃)
dart run build_runner build --delete-conflicting-outputs

# 3. 国际化生成
flutter gen-l10n

# 4. Release 构建测试(dev 环境先验证)
flutter build apk --dart-define-from-file=env.dev.json
flutter build ipa --dart-define-from-file=env.dev.json

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

### Step 11.5 阿里云实名认证(aliyun_face_plugin)

**接入方式**:本地 Plugin(`plugins/aliyun_face_plugin/`),pubspec.yaml 使用 path 依赖:
```yaml
aliyun_face_plugin:
  path: plugins/aliyun_face_plugin
```

**不提前初始化**,用户进入实名认证页时按需初始化:
```dart
// 完整核验流程(顺序不可乱)
// 1. plugin.getMetaInfos()            → 获取设备元信息
// 2. POST /auth/verify/token {meta}   → 后端换 verifyToken
// 3. plugin.verify(verifyToken)       → 唤起实名认证 SDK
// 4. POST /auth/verify/check          → 回调成功后后端核查结果
```

iOS 需要声明 `NSCameraUsageDescription`(人脸识别需要相机权限)。

### Step 11.6 图片与音频处理注意事项

**图片压缩规则**(`flutter_image_compress ^2.4.0`):
```dart
// 文件大小 > 200KB 时压缩,quality=80,输出 JPEG
if (fileSize > 200 * 1024) {
  await FlutterImageCompress.compressAndGetFile(
    path, targetPath, quality: 80
  );
}
// 裁剪返回的临时文件存 getTemporaryDirectory(),上传完成后删除
// 选择器返回 AssetEntity,需调用 .file 转换为 File
```

**音频录制**(`record ^6.2.0`):
```yaml
# Linux 桌面端兼容(项目不用但避免 pub get 报错)
dependency_overrides:
  record_linux: ^1.1.0
```

录制返回 `.m4a` 文件,上传前无需压缩。播放使用 `just_audio ^0.10.5`,**`dispose` 时必须 `await player.dispose()`**,否则内存泄漏。

---

## iOS Podfile 通用完整模板

每次新建项目或 pod install 出问题时,以此模板为基准:

```ruby
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/aliyun/aliyun-specs.git'   # 阿里云 SDK 必须,且必须在官方 source 之后

platform :ios, '13.0'
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
ENV['FLUWX_SKIP_SETUP'] = 'true'      # 微信 SDK,跳过 Ruby setup 脚本
ENV['TOBIAS_SKIP_SETUP'] = 'true'     # 支付宝 SDK

target 'Runner' do
  use_frameworks!

  # 广告 SDK
  pod 'Ads-CN'          # 穿山甲国内版
  pod 'GDTMobSDK'       # 腾讯广告联盟

  # 阿里云
  pod 'AlicloudPush', '~> 3'
  pod 'AlicloudFeedback', '~> 3.4.2'

  # 友盟(必须 modular_headers,否则头文件找不到)
  pod 'UMCommon', :modular_headers => true
  pod 'UMDevice',  :modular_headers => true

  # 微信(必须 modular_headers)
  pod 'WechatOpenSDK-XCFramework', :modular_headers => true

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.build_configurations.each do |config|
    config.build_settings.delete 'VALID_ARCHS'
    config.build_settings['ARCHS'] = 'arm64 x86_64'
  end

  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

    # ① 中和阿里云 SDK 的 Strip 脚本(否则 CI 打包失败)
    target.shell_script_build_phases.each do |phase|
      if phase.name.include?("Strip") || phase.name.include?("archs")
        phase.shell_script = "echo 'neutralized'"
      end
    end

    target.build_configurations.each do |config|
      config.build_settings.delete 'VALID_ARCHS'
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
      config.build_settings['ENABLE_BITCODE'] = 'NO'          # 阿里云不支持 Bitcode
      config.build_settings['GCC_WARN_INHIBIT_ALL_WARNINGS'] = 'YES'
      config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'

      # ② 友盟 + 微信头文件搜索路径(只对这两个 target 注入)
      if ['fluwx'].include?(target.name)
        config.build_settings['HEADER_SEARCH_PATHS'] ||= '$(inherited) '
        # config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/UMCommon" '
        # config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/UMDevice" '
        config.build_settings['HEADER_SEARCH_PATHS'] << '"${PODS_ROOT}/Headers/Public/WechatOpenSDK-XCFramework" '
      end

      # ③ permission_handler 权限宏(只声明实际使用的,多余的会导致 App Store 审核被拒)
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_NOTIFICATIONS=1',
        'PERMISSION_MICROPHONE=1',
        'PERMISSION_CAMERA=1',
        'PERMISSION_PHOTOS=1',
      ]
    end
  end
end
```

---

## SDK 接入顺序总表

| 顺序 | SDK / 能力 | 原因 | 阶段 |
|------|-----------|------|------|
| 1 | MMKV | 所有 SDK 都需要存配置,必须最先 | 阶段一 |
| 2 | envied 1.x | 密钥混淆,各 SDK 初始化需要 | 阶段一 |
| 3 | Dio + 拦截器 | 网络是所有业务的基础 | 阶段三 |
| 4 | freerasp 7.x | 影响所有请求的 X-Device-Risk-Flag | 阶段四 |
| 5 | device_info_plus 12.x + flutter_udid 4.x | 请求 Header 需要设备 ID | 阶段七 |
| 6 | flex_color_scheme + 主题体系 | UI 组件依赖主题,必须先就绪 | 阶段二 |
| 7 | go_router 17.x | 隐私合规弹窗和路由守卫依赖它 | 阶段二 |
| 8 | flutter_inappwebview 6.x | 隐私政策必须用 WebView 展示 | 阶段四 |
| 9 | 隐私合规弹窗 | 合规红线,后续采集 SDK 必须在它之后 | 阶段五 |
<!-- | 10 | umeng_common_sdk | 用户同意隐私协议后才能初始化 | 阶段四/五 | -->
| 11 | flutter_openim_sdk 3.8.x | IM,依赖 auth Token,登录后才 login | 阶段九 |
| 12 | fluwx 5.x + tobias 5.x + IAP | 支付,需要原生配置 | 阶段八 |
| 13 | 阿里云推送(原生) | 纯原生 SDK,需要原生工程配置 | 阶段六 |
| 14 | 穿山甲/腾讯广告(原生) | AppId 由 /app/start 下发,最后初始化 | 阶段六 |
| 15 | aliyun_face_plugin | 按需接入,不提前初始化 | 阶段十一 |

---

## 常见编译错误速查

| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `Undefined class 'XxxRef'`(如 `MainDioRef`、`RouterRef`、`AppStartApiRef`) | Riverpod 3.x 废除了独立 Ref 类型 | 将所有 `XxxRef` 替换为通用 `Ref` |
| `redirect must return FutureOr<String?>` | go_router 17.x redirect 签名变化 | 给 `redirect` 函数加 `async` 关键字 |
| `sdk constraint mismatch` | pubspec.yaml sdk 约束太低 | 改为 `sdk: '>=3.8.0 <4.0.0'` |
| `TalkerRiverpodObserver positional argument` | talker 5.x 参数改为命名参数 | 改为 `TalkerRiverpodObserver(talker: talker)` |
| `第二次启动 App 黑屏(runApp 未执行)` | `Talsec.instance.start()` 在某些机型下永久阻塞 | `security_adapter.dart` 中对 `Talsec.start()` 加 `.timeout(Duration(seconds: 5))`,并用 try-catch 兜底 |
| `'UMDevice/UMDevice.h' file not found` | UMDevice 3.5+ 为 XCFramework,不暴露 .h 头文件;Swift 代码也未直接调用 UMDevice API | 删除桥接头文件中 `#import <UMDevice/UMDevice.h>` 这一行 |
| `Incorrect argument labels (withAccessKey:secretKey:)` | AlicloudPush 3.x API 改名 | 改为 `CloudPushSDK.start(withAppkey:appSecret:)` |
| `Cannot use optional chaining on CloudPushCallbackResult` | AlicloudPush 3.x 回调 result 非 optional | 去掉 `if let result = result`,直接用 `result.success` |
| `Missing argument for parameter 'withCallback' in registerDevice` | AlicloudPush 3.x registerDevice 需要 callback | 改为 `CloudPushSDK.registerDevice(token, withCallback: { _ in })` |
| `Type 'MobClick' has no member 'setPageCollectionMode'` | UMCommon 7.x 将该方法改名 | 改为 `MobClick.setAutoPageEnabled(false)` |
| `'UMCommon/UMConfigure.h' file not found` | 友盟头文件路径未注入 | Podfile 加 `:modular_headers => true` + `post_install` 注入 `HEADER_SEARCH_PATHS` |
| `Command PhaseScriptExecution failed (AlicloudUtils)` | 阿里云 Strip 脚本失败 | `post_install` 中将含 `Strip`/`archs` 的 shell script 替换为 `echo 'neutralized'` |
| `OpenIM Login Error 10004/10005` | IM Token 过期 | 刷新 imToken,重新调用 `login()` |
| `OSS 签名 URL 请求 403` | 用了带 Auth 头的 Dio 实例 | 使用纯净 `Dio()` 不带 Authorization 头 |
| `GoRouter redirect loop` | 路由守卫对 splash/privacy 路由放行条件错误 | 检查 `redirect` 中对 splash 和 privacy 路由的放行逻辑 |
| `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 ^4.0.2` + `hooks_riverpod ^3.3.1` + `riverpod_generator ^4.0.3` 一起升 |
| `build_runner 生成文件冲突` | 旧生成文件残留 | 运行 `dart run build_runner build --delete-conflicting-outputs` |
| `flutter pub run build_runner Deprecated` | 旧命令已废弃 | 改用 `dart run build_runner` |
| `pod install Ruby 报错 (fluwx/tobias)` | fluwx/tobias 自带 Ruby setup 脚本在非标准环境失败 | Podfile 顶部加 `ENV['FLUWX_SKIP_SETUP']='true'` 和 `ENV['TOBIAS_SKIP_SETUP']='true'` |
| `Universal Link 无效,微信无法回调` | 服务器未配置 AASA 文件或 Associated Domains 未添加 | 检查 `.well-known/apple-app-site-association` 是否可访问,Xcode 是否添加 `applinks:` |
| `支付宝 listenAliPayResult 内存泄漏` | dispose 时未取消监听 | 页面 dispose 时调用取消订阅 |
| `IAP 无法购买(StoreKit 报错)` | Xcode 未开启 In-App Purchase Capability | Xcode → Signing & Capabilities → 添加 In-App Purchase |

---

*文档版本:v2.1 · 基于同伴 App 前端开发规范 v5 · Flutter 3.41.x / Dart 3.11.x / Riverpod 3.x / freezed 3.x / go_router 17.x*