# IM App 整体架构设计

> 小型 IM 应用完整架构方案

> Clean Architecture / MVVM / Feature 驱动 / 高内聚低耦合 / 严格分层

## Part 0：开发环境配置

阅读架构之前先把环境跑起来，大约 5 分钟。

### 新机器初始化（只需做一次）

#### 第零步：从 Gitea 拉取代码

项目托管在内部 Gitea，仅支持 HTTPS 访问（SSH 暂不开放）。需要先在 Gitea 生成个人访问令牌（Personal Access Token）。

生成 Token 步骤：

1. 登录 Gitea（`https://gitea.winwayinfo.com`）
2. 点击右上角头像 → Settings
3. 左侧菜单选 Applications
4. 在 "Generate New Token" 填写令牌名称（任意）
5. Repository and organization Access 下选择 All
6. Selected token permissions 下所有权限选 Read and Write
7. 点击 Generate Token，复制生成的 token（只显示一次）

克隆仓库：

    # 将 YOUR_TOKEN 替换为刚才生成的 token，YOUR_USERNAME 替换为你的 Gitea 用户名
    git clone https://YOUR_USERNAME:YOUR_TOKEN@gitea.winwayinfo.com/CUS-IM/customer-im-client.git

    # 或者先 clone 再配置凭据
    git clone https://gitea.winwayinfo.com/CUS-IM/customer-im-client.git
    # 输入用户名和 token（token 作为密码）

注意：

- Token 只在生成时显示一次，务必立即保存
- 如果 clone 时提示输入密码，输入的是 Token 而不是账号密码
- 为避免每次 push/pull 都输入，可在克隆地址里内嵌 token：`https://user:token@gitea.winwayinfo.com/...`
- 切换到 `dev` 分支开发：`git checkout dev`；`main` 为主干保护分支

#### 第一步：安装 Flutter SDK

1. 前往 flutter.dev 下载最新 stable channel macOS 版本（.tar.xz）
   Apple Silicon（M 系列）必须下载 arm64 版本。
   文件名含 `arm64` 的才是 Apple Silicon 版，例如 `flutter_macos_arm64_3.x.x-stable.tar.xz`。
   不含 `arm64` 的（如 `flutter_macos_3.x.x-stable.tar.xz`）是 Intel (x86_64) 版，装在 M 芯片上编译 macOS 时会报：
   `Unable to find a device matching { platform:macOS, arch:arm64 }`
2. 解压到固定目录，推荐 `~/flutter`：

       cd ~
       tar xf ~/Downloads/flutter_macos_arm64_*.tar.xz
       # 解压后目录为 ~/flutter

3. 写入环境变量：

       echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.zshrc

4. 验证（在当前终端临时生效）：

       source ~/.zshrc
       flutter --version
       dart --version
       # 确认 Dart 二进制是 arm64（Apple Silicon 机器）
       file $(which dart)   # 应输出 Mach-O 64-bit executable arm64

#### 第二步：安装 Homebrew + Ruby + CocoaPods（iOS / macOS 必须）

iOS 和 macOS 平台依赖 CocoaPods 管理原生依赖，CocoaPods 需要 Ruby 3.x，通过 Homebrew 安装。

必须安装 Ruby 3.3，不能装最新版（4.x）。
Ruby 4.0 + OpenSSL 3.6 在 macOS 26 beta 上与 `cdn.cocoapods.org` 的 TLS 握手不兼容，`pod install` 会报 `Connection reset by peer - SSL_connect`，导致 iOS / macOS 构建失败。

    # 1. 安装 Homebrew（需要输入开机密码）
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    # 2. 将 Homebrew 本身写入 PATH（Apple Silicon M 芯片路径）
    echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
    source ~/.zshrc

    # 3. 验证 brew 可用
    brew --version

    # 4. 安装 Ruby 3.3（固定版本，不能用 brew install ruby 因为会装 4.x）
    brew install ruby@3.3

    # 5. 将 Ruby 3.3 写入 PATH（必须在系统 Ruby 2.6 之前）
    echo 'export PATH="/opt/homebrew/opt/ruby@3.3/bin:$PATH"' >> ~/.zshrc
    source ~/.zshrc

    # 6. 验证 Ruby 版本（确认是 3.3.x）
    ruby --version

    # 7. 安装 CocoaPods
    gem install cocoapods

    # 8. pod 可执行文件路径写入 PATH
    # gem 路径固定为 3.3.0
    echo 'export PATH="/opt/homebrew/lib/ruby/gems/3.3.0/bin:$PATH"' >> ~/.zshrc

    # 9. 终端编码设置（避免 pod 报 UTF-8 警告）
    echo 'export LANG=en_US.UTF-8' >> ~/.zshrc

    source ~/.zshrc

    # 11. 验证
    pod --version

第 10 步单独说明——在 `~/.zshrc` 末尾手动追加以下内容（这段是写入文件的 shell 代码，不是在终端直接执行的命令）：

    # CocoaPods CDN SSL fix for macOS 26 beta
    # 修复原因：macOS 26 beta + OpenSSL 3.x 与 cdn.cocoapods.org TLS 握手失败
    # 每次开终端自动写入临时配置文件并设置 OPENSSL_CONF，pod install 时读取该配置
    cat > /tmp/openssl_pod.cnf << 'OPENSSL_EOF'
    openssl_conf = openssl_init
    [openssl_init]
    ssl_conf = ssl_sect
    [ssl_sect]
    system_default = system_default_sect
    [system_default_sect]
    MinProtocol = TLSv1.2
    CipherString = DEFAULT:@SECLEVEL=1
    OPENSSL_EOF
    export OPENSSL_CONF=/tmp/openssl_pod.cnf

注意：

- 必须用 `brew install ruby@3.3`，不能用 `brew install ruby`（后者会装最新的 4.x）
- Homebrew 装好后必须先把 `/opt/homebrew/bin` 写入 PATH，否则终端找不到 `brew` 命令
- macOS 自带 Ruby 2.6，CocoaPods 要求 >= 3.0，必须通过 Homebrew 安装新版
- OPENSSL_CONF 修复是 写在 ~/.zshrc 里的 shell 代码段，每次开新终端自动执行，不是一次性命令
- iOS / macOS 编译还需要完整安装 Xcode，仅有 Command Line Tools 不够（见下一步）

#### （补充）安装 Xcode（iOS / macOS 编译必须）

CocoaPods 只是依赖管理工具，实际编译 iOS / macOS 需要完整的 Xcode。Command Line Tools 不够。

1. 在 App Store 搜索 Xcode 并安装（约 10 GB）
2. 安装完成后执行：

       sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
       sudo xcodebuild -runFirstLaunch
       xcodebuild -version   # 验证

不装 Xcode 的后果：`flutter build ios` 报 "Application not configured for iOS"，`flutter build macos` 报 "unable to find utility xcodebuild"，两个平台都无法编译。

#### 第三步：配置 IDE

Android Studio

1. 打开 Android Studio
2. 菜单：`Settings（Windows/Linux）` 或 `Preferences（macOS）`
3. 导航至 `Languages & Frameworks → Flutter`
4. Flutter SDK path 填写解压路径，例如 `/Users/{你的用户名}/flutter`
5. 点击 Apply → OK
6. 重启 Android Studio

VS Code

1. 在 Extensions 市场搜索并安装 Flutter 插件（Dart 插件会自动一并安装）
2. 按 `Cmd+Shift+P` 打开命令面板
3. 输入 `Flutter: Change SDK`，选择 Flutter SDK 解压目录（例如 `/Users/{你的用户名}/flutter`）
4. 重启 VS Code

注意：配置完成后，彻底退出所有终端和 IDE（包括 IDE 内嵌终端），再继续后续步骤。否则新写入的 PATH 不会在已开启的终端中生效。

#### 第三步：运行初始化脚本

重新打开终端，进入项目根目录执行：

    cd customer-im-client
    bash scripts/setup.sh

setup.sh 包含 6 步：安装全局工具 → 配置 PATH → dart pub get → melos bootstrap → mason get → melos run gen。 其中 `melos bootstrap` 负责生成 `.iml` 和 `.idea/modules.xml`（IDE 模块识别，不入库）。

setup.sh 执行完毕后，再次彻底退出所有终端和 IDE。然后重新打开 IDE。
若提示 `melos: command not found`，说明 PATH 未生效，执行 `source ~/.zshrc` 后再重试。
若 IDE 缺少 `[melos_xxx_sdk]` 标签，单独执行 `melos bootstrap` 即可补全。

#### 第四步：验证项目可以运行

重新打开 IDE，在内嵌终端或新终端窗口中，切换到 im_app 目录执行：

    cd customer-im-client/apps/im_app
    flutter run

首次运行会触发 Gradle / CocoaPods 等平台工具链下载，耐心等待即可。

#### 第五步：各平台单独跑一次 pub get

为避免平台插件注册缺失导致运行失败，每个目标平台在首次开发前单独执行一次：

    # 在 apps/im_app 目录下执行
    flutter pub get                          # 所有平台依赖

    # 按需对各平台单独验证
    flutter build apk --debug               # Android
    flutter build ios --debug --no-codesign # iOS（macOS 机器）
    flutter build macos --debug             # macOS

完成以上步骤后，`~/.zshrc` 末尾应包含以下内容（供核对）：

    export PATH="/opt/homebrew/bin:$PATH"
    export PATH="/opt/homebrew/opt/ruby@3.3/bin:$PATH"
    export PATH="/opt/homebrew/lib/ruby/gems/3.3.0/bin:$PATH"
    export PATH="$HOME/flutter/bin:$PATH"
    export LANG=en_US.UTF-8

    # CocoaPods CDN SSL fix for macOS 26 beta
    cat > /tmp/openssl_pod.cnf << 'OPENSSL_EOF'
    openssl_conf = openssl_init
    [openssl_init]
    ssl_conf = ssl_sect
    [ssl_sect]
    system_default = system_default_sect
    [system_default_sect]
    MinProtocol = TLSv1.2
    CipherString = DEFAULT:@SECLEVEL=1
    OPENSSL_EOF
    export OPENSSL_CONF=/tmp/openssl_pod.cnf

完成以上步骤后环境即可正常使用。后续只需保持 `melos run gen:watch` 常驻运行即可（见下方日常工作流）。

### 日常开发工作流

每次开发时开两个终端窗口：

终端 | 命令 | 说明
--- | --- | ---
Terminal 1（常驻） | `melos run gen:watch` | ⚠️ 必须保持运行，保存代码后自动生成 .g.dart
Terminal 2 | `flutter run` | 启动 App，支持热重载

代码生成常见问题

- 保存后红线不消失：检查 Terminal 1 是否有 `melos run gen:watch` 在运行
- 生成报错：`melos run gen` 重新全量生成
- 新增依赖后：先 `dart pub get`，再重启 watch
- .g.dart 冲突：直接删除冲突文件后重新生成，不要手动合并

### Melos 命令速查

命令 | 说明
--- | ---
依赖 & 代码生成
`bash scripts/setup.sh` | 新机器一键环境初始化（只需执行一次）
`melos bootstrap` | 生成 IDE 模块配置（.iml + modules.xml），缺 [melos_xxx] 标签时执行
`dart pub get` | 安装所有依赖（首次或 pubspec 变更后）
`melos run gen` | 一次性代码生成（.g.dart / .freezed.dart）
`melos run gen:watch` | ⚠️ 开发必开：监听模式，保存后自动生成
质量检查
`melos run analyze` | 所有 Package 静态分析
`melos run test` | 所有 Package 单元测试
清理
`melos run clean` | 所有 Package flutter clean
`melos run clean:deep` | 深度清理（含 Gradle / Pods / CMake + 生成文件）
`melos run clean:deep -- android` | 只清 Android 平台缓存（ios / macos / windows 同理）
SDK 版本管理
`melos run sdk:bump` | 从 flutter.dev 拉最新稳定版，Dart + Flutter 一起升级
`melos run sdk:bump -- --dart 3.12.0` | 只升 Dart SDK 约束，Flutter 下限不变
`melos run sdk:bump -- --flutter 3.40.0` | 只升 Flutter 下限，Dart 不变
`melos run sdk:bump -- --dart 3.12.0 --flutter 3.40.0` | 手动指定两者（用于 CI 固定版本）
构建
`melos run new:sdk -- push` | 创建新 SDK 包 packages/push_sdk/（Facade+Wiring 骨架）
`melos run remove:sdk -- push` | 删除 SDK 包，同步清理 workspace、IDE 模块注册
`melos run build:android:apk` | 构建 Android APK
`melos run build:android:aab` | 构建 Android AAB（Google Play）
`melos run build:ios` | 构建 iOS IPA（仅 macOS）
`melos run build:macos` | 构建 macOS app
`melos run build:windows` | 构建 Windows EXE（仅 Windows）

## 第一部分：为什么（Why）- 设计理念

## Mono-Repo 架构

项目组织方式：本项目采用 Mono-Repo（单一代码仓库）方式组织，使用 Melos 进行管理。

#### 什么是 Mono-Repo？

Mono-Repo 是将多个相关项目放在同一个代码仓库中管理的方式，与传统的每个项目一个仓库（Multi-Repo）不同。

我们的 Mono-Repo 包含：

- 主应用（IM App）
- 9 个 Core SDK（NetworkSDK、StorageSDK、ProtocolSDK、MediaSDK、RTCSDK、NotificationSDK、CipherGuardSDK、CryptoSDK、L10nSDK）
- 共享组件库（Widgets、Utils、Extensions）
- 示例应用和测试项目

#### 为什么选择 Mono-Repo？

优势 | 说明 | 实际价值
--- | --- | ---
版本一致性 | 同一个 commit 保证所有 package 兼容 | 不会出现版本不匹配问题
API 变更安全 | 编译期立即发现问题，马上修复 | 不会在发版后才发现问题
重构成本低 | 一次性全 repo 重构 | 不需要跨 repo、分批跟进
开发效率高 | 改 SDK → 主应用立即验证 | 不需要先发版才能验证
统一工具链 | 一套 Melos 指令管理所有项目 | 不需要维护多套脚本
新人友好 | clone 一个 repo 就全到位 | 不需要 clone 多个 repo

#### Melos 管理工具

Melos 是 Flutter/Dart 生态的 Mono-Repo 管理工具，提供：

- 自动依赖解析：自动 link 本地 package，无需手动管理
- 统一脚本命令：一条命令运行所有测试、构建、发布
- 增量测试：只测试受影响的 packages，节省时间
- 版本管理：统一管理所有 package 的版本号

flowchart TD App[主应用
IM App] App --> Root[IM Mono-Repo
根目录] Root --> SDKs[Core SDKs
9个独立SDK] Root --> Shared[共享组件
Widgets/Utils] Root --> Examples[示例应用
Example Apps] SDKs --> SDK1[NetworkSDK] SDKs --> SDK2[StorageSDK] SDKs --> SDK3[ProtocolSDK] SDKs --> SDK4[MediaSDK] SDKs --> SDK5[RTCSDK] SDKs --> SDK6[NotificationSDK] SDKs --> SDK7[CipherGuardSDK
Flutter Plugin] SDKs --> SDK8[CryptoSDK
占位] SDKs --> SDK9[L10nSDK] App -.依赖.-> SDKs App -.依赖.-> Shared Examples -.依赖.-> SDKs style App fill:#e3f2fd,stroke:#0288d1,stroke-width:3px style Root fill:#fff9c4,stroke:#f57f17,stroke-width:2px style SDKs fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style Shared fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Examples fill:#fff4e6,stroke:#f57c00,stroke-width:2px

设计理念：通过 Mono-Repo + Melos 的组合，我们能够在保持模块独立性的同时，获得统一管理的便利性，大幅提升开发效率和代码质量。

### Packages 目录结构（SDK 独立 Package）

所有可复用的基础能力 SDK 从主 App 的 `core/foundation/` 提取为独立 Package，放在 Mono-Repo 的 `packages/` 目录下。主 App 通过 `pubspec.yaml` 的 `path:` 依赖引用。

    packages/
    │
    ├── networks_sdk/                           # 网络通信（HTTP + WebSocket，Flutter Plugin）
    │   ├── build.yaml                          # @ApiRequest 代码生成器注册
    │   └── lib/
    │       ├── networks_sdk.dart               # barrel file（统一导出）
    │       └── src/
    │           ├── annotations/
    │           │   └── api_request.dart        # @ApiRequest 注解定义
    │           ├── generator/
    │           │   ├── api_request_generator.dart  # Request mixin 生成器（toJson / path / method / fromJson 注册）
    │           │   ├── api_response_generator.dart # Response fromJson 生成器（从 @ApiRequest(responseType) 自动推导，递归嵌套类型）
    │           │   └── builder.dart                # SharedPartBuilder 入口（两个生成器合并到同一 .g.dart）
    │           ├── data/
    │           │   ├── datasources/
    │           │   │   ├── http/
    │           │   │   │   ├── api_client.dart              # Dio REST 客户端
    │           │   │   │   ├── token_refresh_manager.dart   # Token 刷新管理（竞态安全 + 超时 + 时间窗口复用）
    │           │   │   │   └── interceptor/
    │           │   │   │       ├── auth_interceptor.dart        # Token + 默认 header 注入
    │           │   │   │       ├── encryption_interceptor.dart  # 加密拦截器（预留给 cipher_guard_sdk）
    │           │   │   │       ├── retry_interceptor.dart       # Token 刷新 + 瞬态错误重试 + 业务错误钩子
    │           │   │   │       └── logging_interceptor.dart     # 请求/响应日志
    │           │   │   ├── socket/
    │           │   │   │   └── socket_client.dart           # WebSocket 长连接（心跳/重连/Stream/加密钩子）
    │           │   │   └── networks_sdk_method_channel_datasource.dart  # 统一执行入口
    │           │   ├── dto/
    │           │   │   ├── api_requestable.dart         # 请求基类 + fromJson 注册表
    │           │   │   └── api_response_wrapper.dart    # { code, message/msg, data } 响应包装解析
    │           │   └── repositories/
    │           │       ├── networks_sdk_repository_impl.dart
    │           │       └── networks_messaging_repository_impl.dart
    │           ├── domain/
    │           │   ├── entities/
    │           │   │   ├── api_error.dart               # @freezed HTTP 错误联合类型（7 变体）
    │           │   │   ├── encrypted_request.dart       # 加密请求结果数据类
    │           │   │   ├── socket_error.dart             # @freezed WebSocket 错误联合类型
    │           │   │   ├── socket_connection_state.dart  # 连接状态 enum
    │           │   │   ├── http_method.dart              # GET/POST/PUT/DELETE/PATCH
    │           │   │   └── api_request_type.dart         # request/login/upload/stream/download
    │           │   └── repositories/
    │           │       ├── networks_sdk_repository.dart
    │           │       └── networks_messaging_repository.dart
    │           └── presentation/
    │               ├── facade/
    │               │   ├── networks_sdk_api.dart         # HTTP 公开 API 接口
    │               │   └── networks_messaging_api.dart   # WebSocket 公开 API 接口
    │               └── wiring/
    │                   ├── api_config.dart               # HTTP 配置（baseURL/Token/回调/加密/重试）
    │                   ├── socket_config.dart            # WebSocket 配置（心跳/重连/加密钩子）
    │                   ├── network_callbacks.dart        # 回调类型定义（认证/加密/业务错误/下载/WS）
    │                   ├── networks_sdk_core.dart
    │                   ├── networks_sdk_api_impl.dart
    │                   ├── networks_messaging_api_impl.dart
    │                   └── networks_sdk_wiring.dart      # 工厂：build() / buildMessagingApi()
    │
    ├── cipher_guard_sdk/                       # 端对端加密（Flutter Plugin）
    │   └── lib/
    │       ├── cipher_guard_sdk.dart           # barrel file
    │       └── src/
    │           ├── data/
    │           │   ├── constants/
    │           │   │   └── method_channel_constants.dart
    │           │   ├── datasources/
    │           │   │   ├── encryption_flutter_service.dart  # RSA/AES 纯 Dart 加解密（pointycastle + encrypt）
    │           │   │   └── encryption_method_channel.dart   # Native 密钥同步（iOS App Group）
    │           │   ├── dto/
    │           │   │   ├── chat_encryption_key_dto.dart
    │           │   │   ├── encrypted_message_dto.dart
    │           │   │   ├── method_channel_response.dart
    │           │   │   └── rsa_key_pair_dto.dart
    │           │   └── repositories/
    │           │       └── encryption_repository_impl.dart
    │           ├── domain/
    │           │   ├── entities/
    │           │   │   ├── chat_encryption_key.dart
    │           │   │   ├── encrypted_message.dart
    │           │   │   ├── rsa_key_pair.dart
    │           │   │   └── session_key.dart
    │           │   └── repositories/
    │           │       └── encryption_repository.dart
    │           └── presentation/
    │               ├── facade/
    │               │   └── cipher_guard_sdk_api.dart    # 公开 API（调用方只依赖这里）
    │               └── wiring/
    │                   ├── cipher_guard_sdk_api_impl.dart
    │                   ├── cipher_guard_sdk_core.dart
    │                   └── cipher_guard_sdk_wiring.dart
    │
    ├── storage_sdk/                            # 本地存储（Facade+Wiring，纯基础设施：连接管理 + 通用 CRUD）
    │   └── lib/
    │       ├── storage_sdk.dart                       # barrel：导出 StorageSdkApi
    │       └── src/
    │           ├── data/
    │           │   ├── local/
    │           │   │   └── datasources/
    │           │   │       └── database_datasource.dart          # 连接生命周期（openDatabase/closeDatabase）
    │           │   └── repositories/
    │           │       └── database_repository_impl.dart         # 通用 CRUD 的 Drift 实现
    │           ├── domain/
    │           │   └── repositories/
    │           │       └── database_repository.dart              # 通用 CRUD 接口（泛型，与表无关）
    │           └── presentation/
    │               ├── facade/
    │               │   └── storage_sdk_api.dart                  # 公开接口（生命周期 + 全量 CRUD）
    │               └── wiring/
    │                   ├── storage_sdk_api_impl.dart             # 委托给 Core
    │                   ├── storage_sdk_core.dart                 # 持有 DataSource + Repo
    │                   └── storage_sdk_wiring.dart               # build(databaseFactory:) → StorageSdkApi
    │
    │   # ── 以下 SDK 均遵循相同的 Facade + Wiring 模式，结构参照 cipher_guard_sdk ──
    │
    ├── notification_sdk/                       # 推送通知（Flutter Plugin）
    │   └── lib/notification_sdk.dart + src/{data,domain,presentation}/ （同上模式）
    │
    ├── media_sdk/                              # 媒体处理（Flutter Plugin）
    │   └── lib/media_sdk.dart + src/{data,domain,presentation}/ （同上模式）
    │
    ├── rtc_sdk/                                # 实时音视频（Flutter Plugin）
    │   └── lib/rtc_sdk.dart + src/{data,domain,presentation}/ （同上模式）
    │
    ├── protocol_sdk/                           # 消息协议（Flutter Plugin）
    │   └── lib/protocol_sdk.dart + src/{data,domain,presentation}/ （同上模式）
    │
    ├── l10n_sdk/                               # 国际化（Flutter Plugin）
    │   └── lib/l10n_sdk.dart + src/{data,domain,presentation}/ （同上模式）

Package 类型说明：

- Flutter Plugin（多个）：所有 SDK 均声明为 Flutter Plugin，包含 `android/` + `ios/` 原生代码入口，Plugin 机制自动注册
- cipher_guard_sdk：E2E 加密核心，RSA/AES 双层加密 + iOS App Group 密钥同步（用于推送通知解密）

主 App 引用方式（pubspec.yaml）：

    dependencies:
      networks_sdk:
        path: packages/networks_sdk
      cipher_guard_sdk:
        path: packages/cipher_guard_sdk
      storage_sdk:
        path: packages/storage_sdk
      notification_sdk:
        path: packages/notification_sdk
      media_sdk:
        path: packages/media_sdk
      rtc_sdk:
        path: packages/rtc_sdk
      protocol_sdk:
        path: packages/protocol_sdk
      l10n_sdk:
        path: packages/l10n_sdk

设计原则：

- 独立发布：每个 SDK 可独立版本号、独立 CHANGELOG，未来可发布到 pub.dev
- 跨项目复用：其他 Flutter 项目可直接依赖这些 SDK，不绑定 IM App 业务逻辑
- 最小依赖：SDK 之间尽量无依赖，必要时通过接口解耦
- Melos 统一管理：`dart pub get` 统一安装依赖，`melos run test` 批量测试

## 代码生成工具

本项目大量使用代码生成工具来提升开发效率、减少样板代码，并保证类型安全。

### 核心代码生成工具

工具 | 作用 | 生成内容 | 优势
--- | --- | --- | ---
riverpod_generator | Riverpod Provider 代码生成 | 自动生成 Provider、依赖注入代码 | 大幅减少样板代码，编译期类型安全
freezed | 不可变数据类生成 | State 类、copyWith、序列化代码 | 消除手写 State 的繁琐，保证不可变性
json_serializable | JSON 序列化/反序列化 | fromJson/toJson 方法 | 自动处理 JSON 转换，类型安全
build_runner | 代码生成执行器 | 监听文件变化，自动生成 | 开发时实时生成，无需手动执行

### 代码生成示例对比

#### 不使用代码生成（手写样板代码）

    // 手写 State 类 - 繁琐且容易出错
    class ChatState {
      final List<Message> messages;
      final bool isLoading;
      final String error;

      const ChatState({
        required this.messages,
        required this.isLoading,
        required this.error,
      });

      // 手写 copyWith - 每个字段都要写
      ChatState copyWith({
        List<Message>? messages,
        bool? isLoading,
        String? error,
      }) {
        return ChatState(
          messages: messages ?? this.messages,
          isLoading: isLoading ?? this.isLoading,
          error: error ?? this.error,
        );
      }

      // 手写 equality - 容易遗漏字段
      @override
      bool operator ==(Object other) {
        if (identical(this, other)) return true;
        return other is ChatState &&
          listEquals(other.messages, messages) &&
          other.isLoading == isLoading &&
          other.error == error;
      }

      @override
      int get hashCode => Object.hash(messages, isLoading, error);
    }

    // 手写 Provider - 样板代码多
    final chatViewModelProvider = StateNotifierProvider.autoDispose<
      ChatViewModel, ChatState
    >((ref) {
      final sendMessageUseCase = ref.watch(sendMessageUseCaseProvider);
      final loadMessagesUseCase = ref.watch(loadMessagesUseCaseProvider);
      return ChatViewModel(sendMessageUseCase, loadMessagesUseCase);
    });

#### 使用代码生成（简洁高效）

    // 使用 @freezed - 自动生成 copyWith、equality、toString
    part 'chat_state.freezed.dart';

    @freezed
    class ChatState with _$ChatState {
      const factory ChatState({
        @Default([]) List<Message> messages,
        @Default(false) bool isLoading,
        @Default('') String error,
      }) = _ChatState;
    }

    // 使用 @riverpod - 自动生成 Provider
    part 'chat_view_model.g.dart';

    @riverpod
    class ChatViewModel extends _$ChatViewModel {
      @override
      ChatState build() => const ChatState();

      Future<void> sendMessage(String content) async {
        state = state.copyWith(isLoading: true);
        await ref.read(sendMessageUseCaseProvider)(content);
        state = state.copyWith(isLoading: false);
      }
    }

### 开发流程

flowchart LR Write[编写注解代码] --> Watch[build_runner watch] Watch --> Generate[自动生成 .g.dart] Generate --> Use[直接使用生成代码] Use --> Modify[修改源码] Modify --> Generate style Write fill:#e3f2fd,stroke:#0288d1 style Generate fill:#e8f5e9,stroke:#388e3c style Use fill:#fff4e6,stroke:#f57c00

### 代码生成命令

    # 一次性生成
    melos run gen

    # 监听模式（开发期间必须常驻）
    melos run gen:watch

### 代码生成的价值

- 大幅减少样板代码：不需要手写 copyWith、equality、hashCode
- 编译期类型安全：生成的代码类型完全正确，无运行时错误
- 自动化维护：修改字段后自动重新生成，无需手动同步
- 统一代码风格：生成的代码风格一致，易于 Code Review
- 提升开发效率：专注业务逻辑，不浪费时间在重复代码上

## 设计理念与目标

### Clean Architecture（整洁架构）

目的：让业务逻辑与界面设计分离

方法：通过结构的分层来约束类别间的使用方向

好处：

- 代码更易维护
- 代码更易测试
- 代码更易扩展
- 业务逻辑独立于框架
- 业务逻辑独立于UI
- 业务逻辑独立于数据库

### MVVM（Model-View-ViewModel）状态管理

#### 演进历史

在响应式应用中，状态管理经历了以下演进过程：

flowchart LR MVC[MVC
Model-View-Controller] --> MVP[MVP
Model-View-Presentation] MVP --> MVVM[MVVM
Model-View-ViewModel] style MVC fill:#fce4ec,stroke:#c2185b style MVP fill:#fff4e6,stroke:#f57c00 style MVVM fill:#e8f5e9,stroke:#388e3c

#### 为什么要这样演变？

架构模式 | 核心问题 | 演进动机
--- | --- | ---
MVC (Model-View-Controller) | Controller 职责过重，成为"上帝类" View 和 Model 耦合，难以测试 View 直接访问 Model，导致业务逻辑泄露到 UI 难以进行单元测试（需要 UI 环境） | 需要更好的关注点分离和可测试性
MVP (Model-View-Presenter) | View 和 Presenter 通过接口通信，代码量大 Presenter 需要手动更新 View（调用 view.updateXXX()） 数据变化时需要手动同步到 UI 样板代码多，维护成本高 | 需要自动化的数据绑定和响应式更新
MVVM (Model-View-ViewModel) | 通过数据绑定实现 UI 自动更新 ViewModel 不持有 View 引用，完全解耦 状态变化自动反映到 UI，无需手动刷新 更易测试，ViewModel 可独立测试 | 响应式编程的最佳实践

##### 演进的本质：逐步解耦和自动化

1. MVC → MVP：解决 View 和 Model 的耦合
   - 引入 Presenter 作为中介，View 不再直接访问 Model
   - View 和 Presenter 通过接口通信，提升可测试性
   - 缺点：手动更新 UI 的样板代码过多
2. MVP → MVVM：引入数据绑定，实现自动化
   - ViewModel 暴露可观察的状态（State）
   - View 通过数据绑定自动订阅状态变化
   - 状态更新时，UI 自动刷新，无需手动调用
   - 优势：代码更简洁，逻辑更清晰，易于维护

#### 前提条件

状态管理方式高度依赖官方 SDK 的支持与否才可以实现。如果官方 SDK 不支持，某些框架将无法实现。

实例：

- 2012-2019年：Android 开发只支持 MVC 状态管理，无法使用 MVVM
- 2020年：Android 官方推出了 BindingView 的 SDK，此后才可以使用 MVVM 做开发
- Flutter：从一开始就支持响应式框架，天然适合 MVVM

#### 技术栈规定

技术栈升级要求

为保证架构的现代化和统一性，必须采用以下技术栈：

平台 | UI 框架 | 状态管理 | 说明
--- | --- | --- | ---
iOS | SwiftUI | Combine + Observation | Apple 官方声明式 UI + 响应式框架
Android | Jetpack Compose | Flow + LiveData | Google 官方声明式 UI + 响应式框架
Flutter | Widget | Riverpod | 跨平台声明式 UI + 现代状态管理

##### 为什么强制使用这些技术栈？

- 统一的架构思想：三端都采用声明式 UI + 响应式状态管理，降低学习成本
- 官方推荐方案：SwiftUI、Compose 分别是 Apple 和 Google 的官方推荐技术栈
- 现代化开发：摒弃 UIKit/XML 等过时技术，拥抱声明式编程范式
- 更好的性能：声明式 UI 框架在渲染性能和内存管理上都更优秀
- 易于维护：代码更简洁、逻辑更清晰、bug 更少
- 团队协作：统一技术栈降低沟通成本，提高开发效率

##### 学习资源

iOS - SwiftUI + Combine + Observation：

- SwiftUI 官方教程 - Apple 官方 SwiftUI 完整学习路径
- SwiftUI 官方文档 - SwiftUI 完整 API 文档
- Combine 官方文档 - Apple 响应式框架完整文档
- Observation 官方文档 - Swift 现代化可观察对象框架（iOS 17+）

Android - Jetpack Compose + Flow + LiveData：

- Jetpack Compose 官方课程 - Google 官方 Compose 学习路径
- Jetpack Compose 官方文档 - Compose 完整文档
- Kotlin Flow 官方文档 - Kotlin 协程和 Flow 完整指南
- LiveData 官方文档 - LiveData 使用指南

#### 参考学习链接

深入了解 Flutter 应用架构和 MVVM 模式：

- Flutter 官方架构指南 - Flutter 官方推荐的应用架构设计指南
- Riverpod 官方文档 - Riverpod 状态管理完整学习指南
- Riverpod 官网 - 了解 Riverpod 的核心特性和优势

### 为什么选择 Riverpod？

本项目使用 Riverpod 作为状态管理方案，基于以下核心技术优势和实践教训：

##### 1. 性能优化机制

###### 刷新颗粒度（Rebuild Granularity）

GetX + Obx 的问题：

- Obx 包裹的整个 Widget 都会重建
- 嵌套 Obx 会导致多次重建
- 无法精确控制重建范围

实际案例：

    // 整个 Container 都会重建
    Obx(() => Container(
      height: controller.height.value,  // 高度变化
      child: Column(
        children: [
          Text(controller.title.value),     // title 变化也会重建整个 Container
          Text(controller.content.value),   // content 变化也会重建整个 Container
        ],
      ),
    ))

Riverpod 的精细化刷新优化：

优化技术 | 作用 | 使用场景
--- | --- | ---
select | 精确订阅状态的某个字段 | 只关心状态中的部分数据，避免整个对象变化导致重建
Consumer | 局部重建 Widget 树的一部分 | 只重建需要响应状态变化的最小 Widget 子树
RepaintBoundary | 隔离重绘边界 | 阻止父级重绘影响子级，或子级重绘影响父级
autoDispose | 自动释放不再使用的 Provider | 页面销毁时自动清理资源，避免内存泄漏
family | 为不同参数创建独立 Provider 实例 | 列表中每个 Item 有独立状态，互不影响

###### 代码示例

    // 1. select：只订阅 title 字段，content 变化时不重建
    final title = ref.watch(chatViewModelProvider.select((s) => s.title));

    // 2. Consumer：只重建 Consumer 包裹的部分
    Widget build(BuildContext context) {
      return Column(
        children: [
          Text('静态内容，永远不重建'),
          Consumer(
            builder: (context, ref, child) {
              final count = ref.watch(counterProvider);
              return Text('动态内容: $count'); // 只有这里会重建
            },
          ),
          Text('静态内容，永远不重建'),
        ],
      );
    }

    // 3. RepaintBoundary：隔离复杂动画，避免影响其他 Widget
    RepaintBoundary(
      child: ComplexAnimationWidget(), // 动画重绘不会影响外部
    )

    // 4. autoDispose：页面销毁时自动释放资源
    final userProvider = StateNotifierProvider.autoDispose<UserViewModel, UserState>(
      (ref) => UserViewModel(),
    );

    // 5. family：列表中每个 Item 有独立状态
    final messageProvider = Provider.family<Message, int>((ref, messageId) {
      return ref.watch(chatRepositoryProvider).getMessageById(messageId);
    });

性能对比：

场景 | 不使用优化 | 使用优化 | 提升
--- | --- | --- | ---
聊天列表滚动 | 所有 Item 重建 | 只重建可见 Item | 性能显著提升
状态局部更新 | 整个页面重建 | 只重建 Consumer 部分 | 大幅减少 rebuild
动画播放 | 影响整个 Widget 树 | RepaintBoundary 隔离 | 流畅度明显提升
列表 Item 状态 | 共享状态，互相影响 | family 独立状态 | 避免无关重建

性能提升效果：

- 大幅减少不必要的 rebuild
- 明显降低 CPU 使用
- 显著提升滑动流畅度

##### 2. 单向数据流（Unidirectional Data Flow）

###### 为什么需要单向数据流？

GetX 的双向绑定问题：

    // GetX 允许在任何地方修改状态，导致数据流混乱
    class ChatPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = Get.find<ChatController>();

        return Column(
          children: [
            // 问题1：UI 直接修改状态
            ElevatedButton(
              onPressed: () => controller.isLoading.value = true,  // UI 直接改状态
            ),

            // 问题2：不知道状态从哪里来，往哪里去
            Obx(() => Text(controller.message.value)),
          ],
        );
      }
    }

数据流混乱导致：

- 无法追踪状态变化来源
- 多处修改同一状态，冲突频发
- Debug 困难，不知道谁改了状态

Riverpod 的单向数据流：

    用户操作 → ViewModel 方法 → 更新 State → UI 自动响应
       ↑                                           ↓
       └─────────────── 只读数据 ──────────────────┘

代码实现：

    class ChatPage extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // 只读：只能读取状态，不能直接修改
        final state = ref.watch(chatViewModelProvider);
        final viewModel = ref.read(chatViewModelProvider.notifier);

        return Column(
          children: [
            // 清晰：通过 ViewModel 方法修改状态
            ElevatedButton(
              onPressed: () => viewModel.sendMessage('hello'),
            ),

            // 响应：UI 自动响应状态变化
            Text(state.message),
          ],
        );
      }
    }

优势：

- 数据流向清晰：用户操作 → ViewModel → State → UI
- 状态只有一个修改入口：ViewModel 的方法
- 易于追踪：DevTools 可以看到完整的状态变化历史
- 状态历史回溯：可以回退查看之前任意时刻的应用状态，像看视频回放一样

##### 3. 编译期类型安全（Compile-time Safety）

###### GetX 的运行时陷阱

问题1：字符串查找 Controller

    // 编译通过，运行时崩溃
    final controller = Get.find<ChatController>();  // 如果忘记 Get.put，运行时炸

问题2：依赖关系不明确

    class ChatController extends GetxController {
      void sendMessage() {
        // 不知道依赖了什么，运行时才知道
        objectMgr.chatMgr.send();  // objectMgr 是什么？从哪来？
      }
    }

问题3：类型不安全

    // 编译通过，运行时类型错误
    Get.put<BaseController>(ChatController());
    final controller = Get.find<UserController>();  // 找到 ChatController，类型不匹配！

运行时错误案例：

- Controller 未初始化
- 类型转换错误
- 依赖循环引用

###### Riverpod 的编译期保证

保证1：依赖必须存在

    @riverpod
    class ChatViewModel extends _$ChatViewModel {
      @override
      ChatState build() {
        // 编译期就知道依赖了什么
        final chatRepo = ref.watch(chatRepositoryProvider);  // 如果 Provider 不存在，编译报错
        return const ChatState();
      }
    }

保证2：类型完全正确

    // 类型由编译器推断，完全正确
    final state = ref.watch(chatViewModelProvider);  // state 类型是 ChatState
    final viewModel = ref.read(chatViewModelProvider.notifier);  // viewModel 类型是 ChatViewModel

保证3：依赖图可视化

    // Riverpod DevTools 可以看到完整的依赖图
    ChatViewModel
      ├─ chatRepositoryProvider
      │   ├─ networkSdkApiProvider
      │   └─ messageLocalDataSourceProvider
      └─ sendMessageUseCaseProvider
          └─ chatRepositoryProvider

编译期保证的价值：

- 大幅减少 Bug：运行时错误显著下降
- 提升开发效率：不需要运行才知道对错
- 重构安全：IDE 自动提示依赖变化
- 团队协作：依赖关系明确，不会互相影响

#### GetX + Obx vs Riverpod：实践教训

典型问题示例

以下内容展示了 GetX + Obx 在大型项目中暴露的典型问题，作为选型参考。

##### 1. 状态管理混乱

GetX + Obx 问题代码示例（chat_list_controller.dart）：

    class ChatListController extends GetxController {
      var lastClickedSpecialChatId = (-1).obs;
      final chatList = <Chat>[].obs;
      final companyChatList = <Chat>[].obs;
      final lockedChatList = <Chat>[].obs;
      final RxBool isNavigating = false.obs;
      final isInitializing = true.obs;
      ValueNotifier<bool> isInitializingValue = ValueNotifier(true);  // 混用！
      final isShowSkeleton = false.obs;
      ValueNotifier<double> offset = ValueNotifier(0);  // 混用！
      RxDouble offsetObx = 0.0.obs;  // 重复状态！
      RxDouble miniAppIconOpacityWhenShowingMiniApp = 1.0.obs;
      RxDouble miniAppIconOpacityWhenShowingChatView = 0.01.obs;
      RxDouble miniAppIconScale = 0.5.obs;
      RxBool isShowingApplet = false.obs;
      RxBool isMiniAppletDraggingIcon = false.obs;
      RxBool isBeingHovered = false.obs;
      RxBool isShowDeleteBar = true.obs;
      // ... 还有 50+ 个响应式变量！
    }

问题分析：

- 状态零散：50+ 个独立的 `.obs` 变量，没有统一结构
- 混合使用：`RxBool`, `RxDouble`, `ValueNotifier` 混杂
- 重复状态：`offset` 和 `offsetObx` 表示同一个东西
- 命名混乱：英文、拼音、中文注释混杂
- UI 状态污染：动画、透明度、偏移量都混在 Controller 里

##### 2. 过度嵌套和性能问题

Obx 嵌套地狱示例（home_view.dart）：

    body: Obx(() => AnimatedPadding(
      child: GetBuilder(
        builder: (_) => Obx(
          () => Stack(
            children: [
              Obx(
                () => Container(
                  child: Obx(
                    () => Opacity(
                      opacity: controller.opacity.value,
                      child: Obx(
                        () => AnimatedContainer(...),  // 第6层 Obx！
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    )),

问题分析：

- 嵌套地狱：单个文件 12 个 Obx，最深 6 层嵌套
- 过度重建：每个 Obx 独立监听，导致大量不必要的 rebuild
- Obx + GetBuilder 混用：逻辑混乱，性能更差
- 难以调试：无法追踪哪个状态导致了 rebuild

##### 3. Controller 过于臃肿

巨型 Controller 反模式示例：

    // chat_list_controller.dart
    import 'dart:async';
    import 'dart:io';
    import 'dart:math';
    import 'dart:ui';
    import 'package:bot_toast/bot_toast.dart';
    import 'package:custom_pop_up_menu/custom_pop_up_menu.dart';
    // ... 共 77 个 import！

    class ChatListController extends GetxController
        with GetTickerProviderStateMixin,
             WidgetsBindingObserver,
             LockedOverlayMixin {  // 多个 Mixin 混杂

      // UI 逻辑
      void handleScroll() { ... }
      void animateOpacity() { ... }

      // 业务逻辑
      Future<void> loadChats() { ... }
      Future<void> sendMessage() { ... }

      // 动画逻辑
      void startAnimation() { ... }
      late AnimationController animController;

      // 路由逻辑
      void navigateToChat() { ... }

      // ... 几千行代码全在一个文件！
    }

问题分析：

- 77 个 import：依赖关系复杂，难以维护
- 职责不清：UI、业务、动画、路由全混在一起
- 文件巨大：单个 Controller 文件几千行
- 难以复用：逻辑耦合，无法独立使用

##### 4. 没有编译时安全

GetX 运行时陷阱：

    // 通过字符串查找 Controller，运行时才知道对错
    Get.find<ChatListController>();
    Get.put(ChatListController());  // 全局单例，容易冲突

    // 依赖注入不明确，不知道依赖了什么
    class ChatListController extends GetxController {
      void loadData() {
        // 直接访问全局 Manager，依赖不透明
        objectMgr.chatMgr.getChats();
        objectMgr.groupMgr.getGroups();
        // 不知道这个 Controller 到底依赖了什么
      }
    }

问题分析：

- 运行时错误：编译通过，运行时才崩溃
- 全局污染：`Get.find` 全局查找，容易命名冲突
- 依赖不明：不知道 Controller 依赖了哪些服务
- 难以重构：改一个地方，不知道影响范围

##### 5. 难以测试

GetX 测试困境：

    // 测试时必须初始化整个 GetX 框架
    testWidgets('test chat list', (tester) async {
      // 必须先初始化所有依赖
      Get.put(ObjectMgr());
      Get.put(ChatMgr());
      Get.put(GroupMgr());
      // ... 几十个依赖

      final controller = Get.put(ChatListController());

      // 无法 mock，因为 Controller 直接依赖全局对象
      // objectMgr.chatMgr 是全局单例，无法替换
    });

问题分析：

- 测试困难：需要初始化整个 GetX 生态
- 无法 Mock：依赖全局对象，无法注入 mock
- 测试耦合：测试依赖 GetX 框架
- 覆盖率低：太难测，团队放弃测试

##### GetX + Obx vs Riverpod 实际对比

技术维度 | GetX + Obx | Riverpod | 技术优势
--- | --- | --- | ---
刷新颗粒度 | Obx 包裹的整个 Widget 无法精确控制范围 | 精确依赖追踪 支持 select 细粒度订阅 | 大幅减少不必要 rebuild
性能基准 | 状态更新较慢 内存占用较高 | 状态更新快速 内存占用较低 | 性能明显提升 内存显著减少
数据流 | 双向绑定 状态可在任意位置修改 | 单向数据流 状态只能通过 ViewModel 修改 | 数据流清晰可追踪 便于状态历史回溯
编译期安全 | 运行时查找 Controller 类型错误运行时才发现 | 编译期类型检查 依赖不存在立即报错 | 大幅减少 Bug 提升开发效率
代码生成 | 不支持 | riverpod_generator freezed 全面支持 | 大幅减少样板代码 显著提升开发效率
依赖管理 | 全局 Get.put/find 依赖关系不透明 | Provider 依赖图 编译期验证依赖 | 依赖关系可视化 重构安全有保障
测试支持 | 需要初始化 GetX 框架 无法 Mock 全局依赖 | Provider 可轻松覆盖 纯函数易于测试 | 测试更容易 运行速度更快
DevTools | 基础日志 | 完整依赖图 状态历史回溯 性能分析 | Debug 效率显著提升

##### 真实案例对比

GetX + Obx

状态混乱：

    class ChatListController extends GetxController {
      final chatList = <Chat>[].obs;
      final isLoading = false.obs;
      ValueNotifier<double> offset = ValueNotifier(0);  // 混用！
      RxDouble offsetObx = 0.0.obs;  // 重复！
      RxBool isShowingApplet = false.obs;
      RxBool isMiniAppletDraggingIcon = false.obs;
      // ... 50+ 个零散变量

      void loadChats() {
        objectMgr.chatMgr.getChats();  // 全局依赖，不可测
      }
    }

UI 嵌套地狱：

    Obx(() => GetBuilder(
      builder: (_) => Obx(
        () => Obx(
          () => Obx(() => ...), // 6 层嵌套！
        ),
      ),
    ))

Riverpod（新架构）

状态清晰：

    @freezed
    class ChatListState with _$ChatListState {
      const factory ChatListState({
        @Default([]) List<Chat> chats,
        @Default(false) bool isLoading,
      }) = _ChatListState;
    }

    class ChatListViewModel extends StateNotifier<ChatListState> {
      ChatListViewModel(this._chatRepository)
          : super(const ChatListState());

      final ChatRepository _chatRepository;  // 依赖明确

      Future<void> loadChats() async {
        state = state.copyWith(isLoading: true);
        final chats = await _chatRepository.getChats();
        state = state.copyWith(chats: chats, isLoading: false);
      }
    }

UI 清晰：

    class ChatListPage extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final state = ref.watch(chatListViewModelProvider);
        return state.isLoading
          ? LoadingWidget()
          : ChatListView(chats: state.chats);
      }
    }

##### GetX + Obx 痛点总结

从实践经验中，我们总结出 GetX + Obx 的核心问题：

1. "快速开发"变成"技术债"：初期确实快，但 6 个月后代码无法维护
2. "响应式"变成"性能杀手"：Obx 嵌套导致过度重建，页面卡顿
3. "灵活"变成"混乱"：没有约束，代码风格千差万别
4. "全局管理"变成"依赖噩梦"：牵一发动全身，不敢重构
5. "简单上手"变成"难以精通"：团队成员写出的代码质量参差不齐

Riverpod 的技术优势

基于以上实践教训，我们选择 Riverpod 作为新架构的状态管理方案，因为它从根本上解决了 GetX 的所有问题：

- 编译时安全：不会再有运行时崩溃
- 结构化状态：@freezed 强制统一状态结构
- 精确重建：依赖追踪精确，性能优异
- 职责清晰：ViewModel、Repository、Service 分离明确
- 易于测试：Provider 可轻松覆盖和 Mock
- 团队规范：代码风格统一，质量可控

#### Riverpod vs Provider 对比

特性 | Provider | Riverpod
--- | --- | ---
编译时安全 | 不支持 | 支持
需要 BuildContext | 需要 | 不需要
代码生成 | 不支持 | 支持
测试友好 | 一般 | 优秀
性能 | 好 | 更好
学习曲线 | 平缓 | 稍陡

#### MVVM + Riverpod 优势

- 清晰的数据流动：数据流向明确，易于追踪
- 响应式更新：UI 与数据自动同步，无需手动刷新
- 状态管理：统一管理应用状态，避免状态混乱
- 可测试性：ViewModel 可独立测试，无需 Widget 环境
- 解耦：View 与 Model 完全分离，业务逻辑独立
- 类型安全：编译时检查，避免运行时错误

### Feature 驱动开发

#### 以页面为单位

App 是以页面为导向，设计架构时，必须明确针对平台页面进行开发。每个功能页面独立成一个 Feature。

#### 完整生命周期

每个 Feature 包含完整的生命周期：

flowchart TD UI[UI Layer
用户界面] --> Presentation[Presentation Layer
视图模型] Presentation --> Domain[Domain Layer
业务逻辑] Domain --> Data[Data Layer
数据访问] Data --> Core[Core Layer
应用级基础设施] Data --> SDKs[SDK Packages
packages/*_sdk] style UI fill:#e1f5ff,stroke:#0288d1 style Presentation fill:#fff4e6,stroke:#f57c00 style Domain fill:#f3e5f5,stroke:#7b1fa2 style Data fill:#e8f5e9,stroke:#388e3c style Core fill:#fce4ec,stroke:#c2185b style SDKs fill:#e8f5e9,stroke:#2e7d32

#### 高内聚低耦合

- Feature 之间通过接口通信
- 每个 Feature 可独立开发、测试、部署
- Feature 内部高度内聚，外部低耦合
- 便于团队并行开发

### 模块设计哲学

#### 核心设计原则

> "实现层高度封装，使用侧傻瓜式"

这是本架构所有模块设计遵循的核心哲学：

- 实现层高度封装：将复杂的技术细节、错误处理、类型转换等全部封装在底层
- 使用侧傻瓜式：上层使用者只需关注业务逻辑，无需了解底层实现细节
- 按需使用：提供合理的默认值和可选参数，使用者可以按需定制

#### 网络层设计示例

以网络层为例，展示如何实现"实现层高度封装，使用侧傻瓜式"的设计原则。

##### 1. APIRequestable 协议 - 统一请求接口

设计思想：定义统一的 API 请求协议，所有请求都实现相同的接口

    /// API 请求协议 - 所有请求的基础接口
    /// parameters 自动序列化
    abstract class APIRequestable<T> {
      /// API 路径
      String get path;

      /// HTTP 方法
      HTTPMethod get method;

      /// 序列化为 JSON（由 @JsonSerializable 自动生成）
      Map<String, dynamic> toJson();

      /// 自定义请求头
      Map<String, String>? get customHeaders => null;

      /// 请求类型（决定 header 处理方式）
      APIRequestType get requestType => APIRequestType.request;

      /// 解码响应（默认实现由扩展提供）
      T? decodeResponse(Response response);
    }

    /// 默认实现 - parameters 自动调用 toJson()
    extension APIRequestableDefaults<T> on APIRequestable<T> {
      /// 请求参数（自动序列化，用户无需手动定义）
      Map<String, dynamic>? get parameters {
        // 对于 upload 类型，不序列化参数
        if (requestType == APIRequestType.upload) {
          return null;
        }
        // 自动调用 toJson() 序列化请求对象
        return toJson();
      }
    }

核心优势：

- 自动序列化：parameters 自动调用 toJson()，用户无需手动定义
- 统一接口：所有 API 请求都实现同一个协议
- 类型安全：泛型 T 指定响应数据类型
- 注解驱动：通过注解自动生成代码

##### 2. 统一执行入口 - executeRequest

设计思想：提供唯一的请求执行入口，自动处理所有技术细节

    /// 执行 API 请求 - 唯一的请求入口
    Future<T?> executeRequest<T>(Ref ref, APIRequestable<T> request) async {
      final client = ref.read(networkSdkApiProvider);
      final config = ref.read(apiConfigProvider);

      // 1. 检查网络连接
      if (!networkManager.isNetworkAvailable) {
        throw const APIError.noNetworkConnection();
      }

      try {
        // 2. 根据请求类型构建 header
        final headers = configNotifier.defaultHeaders(
          customHeaders: request.customHeaders,
          includeToken: request.requestType != APIRequestType.login,
        );

        // 3. 执行请求
        final response = await dio.request(
          '${config.baseURL}${request.path}',
          data: request.parameters,
          options: Options(method: request.method.value, headers: headers),
        );

        // 4. 自动解码响应
        return request.decodeResponse(response);
      } on DioException catch (e) {
        // 5. 统一错误处理
        throw _handleDioError(e);
      }
    }

封装的技术细节：

1. 网络可用性检查
2. 请求头自动构建（Token、Content-Type 等）
3. URL 拼接
4. 响应自动解码
5. 错误统一处理和转换

##### 3. 自动响应解码 - 注册机制

设计思想：通过注册机制，自动查找 fromJson 函数，实现响应的自动解码

    /// 响应类型注册表
    final _fromJsonRegistry = <Type, Function>{};

    /// 注册响应类型 - 一次注册，全局可用
    T Function(Map<String, dynamic>)? registerResponse<T>(
      T Function(Map<String, dynamic>) fromJson,
    ) {
      _fromJsonRegistry[T] = fromJson;
      return fromJson;
    }

    /// 自动解码扩展 - 使用侧无需关心
    extension APIRequestableExtension<T> on APIRequestable<T> {
      T? decodeResponse(Response response) {
        final data = response.data as Map<String, dynamic>;

        // 从注册表查找 fromJson 函数
        final fromJsonFunc = _fromJsonRegistry[T] as T Function(Map<String, dynamic>)?;

        if (fromJsonFunc == null) {
          throw StateError('fromJson not registered for type $T');
        }

        // 自动解码 APIResponseWrapper
        final wrapper = APIResponseWrapper<T>.fromJson(
          data,
          (json) => fromJsonFunc(json as Map<String, dynamic>),
        );

        // 检查业务错误码
        if (wrapper.code != 0) {
          throw APIError.apiError(code: wrapper.code, message: wrapper.message);
        }

        return wrapper.data;
      }
    }

使用示例：

一个端点 = 一个文件（`data/remote/login_request.dart`），Response DTO + Request 放在同一文件中。

    import 'package:networks_sdk/networks_sdk.dart';

    part 'login_request.g.dart';

    // ── Response DTO（纯 Dart 类，零注解，零样板）──
    // _$LoginResponseFromJson 由 ApiResponseGenerator 从 @ApiRequest(responseType: T) 自动推导生成

    class LoginResponse {
      final String token;
      @JsonKey(name: 'user_id')    // @JsonKey 由生成器读取，Response 类不需要 @JsonSerializable
      final String userId;
      final String email;

      const LoginResponse({required this.token, required this.userId, required this.email});
      // 无 factory fromJson — 生成器在 .g.dart 中提供私有 _$LoginResponseFromJson

      User toEntity() => User(id: userId, email: email); // DTO → Domain Entity
    }

    // ── Request（零样板：只需 @ApiRequest，无需 @JsonSerializable / fromJson / toJson）──
    // @ApiRequest 自动生成 path / method / requestType / includeToken / toJson / fromJson 注册

    @ApiRequest(
      path: ApiPaths.authLogin,                // 路径统一在 core/foundation/api_paths.dart 管理
      method: HttpMethod.post,
      responseType: LoginResponse,
      requestType: ApiRequestType.login,
    )
    class LoginRequest extends ApiRequestable<LoginResponse> with _$LoginRequestApi {
      final String email;
      final String password;

      LoginRequest({required this.email, required this.password});
      // 完毕！toJson 由 mixin 从类字段自动生成，fromJson 注册也全部自动处理
    }

    // 使用 - 超级简单！
    final loginResponse = await apiClient.executeRequest(
      LoginRequest(email: 'user@example.com', password: '123456'),
    );
    final user = loginResponse?.toEntity(); // DTO → Domain Entity

##### 设计思想对比

方案 | 需要定义的内容 | 自动生成 | 维护成本
--- | --- | --- | ---
手动方式 | 字段 + 构造函数 + extends + path + method + toJson + parameters + registerResponse | 无 | 高
JsonSerializable | 字段 + 构造函数 + extends + path + method | toJson / fromJson | 中
@ApiRequest（当前方案） | 字段 + 构造函数 + @ApiRequest （Response DTO：字段 + 构造函数，零注解） | Request: path / method / requestType / includeToken / toJson / fromJson 注册 Response: _$XFromJson 私有反序列化函数（按需递归生成嵌套类型） | 极低

核心优势：

- Request 零样板：`@ApiRequest` 一个注解生成 mixin（含 toJson），无需 `@JsonSerializable`
- Response 零注解：Response DTO 不需要 `@JsonSerializable`，不需要 `factory fromJson`，`_$XFromJson` 由生成器从 `@ApiRequest(responseType: T)` 自动推导；嵌套自定义类型递归生成，依赖关系自动处理
- 自动注册：fromJson 在首次请求时自动注册到全局注册表，无需手动 `registerApiResponses()`
- 一个端点 = 一个文件：Response DTO + Request 放在同一文件，打开即看全貌
- 类型安全：`ApiRequestable<T>` 泛型 + `responseType` 编译期检查

跨平台对比：

平台 | 代码示例 | 简洁度
--- | --- | ---
Swift | `struct LoginRequest: APIRequestable { typealias Response = LoginResponse ... }` | 协议直接实现，最简洁
Dart | `@ApiRequest(...) class LoginRequest extends ApiRequestable<LoginResponse> with _$LoginRequestApi { ... }` | 注解 + 代码生成，接近 Swift 体验

Dart 通过注解 + 代码生成弥补语言层面没有协议默认实现的不足，达到接近 Swift 的简洁度。

##### 4. 注解定义与代码生成器

设计思想：通过注解 + 代码生成器，自动生成所有技术代码

###### 4.1 注解定义

文件：`packages/networks_sdk/lib/src/annotations/api_request.dart`

    /// API 请求注解 - 标记一个类为 API 请求
    ///
    /// 代码生成器会自动生成 mixin `_$Api`，提供：
    /// - path / method / requestType / includeToken 协议实现
    /// - 自动注册 responseType 的 fromJson（在 parameters getter 中触发）
    class ApiRequest {
      /// API 路径（如 '/auth/login'）
      final String path;

      /// HTTP 方法（默认 POST）
      final HttpMethod method;

      /// 响应类型（用于泛型绑定 + 自动注册 fromJson）
      final Type responseType;

      /// 请求类型（决定 header 处理方式）
      final ApiRequestType requestType;

      /// 是否携带 Token（默认根据 requestType 推断：login → false，其余 → true）
      final bool? includeToken;

      /// 自定义请求头
      final Map<String, String>? customHeaders;

      const ApiRequest({
        required this.path,
        this.method = HttpMethod.post,
        required this.responseType,
        this.requestType = ApiRequestType.request,
        this.includeToken,
        this.customHeaders,
      });
    }

###### 4.2 代码生成器核心逻辑

文件：`packages/networks_sdk/lib/src/generator/api_request_generator.dart`

生成 mixin（非 extension），因为 mixin 可以 override 基类方法、调用 `super`，并在 `parameters` getter 中自动注册 fromJson。

toJson 生成机制：生成器读取类的声明字段（非继承），直接在 mixin 中生成 Map 字面量。不依赖 `@JsonSerializable`，避免了继承属性被序列化导致的递归问题。支持 `@JsonKey(name: '...')` 字段重命名和 `@JsonKey(includeToJson: false)` 跳过字段。

    /// API 请求代码生成器
    class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
      @override
      String generateForAnnotatedElement(element, annotation, buildStep) {
        final className = element.name;
        // ... 读取 path / method / responseType / requestType / includeToken ...

        // 从类的声明字段生成 toJson()，只序列化自身字段，不含继承属性
        final toJsonBody = _buildToJsonBody(element, className);

        return '''
    mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
      @override String get path => '$path';
      @override HttpMethod get method => HttpMethod.$methodName;
      @override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
      @override bool get includeToken => $includeToken;
      @override
      Map<String, dynamic> toJson() => $toJsonBody;
      @override
      Map<String, dynamic>? get parameters {
        registerResponse<$responseTypeName>($responseTypeName.fromJson);
        return super.parameters;
      }
    }
    ''';
      }

      /// 读取类的声明字段，生成 Map 字面量
      /// 支持 @JsonKey(name: '...') 重命名
      String _buildToJsonBody(ClassElement element, String className) {
        final fields = element.fields.where((f) => !f.isStatic && !f.isSynthetic);
        // => {'email': (this as LoginRequest).email, 'password': ...}
      }
    }

关键设计：

- `toJson()` 只序列化类自身声明的字段，不含 `ApiRequestable` 的继承属性（path / method / parameters 等），避免递归
- `parameters` getter 在首次请求时自动调用 `registerResponse`，将 Response 的 `fromJson` 注册到全局注册表
- Upload 等特殊请求在类中 override `toJson()`，类的 override 优先于 mixin

###### 4.3 build.yaml 配置

文件：`packages/networks_sdk/build.yaml`

使用 `SharedPartBuilder`，与 `@JsonSerializable`（Response DTO 用）共享同一个 `.g.dart` 文件，无需额外 part 指令。

    builders:
      api_request:
        import: "package:networks_sdk/src/generator/builder.dart"
        builder_factories: ["apiRequestBuilder"]
        build_extensions: {".dart": [".api_request.g.part"]}
        auto_apply: dependents
        build_to: cache
        applies_builders: ["source_gen|combining_builder"]

###### 4.4 运行命令

⚠️ 强制要求：开发期间必须常驻 watch 模式

    # 在项目根目录打开一个独立终端窗口，执行（整个开发期间只需一次）：
    melos run gen:watch

启动后，每次保存 .dart 文件都会自动重新生成 .g.dart。
手写代码时 IDE 报红是正常的，保存后红线自动消失。

    # 仅在 watch 出问题时使用：全量重新生成
    melos run gen

###### 4.5 更多使用示例

所有示例遵循同一模式：`@ApiRequest` + `extends ApiRequestable<T> with _$XxxApi`。Request 和 Response 均无需 `@JsonSerializable`，Response DTO 连 `factory fromJson` 也不需要。

发送消息请求（POST + @JsonKey 字段重命名）：

    // data/remote/send_message_request.dart

    // ── Response DTO（纯 Dart 类，零注解，_$SendMessageResponseFromJson 由生成器自动提供）──
    class SendMessageResponse {
      @JsonKey(name: 'message_id')
      final String messageId;
      final int timestamp;

      const SendMessageResponse({required this.messageId, required this.timestamp});
    }

    // ── Request（零样板）──
    @ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageResponse)
    class SendMessageRequest extends ApiRequestable<SendMessageResponse>
        with _$SendMessageRequestApi {
      @JsonKey(name: 'chat_id')  // 生成器会读取，JSON 键名为 'chat_id'
      final String chatId;
      final String content;

      SendMessageRequest({required this.chatId, required this.content});
      // toJson 自动生成：{'chat_id': chatId, 'content': content}
    }

获取用户资料（GET，靠 token 标识当前用户，无需传参）：

    // data/remote/get_profile_request.dart

    // ── Response DTO（纯 Dart 类，零注解，_$ProfileResponseFromJson 由生成器自动提供）──
    class ProfileResponse {
      @JsonKey(name: 'user_id')
      final String userId;
      final String email;
      final String? nickname;
      final String? avatar;

      const ProfileResponse({required this.userId, required this.email, this.nickname, this.avatar});

      User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
    }

    @ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileResponse)
    class GetProfileRequest extends ApiRequestable<ProfileResponse>
        with _$GetProfileRequestApi {
      GetProfileRequest();  // 无参数 — toJson 自动生成空 map
    }

上传文件请求（FormData multipart）：

    // data/remote/upload_file_request.dart

    // ── Response DTO（纯 Dart 类，零注解，_$UploadResultFromJson 由生成器自动提供）──
    class UploadResult {
      final String url;
      @JsonKey(name: 'file_id')
      final String fileId;
      const UploadResult({required this.url, required this.fileId});
    }

    @ApiRequest(
      path: ApiPaths.uploadFile,
      method: HttpMethod.post,
      responseType: UploadResult,
      requestType: ApiRequestType.upload,
    )
    class UploadFileRequest extends ApiRequestable<UploadResult>
        with _$UploadFileRequestApi {
      final String filePath;
      final String? fileName;

      UploadFileRequest({required this.filePath, this.fileName});

      @override
      Map<String, dynamic> toJson() => {};  // upload 不走 toJson

      @override
      Object? get uploadData => FormData.fromMap({
        'file': MultipartFile.fromFileSync(filePath, filename: fileName),
      });
    }

核心价值

- Request 极简：字段 + 构造函数 + `@ApiRequest`（无需 `@JsonSerializable`、无需手写 `toJson`）
- Response 零注解：Response DTO 是纯 Dart 类，无需 `@JsonSerializable`，无需 `factory fromJson`，`_$XFromJson` 由生成器从 `responseType` 自动推导
- 零维护：path / method / requestType / includeToken / toJson / fromJson 注册 全部自动生成
- 类型安全：泛型 `ApiRequestable<T>` + `responseType` 编译期检查
- 一个端点 = 一个文件：Response DTO + Request 放在同一文件，打开即看全貌

##### 5. Riverpod 集成 - 依赖注入

设计思想：Network SDK 本身零 Flutter / 零 Riverpod 依赖。App 层通过 Provider 包装，实现全局单例 + 依赖注入。

DI 装配：

    // ── app/di/network_provider.dart ── （SDK 基础设施，全局唯一）

    /// 1. API 配置（baseURL 来自 config.json → --dart-define-from-file）
    final apiConfigProvider = Provider<ApiConfig>((ref) {
      return ApiConfig(
        baseURL: AppConfig.apiBaseUrl,
        platformHeaders: {
          'platform': DeviceInfo.platform,        // 运行时同步读取（DeviceInfo.init() 预取）
          'os-type': DeviceInfo.osType.toString(), // Android=1 iOS=2 Windows=3 macOS=4 Linux=5
          'client-version': AppConfig.appVersion,
          'channel': AppConfig.channel,
          'lang': DeviceInfo.lang,
          'device-id': DeviceInfo.deviceId,
          'device-name': DeviceInfo.deviceName,
        },
        tokenExpiredCodes: {30002, 30003, 30124},
        forceLogoutCodes: {30125},
        onForceLogout: () { /* 清除登录态，跳转登录页 */ },
        onTokenRefresh: () async { /* 刷新 token */ return null; },
        onLog: (message, {tag}) { print('[$tag] $message'); },
      );
    });

    /// 2. Networks SDK API Provider（全局单例，Facade 接口）
    /// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
    final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
      final config = ref.read(apiConfigProvider);
      return NetworksSdkWiring.build(config: config);
    });

    // ── features/auth/di/auth_providers.dart ── （Auth 模块完整 DI 链路）

    /// 3. Repository（注入 Facade 接口类型，ViewModel 不感知具体实现）
    final authRepositoryProvider = Provider<AuthRepository>((ref) {
      final apiConfig = ref.read(apiConfigProvider);
      return AuthRepositoryImpl(
        client: ref.read(networkSdkApiProvider),       // 注入 Facade 接口
        onTokenUpdate: (token) {
          apiConfig.updateToken(token);               // 内存（networks_sdk）
          // secureStorage.saveToken(token);          // 持久化（storage_sdk，待接入）
        },
      );
    });

    /// 4. UseCase（登录有多步编排 — 校验 + 登录 + WS 连接 + DB 开库 + 用户持久化）
    final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
      return LoginUseCase(
        authRepository: ref.read(authRepositoryProvider),
        socketManager:  ref.read(socketManagerProvider),
        apiConfig:      ref.read(apiConfigProvider),
        storageApi:     ref.read(storageSdkProvider),
        userRepository: ref.read(userRepositoryProvider),
      );
    });

ViewModel 中使用：

    @riverpod
    class LoginViewModel extends _$LoginViewModel {
      @override
      LoginState build() => const LoginState();

      Future<void> login(String email, String password) async {
        state = state.copyWith(isLoading: true);

        try {
          // UseCase 封装格式校验 + 业务编排，ViewModel 只需一行
          final user = await ref.read(loginUseCaseProvider).execute(
            email: email, password: password);

          state = state.copyWith(user: user, isLoading: false);
        } on FormatException catch (e) {
          // 格式校验失败（UseCase 层抛出）
          state = state.copyWith(error: e.message, isLoading: false);
        } on ApiError catch (e) {
          // 统一错误处理 - Freezed union type
          state = state.copyWith(error: e.displayMessage, isLoading: false);
        }
      }
    }

完整装配链路：

    View: ref.watch(loginViewModelProvider)
      → ViewModel: ref.read(loginUseCaseProvider).execute(...)
        → LoginUseCase: 格式校验（邮箱 + 密码）
        → LoginUseCase: authRepository.login(...)
          → Repository: _client.executeRequest(LoginRequest(...))
            → ApiClient.executeRequest()             ← networks_sdk 内部
              → AuthInterceptor                      ← 注入 token + headers
              → EncryptionInterceptor                ← 加密请求体（预留）
              → Dio.request(baseURL + path, data)    ← 实际 HTTP 请求
              → RetryInterceptor                     ← token 过期自动刷新重试 + 业务错误钩子
              → LoggingInterceptor                   ← 请求/响应日志
            ← request.decodeResponse(response)       ← 自动解码
              ← ApiResponseWrapper.fromJson          ← 拆 { code, msg, data }
              ← fromJsonRegistry[LoginResponse]       ← 查注册表
              ← _$LoginResponseFromJson(data)        ← 反序列化（生成的私有函数）
          ← LoginResponse（DTO）
          → onTokenUpdate(token)                     ← 回调写入 Token（内存 + 持久化）
          ← loginResponse.toEntity() → User          ← DTO → Domain Entity
        ← User
      ← state.copyWith(user: user)                   ← 更新状态
    View: ref.watch → 自动 rebuild                    ← UI 刷新

#### 设计哲学在本架构中的体现

层级 | 实现层（高度封装） | 使用侧（傻瓜式）
--- | --- | ---
网络层 | executeRequest 封装所有细节 自动 header、解码、错误处理 | 定义 Request 类 只需 3 个字段：path、method、parameters
数据层 | Repository 封装数据源切换 自动缓存、错误转换 | 调用 Repository 方法 返回 Domain 模型，无需关心数据来源
业务层 | UseCase 封装业务逻辑 单一职责，可组合 | ViewModel 调用 UseCase 专注状态管理，不关心业务细节
UI层 | ViewModel 提供响应式状态 自动重建、错误处理 | Widget 监听 Provider 只需 ref.watch，无需手动管理状态

#### 核心优势

- 降低认知负担：使用者无需了解底层实现，专注业务逻辑
- 减少重复代码：通用逻辑封装在底层，上层不重复
- 提升可维护性：修改底层实现不影响上层代码
- 易于测试：每层职责清晰，可独立 Mock 和测试
- 团队协作：新人快速上手，代码风格统一

设计哲学总结

本架构中的每一个模块都遵循"实现层高度封装，使用侧傻瓜式"的原则：

- 底层模块负责技术复杂性，提供简洁的 API
- 上层模块负责业务逻辑，使用简单的接口
- 通过这种分层，实现关注点分离，让每个开发者专注于自己擅长的领域

## 第二部分：结构是什么（Structure）- 整体架构

## 设计原则

### 1.1 SOLID 原则

原则 | 说明 | 体现
--- | --- | ---
单一职责 (SRP) | 一个模块只负责一项职责 | 每个 UseCase 只处理一个业务场景
开闭原则 (OCP) | 对扩展开放，对修改关闭 | 通过接口、策略模式实现扩展点
里氏替换 (LSP) | 子类可替换父类 | 所有 Repository 实现可互换
接口隔离 (ISP) | 客户端不依赖不需要的接口 | Repository 按功能拆分，不做大而全接口
依赖倒置 (DIP) | 依赖抽象而非具体实现 | Domain 层定义接口，Data 层实现

### 1.2 分层依赖规则

flowchart TD UI[UI Layer
界面层] Presentation[Presentation Layer
表现层] Domain[Domain Layer
Domain 层] Data[Data Layer
数据层] Core[Core Layer
应用级基础设施] SDKs[SDK Packages
packages/*_sdk] UI -->|依赖| Presentation Presentation -->|依赖| Domain Domain -.定义接口.-> Data Data -->|依赖| Core Data -->|依赖| SDKs style UI fill:#e1f5ff,stroke:#0288d1,stroke-width:2px style Presentation fill:#fff4e6,stroke:#f57c00,stroke-width:2px style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Data fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style Core fill:#fce4ec,stroke:#c2185b,stroke-width:2px style SDKs fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

依赖方向：单向向下，严格禁止反向依赖和跨层调用

严格规则：

- UI 层只能调用 Presentation 层
- Presentation 层只能调用 Domain 层
- Domain 层定义接口，不依赖具体实现
- Data 层实现 Domain 接口，调用 Core 层和 SDK Packages
- Core 层提供应用级基础设施，SDK Packages 提供可复用技术能力，均不依赖任何上层

### 1.3 模块化原则

- 高内聚：相关功能聚合在同一模块
- 低耦合：模块间通过接口通信
- 可替换：底层实现可替换，上层不受影响
- 可测试：每层可独立测试

## 整体架构（3图）

### 2.1 整体模块图

#### 图表说明

下图展示基于 Feature 驱动的整体模块划分，每个 Feature 包含完整的 UI → Presentation → Domain 层级。

flowchart TD subgraph App[App Layer] AppMain[app.dart] Router[router.dart] DI[dependency injection] end subgraph Features[Features Layer - 按页面组织] subgraph Chat[Chat Feature] ChatUI[UI: chat_page.dart] ChatVM[Presentation: chat_view_model.dart] ChatDomain[Domain: usecases + entities] end subgraph ChatList[Chat List Feature] ChatListUI[UI: chat_list_page.dart] ChatListVM[Presentation: chat_list_view_model.dart] ChatListDomain[Domain: usecases] end subgraph Contact[Contact Feature] ContactUI[UI: contact_page.dart] ContactVM[Presentation: contact_view_model.dart] ContactDomain[Domain: usecases] end subgraph Search[Search Feature] SearchUI[UI: search_page.dart] SearchVM[Presentation: search_view_model.dart] SearchDomain[Domain: usecases] end subgraph Call[Call Feature] CallUI[UI: call_page.dart] CallVM[Presentation: call_view_model.dart] CallDomain[Domain: usecases] end end subgraph Domain[Domain Layer - 共享 Domain] Repositories[Repositories 接口] ValueObjects[Value Objects] end subgraph Data[Data Layer] RepoImpl[Repository 实现] Models[DTO Models] end subgraph Packages[SDK Packages - Melos 管理] Network[NetworkSDK] Storage[StorageSDK] CipherGuard[CipherGuardSDK
Flutter Plugin] L10nPkg[L10nSDK] Crypto[CryptoSDK
占位] OtherSDK[Media/RTC/Push/Protocol] end subgraph Core[Core Layer - 主 App 内部] subgraph Foundation[core/foundation/ - 应用级基础设施] Utils[Constants / Config / Errors
Logger / Types / Utils / Extensions] end subgraph CoreUI[core/ui/ - UI 基础设施] Base[基础定义] Components[基础组件] Composites[业务组合组件] end end App --> Features Features --> Domain Domain -.定义接口.-> Data Data --> Network Data --> Storage Data --> Packages Features -->|UI 复用| CoreUI Features -->|本地化文案| L10nPkg CoreUI -->|组件内置文案| L10nPkg CoreUI -->|引用| Foundation style App fill:#667eea,stroke:#5568d3,color:#fff style Features fill:#e1f5ff,stroke:#0288d1 style Domain fill:#f3e5f5,stroke:#7b1fa2 style Data fill:#e8f5e9,stroke:#388e3c style Core fill:#f5f5f5,stroke:#9e9e9e style Packages fill:#e8f5e9,stroke:#388e3c style Foundation fill:#fce4ec,stroke:#c2185b style CoreUI fill:#fff4e6,stroke:#f57c00

### 2.2 整体目录图最后更新 2026-03-10-10-17

#### 图表说明

完整的项目目录结构，展示了 Feature 驱动的组织方式和清晰的层级关系。

    lib/
    ├── main.dart                                # 应用入口：调用 bootstrap()，不含任何业务逻辑
    │
    ├── app/                                     # 应用壳（组合根）：负责拼装所有模块，禁止在此写业务逻辑
    │   ├── app.dart                             # MaterialApp 根组件 + WidgetsBindingObserver（前后台事件）
    │   ├── bootstrap.dart                       # 启动入口：ProviderScope 包裹 + 依赖初始化
    │   │
    │   ├── router/                              # 路由管理（go_router）
    │   │   ├── app_router.dart                  # routerProvider：StatefulShellRoute + 全局 redirect
    │   │   ├── app_route_name.dart              # AppRouteName 枚举，路径常量 + fromPath()
    │   │   └── guards/
    │   │       └── auth_guard.dart             # 登录守卫（switch AppRouteName，穷举防漏路由）
    │   │
    │   ├── di/                                  # 全局 DI — 手动装配的 Provider
    │   │   ├── network_provider.dart            # NetworkMonitor + ApiConfig + NetworksSdkApi + SocketConfig + SocketClient + SocketManager
    │   │   ├── db_provider.dart                 # storageSdkProvider + storageSdkLifecycleProvider
    │   │   ├── app_providers.dart               # AppInitializer + ThemeModeNotifier + AuthNotifier
    │   │   └── user_provider.dart               # userRepositoryProvider / CRUD UseCase Providers / users + allUsers Stream
    │   └── notifiers/                           # 全局 Notifier（@riverpod，跨模块共用）
    │       └── user_notifier.dart               # UserNotifier — 单用户状态（family per-uid，含 watch + CRUD）
    │
    ├── features/                                # 功能模块（垂直切片）：Feature 间禁止直接 import
    │   │
    │   ├── app_tab/                             # Tab 容器（底部导航栏）
    │   │   └── view/
    │   │       └── app_tab.dart                 # StatefulShellRoute 子壳 + 底部导航逻辑
    │   │
    │   ├── login/                               # 登录 ── 已实现
    │   │   ├── di/
    │   │   │   └── auth_providers.dart          # authRepositoryProvider / loginUseCaseProvider
    │   │   ├── presentation/
    │   │   │   ├── login_view_model.dart        # @riverpod ViewModel（生成 login_view_model.g.dart）
    │   │   │   └── login_state.dart             # @freezed State（step / contact / isLoading / error）
    │   │   ├── usecases/
    │   │   │   └── login_usecase.dart           # 校验 → OTP → 登录 → WS 连接 → DB 开库 → 用户持久化
    │   │   └── view/
    │   │       ├── login_page.dart              # 登录页（装配 + 控制器 + 回调）
    │   │       └── widgets/
    │   │           ├── login_phone_step.dart    # 手机号输入步骤（纯展示）
    │   │           └── login_otp_step.dart      # 验证码输入步骤（纯展示）
    │   │
    │   ├── chat/                                # 聊天 ── 开发中
    │   │   ├── di/
    │   │   │   └── chat_bot_provider.dart       # chatBotRepositoryProvider / allChatBots / chatBot(id)
    │   │   ├── call/                            # 通话子模块
    │   │   │   └── di/
    │   │   │       └── call_log_provider.dart   # callLogRepositoryProvider / allCallLogs / callLog(id)
    │   │   ├── presentation/
    │   │   │   ├── chat_view_model.dart         # @riverpod ViewModel（会话列表）
    │   │   │   ├── chat_db_test_state.dart      # Drift DB 测试 State（手写，含分页字段）
    │   │   │   └── chat_db_test_view_model.dart # Drift DB 测试 ViewModel（startTest / loadMore）
    │   │   ├── usecases/
    │   │   │   ├── insert_users_use_case.dart   # 批量插入用户（200 条/批，去重）
    │   │   │   ├── update_users_use_case.dart   # 更新前 10 条用户昵称
    │   │   │   └── delete_users_use_case.dart   # 删除前 10 条用户
    │   │   └── view/
    │   │       ├── chat_page.dart               # 会话列表页（Tab 1）
    │   │       ├── chat_detail_page.dart        # 聊天详情页
    │   │       └── chat_db_test_page.dart       # Drift DB 测试页
    │   │
    │   ├── contact/                             # 通讯录 ── 骨架
    │   │   └── view/
    │   │       └── contact_page.dart            # 通讯录页（Tab 2）
    │   │
    │   └── settings/                            # 设置 ── 已实现（主题切换）
    │       ├── di/
    │       │   └── settings_providers.dart      # setThemeUseCaseProvider
    │       ├── presentation/
    │       │   ├── settings_view_model.dart     # @riverpod ViewModel（设置页导航）
    │       │   └── theme_view_model.dart        # @riverpod ViewModel（主题切换）
    │       ├── usecases/
    │       │   └── set_theme_usecase.dart       # 主题切换用例（幂等校验，防重复切换）
    │       └── view/
    │           ├── settings_page.dart           # 设置主页（Tab 3）
    │           ├── theme_view.dart              # 主题选择页
    │           └── widgets/
    │               ├── settings_section_header.dart
    │               └── theme_option_tile.dart
    │
    ├── domain/                                  # Domain 层（纯 Dart，零 Flutter / 零网络依赖）
    │   ├── entities/                            # 21 个实体（每实体一文件）
    │   │   ├── user.dart                        # 用户
    │   │   ├── message.dart                     # 消息
    │   │   ├── chat.dart                        # 会话
    │   │   ├── call_log.dart                    # 通话记录
    │   │   ├── chat_bot.dart                    # 聊天机器人
    │   │   ├── chat_category.dart               # 会话分类
    │   │   ├── group.dart                       # 群组
    │   │   ├── group_member.dart                # 群成员
    │   │   ├── workspace.dart                   # 工作区
    │   │   ├── company_member.dart              # 企业成员
    │   │   ├── favourite.dart / favourite_detail.dart
    │   │   ├── sound.dart / tag.dart / retry.dart
    │   │   ├── pending_friend_request_history.dart
    │   │   ├── user_request_history.dart
    │   │   └── {discover,explore,favorite,recent}_mini_app.dart
    │   └── repositories/                        # 4 个抽象接口
    │       ├── auth_repository.dart             # sendOtp / verifyOtp / login / logout
    │       ├── user_repository.dart             # watch / get / insert / update / upsert / delete
    │       ├── call_log_repository.dart         # 通话记录 CRUD + watch
    │       └── chat_bot_repository.dart         # 聊天机器人 CRUD + watch
    │
    ├── data/                                    # Data 层（implements domain 接口）
    │   ├── repositories/                        # 4 个实现类
    │   │   ├── auth_repository_impl.dart        # 认证（登录 / 登出 / Token 回调）
    │   │   ├── user_repository_impl.dart        # 用户（Drift CRUD + Stream watch）
    │   │   ├── call_log_repository_impl.dart    # 通话记录
    │   │   └── chat_bot_repository_impl.dart    # 聊天机器人
    │   ├── local/
    │   │   └── drift/                           # Drift 本地数据库
    │   │       ├── app_database.dart            # @DriftDatabase（19 张表）+ onUpgrade 自动补列
    │   │       ├── mapper/
    │   │       │   └── drift_path_mapper.dart   # Drift 路径映射工具
    │   │       └── tables/                      # 19 个表定义（每表一文件）
    │   │           ├── users.dart / messages.dart / chats.dart / call_logs.dart
    │   │           ├── chat_bots.dart / chat_categories.dart / groups.dart / workspaces.dart
    │   │           ├── favourites.dart / favourite_details.dart
    │   │           ├── sounds.dart / tags.dart / retries.dart
    │   │           ├── pending_friend_request_histories.dart / user_request_histories.dart
    │   │           └── {discover,explore,favorite,recent}_mini_apps.dart
    │   ├── remote/                              # Request 文件（一个端点一个文件）
    │   │   ├── send_otp_request.dart            # 发送验证码
    │   │   ├── verify_otp_request.dart          # 校验验证码
    │   │   ├── login_request.dart               # 登录
    │   │   ├── logout_request.dart              # 登出
    │   │   ├── get_profile_request.dart         # 获取用户信息
    │   │   └── upload_file_request.dart         # 文件上传
    │   └── models/                              # 持久化 DTO（17 个，与表一一对应）
    │       ├── user_dto.dart / message_dto.dart / chat_dto.dart
    │       ├── chat_bot_dto.dart / chat_category_dto.dart / group_dto.dart / workspace_dto.dart
    │       ├── favourite_dto.dart / favourite_detail_dto.dart
    │       ├── sound_dto.dart / tag_dto.dart / retry_dto.dart
    │       ├── pending_friend_request_history_dto.dart / user_request_history_dto.dart
    │       └── {discover,explore,favorite,recent}_mini_app_dto.dart
    │
    └── core/                                    # Core 层：零业务逻辑，禁止反向依赖 features / domain / data
        ├── foundation/                          # 基础配置（各为单独文件，非子目录）
        │   ├── api_paths.dart                   # API 路径常量（ApiPaths.authLogin 等）
        │   ├── config.dart                      # 运行时配置（AppConfig，通过 --dart-define-from-file 注入）
        │   ├── constants.dart                   # 全局常量（AppConstants：重试次数 / 退避延迟 / 超时等）
        │   ├── errors.dart                      # 统一异常体系
        │   ├── extensions.dart                  # Dart 扩展方法（String / DateTime / List 等）
        │   ├── logger.dart                      # 日志门面（分级日志，生产环境自动关闭 debug）
        │   ├── types.dart                       # 通用类型（Result<T> / typedefs / Unit）
        │   └── utils.dart                       # 工具函数（纯函数，无副作用）
        │
        ├── services/                            # 跨模块服务（有状态，作为独立 Provider）
        │   ├── app_initializer.dart             # 启动初始化编排（按序初始化各依赖）
        │   ├── network_backoff_debouncer.dart   # 网络恢复退避防抖（4s→8s→...→60s，2min 重置）
        │   ├── network_monitor.dart             # 网络状态监听（connectivity_plus）
        │   └── socket_manager.dart              # WebSocket 生命周期（连接/断开/重连编排）
        │
        └── ui/                                  # Core UI（设计系统 + 可复用组件）
            ├── base/                            # 设计基础（颜色 / 字体 / 间距 / 圆角 / 阴影）
            │   ├── assets.dart                  # 静态资源路径常量（AppAssets：logo / 占位图）
            │   ├── icons.dart                   # 图标常量（AppIcons：导航 / 操作 / 聊天 / 用户 / 状态）
            │   ├── app_theme.dart               # ThemeData 组装（Light / Dark）
            │   ├── colors.dart                  # 颜色体系（品牌色 / 语义色 / 灰阶）
            │   ├── context_theme_ext.dart       # BuildContext 扩展（context.colors / context.styles / context.shadows）
            │   └── font.dart                    # 字体（TextStyle 定义 + textTheme(brightness)）
            ├── components/                      # 原子组件
            │   └── app_button.dart              # 按钮
            │   # app_text_field / app_avatar / app_badge 等  待开发
            └── composites/                      # 组合组件（目录预留，待开发）
                # app_dialog / app_toast / app_empty_state 等

### 2.3 整体分层图（MVVM + Riverpod 数据流）最后更新 2026-03-10-10-17

#### 图表说明

展示完整的五层架构，标注 MVVM 角色映射和 Riverpod 驱动的单向数据流。

flowchart TD subgraph Layer5["View 层（MVVM 的 V）"] direction TB Pages["ConsumerWidget 页面
ref.watch(viewModel) 订阅状态"] Widgets["UI Widgets
纯展示组件"] end subgraph Layer4["ViewModel 层（MVVM 的 VM）"] direction TB ViewModels["Notifier<State>
状态管理 + 直接方法调用"] end subgraph Layer3[Domain 层] direction TB UseCases["Use Cases
业务用例"] Entities["Entities
Domain 实体"] RepoInterfaces["Repository 接口
（依赖倒置）"] end subgraph Layer2[Data 层] direction TB RepoImpls[Repository 实现] LocalDS[Local DataSource] DTOs["DTO Models（MVVM 的 M）"] end subgraph Layer1[Core 层 - 主 App 内部] direction TB Foundation[foundation/
Constants/Config/Errors/Logger/Types/Utils] CoreUI[ui/] end subgraph Layer0[SDK Packages - Melos 管理] direction TB SDKPkgs[networks_sdk / storage_sdk / cipher_guard_sdk / l10n_sdk
media_sdk / rtc_sdk / notification_sdk
protocol_sdk] end Pages -->|"① 用户操作 → ref.read(vm.notifier).action()"| ViewModels ViewModels -->|"② 调用业务逻辑"| UseCases UseCases --> RepoInterfaces RepoInterfaces -.实现.-> RepoImpls RepoImpls --> LocalDS RepoImpls --> SDKPkgs LocalDS --> SDKPkgs ViewModels -.->|"③ state = newState"| ViewModels ViewModels -.->|"④ ref.watch 自动触发 rebuild"| Pages style Layer5 fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style Layer4 fill:#fff4e6,stroke:#f57c00,stroke-width:3px style Layer3 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px style Layer2 fill:#e8f5e9,stroke:#388e3c,stroke-width:3px style Layer1 fill:#fce4ec,stroke:#c2185b,stroke-width:3px style Layer0 fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px

### 2.4 MVVM + Riverpod 数据流映射

目录结构到 MVVM 和 Riverpod 数据流的精确映射：

flowchart LR subgraph MVVM["MVVM 角色映射"] direction TB V["View
view/ 目录
ConsumerWidget"] VM["ViewModel
presentation/ 目录
StateNotifier"] M["Model
model/ + domain/entities/
UI Model + Entity"] end subgraph RiverpodFlow["Riverpod 单向数据流"] direction TB Step1["① 用户点击发送按钮"] Step2["② ref.read(chatVM.notifier)
.sendMessage(content)"] Step3["③ ViewModel 调用 UseCase
→ Repository → NetworksSdkApi"] Step4["④ state = state.copyWith(
messages: [..., newMsg])"] Step5["⑤ ref.watch(chatVM) 检测变化
→ ConsumerWidget 自动 rebuild"] Step6["⑥ UI 展示最新消息列表"] end Step1 --> Step2 Step2 --> Step3 Step3 --> Step4 Step4 --> Step5 Step5 --> Step6 subgraph DirMapping["目录 ↔ 角色"] direction TB D1["chat_page.dart → View"] D2["chat_view_model.dart → ViewModel"] D5["domain/entities/message.dart → Model"] end style MVVM fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px style RiverpodFlow fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style DirMapping fill:#fff4e6,stroke:#f57c00,stroke-width:2px

> 三大核心逻辑：

> 1. MVVM 分层职责：View（view/）只负责渲染和用户交互，ViewModel（presentation/）持有状态并处理业务逻辑，Model（model/ + entities/）定义数据结构 —— 三者通过 Riverpod Provider 连接，职责严格分离。

> 2. Riverpod 单向数据流：用户操作 → `ref.read(vm.notifier).action()` → ViewModel 处理逻辑 → `state = newState` → `ref.watch(vm)` 检测变化 → View 自动 rebuild。数据永远单向流动，UI 永远是状态的函数。

> 3. Widget 纯展示原则：`build()` 只做一件事——把 State 属性映射成 Widget 树，不允许出现任何计算或逻辑。

> - 派生显示值必须是 State getter：凡是需要从 State 字段推导出另一个值（文本、颜色、样式等），一律写成 State 的 getter，Widget 只读取结果。例如：`String get buttonLabel => testStarted ? '结束' : '开始';`，Widget 写 `state.buttonLabel`，不在 `build()` 里写三元。
> - 禁止 `build()` 内定义局部计算变量：`final isSelected = current == mode` 这类从构造参数/state 派生值的局部变量，不属于 `build()`。组件应接受已算好的 `bool isSelected` 参数，由父级或 State getter 负责计算。
> - 导航、CRUD、状态变更全部在 ViewModel 中完成，Widget 只转发事件：`onTap: () => ref.read(vm.notifier).doAction()`。
> - demo/测试页面同样适用：demo 代码是示范，别人会照着模仿，写法必须与正式页面完全一致。

## 第三部分：核心概念（Core Concepts）

## Riverpod 核心概念

在深入了解各层实现之前，先理解 Riverpod 的核心概念和使用方式。

概念 | 说明 | 使用场景
--- | --- | ---
StateNotifier | 管理可变状态的类 | ViewModel 实现
StateNotifierProvider | 提供 StateNotifier 的 Provider | ViewModel Provider
Provider | 提供不可变对象的 Provider | UseCase、Repository 依赖注入
ConsumerWidget | 可以监听 Provider 的 Widget | UI 层页面组件
WidgetRef | 访问 Provider 的引用 | 在 Widget 中读取和监听 Provider
@riverpod | 代码生成注解 | 自动生成 Provider 代码
@freezed | 不可变类注解 | 生成 State 类的 copyWith 等方法
autoDispose | 自动释放 Provider | 页面销毁时自动清理资源

## Clean Architecture 分层说明

本架构严格遵循 Clean Architecture 的分层原则，每层都有明确的职责和依赖方向。

### 分层职责

层级 | 职责 | 依赖方向 | 示例
--- | --- | --- | ---
UI Layer | 界面展示、用户交互 | → Presentation | ChatPage、MessageItem Widget
Presentation Layer | 状态管理、UI 逻辑 | → Domain | ChatViewModel、MessageState
Domain Layer | 业务逻辑、业务规则 | → Repository 接口 | SendMessageUseCase、ChatEntity
Data Layer | 数据访问、数据源管理 | → Core | ChatRepository、ChatRepositoryImpl
Core Layer | 应用级基础设施（Constants/Config/Errors 等） | 无依赖 | app_config.dart、error_mapper.dart
SDK Packages | 可复用技术能力（packages/*_sdk） | 无依赖 | NetworkSDK、StorageSDK、L10nSDK
Core UI Layer | 基础定义、基础组件、业务组合组件 | → Core | AppButton、AppDialog、base/colors

### 依赖倒置原则（DIP）

核心思想：高层模块不依赖低层模块，两者都依赖抽象（接口）

flowchart TD VM[ViewModel
表现层] UC[UseCase
业务层] RI[Repository Interface
接口定义] RImpl[Repository Impl
数据层实现] VM --> UC UC --> RI RImpl -.实现.-> RI style VM fill:#fff4e6,stroke:#f57c00 style UC fill:#f3e5f5,stroke:#7b1fa2 style RI fill:#e3f2fd,stroke:#0288d1 style RImpl fill:#e8f5e9,stroke:#388e3c

示例：

    // Domain 层定义接口
    abstract class ChatRepository {
      Future<List<Message>> getMessages(String chatId);
      Future<void> sendMessage(Message message);
    }

    // Data 层实现接口
    class ChatRepositoryImpl implements ChatRepository {
      final NetworksSdkApi _client;
      final MessageLocalDataSource _localDataSource;

      @override
      Future<List<Message>> getMessages(String chatId) async {
        // 实现数据获取逻辑
      }
    }

    // Presentation 层使用接口
    class ChatViewModel {
      final ChatRepository _repository;  // 依赖接口，不依赖实现

      Future<void> loadMessages() async {
        final messages = await _repository.getMessages(chatId);
        // ...
      }
    }

### Repository 模式

作用：将数据访问逻辑封装起来，对上层提供统一的数据访问接口

优势：

- 数据源可替换：可以从网络、本地数据库、缓存等任意数据源获取
- 业务逻辑与数据访问分离：业务层不关心数据来自哪里
- 易于测试：可以轻松 Mock Repository

flowchart LR UC[UseCase
业务逻辑] Repo[Repository
数据仓库] Remote[Remote Data Source
网络数据源] Local[Local Data Source
本地数据源] Cache[Cache
缓存] UC --> Repo Repo --> Remote Repo --> Local Repo --> Cache style UC fill:#f3e5f5,stroke:#7b1fa2 style Repo fill:#e3f2fd,stroke:#0288d1 style Remote fill:#e8f5e9,stroke:#388e3c style Local fill:#fff4e6,stroke:#f57c00 style Cache fill:#fce4ec,stroke:#c2185b

## 第四部分：怎么做（How）- 详细实现

## UI 层模块详解

### 3.1 UI 层职责

UI 层是应用的最外层，负责：

- 展示用户界面
- 接收用户交互
- 调用 ViewModel 方法
- 监听 ViewModel 状态变化
- 响应式更新 UI

### 3.1.1 UI 层详细分层结构

UI 层不是单一层级，而是有明确的分层结构：

flowchart TD UI[UI Layer] --> DesignSystem[Design System
设计系统] UI --> Foundation[Foundation
基础组件] UI --> Business[Business Components
业务组件] UI --> Pages[Pages
页面] DesignSystem --> Colors[Colors 颜色] DesignSystem --> Typography[Typography 字体] DesignSystem --> Tokens[间距 / 圆角 / 阴影] DesignSystem --> Theme[Theme 主题] Foundation --> Atoms[Atoms 原子组件] Foundation --> Molecules[Molecules 分子组件] Foundation --> Organisms[Organisms 有机组件] Business --> FeatureWidgets[Feature Widgets
功能组件] Pages --> FeaturePages[Feature Pages
功能页面] style UI fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style DesignSystem fill:#fff9c4,stroke:#f57f17,stroke-width:2px style Foundation fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style Business fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Pages fill:#fce4ec,stroke:#c2185b,stroke-width:2px

#### UI 层分层说明

层级 | 职责 | 示例 | 特点
--- | --- | --- | ---
L1: Design System 设计系统 | 定义应用的视觉语言、颜色、字体、间距等设计规范 | Colors、Typography、Spacing、BorderRadius | 与 Figma 设计稿一一对应
L2: Foundation 基础组件 | 可复用的 UI 基础组件，不包含业务逻辑 | Button、TextField、Card、Avatar | Atomic Design 原则
L3: Business Components 业务组件 | 包含业务逻辑的复用组件 | MessageBubble、ChatItem、ContactCard | Feature 级别复用
L4: Pages 页面 | 完整的页面，组合各种组件 | ChatPage、ChatListPage、ContactPage | Feature 独有

#### L1: Design System（设计系统）

核心原则：与 Figma 设计稿完全对应，确保设计与实现一致。

##### 1.1 Colors（颜色）— 5 层架构

    /// Layer 1: 颜色常量，不带语义
    abstract class ColorBases {
      static const primary400 = Color(0xFF5BA3F5);
      static const primary500 = Color(0xFF2F80ED);
      static const primary700 = Color(0xFF1A6BD4);
      static const neutral0   = Color(0xFFFFFFFF);
      static const neutral50  = Color(0xFFF8F9FA);
      // ... neutral100~950, error500, success500, warning500
    }

    /// Layer 2: 语义颜色接口
    abstract class AppColors {
      Color get bgPrimary;      // Scaffold 底色
      Color get bgSecondary;    // Card / Surface
      Color get textPrimary;    // 主文本
      Color get textSecondary;  // 辅助文本
      Color get brandPrimary;   // 主品牌色
      Color get statusError;    // 错误
      // ... 完整定义见 colors.dart
    }

    /// Layer 3: 亮暗实现
    class LightColors implements AppColors {
      const LightColors();
      @override Color get bgPrimary     => ColorBases.neutral50;
      @override Color get textPrimary   => ColorBases.neutral900;
      @override Color get brandPrimary  => ColorBases.primary500;
      // ...
    }

    class DarkColors implements AppColors {
      const DarkColors();
      @override Color get bgPrimary     => ColorBases.neutral900;
      @override Color get textPrimary   => ColorBases.neutral0;
      @override Color get brandPrimary  => ColorBases.primary400;
      // ...
    }

##### 1.2 Typography（字体）

    /// 字体定义 - 与 Figma 设计稿对应
    class AppTypography {
      // Display - 展示标题
      static const displayLarge = TextStyle(
        fontSize: 57,
        fontWeight: FontWeight.w700,
        height: 1.12,
      );

      static const displayMedium = TextStyle(
        fontSize: 45,
        fontWeight: FontWeight.w700,
        height: 1.16,
      );

      // Headline - 标题
      static const headlineLarge = TextStyle(
        fontSize: 32,
        fontWeight: FontWeight.w600,
        height: 1.25,
      );

      static const headlineMedium = TextStyle(
        fontSize: 28,
        fontWeight: FontWeight.w600,
        height: 1.29,
      );

      // Body - 正文
      static const bodyLarge = TextStyle(
        fontSize: 16,
        fontWeight: FontWeight.w400,
        height: 1.5,
      );

      static const bodyMedium = TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.w400,
        height: 1.43,
      );

      // Label - 标签
      static const labelLarge = TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.w500,
        height: 1.43,
      );

      static const labelMedium = TextStyle(
        fontSize: 12,
        fontWeight: FontWeight.w500,
        height: 1.33,
      );
    }

##### 1.3 间距 / 圆角 / 阴影

    /// 间距常量 — spacing.dart
    class AppSpacing {
      static const double s4 = 4;
      static const double s8 = 8;
      static const double s12 = 12;
      static const double s16 = 16;
      static const double s24 = 24;
      static const double s32 = 32;
    }

    /// 圆角常量 — radius.dart
    class AppRadius {
      // 基础 Radius
      static const Radius r4 = Radius.circular(4);
      static const Radius r8 = Radius.circular(8);
      static const Radius r12 = Radius.circular(12);
      static const Radius r16 = Radius.circular(16);

      // 组件级圆角
      static const BorderRadius card = BorderRadius.all(r12);
      static const BorderRadius button = BorderRadius.all(r8);
      static const BorderRadius dialog = BorderRadius.all(r16);
    }

    /// 阴影常量 — shadows.dart
    /// 颜色走 AppColors.shadow 自动适配亮暗模式
    class AppShadows {
      List<BoxShadow> get bs4;   // Elevation 4 — 小卡片
      List<BoxShadow> get bs8;   // Elevation 8 — Card
      List<BoxShadow> get bs12;  // Elevation 12 — Dropdown
      List<BoxShadow> get bs16;  // Elevation 16 — Dialog
    }

##### 1.4 Theme（主题组装）

    /// Layer 4: 主题组装 — 接收 AppColors 实例，零 isDark 分支
    class AppTheme {
      AppTheme._();

      static ThemeData get theme     => _build(const LightColors());
      static ThemeData get darkTheme => _build(const DarkColors());

      static ThemeData _build(AppColors c) {
        final brightness = c is DarkColors ? Brightness.dark : Brightness.light;
        return ThemeData(
          useMaterial3: true,
          brightness: brightness,
          scaffoldBackgroundColor: c.bgPrimary,
          colorScheme: ColorScheme(
            brightness: brightness,
            primary: c.brandPrimary,
            onPrimary: c.textOnPrimary,
            surface: c.bgSecondary,
            onSurface: c.textPrimary,
            error: c.statusError,
            onError: c.textOnPrimary,
            // ...
          ),
          appBarTheme: AppBarTheme(backgroundColor: c.bgPrimary, foregroundColor: c.textPrimary),
          elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(
            backgroundColor: c.brandPrimary, foregroundColor: c.textOnPrimary,
          )),
          inputDecorationTheme: InputDecorationTheme(fillColor: c.bgSecondary, filled: true),
          cardTheme: CardThemeData(color: c.bgSecondary),
          dividerTheme: DividerThemeData(color: c.divider),
          bottomNavigationBarTheme: BottomNavigationBarThemeData(
            backgroundColor: c.navBarBg, selectedItemColor: c.navBarSelected,
          ),
        );
      }
    }

##### 1.5 Figma 设计稿对应规范

重要原则：代码中的命名必须与 Figma 设计稿完全对应

Figma 命名 | 代码命名 | 说明
--- | --- | ---
`Primary/Main` | `context.colors.brandPrimary` | 主品牌色
`Button/Primary` | `AppButton.primary()` | 主按钮
`Text/Headline/Large` | `context.styles.headlineLarge` | 大标题
`Spacing/16` | `AppSpacing.s16` | 16pt 间距
`Radius/Card` | `AppRadius.card` | 卡片圆角

好处：

- 设计师与开发者使用相同的术语，沟通零障碍
- 代码审查时可直接对照 Figma 检查实现
- 设计变更时快速定位需要修改的代码

#### L2: Foundation（基础组件 - Atomic Design）

遵循 Atomic Design 原则，分为三个层级：

##### 2.1 Atoms（原子组件）

最小的 UI 单元，不可再分。

    /// AppButton - 按钮原子组件
    class AppButton extends StatelessWidget {
      final String text;
      final VoidCallback? onPressed;
      final ButtonVariant variant;
      final ButtonSize size;

      const AppButton.primary({
        required this.text,
        this.onPressed,
        this.size = ButtonSize.medium,
      }) : variant = ButtonVariant.primary;

      const AppButton.secondary({
        required this.text,
        this.onPressed,
        this.size = ButtonSize.medium,
      }) : variant = ButtonVariant.secondary;

      @override
      Widget build(BuildContext context) {
        // 使用 Design System 的颜色和字体
        return ElevatedButton(
          onPressed: onPressed,
          style: ElevatedButton.styleFrom(
            backgroundColor: _getBackgroundColor(),
            foregroundColor: _getForegroundColor(),
            padding: _getPadding(),
            shape: RoundedRectangleBorder(
              borderRadius: AppRadius.button,
            ),
          ),
          child: Text(text, style: _getTextStyle()),
        );
      }
    }

    /// AppTextField - 文本框原子组件
    class AppTextField extends StatelessWidget {
      final String? label;
      final String? hint;
      final TextEditingController? controller;

      const AppTextField({
        this.label,
        this.hint,
        this.controller,
      });

      @override
      Widget build(BuildContext context) {
        return TextField(
          controller: controller,
          style: AppTypography.bodyMedium,
          decoration: InputDecoration(
            labelText: label,
            hintText: hint,
            border: OutlineInputBorder(
              borderRadius: AppRadius.button,
            ),
          ),
        );
      }
    }

##### 2.2 Molecules（分子组件）

由多个原子组件组合而成。

    /// SearchBar - 搜索栏分子组件
    class SearchBar extends StatelessWidget {
      final String hint;
      final ValueChanged<String>? onChanged;

      const SearchBar({
        required this.hint,
        this.onChanged,
      });

      @override
      Widget build(BuildContext context) {
        return Container(
          padding: EdgeInsets.all(AppSpacing.s12),
          child: Row(
            children: [
              Icon(Icons.search, color: context.colors.textSecondary),
              SizedBox(width: AppSpacing.s8),
              Expanded(
                child: AppTextField(
                  hint: hint,
                  controller: TextEditingController(),
                ),
              ),
            ],
          ),
        );
      }
    }

##### 2.3 Organisms（有机组件）

由原子和分子组件组合成的复杂组件。

    /// UserCard - 用户卡片有机组件
    class UserCard extends StatelessWidget {
      final String name;
      final String avatar;
      final String lastMessage;
      final VoidCallback? onTap;

      const UserCard({
        required this.name,
        required this.avatar,
        required this.lastMessage,
        this.onTap,
      });

      @override
      Widget build(BuildContext context) {
        return Card(
          child: ListTile(
            leading: Avatar(url: avatar),
            title: Text(name, style: AppTypography.bodyLarge),
            subtitle: Text(lastMessage, style: AppTypography.bodyMedium),
            trailing: AppButton.secondary(
              text: '发消息',
              onPressed: onTap,
            ),
          ),
        );
      }
    }

#### L3: Business Components（业务组件）

包含业务逻辑的组件，通常与 Feature 相关。

    /// MessageBubble - 消息气泡（业务组件）
    class MessageBubble extends ConsumerWidget {
      final Message message;

      const MessageBubble({required this.message});

      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final isSender = message.senderId == ref.watch(currentUserProvider).id;

        return Align(
          alignment: isSender ? Alignment.centerRight : Alignment.centerLeft,
          child: Container(
            margin: EdgeInsets.symmetric(
              horizontal: AppSpacing.s16,
              vertical: AppSpacing.s8,
            ),
            padding: EdgeInsets.all(AppSpacing.s12),
            decoration: BoxDecoration(
              color: isSender ? context.colors.brandPrimary : context.colors.bgTertiary,
              borderRadius: AppRadius.card,
            ),
            child: Text(
              message.content,
              style: context.styles.bodyMedium?.copyWith(
                color: isSender ? context.colors.textOnPrimary : context.colors.textPrimary,
              ),
            ),
          ),
        );
      }
    }

#### L4: Pages（页面）

完整的页面，组合各种组件，连接 ViewModel。

    /// ChatPage - 聊天页面
    class ChatPage extends ConsumerWidget {
      final String chatId;

      const ChatPage({required this.chatId});

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

        return Scaffold(
          appBar: AppBar(title: Text(state.chatName)),
          body: Column(
            children: [
              Expanded(
                child: ListView.builder(
                  itemCount: state.messages.length,
                  itemBuilder: (context, index) {
                    return MessageBubble(message: state.messages[index]);
                  },
                ),
              ),
              ChatInputBar(
                onSend: (text) {
                  ref.read(chatViewModelProvider(chatId).notifier).sendMessage(text);
                },
              ),
            ],
          ),
        );
      }
    }

#### UI 层目录结构

    lib/
    ├── core/ui/
    │   ├── base/                # L1: 基础定义（已实现）
    │   │   ├── colors.dart      # 颜色体系（品牌色 / 语义色 / 灰阶）
    │   │   ├── font.dart        # TextStyle 定义 + textTheme(brightness)
    │   │   └── app_theme.dart   # ThemeData 组装（Light / Dark）
    │   │
    │   ├── components/          # L2: 原子组件
    │   │   └── app_button.dart  # 按钮（已实现）
    │   │   # app_text_field / app_icon / app_avatar 等  待开发
    │   │
    │   └── composites/          # L3: 组合组件
    │       └── app_dialog.dart  # 确认弹窗（已实现）
    │       # app_action_sheet / app_toast 等  待开发
    │
    └── features/
        └── chat/
            └── view/            # L4: 页面 + Feature 专属组件（待开发）
                ├── chat_page.dart
                └── widgets/
                    ├── message_bubble.dart
                    ├── message_input_bar.dart
                    └── message_list_view.dart

核心价值

- 设计系统：确保设计与实现一致，与 Figma 完全对应
- 原子设计：从小到大构建组件，提升复用性
- 清晰分层：基础组件 vs 业务组件，职责明确
- 主题支持：亮色/暗色模式统一管理
- 易于维护：设计变更只需修改 Design System

### 3.2 多平台适配

核心理念：一套代码适配多个平台（iOS、Android、Web、Windows、macOS、Linux），通过平台检测和自适应组件实现平台特定的 UI 和交互。

#### 支持的平台

平台类型 | 具体平台 | 设计规范 | 特点
--- | --- | --- | ---
移动端 | iOS、Android | Cupertino / Material Design | 触摸交互、竖屏优先
桌面端 | Windows、macOS、Linux | Fluent / macOS / GNOME | 鼠标键盘、大屏幕
Web 端 | 浏览器 | 响应式设计 | 跨浏览器兼容

#### 3.2.1 平台检测与适配策略

    /// 平台工具类 - 统一平台检测
    class PlatformAdapter {
      // 平台类型判断
      static bool get isMobile => Platform.isIOS || Platform.isAndroid;
      static bool get isDesktop => Platform.isWindows || Platform.isMacOS || Platform.isLinux;
      static bool get isIOS => Platform.isIOS;
      static bool get isAndroid => Platform.isAndroid;
      static bool get isWeb => kIsWeb;
      static bool get isMacOS => Platform.isMacOS;
      static bool get isWindows => Platform.isWindows;

      // 设备类型判断（基于屏幕尺寸）
      static DeviceType getDeviceType(BuildContext context) {
        final width = MediaQuery.of(context).size.width;
        if (width < 600) return DeviceType.mobile;
        if (width < 1200) return DeviceType.tablet;
        return DeviceType.desktop;
      }

      // 获取平台特定的设计风格
      static DesignStyle get designStyle {
        if (isIOS) return DesignStyle.cupertino;
        if (isAndroid) return DesignStyle.material;
        if (isMacOS) return DesignStyle.macos;
        if (isWindows) return DesignStyle.fluent;
        return DesignStyle.material; // 默认
      }
    }

    enum DeviceType { mobile, tablet, desktop }
    enum DesignStyle { material, cupertino, fluent, macos }

#### 3.2.2 响应式布局

根据屏幕尺寸自动调整布局：

    /// 响应式布局组件
    class ResponsiveLayout extends StatelessWidget {
      final Widget mobile;
      final Widget? tablet;
      final Widget? desktop;

      const ResponsiveLayout({
        required this.mobile,
        this.tablet,
        this.desktop,
      });

      @override
      Widget build(BuildContext context) {
        return LayoutBuilder(
          builder: (context, constraints) {
            // 桌面布局（>= 1200px）
            if (constraints.maxWidth >= 1200) {
              return desktop ?? tablet ?? mobile;
            }
            // 平板布局（>= 600px）
            else if (constraints.maxWidth >= 600) {
              return tablet ?? mobile;
            }
            // 手机布局（< 600px）
            else {
              return mobile;
            }
          },
        );
      }
    }

    /// 使用示例
    class ChatPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return ResponsiveLayout(
          // 手机：单栏布局
          mobile: SingleColumnChatView(),
          // 平板：两栏布局（会话列表 + 聊天）
          tablet: TwoColumnChatView(),
          // 桌面：三栏布局（联系人 + 会话列表 + 聊天）
          desktop: ThreeColumnChatView(),
        );
      }
    }

#### 3.2.3 平台自适应组件

根据平台自动选择 Material 或 Cupertino 风格：

    /// 平台自适应按钮
    class PlatformButton extends StatelessWidget {
      final String text;
      final VoidCallback? onPressed;

      const PlatformButton({
        required this.text,
        this.onPressed,
      });

      @override
      Widget build(BuildContext context) {
        // iOS 使用 Cupertino 风格
        if (PlatformAdapter.isIOS) {
          return CupertinoButton(
            onPressed: onPressed,
            color: context.colors.brandPrimary,
            child: Text(text),
          );
        }
        // Android 和其他平台使用 Material 风格
        else {
          return ElevatedButton(
            onPressed: onPressed,
            child: Text(text),
          );
        }
      }
    }

    /// 平台自适应导航栏
    class PlatformAppBar extends StatelessWidget implements PreferredSizeWidget {
      final String title;
      final List<Widget>? actions;

      const PlatformAppBar({
        required this.title,
        this.actions,
      });

      @override
      Widget build(BuildContext context) {
        // iOS 使用 CupertinoNavigationBar
        if (PlatformAdapter.isIOS) {
          return CupertinoNavigationBar(
            middle: Text(title),
            trailing: actions != null ? Row(children: actions!) : null,
          );
        }
        // Android 使用 Material AppBar
        else {
          return AppBar(
            title: Text(title),
            actions: actions,
          );
        }
      }

      @override
      Size get preferredSize => Size.fromHeight(56);
    }

    /// 平台自适应对话框
    class PlatformDialog {
      static Future<bool?> showConfirm(
        BuildContext context, {
        required String title,
        required String content,
      }) {
        // iOS 使用 CupertinoAlertDialog
        if (PlatformAdapter.isIOS) {
          return showCupertinoDialog<bool>(
            context: context,
            builder: (context) => CupertinoAlertDialog(
              title: Text(title),
              content: Text(content),
              actions: [
                CupertinoDialogAction(
                  child: Text('取消'),
                  onPressed: () => Navigator.pop(context, false),
                ),
                CupertinoDialogAction(
                  child: Text('确定'),
                  isDestructiveAction: true,
                  onPressed: () => Navigator.pop(context, true),
                ),
              ],
            ),
          );
        }
        // Android 使用 Material AlertDialog
        else {
          return showDialog<bool>(
            context: context,
            builder: (context) => AlertDialog(
              title: Text(title),
              content: Text(content),
              actions: [
                TextButton(
                  child: Text('取消'),
                  onPressed: () => Navigator.pop(context, false),
                ),
                TextButton(
                  child: Text('确定'),
                  onPressed: () => Navigator.pop(context, true),
                ),
              ],
            ),
          );
        }
      }
    }

#### 3.2.4 平台特定交互

    /// 平台特定的滑动返回手势
    class PlatformScaffold extends StatelessWidget {
      final Widget body;
      final PreferredSizeWidget? appBar;

      const PlatformScaffold({
        required this.body,
        this.appBar,
      });

      @override
      Widget build(BuildContext context) {
        final scaffold = Scaffold(
          appBar: appBar,
          body: body,
        );

        // iOS 支持侧滑返回
        if (PlatformAdapter.isIOS) {
          return CupertinoPageScaffold(
            navigationBar: appBar as ObstructingPreferredSizeWidget?,
            child: body,
          );
        }

        return scaffold;
      }
    }

    /// 平台特定的右键菜单（桌面端）
    class PlatformContextMenu extends StatelessWidget {
      final Widget child;
      final List<ContextMenuItem> menuItems;

      const PlatformContextMenu({
        required this.child,
        required this.menuItems,
      });

      @override
      Widget build(BuildContext context) {
        // 桌面端支持右键菜单
        if (PlatformAdapter.isDesktop) {
          return GestureDetector(
            onSecondaryTapDown: (details) {
              _showContextMenu(context, details.globalPosition);
            },
            child: child,
          );
        }

        // 移动端使用长按显示菜单
        return GestureDetector(
          onLongPress: () {
            _showMobileMenu(context);
          },
          child: child,
        );
      }

      void _showContextMenu(BuildContext context, Offset position) {
        // 显示桌面端右键菜单
      }

      void _showMobileMenu(BuildContext context) {
        // 显示移动端底部菜单
      }
    }

#### 3.2.5 屏幕尺寸断点

    /// 屏幕断点定义
    class ScreenBreakpoints {
      // 手机
      static const double mobile = 0;
      static const double mobileMax = 599;

      // 平板
      static const double tablet = 600;
      static const double tabletMax = 1199;

      // 桌面
      static const double desktop = 1200;
      static const double desktopMax = 1919;

      // 大屏
      static const double ultraWide = 1920;

      // 判断当前断点
      static ScreenSize getSize(BuildContext context) {
        final width = MediaQuery.of(context).size.width;
        if (width < tablet) return ScreenSize.mobile;
        if (width < desktop) return ScreenSize.tablet;
        if (width < ultraWide) return ScreenSize.desktop;
        return ScreenSize.ultraWide;
      }
    }

    enum ScreenSize { mobile, tablet, desktop, ultraWide }

    /// 响应式间距
    class ResponsiveSpacing {
      static double get(BuildContext context, {
        double mobile = 16,
        double tablet = 24,
        double desktop = 32,
      }) {
        final size = ScreenBreakpoints.getSize(context);
        switch (size) {
          case ScreenSize.mobile:
            return mobile;
          case ScreenSize.tablet:
            return tablet;
          case ScreenSize.desktop:
          case ScreenSize.ultraWide:
            return desktop;
        }
      }
    }

#### 3.2.6 多平台目录结构

    lib/
    ├── core/ui/
    │   ├── base/                      # 基础定义（已实现：colors / font / app_theme）
    │   │   ├── colors.dart            # 颜色体系（品牌色 / 语义色 / 灰阶）
    │   │   ├── font.dart              # TextStyle 定义 + textTheme(brightness)
    │   │   └── app_theme.dart         # ThemeData 组装（可按平台扩展 Material / Cupertino）
    │   │
    │   ├── components/                  # 基础组件
    │   │   ├── app_button.dart
    │   │   ├── app_text_field.dart
    │   │   └── app_avatar.dart
    │   │
    │   ├── composites/                  # 业务组合组件
    │   │   ├── app_alert_dialog.dart
    │   │   └── app_action_sheet.dart
    │   │
    │   └── platform/                    # 平台适配（可选）
    │       ├── platform_adapter.dart    # 平台检测
    │       ├── responsive_layout.dart   # 响应式布局
    │       └── screen_breakpoints.dart  # 屏幕断点
    │
    └── features/
        └── chat/
            └── view/
                ├── chat_page.dart             # 主入口（平台自适应）
                └── layouts/                   # 不同布局
                    ├── mobile_layout.dart     # 手机布局
                    ├── tablet_layout.dart     # 平板布局
                    └── desktop_layout.dart    # 桌面布局

多平台适配核心价值

- 一套代码：维护成本低，所有平台同步更新
- 平台原生感：自动适配平台特定的设计规范和交互
- 响应式设计：自动适应不同屏幕尺寸
- 性能优化：根据平台特性优化渲染和交互
- 用户体验一致：核心功能在所有平台保持一致

#### 3.2.7 平台适配最佳实践

场景 | 推荐方案 | 说明
--- | --- | ---
导航栏 | PlatformAppBar | iOS 用 CupertinoNavigationBar，Android 用 AppBar
对话框 | PlatformDialog | iOS 用 CupertinoAlertDialog，Android 用 AlertDialog
按钮 | PlatformButton | iOS 用 CupertinoButton，Android 用 ElevatedButton
滑动返回 | 自动检测平台 | iOS 支持侧滑返回，Android 使用返回按钮
右键菜单 | 桌面端显示，移动端长按 | 根据平台调整交互方式
布局 | ResponsiveLayout | 手机单栏、平板双栏、桌面三栏

### 3.3 Feature UI 组织

核心理念：UI 层按 Feature 组织，每个页面的 UI 组件都在其对应的 Feature 目录下。

flowchart TD UI[UI Layer 界面层] --> Chat[Chat Feature UI] UI --> ChatList[Chat List Feature UI] UI --> Contact[Contact Feature UI] UI --> Search[Search Feature UI] UI --> Call[Call Feature UI] Chat --> ChatPage[features/chat/view/
chat_page.dart] Chat --> ChatWidgets[features/chat/view/widgets/
message_bubble.dart
message_input_bar.dart] ChatList --> ChatListPage[features/chat_list/view/
chat_list_page.dart] ChatList --> ChatListWidgets[features/chat_list/view/widgets/
chat_list_item.dart] Contact --> ContactPage[features/contact/view/
contact_page.dart] Contact --> ContactWidgets[features/contact/view/widgets/
contact_item.dart] Search --> SearchPage[features/search/view/
search_page.dart] Call --> CallPage[features/call/view/
call_page.dart] style UI fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style Chat fill:#fff9c4,stroke:#f57f17 style ChatList fill:#f3e5f5,stroke:#7b1fa2 style Contact fill:#e8f5e9,stroke:#388e3c style Search fill:#fce4ec,stroke:#c2185b style Call fill:#fff4e6,stroke:#f57c00

### 3.4 UI 层目录结构

    lib/features/
    ├── chat/
    │   └── view/
    │       ├── chat_page.dart              # 聊天页面
    │       └── widgets/                    # 聊天专用组件
    │           ├── message_bubble.dart     # 消息气泡
    │           ├── message_input_bar.dart  # 消息输入栏
    │           └── message_list_view.dart  # 消息列表
    │
    ├── chat_list/
    │   └── view/
    │       ├── chat_list_page.dart         # 会话列表页面
    │       └── widgets/
    │           ├── chat_list_item.dart     # 会话列表项
    │           ├── unread_badge.dart       # 未读角标
    │           └── pinned_indicator.dart   # 置顶标识
    │
    ├── contact/
    │   └── view/
    │       ├── contact_page.dart           # 联系人页面
    │       └── widgets/
    │           ├── contact_item.dart       # 联系人项
    │           └── section_header.dart     # 分组头
    │
    ├── search/
    │   └── view/
    │       ├── search_page.dart            # 搜索页面
    │       └── widgets/
    │           └── search_result_item.dart # 搜索结果项
    │
    └── call/
        └── view/
            ├── call_page.dart              # 通话页面
            └── widgets/
                └── call_controls.dart      # 通话控制按钮

### 3.4 主要 Feature 页面

#### Chat Feature - 聊天功能

- 位置：`features/chat/view/chat_page.dart`
- 职责：消息列表展示、消息发送、多媒体消息、消息状态显示
- 专用组件：MessageBubble、InputBar、MessageList

#### Chat List Feature - 会话列表功能

- 位置：`features/chat_list/view/chat_list_page.dart`
- 职责：显示所有会话、未读消息提示、会话操作（删除/置顶）
- 专用组件：ChatItem、SwipeActions

#### Contact Feature - 联系人功能

- 位置：`features/contact/view/contact_page.dart`
- 职责：联系人列表、分组展示、联系人搜索、联系人详情
- 专用组件：ContactItem、SectionHeader

#### Search Feature - 搜索功能

- 位置：`features/search/view/search_page.dart`
- 职责：全局搜索、消息搜索、联系人搜索、搜索历史
- 专用组件：SearchResultItem

#### Call Feature - 通话功能

- 位置：`features/call/view/call_page.dart`
- 职责：语音通话、视频通话、通话控制、通话状态
- 专用组件：CallControls

> 设计原则：UI 层的每个页面都在其对应的 Feature 目录下，与该 Feature 的 Presentation 层和 Domain 层垂直对齐，形成高内聚的功能模块。

## 路由系统（go_router）

### 路由是什么

路由就是「页面地址 → 页面」的映射表。打开 App 时系统根据当前地址决定显示哪个页面，点击按钮时通过地址跳转到另一个页面。

### Shell 是什么

Shell（壳层）是一个持久存在的 UI 框架，内容区域在里面切换，而框架本身不销毁。类比一下：

- 微信底部有「微信 / 通讯录 / 发现 / 我」四个 Tab，这四个 Tab 的底部导航栏始终可见，这就是 Shell
- 点击不同 Tab，底部导航栏不动，只有上方内容在切换
- 但打开「设置 → 关于微信」这类页面时，底部导航栏消失了，这是跳出了 Shell

在本项目里：

- Shell 内（带导航栏）：`/chat`、`/contact`、`/settings`——底部导航栏始终可见
- Shell 外（全屏）：`/chat/detail`、`/chat/:id`、`/settings/theme`、`/login`——全屏独占，没有底部导航栏

`AppTab` 就是 Shell 组件，它只负责渲染底部导航栏和容纳当前页面内容，自身不包含任何业务逻辑。

### 为什么禁止使用 Navigator.push

传统写法：

    // 禁止 ❌
    Navigator.push(context, MaterialPageRoute(builder: (_) => const ThemeView()));

禁止原因：

- 绕过了守卫：直接 `Navigator.push` 不经过 go_router 的 `redirect`，未登录用户可以直接跳进受保护页面
- 路径分散：目标页面的引用散落在各处 `onTap`，重构时要全局搜索替换
- 破坏 Shell：在 go_router 管理的路由中混用 `Navigator.push`，可能导致底部导航栏消失或 Tab 状态丢失
- 深链接失效：go_router 无法感知通过 `Navigator.push` 打开的页面，通知点击跳转等场景会出问题

正确写法：

    // 正确 ✅
    context.push(AppRouteName.settingsTheme.path);   // 压栈，可以返回
    context.go(AppRouteName.chat.path);              // 替换历史，不可返回

go_router 集中解决上述问题：统一声明路由、统一拦截、路径字符串集中在 `AppRouteName` 枚举管理。

### 文件结构

    app/router/
    ├── app_router.dart        # 路由表 + routerProvider（核心）
    ├── app_route_name.dart         # AppRouteName 枚举，路径常量 + fromPath()
    └── guards/
        └── auth_guard.dart    # 登录守卫（拦截未登录访问）

### 路径常量：app_route_name.dart

所有路径字符串只在这一个文件里写，用枚举定义。其他地方引用 `AppRouteName.xxx.path`，不允许硬编码字符串。

用枚举而不是常量类，是因为守卫里的 `switch` 需要穷举枚举值：新加路由时，若守卫的 `switch` 没有补对应的 `case`，编译器直接报错，防止漏掉权限判断。

    enum AppRouteName {
      // ── Shell 内（Tab 根路由）────────────────────────────────────────
      chat('/chat'),
      contact('/contact'),
      settings('/settings'),

      // ── Shell 外（全屏页面，无底部导航栏）──────────────────────────────
      // extra: ({String conversationId, String title})
      chatDetail('/chat/detail'),
      // 路径参数形式：导航用 AppRouteName.chatDetailByIdPath(id)，不直接用 .path
      chatDetailById('/chat/:id'),
      settingsTheme('/settings/theme'),
      login('/login');

      const AppRouteName(this.path);

      /// 绝对路径，用于 context.push / context.go 导航及顶层路由表声明
      final String path;

      /// 从绝对路径反查枚举值，路径未注册时返回 null
      /// 注意：含路径参数的路由（如 /chat/99）无法匹配，返回 null，
      /// auth_guard 会按受保护路由处理
      static AppRouteName? fromPath(String path) =>
          AppRouteName.values.where((r) => r.path == path).firstOrNull;

      /// 生成 chatDetailById 的实际导航路径，将 :id 替换为真实 id
      /// 例：AppRouteName.chatDetailByIdPath('99') → '/chat/99'
      static String chatDetailByIdPath(String id) => '/chat/$id';
    }

> 规则：任何地方都不允许硬编码路径字符串。`context.push / context.go` 用 `.path`；含路径参数的路由用对应的静态方法（如 `chatDetailByIdPath`）；路径字符串只在枚举里写一次。

### 路由表结构：app_router.dart

整个路由表分两类：

类型 | 路由 | TabBar
--- | --- | ---
Shell 内（StatefulShellRoute branches） | /chat、/contact、/settings | 始终可见
Shell 外（parentNavigatorKey = _rootKey） | /chat/detail、/chat/:id、/settings/theme、/login | 隐藏

    // Root Navigator Key：全屏路由声明 parentNavigatorKey 时引用，
    // 确保 push 时覆盖整个 Shell（TabBar 消失）
    final _rootKey = GlobalKey<NavigatorState>();

    final routerProvider = Provider<GoRouter>((ref) {
      final authNotifier = ref.read(authNotifierProvider);

      return GoRouter(
        // Root Navigator 的 Key，供全屏路由声明 parentNavigatorKey 使用
        navigatorKey: _rootKey,

        // 冷启动默认落地页；authGuard 会在进入前检查登录状态并按需重定向
        initialLocation: AppRouteName.chat.path,

        // 在控制台打印每次路由变化，方便开发期间调试；上线前设为 false
        debugLogDiagnostics: true,

        // 监听 authNotifier：登录 / 退出时自动触发 redirect 重新执行
        refreshListenable: authNotifier,

        redirect: (context, state) => authGuard(authNotifier, state),

        routes: [
          // ── Shell 内：底部导航栏始终可见 ─────────────────────────────────
          StatefulShellRoute.indexedStack(
            builder: (context, state, navigationShell) {
              return AppTab(navigationShell: navigationShell);
            },
            branches: [
              StatefulShellBranch(routes: [
                GoRoute(path: AppRouteName.chat.path, builder: (_, __) => const ChatPage()),
              ]),
              StatefulShellBranch(routes: [
                GoRoute(path: AppRouteName.contact.path, builder: (_, __) => const ContactPage()),
              ]),
              StatefulShellBranch(routes: [
                GoRoute(path: AppRouteName.settings.path, builder: (_, __) => const SettingsPage()),
              ]),
            ],
          ),

          // ── Shell 外：全屏页面，无底部导航栏 ─────────────────────────────
          // parentNavigatorKey: _rootKey 确保路由覆盖 Shell，TabBar 消失
          GoRoute(
            parentNavigatorKey: _rootKey,
            path: AppRouteName.chatDetail.path,
            builder: (context, state) {
              final extra = state.extra as ({String conversationId, String title});
              return ChatDetailPage(conversationId: extra.conversationId, title: extra.title);
            },
          ),
          GoRoute(
            parentNavigatorKey: _rootKey,
            path: AppRouteName.chatDetailById.path,
            builder: (context, state) {
              final id = state.pathParameters['id']!;
              return ChatDetailPage(conversationId: id, title: '路径参数详情');
            },
          ),
          GoRoute(
            parentNavigatorKey: _rootKey,
            path: AppRouteName.settingsTheme.path,
            builder: (_, __) => const ThemeView(),
          ),
          GoRoute(
            parentNavigatorKey: _rootKey,
            path: AppRouteName.login.path,
            builder: (_, __) => const LoginPage(),
          ),
        ],
      );
    });

### StatefulShellRoute 是什么

`StatefulShellRoute.indexedStack` 负责维护底部 Tab 导航，每个 `branches` 对应一个 Tab 分支。它和普通 `IndexedStack` 的区别：

| IndexedStack（旧方式） | StatefulShellRoute（go_router）
--- | --- | ---
Tab 状态保持 | ✅ 是 | ✅ 是
路径支持 | ❌ 无路径概念 | ✅ 每个 Tab 都有真实 URL 路径
深链接 | ❌ 不支持 | ✅ 支持
Tab 内子页面 | ❌ 需手动处理 | ✅ 独立 Navigator 栈
守卫 | ❌ 无统一拦截 | ✅ 全局 redirect

`branches` 是并列关系，不是层级关系。三个 Tab 分支互相独立，各自维护自己的导航栈：

    StatefulShellRoute（Shell 内，TabBar 可见）
      branches[0]  →  /chat                ← 聊天 Tab（独立栈）
      branches[1]  →  /contact             ← 联系人 Tab（独立栈）
      branches[2]  →  /settings            ← 设置 Tab（独立栈）

    Root Navigator（Shell 外，TabBar 隐藏，parentNavigatorKey: _rootKey）
      /chat/detail        ← extra 传参详情页
      /chat/:id           ← 路径参数详情页
      /settings/theme     ← 主题设置页
      /login              ← 登录页

### 如何在页面间跳转

用 `context` 调用，不需要 `ref`：

场景 | 方法 | 说明
--- | --- | ---
进入子页面（可返回，TabBar 隐藏） | `context.push(AppRouteName.xxx.path)` | 压到 Root Navigator，覆盖 Shell，有返回按钮
带参数进入子页面（extra） | `context.push(AppRouteName.xxx.path, extra: obj)` | extra 传 Dart 对象；路由 builder 解包后以构造参数注入目标页
带参数进入子页面（路径参数） | `context.push(AppRouteName.xxxByIdPath(id))` | id 内嵌在 URL 中；适合深链接 / 推送通知跳转
切换 Tab（TabBar 可见） | `context.go(AppRouteName.xxx.path)` | 替换整个历史，Tab 高亮切换，不可返回
登录成功跳首页 | `context.go(AppRouteName.chat.path)` | 替换历史，防止返回到登录页
返回上一页 | `context.pop()` | 弹栈
返回并传值给上层 | `context.pop(result)` | 上层用 `await context.push(...)` 接收
弹窗 / Alert | `showDialog(...)` | Flutter 原生，go_router 不管理，直接用
底部弹层 | `showModalBottomSheet(...)` | Flutter 原生，go_router 不管理，直接用

    // 进入子页面（TabBar 隐藏，可返回）
    context.push(AppRouteName.settingsTheme.path);

    // 带参数进入子页面：extra 传 Dart Record
    context.push(
      AppRouteName.chatDetail.path,
      extra: (conversationId: '42', title: '技术支持'),
    );

    // 带参数进入子页面：路径参数
    context.push(AppRouteName.chatDetailByIdPath('99'));

    // 切换 Tab（TabBar 可见，不可返回）
    context.go(AppRouteName.contact.path);

    // 返回
    context.pop();

    // 返回并传值
    final result = await context.push<String>(AppRouteName.settingsTheme.path);

    // 弹窗（Flutter 原生）
    showDialog(context: context, builder: (_) => const AlertDialog(...));

> 禁止使用 `Navigator.push(context, MaterialPageRoute(...))`：不经过 go_router redirect，守卫失效；路径散落各处；破坏 Tab 状态；深链接 / 通知跳转失效。

### 带参数路由

go_router 有两种传参方式，根据场景选择：

| extra（传对象） | 路径参数（:id）
--- | --- | ---
适用场景 | 列表点入详情，导航时已有完整数据 | 推送通知深链接，只携带一个 ID
URL 可见 | 否 | 是
数据来源 | 直接传对象，不需要额外请求 | 按 ID 从 Repository 重新拉取
代码量 | 少 | 多（需 .family provider）

日常开发优先用 extra；接入推送通知后，需要从通知冷启动进入会话时，再补路径参数版本。

#### extra 传参完整示例

Step 1：枚举声明路由（静态路径，注释写明 extra 类型）

    // app_route_name.dart
    // Chat 子路由
    // extra: ({String conversationId, String title})
    chatDetail('/chat/detail'),

Step 2：路由表注册，builder 负责解包 extra

    // app_router.dart
    // Shell 外顶层路由：parentNavigatorKey: _rootKey 覆盖整个 Shell，TabBar 隐藏
    GoRoute(
      parentNavigatorKey: _rootKey,
      path: AppRouteName.chatDetail.path,   // '/chat/detail'
      builder: (context, state) {
        // 路由层唯一做类型转换的地方，目标页面只知道构造参数
        final extra = state.extra as ({String conversationId, String title});
        return ChatDetailPage(
          conversationId: extra.conversationId,
          title: extra.title,
        );
      },
    ),

Step 3：目标页面只接受构造参数，不感知 GoRouter

    // chat_detail_page.dart
    class ChatDetailPage extends StatelessWidget {
      const ChatDetailPage({
        super.key,
        required this.conversationId,
        required this.title,
      });

      final String conversationId;
      final String title;
      // ...
    }

Step 4：导航时附带 extra

    context.push(
      AppRouteName.chatDetail.path,
      extra: (conversationId: conversation.id, title: conversation.title),
    );

extra 使用 Dart 3 匿名 Record，轻量且类型安全，无需定义额外 class。

### AppTab：Tab 如何切换

`AppTab` 是底部导航栏的持久容器，负责渲染底部 Tab 栏和持有当前 Tab 的内容区域。它自身不管理任何状态，Tab 的当前索引由 go_router 传入的 `StatefulNavigationShell` 维护：

    class AppTab extends StatelessWidget {
      const AppTab({super.key, required this.navigationShell});
      final StatefulNavigationShell navigationShell;

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: navigationShell,   // 当前 Tab 的 Navigator 内容
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: navigationShell.currentIndex, // go_router 维护
            onTap: (index) => navigationShell.goBranch(
              index,
              // 再次点击已激活的 Tab 时回到该 Tab 的首页
              initialLocation: index == navigationShell.currentIndex,
            ),
            items: const [...],
          ),
        );
      }
    }

上面的 `goBranch` 只用在 AppTab 内部的 `onTap`，处理「重复点同一 Tab 时回到该 Tab 首页」的导航栏专属逻辑。业务页面里不调 `goBranch`。

#### 从业务页面切换 Tab

在任意页面主动切换 Tab，用 `context.go`：

    // 切换到联系人 Tab
    context.go(AppRouteName.contact.path);

    // 切换到设置 Tab
    context.go(AppRouteName.settings.path);

| context.go | context.push
--- | --- | ---
历史栈 | 替换（不可返回） | 压栈（可返回）
返回按钮 | 不显示 | 自动显示
适用场景 | 切换 Tab、登录后跳首页 | 进入子页面

#### Shell 外路由的路径前缀不影响 TabBar

Shell 外路由（声明了 `parentNavigatorKey: _rootKey`）放到 Root Navigator 上，会覆盖整个 Shell，TabBar 始终隐藏，与路径前缀无关。

例如 `/settings/theme` 虽然前缀是 `/settings`，但它是 Shell 外路由，导航后 TabBar 不可见：

    // 全屏打开主题页，TabBar 隐藏，可返回
    context.push(AppRouteName.settingsTheme.path);

如果需要"在某个 Tab 内打开子页面、保留 TabBar"，要把子路由注册为 Shell 内路由（放进对应 `StatefulShellBranch` 的 `routes`，不加 `parentNavigatorKey`）。

### 登录守卫：auth_guard.dart

守卫是 go_router 的全局拦截器，每次跳转路由前都会执行。返回 `null` 表示放行，返回路径字符串表示重定向到那个路径。

    String? authGuard(AuthNotifier authNotifier, GoRouterState state) {
      final isLoggedIn = authNotifier.isLoggedIn;
      final route = AppRouteName.fromPath(state.matchedLocation);

      // 路径不在枚举中（理论上不应出现）→ 按受保护处理
      if (route == null) return isLoggedIn ? null : AppRouteName.login.path;

      switch (route) {
        case AppRouteName.login:
          // 已登录还在登录页 → 跳聊天页
          return isLoggedIn ? AppRouteName.chat.path : null;

        case AppRouteName.chat:
        case AppRouteName.chatDetail:
        case AppRouteName.chatDetailById:
        case AppRouteName.contact:
        case AppRouteName.settings:
        case AppRouteName.settingsTheme:
          // 受保护路由 → 未登录跳登录页
          return isLoggedIn ? null : AppRouteName.login.path;
      }
    }

用 `switch(route)` 而不是 `if-else` 的原因：Dart 的枚举 `switch` 是穷举的。在 `AppRouteName` 里新增一个枚举值（即新加路由），如果忘了在守卫的 `switch` 里补对应的 `case`，编译器会直接报错，强制你决定这条路由的权限。

### refreshListenable 机制

守卫只在「路由跳转」时执行一次。但当用户「退出登录」后，页面没有跳转，守卫不会自动重新执行。

`refreshListenable` 解决这个问题：把 `AuthNotifier`（它继承自 `ChangeNotifier`）传给 go_router，每当 `AuthNotifier.notifyListeners()` 被调用时，go_router 自动重新执行 redirect：

    GoRouter(
      refreshListenable: authNotifier,   // 监听 AuthNotifier
      redirect: (context, state) => authGuard(authNotifier, state),
      ...
    )

    // 当用户点击「退出登录」：
    ref.read(authNotifierProvider).logout();
    // ↓ AuthNotifier.logout() 调用 notifyListeners()
    // ↓ go_router 收到通知，重新执行 authGuard
    // ↓ authGuard 发现未登录，返回 AppRouteName.login.path
    // ↓ 自动跳转到登录页

### 如何添加一个新路由

以添加「个人资料页 /profile」为例，共三步：

Step 1：在 AppRouteName 枚举追加新值

    // app_route_name.dart
    enum AppRouteName {
      chat('/chat'),
      contact('/contact'),
      settings('/settings'),
      settingsTheme('/settings/theme'),
      login('/login'),
      profile('/profile');  // 新增
      ...
    }

加完之后，守卫的 `switch` 会立即编译报错，提示你补上 `case AppRouteName.profile:`，决定这条路由是否需要登录。

Step 2：在守卫 switch 补 case

    // auth_guard.dart
    switch (route) {
      case AppRouteName.login:
        return isLoggedIn ? AppRouteName.chat.path : null;
      case AppRouteName.chat:
      case AppRouteName.chatDetail:
      case AppRouteName.chatDetailById:
      case AppRouteName.contact:
      case AppRouteName.settings:
      case AppRouteName.settingsTheme:
      case AppRouteName.profile:  // 新增，受保护
        return isLoggedIn ? null : AppRouteName.login.path;
    }

Step 3：在 app_router.dart 注册路由

全屏页面（无底部导航栏）：加到顶层 `routes` 列表，加上 `parentNavigatorKey: _rootKey`，确保覆盖整个 Shell、TabBar 隐藏。

    // app_router.dart（Shell 外，全屏）
    GoRoute(
      parentNavigatorKey: _rootKey,
      path: AppRouteName.profile.path,
      builder: (_, __) => const ProfilePage(),
    ),

Tab 内子页面（保留 TabBar）：加到对应 `StatefulShellBranch` 的 `routes` 里，不加 `parentNavigatorKey`，路由放到 Branch Navigator，TabBar 保持可见。

    // app_router.dart（Shell 内，TabBar 可见）
    StatefulShellBranch(
      routes: [
        GoRoute(
          path: AppRouteName.settings.path,
          builder: (_, __) => const SettingsPage(),
          routes: [
            // 此处不加 parentNavigatorKey，路由在 Branch Navigator 内
            GoRoute(path: AppRouteName.profile.segment, builder: (_, __) => const ProfilePage()),
          ],
        ),
      ],
    ),

Step 4：在需要的地方跳转

    onTap: () => context.push(AppRouteName.profile.path),

### 接入正式 token（storage_sdk 就绪后）

当前守卫用 `AuthNotifier._isLoggedIn`（内存变量，重启后重置）做 Demo。storage_sdk 接入后只需修改 `AuthNotifier`，守卫本身无需改动：

    class AuthNotifier extends ChangeNotifier {
      bool _isLoggedIn = false;

      // 改为从安全存储读取：
      Future<void> initialize() async {
        final token = await secureStorage.read('token');
        _isLoggedIn = token != null && token.isNotEmpty;
        notifyListeners();
      }

      Future<void> login(String token) async {
        await secureStorage.write('token', token);
        _isLoggedIn = true;
        notifyListeners();
      }

      Future<void> logout() async {
        await secureStorage.delete('token');
        _isLoggedIn = false;
        notifyListeners();
      }
    }

## Presentation 层模块详解

### 4.1 Presentation 层职责

Presentation 层实现 MVVM 模式中的 ViewModel，负责：

- 管理 UI 状态
- 处理用户交互逻辑
- 调用 Repository（复杂多步编排场景可提取 UseCase）
- 数据格式转换（Entity → UI Model）
- 通知 UI 更新

### 4.2 Feature Presentation 组织

核心理念：每个 Feature 的 ViewModel 都在其对应的 Feature 目录下的 presentation/ 子目录中。

flowchart TD Presentation[Presentation Layer] --> ChatPres[Chat Feature Presentation] Presentation --> ChatListPres[Chat List Feature Presentation] Presentation --> ContactPres[Contact Feature Presentation] Presentation --> SearchPres[Search Feature Presentation] Presentation --> CallPres[Call Feature Presentation] ChatPres --> ChatVM[features/chat/presentation/
chat_view_model.dart + chat_state.dart] ChatListPres --> ChatListVM[features/chat_list/presentation/
chat_list_view_model.dart] ContactPres --> ContactVM[features/contact/presentation/
contact_view_model.dart] SearchPres --> SearchVM[features/search/presentation/
search_view_model.dart] CallPres --> CallVM[features/call/presentation/
call_view_model.dart] style Presentation fill:#fff4e6,stroke:#f57c00,stroke-width:3px style ChatPres fill:#fff9c4,stroke:#f57f17 style ChatListPres fill:#f3e5f5,stroke:#7b1fa2 style ContactPres fill:#e8f5e9,stroke:#388e3c style SearchPres fill:#fce4ec,stroke:#c2185b style CallPres fill:#e1f5ff,stroke:#0288d1

### 4.3 Presentation 层目录结构

    lib/features/
    ├── chat/
    │   └── presentation/
    │       ├── chat_view_model.dart        # 聊天状态管理（直接方法调用，副作用用 ref.listen）
    │       └── chat_state.dart            # State（@freezed 不可变状态）
    │
    ├── chat_list/
    │   └── presentation/
    │       ├── chat_list_view_model.dart   # 会话列表状态管理
    │       └── chat_list_state.dart       # State
    │
    ├── contact/
    │   └── presentation/
    │       └── contact_view_model.dart     # 联系人状态管理（简单 feature 可 State 内联）
    │
    ├── search/
    │   └── presentation/
    │       └── search_view_model.dart      # 搜索状态管理（简单 feature 可 State 内联）
    │
    └── call/
        └── presentation/
            ├── call_view_model.dart        # 通话状态管理
            └── call_state.dart            # State

### 4.4 ViewModel 设计

flowchart TD UI["UI Layer"] -->|"用户操作 vm.method"| VM["ViewModel"] VM -->|"调用"| Repo["Repository"] VM -.->|"复杂场景"| UC["UseCase（按需）"] UC -.-> Repo Repo -->|"返回 Entity"| VM VM -->|"state = newState"| UIState["UI State"] UIState -->|"ref.watch 自动刷新"| UI style UI fill:#e1f5ff,stroke:#0288d1 style VM fill:#fff4e6,stroke:#f57c00 style Repo fill:#e8f5e9,stroke:#388e3c style UC fill:#f3e5f5,stroke:#7b1fa2 style UIState fill:#fff4e6,stroke:#f57c00

#### Riverpod ViewModel 实现方式

使用 Riverpod，有两种主要的 ViewModel 实现方式：

##### 方式一：标准方式（StateNotifier + 手动定义）

    // 1. 定义 State 类（使用 freezed）
    @freezed
    class ChatState with _$ChatState {
      const factory ChatState({
        @Default([]) List<Message> messages,
        @Default(false) bool isLoading,
        @Default('') String error,
        Message? selectedMessage,
      }) = _ChatState;
    }

    // 2. 定义 ViewModel — 直接调用 Repository
    class ChatViewModel extends StateNotifier<ChatState> {
      ChatViewModel(this._chatRepository) : super(const ChatState());

      final ChatRepository _chatRepository;

      // 发送消息
      Future<void> sendMessage(String content) async {
        state = state.copyWith(isLoading: true, error: '');

        try {
          await _chatRepository.sendMessage(content);
          await loadMessages(); // 重新加载消息列表
        } catch (e) {
          state = state.copyWith(error: e.toString());
        } finally {
          state = state.copyWith(isLoading: false);
        }
      }

      // 加载消息
      Future<void> loadMessages() async {
        state = state.copyWith(isLoading: true);

        try {
          final messages = await _chatRepository.getMessages();
          state = state.copyWith(messages: messages);
        } catch (e) {
          state = state.copyWith(error: e.toString());
        } finally {
          state = state.copyWith(isLoading: false);
        }
      }

      // 选择消息
      void selectMessage(Message message) {
        state = state.copyWith(selectedMessage: message);
      }
    }

    // 3. 定义 Provider
    final chatViewModelProvider =
        StateNotifierProvider.autoDispose<ChatViewModel, ChatState>((ref) {
      return ChatViewModel(ref.watch(chatRepositoryProvider));
    });

##### 方式二：现代方式（Notifier + 代码生成）⭐ 推荐

    // 使用 riverpod_generator 和 freezed

    part 'chat_view_model.g.dart';

    @freezed
    class ChatState with _$ChatState {
      const factory ChatState({
        @Default([]) List<Message> messages,
        @Default(false) bool isLoading,
        @Default('') String error,
      }) = _ChatState;
    }

    @riverpod
    class ChatViewModel extends _$ChatViewModel {
      @override
      ChatState build() => const ChatState();

      // ViewModel 直接调用 Repository
      Future<void> sendMessage(String content) async {
        state = state.copyWith(isLoading: true);
        try {
          await ref.read(chatRepositoryProvider).sendMessage(content);
          await loadMessages();
        } catch (e) {
          state = state.copyWith(error: e.toString());
        } finally {
          state = state.copyWith(isLoading: false);
        }
      }

      Future<void> loadMessages() async {
        state = state.copyWith(isLoading: true);
        try {
          final messages = await ref.read(chatRepositoryProvider).getMessages();
          state = state.copyWith(messages: messages);
        } finally {
          state = state.copyWith(isLoading: false);
        }
      }
    }

#### UI 层使用 ViewModel

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

      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // 监听状态变化 → 自动重建 UI
        final state = ref.watch(chatViewModelProvider);
        final viewModel = ref.read(chatViewModelProvider.notifier);

        // ─── 副作用处理（替代 Effect 文件） ───
        // ref.listen() 在状态变化时触发，不会重建 Widget
        ref.listen(chatViewModelProvider.select((s) => s.error), (prev, next) {
          if (next.isNotEmpty) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(next)),
            );
          }
        });

        return Scaffold(
          appBar: AppBar(title: const Text('聊天')),
          body: Column(
            children: [
              // 消息列表
              Expanded(
                child: state.isLoading
                    ? const Center(child: CircularProgressIndicator())
                    : ListView.builder(
                        itemCount: state.messages.length,
                        itemBuilder: (context, index) {
                          final message = state.messages[index];
                          return MessageBubble(message: message);
                        },
                      ),
              ),

              // 输入框
              ChatInputArea(
                onSend: (content) => viewModel.sendMessage(content),
              ),
            ],
          ),
        );
      }
    }

> 为什么不需要 Event / Effect 文件？

> 在 BLoC 模式中，Event 用于触发状态变更，Effect 用于处理副作用（导航、Toast）。

> Riverpod 中这两个概念有更自然的替代方案：

> • Event → 直接方法调用：`viewModel.sendMessage(content)`，无需中间层

> • Effect → `ref.listen()`：监听状态字段变化，在 View 层触发导航/弹窗

### 4.5 主要 ViewModel

#### ChatViewModel

- 位置：`features/chat/presentation/chat_view_model.dart`
- 职责：消息列表状态管理、发送消息逻辑、消息加载分页、消息状态更新
- 调用：ChatRepository（直接调用）

#### ChatListViewModel

- 位置：`features/chat_list/presentation/chat_list_view_model.dart`
- 职责：会话列表状态、未读数管理、会话操作
- 调用：ChatRepository（直接调用）

#### ContactViewModel

- 位置：`features/contact/presentation/contact_view_model.dart`
- 职责：联系人列表状态、联系人搜索、联系人操作
- 调用：ContactRepository（直接调用）

#### SearchViewModel

- 位置：`features/search/presentation/search_view_model.dart`
- 职责：全局搜索状态管理、搜索历史管理
- 调用：多个 Repository（跨模块搜索）

#### CallViewModel

- 位置：`features/call/presentation/call_view_model.dart`
- 职责：通话状态管理、通话控制逻辑
- 调用：CallRepository（直接调用）

> 设计原则：每个 Feature 的 ViewModel 独立管理该 Feature 的状态，直接调用 Repository 执行数据操作。当业务逻辑复杂（多步编排、跨模块协调）时，可提取 UseCase 封装。

## Domain 层模块详解

### 5.1 Domain 层职责

Domain 层是整洁架构的核心，分为两部分：

#### Feature 专属 Domain（features/*/domain/）

- Use Cases：封装该 Feature 的业务逻辑
- Entities：该 Feature 特有的Domain 实体

#### 全局共享 Domain（domain/）

- Repository 接口：定义数据访问接口
- Value Objects：跨 Feature 的值对象

### 5.2 Domain 层架构

flowchart TD subgraph FeatureDomain[Feature 专属 Domain] ChatDomain[features/chat/domain/] ChatListDomain[features/chat_list/domain/] ContactDomain[features/contact/domain/] end subgraph GlobalDomain[全局共享 Domain] RepoInterfaces[domain/repositories/
Repository 接口定义] ValueObjects[domain/value_objects/
共享值对象] end ChatDomain --> |使用| RepoInterfaces ChatListDomain --> |使用| RepoInterfaces ContactDomain --> |使用| RepoInterfaces style FeatureDomain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style GlobalDomain fill:#e8f5e9,stroke:#388e3c,stroke-width:2px

### 5.3 Feature Domain 目录结构

    lib/features/
    ├── chat/
    │   └── domain/
    │       ├── usecases/
    │       │   ├── send_message_usecase.dart       # 发送消息
    │       │   ├── load_messages_usecase.dart      # 加载消息
    │       │   └── delete_message_usecase.dart     # 删除消息
    │       └── entities/
    │           └── message.dart                    # 消息实体
    │
    ├── chat_list/
    │   └── domain/
    │       └── usecases/
    │           ├── load_chat_list_usecase.dart     # 加载会话列表
    │           ├── update_chat_usecase.dart        # 更新会话
    │           └── delete_chat_usecase.dart        # 删除会话
    │
    ├── contact/
    │   └── domain/
    │       ├── usecases/
    │       │   ├── load_contacts_usecase.dart      # 加载联系人
    │       │   └── search_contact_usecase.dart     # 搜索联系人
    │       └── entities/
    │           └── contact.dart                    # 联系人实体
    │
    ├── search/
    │   └── domain/
    │       └── usecases/
    │           └── search_usecase.dart             # 全局搜索
    │
    └── call/
        └── domain/
            └── usecases/
                ├── initiate_call_usecase.dart      # 发起通话
                ├── answer_call_usecase.dart        # 接听通话
                └── end_call_usecase.dart           # 结束通话

### 5.4 全局 Domain 目录结构

    lib/domain/
    ├── repositories/                       # Repository 接口定义（依赖倒置）
    │   ├── message_repository.dart         # 消息仓库接口
    │   ├── chat_repository.dart            # 会话仓库接口
    │   ├── contact_repository.dart         # 联系人仓库接口
    │   ├── user_repository.dart            # 用户仓库接口
    │   └── call_repository.dart            # 通话仓库接口
    │
    └── value_objects/                      # 值对象
        ├── user_id.dart                    # 用户 ID 值对象
        ├── message_id.dart                 # 消息 ID 值对象
        └── chat_id.dart                    # 会话 ID 值对象

### 5.5 Use Case 设计（可选层）

> ⚠️ UseCase 是可选的

> 大多数 Feature 的 ViewModel 可以直接调用 Repository，无需 UseCase 中间层。

> 只在以下场景提取 UseCase：

> • 多步业务编排：如登录后需写 Token + 更新用户信息 + 上报设备

> • 跨模块协调：一个操作需要调用多个 Repository

> • 复杂业务规则：格式校验、权限判断、重试策略等

> • 多 ViewModel 复用：同一业务逻辑被多个页面调用

> 典型案例：`LoginUseCase`（登录 = 调接口 + 写 Token + 转实体，属于多步编排）

当确实需要 UseCase 时，遵循单一职责原则：

flowchart TD VM[ViewModel] -->|大多数场景| Repo[Repository Interface
domain/repositories/] VM -.->|复杂场景| UC[UseCase
features/*/usecases/] UC -->|通过接口| Repo Repo -.实现.-> RepoImpl[Repository Impl
data/repositories/] RepoImpl -->|返回| Entity[Entity] style VM fill:#fff4e6,stroke:#f57c00 style UC fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5 style Repo fill:#e8f5e9,stroke:#388e3c style RepoImpl fill:#e8f5e9,stroke:#388e3c style Entity fill:#f3e5f5,stroke:#7b1fa2

### 5.6 主要 Use Cases

#### Chat Feature Use Cases

- 位置：`features/chat/usecases/`
- `SendMessageUseCase`：发送消息
- `LoadMessagesUseCase`：加载消息
- `DeleteMessageUseCase`：删除消息

#### Chat List Feature Use Cases

- 位置：`features/chat_list/usecases/`
- `LoadChatListUseCase`：加载会话列表
- `UpdateChatUseCase`：更新会话
- `DeleteChatUseCase`：删除会话

#### Contact Feature Use Cases

- 位置：`features/contact/usecases/`
- `LoadContactsUseCase`：加载联系人
- `SearchContactUseCase`：搜索联系人

#### Call Feature Use Cases

- 位置：`features/call/usecases/`
- `InitiateCallUseCase`：发起通话
- `AnswerCallUseCase`：接听通话
- `EndCallUseCase`：结束通话

### 5.7 Repository 接口

位置：`domain/repositories/`（全局目录）

Domain 层只定义接口，不包含实现：

- `MessageRepository`：消息数据访问接口
- `ChatRepository`：会话数据访问接口
- `ContactRepository`：联系人数据访问接口
- `CallRepository`：通话数据访问接口

> 关键原则：

> 1. UseCase 按需创建：只在多步编排、跨模块协调等复杂场景使用，简单 CRUD 直接调 Repository

> 2. UseCase 在 Feature 目录：需要时，放在 features/*/usecases/ 下

> 3. Repository 接口在全局 domain/：所有 Repository 接口定义在 domain/repositories/ 下

> 4. 依赖倒置：UseCase / ViewModel 依赖 Repository 接口，不依赖具体实现

## Data 层模块详解

### 6.1 Data 层职责

Data 层是全局目录，负责：

- 实现 Domain 层（全局 domain/）定义的 Repository 接口
- 协调本地和远程数据源
- 数据缓存策略
- 数据格式转换（DTO ↔ Entity）

### 6.2 Data 层目录结构

    lib/data/
    ├── repositories/                               # Repository 实现
    │   ├── message_repository_impl.dart            # 实现 MessageRepository 接口
    │   ├── chat_repository_impl.dart               # 实现 ChatRepository 接口
    │   ├── contact_repository_impl.dart            # 实现 ContactRepository 接口
    │   ├── user_repository_impl.dart               # 实现 UserRepository 接口
    │   └── call_repository_impl.dart               # 实现 CallRepository 接口
    │
    ├── local/                                      # 本地数据源
    │   ├── message_local_ds.dart                   # 消息本地数据源
    │   ├── chat_local_ds.dart                      # 会话本地数据源
    │   ├── contact_local_ds.dart                   # 联系人本地数据源
    │   ├── user_local_ds.dart                      # 用户本地数据源
    │   │
    │   ├── drift/                                  # Drift 数据库
    │   │   ├── app_database.dart                   # Drift 数据库定义
    │   │   ├── app_database.g.dart                 # Drift 生成代码
    │   │   # database_connection.dart 已迁移至 storage_sdk（数据库连接与 Isolate 生命周期由 SDK 层统一管理）
    │   │   ├── tables/                             # 表定义
    │   │   │   ├── message_table.dart              # 消息表
    │   │   │   ├── conversation_table.dart         # 会话表
    │   │   │   └── user_table.dart                 # 用户表
    │   │   ├── daos/                               # 数据访问对象
    │   │   │   ├── message_dao.dart                # 消息 DAO
    │   │   │   ├── conversation_dao.dart           # 会话 DAO
    │   │   │   └── user_dao.dart                   # 用户 DAO
    │   │   ├── migrations/                         # 数据库迁移
    │   │   │   ├── migration_v1.dart               # V1 迁移脚本
    │   │   │   └── migration_runner.dart           # 迁移执行器
    │   │   └── mappers/                            # DB ↔ DTO 映射
    │   │       ├── message_mapper.dart             # 消息映射
    │   │       └── conversation_mapper.dart        # 会话映射
    │   │
    │   └── storage/                                # 其他本地存储
    │       ├── preference_storage.dart             # SharedPreferences 封装
    │       ├── file_storage.dart                   # 文件存储管理
    │       └── image_cache.dart                    # 图片缓存
    │
    ├── remote/                                     # Request 文件（一个端点一个文件，Repository 直接调 NetworksSdkApi）
    │   ├── login_request.dart                      # 登录端点
    │   ├── logout_request.dart                     # 登出端点
    │   ├── send_message_request.dart               # 发消息端点
    │   └── ...                                     # 其他端点 Request 文件
    │
    ├── cache/                                      # 缓存
    │   ├── cache_manager.dart                      # 缓存管理器
    │   └── cache_policies.dart                     # 缓存策略
    │
    └── models/                                     # DTO（统一归口，local / remote 共用）
        ├── message_dto.dart                        # 消息 DTO
        ├── conversation_dto.dart                   # 会话 DTO
        ├── user_dto.dart                           # 用户 DTO
        ├── contact_dto.dart                        # 联系人 DTO
        └── call_dto.dart                           # 通话 DTO

### 6.3 Repository 实现

flowchart TD Domain[Domain Layer
domain/repositories/
Repository 接口] -.实现.-> Repo[Data Layer
data/repositories/
Repository 实现] Repo -->|读取| LocalDS[Local DataSource
data/local/] Repo -->|请求| SdkApi[NetworksSdkApi
networks_sdk] Repo -->|缓存| Cache[Cache Manager
data/cache/] LocalDS -->|Drift| DB[(Database)] SdkApi -->|HTTP/WebSocket| API[API Server] Cache -->|内存| Memory[Memory Cache] style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Repo fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style LocalDS fill:#c8e6c9,stroke:#388e3c style SdkApi fill:#c8e6c9,stroke:#388e3c style Cache fill:#c8e6c9,stroke:#388e3c

### 6.4 Data Source 详解

#### Local DataSource（data/local/）

- Drift 数据库访问：使用 packages/storage_sdk 提供的 Drift 封装
- 本地文件存储：媒体文件、缓存文件
- Secure Storage：敏感数据（token、密钥）

    // 示例：MessageLocalDataSource
    class MessageLocalDataSource {
      final DriftSDK _db;

      Future<List<MessageDTO>> getMessages(String chatId) {
        return _db.query('messages', where: 'chat_id = ?', whereArgs: [chatId]);
      }
    }

#### Request 文件（data/remote/）

- 一个端点 = 一个文件：Response DTO + Request 类放在同一文件中
- Repository 直接调 NetworksSdkApi：无需 RemoteDataSource 中间层
- @ApiRequest 注解 + 代码生成：自动实现 path / method / toJson / fromJson 注册（Request 无需 @JsonSerializable）

    // 示例：Repository 直接调用 Request
    // data/repositories/message_repository_impl.dart
    class MessageRepositoryImpl implements MessageRepository {
      final NetworksSdkApi _client;

      Future<SendMessageResponse?> sendMessage({
        required String chatId,
        required String content,
      }) {
        return _client.executeRequest(
          SendMessageRequest(chatId: chatId, content: content),
        );
      }
    }

#### Cache Manager（data/cache/）

- 内存缓存：频繁访问的数据
- 缓存策略：TTL、LRU 等策略
- 缓存失效：自动或手动失效

### 6.5 DTO Models（data/models/）

Data Transfer Objects 用于数据传输：

- 与 API 数据结构对应：JSON 序列化/反序列化
- 与 Entity 互相转换：DTO ↔ Entity 转换方法
- 包含数据库映射：Drift 表结构映射

    // 示例：MessageDTO
    class MessageDTO {
      final String id;
      final String content;
      final String chatId;
      final DateTime timestamp;

      // 从 JSON 反序列化
      factory MessageDTO.fromJson(Map<String, dynamic> json) { ... }

      // 转换为 Entity
      Message toEntity() {
        return Message(
          id: MessageId(id),
          content: content,
          chatId: ChatId(chatId),
          timestamp: timestamp,
        );
      }

      // 从 Entity 转换
      factory MessageDTO.fromEntity(Message entity) { ... }
    }

### 6.6 数据流转

flowchart LR UC[UseCase] -->|调用| RepoInterface[Repository Interface
domain/repositories/] RepoInterface -.实现.-> RepoImpl[Repository Impl
data/repositories/] RepoImpl -->|1. 检查缓存| Cache[Cache] RepoImpl -->|2. 读取本地| LocalDS[Local DS] RepoImpl -->|3. 请求远程| SdkApi2[NetworksSdkApi] SdkApi2 -->|DTO| RepoImpl RepoImpl -->|转换| Entity[Entity] Entity -->|返回| UC style RepoInterface fill:#f3e5f5,stroke:#7b1fa2 style RepoImpl fill:#e8f5e9,stroke:#388e3c style UC fill:#f3e5f5,stroke:#7b1fa2 style Entity fill:#f3e5f5,stroke:#7b1fa2

> 关键原则：

> 1. Data 层是全局目录：所有 Repository 实现都在 data/repositories/ 下

> 2. 实现全局接口：实现 domain/repositories/ 中定义的接口

> 3. 统一数据源管理：按类型组织（local/remote/cache），不按 Feature 划分

> 4. DTO 与 Entity 分离：DTO 在 Data 层，Entity 在 Domain 层

## Core 层模块详解

### 7.1 Core 层职责

Core 层保留主 App 内部两部分，类似 Apple 的 CoreFoundation / CoreUI 的关系：

- core/foundation/（Core Foundation）—— 应用级非 UI 基础设施：常量、配置、异常、日志、类型、工具函数、扩展
- core/ui/（Core UI）—— UI 基础设施：基础定义（颜色/字体/间距/主题）、基础组件（按钮/输入框/头像）、业务组合组件（弹窗/面板/Toast）

> 多语言国际化（l10n）已提取为独立 Package（`packages/l10n_sdk`），与其他 SDK 统一由 Melos 管理。

依赖方向（严格单向）：core/ui/ → l10n_sdk → core/foundation/。foundation 是最底层；l10n_sdk 仅依赖 foundation（持久化语言偏好）；ui 可依赖 l10n_sdk（组件内置文案）和 foundation。

#### core/foundation/（Core Foundation）包含：

- 常量定义（constants）
- 应用配置（config）
- 统一异常体系（errors）
- 日志系统（logger）
- 基础类型（types）
- 工具函数（utils）
- Dart 扩展（extensions）

> 注意：网络/存储/加密/媒体/RTC/推送/协议/原生通信/多语言国际化等 SDK 能力已提取为独立 Package（`packages/*_sdk`），由 Melos 统一管理，主 App 通过 `pubspec.yaml` 引用。详见「Mono-Repo 架构」章节和下方「7.3 核心 SDK（独立 Package）」。

### 7.2 基础设施

#### Constants（core/foundation/constants/）

- `app_constants.dart`：应用级常量（分页大小、超时时间等）
- `db_constants.dart`：数据库相关常量（表名、字段名等）
- `socket_constants.dart`：Socket 相关常量（事件名、重连策略等）
- `api_constants.dart`：API 相关常量（Base URL、端点路径等）

#### Config（core/foundation/config.dart）

- `AppConfig`：编译期注入的配置聚合类，通过 `AppConfig.current` 获取当前环境配置
- 每个字段用 `bool.fromEnvironment` / `String.fromEnvironment` 独立读取，新增字段只需在 `config.json` 和此类各加一行
- `isDebug`：核心环境标志（`true`=dev，`false`=prod），由 CI 脚本在打包前写入 `config/config.json`
- `feature_flags.dart`：功能开关（灰度 / A/B 实验，后续单独提取）

#### Errors（core/foundation/errors/）

- `app_exception.dart`：统一异常基类
- `network_exception.dart`：网络异常（超时、断连等）
- `database_exception.dart`：数据库异常
- `auth_exception.dart`：认证异常（Token 过期、未授权等）
- `error_mapper.dart`：异常 → 错误码 / 错误键映射（UI 层通过 l10n 将错误键转为本地化文案）

#### Logger（core/foundation/logger/）

- `app_logger.dart`：统一日志门面
- `log_printer.dart`：日志格式化输出
- `log_interceptor.dart`：网络请求日志拦截器

#### Types（core/foundation/types/）

- `result.dart`：Result<T> 类型（Success / Failure）
- `either.dart`：Either<L, R> 类型
- `typedefs.dart`：全局类型别名
- `unit.dart`：Unit 类型（无返回值占位）

### 7.3 核心 SDK（独立 Package）

以下 SDK 均为 `packages/` 下的 Flutter Plugin（含 Android/iOS 原生代码入口），主 App 通过 `import 'package:xxx_sdk/xxx_sdk.dart'` 引用。所有 SDK 遵循 Facade + Wiring 内部架构（见「SDK 内部架构」章节）。

#### Networks SDK（packages/networks_sdk/）

HTTP + WebSocket 客户端 SDK（Flutter Plugin 声明，无实际 Native 代码）。App 层通过 `import 'package:networks_sdk/networks_sdk.dart'` 引用，遵循 Facade + Wiring 内部架构。

##### Package 目录结构

    packages/networks_sdk/
    ├── pubspec.yaml
    ├── build.yaml                              # @ApiRequest 代码生成器注册
    └── lib/
        ├── networks_sdk.dart                   # barrel file（统一导出，见下方「导出清单」）
        └── src/
            ├── annotations/
            │   └── api_request.dart            # @ApiRequest 注解定义
            ├── generator/
            │   ├── api_request_generator.dart  # build_runner 代码生成器实现
            │   └── builder.dart                # SharedPartBuilder 入口
            ├── data/
            │   ├── datasources/
            │   │   ├── http/
            │   │   │   ├── api_client.dart              # Dio REST 客户端
            │   │   │   ├── token_refresh_manager.dart   # Token 刷新管理（竞态安全 + 超时 + 时间窗口复用 + 主动刷新）
            │   │   │   └── interceptor/
            │   │   │       ├── auth_interceptor.dart        # Token + 默认 header 注入
            │   │   │       ├── encryption_interceptor.dart  # 请求加密 / 响应解密（预留给 cipher_guard_sdk）
            │   │   │       ├── retry_interceptor.dart       # Token 刷新 + 瞬态错误重试 + 业务错误钩子
            │   │   │       └── logging_interceptor.dart     # 请求/响应日志
            │   │   ├── socket/
            │   │   │   └── socket_client.dart           # WebSocket 长连接（心跳/重连/Stream/Token 热更新/加密钩子）
            │   │   └── networks_sdk_method_channel_datasource.dart  # 统一执行入口（executeRequest / executeDownload）
            │   ├── dto/
            │   │   ├── api_requestable.dart         # 请求基类 + fromJson 注册表 + 解码扩展
            │   │   └── api_response_wrapper.dart    # { code, message/msg, data } 响应包装解析
            │   └── repositories/
            │       ├── networks_sdk_repository_impl.dart
            │       └── networks_messaging_repository_impl.dart
            ├── domain/
            │   ├── entities/
            │   │   ├── api_error.dart               # @freezed HTTP 错误联合类型（7 变体，含 cancelled）
            │   │   ├── encrypted_request.dart       # 加密请求结果数据类（path / headers / body 覆盖）
            │   │   ├── socket_error.dart             # @freezed WebSocket 错误联合类型
            │   │   ├── socket_connection_state.dart  # 连接状态 enum
            │   │   ├── http_method.dart              # GET / POST / PUT / DELETE / PATCH
            │   │   └── api_request_type.dart         # request / login / upload / stream / download
            │   └── repositories/
            │       ├── networks_sdk_repository.dart
            │       └── networks_messaging_repository.dart
            └── presentation/
                ├── facade/
                │   ├── networks_sdk_api.dart         # HTTP 公开 API 接口（含 executeDownload）
                │   └── networks_messaging_api.dart   # WebSocket 公开 API 接口（含 updateToken / sendBytes）
                └── wiring/
                    ├── api_config.dart               # HTTP 配置（baseURL / token / 回调 / 加密 / 重试 / 主动刷新）
                    ├── socket_config.dart            # WebSocket 配置（心跳 / 重连 / 加密钩子 / 压缩）
                    ├── network_callbacks.dart        # 回调类型定义（认证 / 加密 / 业务错误 / 下载进度 / WS 加密）
                    ├── networks_sdk_core.dart
                    ├── networks_sdk_api_impl.dart
                    ├── networks_messaging_api_impl.dart
                    └── networks_sdk_wiring.dart      # 工厂：build() / buildMessagingApi()

##### SDK 与 App 层边界

职责 | SDK (networks_sdk) | App 层 (im_app)
--- | --- | ---
Dio 管理 | ApiClient 内部创建管理 | 通过 NetworksSdkWiring.build(config:) 创建
baseURL | ApiConfig.baseURL | AppConfig.apiBaseUrl 提供初始值
Token 存储 | ApiConfig.token（内存） | 安全存储、持久化
Token 刷新 | 检测过期 → 调 onTokenRefresh | 提供回调实现
强制登出 | 检测条件 → 调 onForceLogout | 提供回调（清状态、跳转登录）
错误码定义 | 通用 code != 0 判断 | 定义具体业务码传入
请求定义 | ApiRequestable 协议 + @ApiRequest 注解 | 各 feature 实现具体 Request
Upload | uploadData getter + FormData/Uint8List 支持 | override uploadData + decodeResponse
WebSocket 连接 | SocketClient 内部管理（连接/心跳/重连） | 调 connect/disconnect/send
WebSocket 心跳 | 双层心跳自动管理（底层 ping 5s + 应用层 10s） | 无需关心
WebSocket 重连 | 指数退避自动重连（1s→2s→4s→8s→16s→30s） | 无需关心
WebSocket 生命周期 | 提供 onEnterForeground/Background | App 层调用（AppLifecycleListener）。本项目 disconnectInBackground=false，所有平台后台保活、心跳不停
WebSocket 消息解析 | JSON.decode → Stream 输出 | App 层按 type 过滤 + DTO 解析
Riverpod | 无依赖 | Provider 包装 NetworksSdkApi / SocketClient

##### 命名规范（全链路一致性）

从 Request 文件到 Domain Entity，所有文件命名必须遵循统一规则，方便区分职责和业务模块。

层级 | 文件命名 | 类命名 | 示例
--- | --- | --- | ---
接口定义 | `{action}_request.dart` | Request: `{Action}Request` Response DTO: `{Action}Response` | `login_request.dart` → `LoginRequest` + `LoginResponse`
持久化 DTO | `data/models/{entity}_dto.dart` | `{Entity}Dto` | `user_dto.dart` → `UserDto`
Repository 接口 | `domain/repositories/{module}_repository.dart` | `{Module}Repository` | `auth_repository.dart` → `AuthRepository`
Repository 实现 | `data/repositories/{module}_repository_impl.dart` | `{Module}RepositoryImpl` | `auth_repository_impl.dart` → `AuthRepositoryImpl`
Domain Entity | `domain/entities/{entity}.dart` | `{Entity}` | `user.dart` → `User`
UseCase（按需） | `features/{module}/usecases/{action}_usecase.dart` | `{Action}UseCase` | `login_usecase.dart` → `LoginUseCase`

关键规则：

- 一个端点 = 一个 Request 文件：Response DTO + Request 类放在同一文件中
- Response DTO 是纯 Dart 类：零注解、零 `factory fromJson`；只需字段 + 构造函数，`toEntity()` 按需添加
- 持久化 DTO 和 Response DTO 分开：Response DTO（`XxxResponse`）在 request 文件中，持久化 DTO（`XxxDto`）在 `data/models/`
- 禁止跳层：ViewModel → Repository（→ UseCase 按需）→ NetworksSdkApi，每层职责明确

##### 傻瓜式教程：从零开始定义并发送一个接口

前置条件（只需做一次）

打开一个独立终端窗口，在项目根目录执行以下命令并保持运行，不要关闭：

    melos run gen:watch

这个命令会监听你的代码变化，每次保存 .dart 文件后自动生成 .g.dart。
整个开发期间这个终端窗口必须常驻，不要关。

以「登录接口」为完整示例。假设后端给你的接口文档是：

    POST /auth/login
    请求体：{ "email": "xxx", "password": "xxx" }
    响应体：{ "code": 0, "message": "ok", "data": { "token": "xxx", "user_id": "123", "email": "xxx", "nickname": "xxx", "avatar": "xxx" } }

你需要做 4 步，每一步都告诉你在哪个文件写什么。

###### 第 1 步：创建接口定义文件

在哪创建：`lib/data/remote/` 目录下

文件名：`login_request.dart`（以端点名命名，一个端点 = 一个文件）

规则：Response DTO + Request 放在同一个文件里。打开这一个文件就能看到请求参数和响应结构。

第 1.1 步：写文件头

    import 'package:json_annotation/json_annotation.dart';
    import 'package:networks_sdk/networks_sdk.dart';

    import '../../../core/foundation/api_paths.dart';  // API 路径常量
    import '../../../domain/entities/user.dart';        // Domain Entity（后面 toEntity 要用）

    part 'login_request.g.dart';  // 这行必须写！指向即将自动生成的文件

`part 'login_request.g.dart';` 写完后 IDE 会短暂报红（因为 .g.dart 还没生成）。如果 `watch` 模式已启动，保存文件后几秒内红线会自动消失。

第 1.2 步：写 Response DTO（服务端返回什么字段，就写什么字段）

    // ── Response DTO ──

    /// 服务端返回的登录数据
    /// 纯 Dart 类，零注解，零样板。
    /// _$LoginResponseFromJson 由 ApiResponseGenerator 从 @ApiRequest(responseType: LoginResponse) 自动推导生成，
    /// 无需手动添加任何注解或 factory fromJson。
    class LoginResponse {
      final String token;                      // 服务端返回的字段
      @JsonKey(name: 'user_id')               // 服务端字段名是 user_id，Dart 字段名是 userId
      final String userId;
      final String email;
      final String? nickname;                  // 可选字段用 String?
      final String? avatar;

      const LoginResponse({                    // 构造函数，参数和字段一一对应
        required this.token,
        required this.userId,
        required this.email,
        this.nickname,
        this.avatar,
      });
      // 不需要 factory fromJson，不需要 toJson，不需要任何注解
      // 生成器自动在 login_request.g.dart 中生成 _$LoginResponseFromJson 私有函数

      /// DTO → Domain Entity（把网络层数据转为业务层数据）
      User toEntity() {
        return User(
          id: userId,
          email: email,
          nickname: nickname,
          avatar: avatar,
        );
      }
    }

第 1.3 步：在 ApiPaths 中添加路径常量

文件：`lib/core/foundation/api_paths.dart`（全局路径常量表，所有接口路径都在这里统一管理）

    class ApiPaths {
      ApiPaths._();

      // ── Auth ──
      static const authLogin = '/auth/login';          // ← 新接口在这里加一行
      static const authRefreshToken = '/auth/refresh-token';
      static const authLogout = '/auth/logout';

      // ── User ──
      static const userProfile = '/user/profile';
      // ...
    }

新增接口时，先在这里加路径常量，然后在 Request 中引用 `ApiPaths.xxx`。
好处：全局搜索 `ApiPaths.` 就能看到所有接口列表，路径变更只改一处。

第 1.4 步：写 Request（你要发什么参数给服务端）

    // ── Request ──

    @ApiRequest(                               // ← 这个注解让 build_runner 自动生成 path / method 等
      path: ApiPaths.authLogin,                //    路径常量，定义在 core/foundation/api_paths.dart
      method: HttpMethod.post,                 //    HTTP 方法，从接口文档抄
      responseType: LoginResponse,             //    响应类型，就是上面写的 LoginResponse
      requestType: ApiRequestType.login,       //    login 类型不携带 Token（登录前还没有 Token）
    )
    class LoginRequest extends ApiRequestable<LoginResponse>  // ← 固定写法：extends ApiRequestable<响应类型>
        with _$LoginRequestApi {                              // ← 固定写法：with _$类名Api（短暂报红，保存后自动消失）
      final String email;                      // 请求参数：要发给服务端的字段
      final String password;

      LoginRequest({required this.email, required this.password});
      // 完毕！toJson 由 _$LoginRequestApi mixin 从类字段自动生成，不需要 @JsonSerializable，不需要手写 toJson
    }

保存文件。后台的 `melos run gen:watch` 自动检测到变化，几秒后生成 `login_request.g.dart`，所有红线消失。

命名规则速查（写之前就能确定引用名）

你写的类名 | fromJson（私有函数） | toJson | Api mixin | 来源
--- | --- | --- | --- | ---
`LoginResponse` | `_$LoginResponseFromJson` | -（不需要） | - | ApiResponseGenerator 自动推导
`LoginRequest` | -（不需要） | 由 mixin 自动生成 | `_$LoginRequestApi` | ApiRequestGenerator 生成 mixin
`SendMessageRequest` | -（不需要） | 由 mixin 自动生成 | `_$SendMessageRequestApi` | ApiRequestGenerator 生成 mixin

规则：Response DTO 类名拼 `_$` + 类名 + `FromJson`（私有，只在 .g.dart 内部使用）；Request 类名拼 `_$` + 类名 + `Api`（mixin）。

###### 第 2 步：在 Repository 中调用 NetworksSdkApi，转为 Domain Entity

在哪写：`lib/data/repositories/auth_repository_impl.dart`

做什么：调 NetworksSdkApi.executeRequest → 拿到 DTO → 回调写 Token → 转为 Domain Entity → 返回。

    import 'package:networks_sdk/networks_sdk.dart';
    import '../../domain/entities/user.dart';
    import '../../domain/repositories/auth_repository.dart';
    import '../remote/login_request.dart';

    class AuthRepositoryImpl implements AuthRepository {
      final NetworksSdkApi _client;                   // ← 注入 Facade 接口
      final void Function(String?) _onTokenUpdate;    // ← 回调，由 Provider 层组合

      AuthRepositoryImpl({
        required NetworksSdkApi client,
        required void Function(String?) onTokenUpdate,
      })  : _client = client,
            _onTokenUpdate = onTokenUpdate;

      @override
      Future<User> login({
        required String email,
        required String password,
      }) async {
        // 1. 调 NetworksSdkApi，构造请求 → 发 HTTP → 自动解码 → 返回 DTO
        final LoginResponse? loginResponse = await _client.executeRequest(
          LoginRequest(email: email, password: password),
        );

        if (loginResponse == null) {
          throw Exception('Login failed: empty response');
        }

        // 2. 回调写入 Token（内存 + 持久化由 Provider 层组合）
        _onTokenUpdate(loginResponse.token);

        // 3. DTO → Domain Entity，返回给上层
        return loginResponse.toEntity();
      }
    }

###### 第 3 步：注册 Provider + 编写 ViewModel

3.1 注册 Provider（DI 装配）

在哪写：`lib/features/{模块}/di/{模块}_providers.dart`

做什么：在 Feature 目录下创建 Provider 文件，注册该模块的 DI 链路（Repository → UseCase 按需）。`app/di/` 只提供 SDK 基础设施（ApiConfig / NetworksSdkApi），业务模块的 Provider 内聚在 Feature 目录下。

    // ── features/auth/di/auth_providers.dart ──

    // Repository（注入 Facade 接口 + 回调组合多个 SDK 能力）
    final authRepositoryProvider = Provider<AuthRepository>((ref) {
      final apiConfig = ref.read(apiConfigProvider);
      return AuthRepositoryImpl(
        client: ref.read(networkSdkApiProvider),       // 注入 Facade 接口
        onTokenUpdate: (token) {
          apiConfig.updateToken(token);               // 内存（networks_sdk）
          // secureStorage.saveToken(token);          // 持久化（storage_sdk，待接入）
        },
      );
    });

    // UseCase（登录有多步编排 — 校验 + 登录 + WS 连接 + DB 开库 + 用户持久化）
    final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
      return LoginUseCase(
        authRepository: ref.read(authRepositoryProvider),
        socketManager:  ref.read(socketManagerProvider),
        apiConfig:      ref.read(apiConfigProvider),
        storageApi:     ref.read(storageSdkProvider),
        userRepository: ref.read(userRepositoryProvider),
      );
    });

新模块示例：添加「消息」模块的 Provider

假设你新建了 `MessageRepositoryImpl`，需要注册 Provider：

创建 `lib/features/chat/di/chat_providers.dart`：

    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import '../../../app/di/network_provider.dart';
    import '../../../data/repositories/message_repository_impl.dart';
    import '../../../domain/repositories/message_repository.dart';

    // ── Repository ──
    final messageRepositoryProvider = Provider<MessageRepository>((ref) {
      return MessageRepositoryImpl(
        client: ref.read(networkSdkApiProvider),    // 注入 Facade 接口
      );
    });

    // 大多数模块只需到 Repository 这一层，ViewModel 直接调 Repository 即可。
    // 如需 UseCase（多步编排、跨模块协调），参考 auth_providers.dart 中的 loginUseCaseProvider。

原则：`app/di/` 只放 SDK 基础设施（ApiConfig / NetworksSdkApi），业务模块的 DI 链路（Repository → UseCase 按需）内聚在 `features/{模块}/di/{模块}_providers.dart` 中。

3.2 编写 ViewModel

在哪写：`lib/features/auth/presentation/login_view_model.dart`

常规写法：ViewModel 直接调 Repository

    @riverpod
    class LoginViewModel extends _$LoginViewModel {
      @override
      LoginState build() => const LoginState();

      Future<void> login(String email, String password) async {
        state = state.copyWith(isLoading: true);

        try {
          // 直接调 Repository
          final user = await ref.read(authRepositoryProvider).login(
            email: email,
            password: password,
          );

          state = state.copyWith(user: user, isLoading: false);
        } on ApiError catch (e) {
          state = state.copyWith(error: e.displayMessage, isLoading: false);
        }
      }
    }

进阶写法：有 UseCase 时（如登录需格式校验 + 多步编排）

    @riverpod
    class LoginViewModel extends _$LoginViewModel {
      @override
      LoginState build() => const LoginState();

      Future<void> login(String email, String password) async {
        state = state.copyWith(isLoading: true);

        try {
          // 通过 UseCase 调用（格式校验 → 登录 → 返回 User）
          final user = await ref.read(loginUseCaseProvider).execute(
            email: email,
            password: password,
          );

          state = state.copyWith(user: user, isLoading: false);
        } on FormatException catch (e) {
          // 格式校验失败（UseCase 层抛出）
          state = state.copyWith(error: e.message, isLoading: false);
        } on ApiError catch (e) {
          // 网络错误统一处理
          state = state.copyWith(error: e.displayMessage, isLoading: false);
        }
      }
    }

View 层只需 `ref.watch(loginViewModelProvider)`，状态变化时自动刷新 UI。

###### 完整数据流（从用户点击按钮到 UI 刷新）

常规路径：ViewModel → Repository（大多数场景）

    用户点击按钮
      → View: vm.doSomething(...)
        → ViewModel: ref.read(xxxRepositoryProvider).doSomething(...)
          → RepositoryImpl.doSomething()                               // data/repositories/
            → _client.executeRequest(XxxRequest)                       // 调 NetworksSdkApi
              → 自动注入 header → HTTP 请求 → 自动解码 → DTO
            → dto.toEntity() → Domain Entity
        ← state = state.copyWith(...)                                  // 更新状态
      ← View: ref.watch → 自动 rebuild                                // 自动刷新

进阶路径：ViewModel → UseCase → Repository（登录等复杂场景）

    用户点击「登录」按钮
      → View: vm.login(email, password)
        → ViewModel: ref.read(loginUseCaseProvider).execute(...)
          → LoginUseCase: 格式校验（邮箱 + 密码）                      // features/auth/usecases/
          → LoginUseCase: authRepository.login(...)
            → AuthRepositoryImpl.login()                               // data/repositories/
              → _client.executeRequest(LoginRequest)                   // 调 NetworksSdkApi
                → Auth → Encryption → Dio.request → Retry → Logging // 拦截器链自动处理
              ← request.decodeResponse → _$LoginResponseFromJson      // 自动解码（生成的私有函数）
            ← LoginResponse（DTO）
            → onTokenUpdate(token)                                    // 回调：内存写入 + 持久化
            ← loginResponse.toEntity() → User（Domain Entity）
          ← User
        ← state = state.copyWith(user: user)                         // 更新状态
      ← View: ref.watch → 自动 rebuild → UI 显示用户信息              // 自动刷新

你只写了：接口定义文件 + Repository 一个方法 + ViewModel 一个方法。
网络请求、header 注入、token 管理、响应解码、错误处理、JSON 序列化 —— 全部自动完成，你不需要写任何一行。

###### 再来一个：发送消息接口（POST /chat/send-message）

后端接口文档：

    POST /chat/send-message
    请求体：{ "chat_id": "xxx", "content": "hello" }
    响应体：{ "code": 0, "data": { "message_id": "456", "timestamp": 1700000000 } }

你只需创建一个文件：`lib/data/remote/send_message_request.dart`，然后在 Repository 中调用即可。

    import 'package:networks_sdk/networks_sdk.dart';

    part 'send_message_request.g.dart';

    // ── Response DTO（纯 Dart 类，零注解，_$SendMessageResponseFromJson 由生成器自动提供）──

    class SendMessageResponse {
      @JsonKey(name: 'message_id')
      final String messageId;
      final int timestamp;

      const SendMessageResponse({required this.messageId, required this.timestamp});
    }

    // ── Request ──

    @ApiRequest(
      path: ApiPaths.chatSendMessage,           // 路径常量，定义在 api_paths.dart
      method: HttpMethod.post,                 // HTTP 方法
      responseType: SendMessageResponse,       // 响应类型
      // requestType 不写，默认 ApiRequestType.request（会携带 Token）
    )
    class SendMessageRequest extends ApiRequestable<SendMessageResponse>
        with _$SendMessageRequestApi {
      @JsonKey(name: 'chat_id')               // JSON 字段名和 Dart 字段名不一样时用 @JsonKey
      final String chatId;
      final String content;

      SendMessageRequest({required this.chatId, required this.content});
      // toJson 自动生成：{'chat_id': chatId, 'content': content}
    }

保存 → 自动生成 → 然后在 Repository 中调 NetworksSdkApi 就完了：

    // 在 MessageRepositoryImpl 中添加
    Future<SendMessageResponse?> sendMessage({
      required String chatId,
      required String content,
    }) {
      return _client.executeRequest(
        SendMessageRequest(chatId: chatId, content: content),
      );
    }

###### 再来一个：获取用户资料（GET /user/profile）

    GET /user/profile          ← 无 query 参数，靠 Authorization token 标识当前用户
    响应体：{ "code": 0, "data": { "user_id": "123", "email": "tom@example.com", "nickname": "Tom", "avatar": "https://..." } }

    // lib/data/remote/get_profile_request.dart

    import 'package:networks_sdk/networks_sdk.dart';

    part 'get_profile_request.g.dart';

    // ── Response DTO（纯 Dart 类，零注解，_$ProfileResponseFromJson 由生成器自动提供）──
    class ProfileResponse {
      @JsonKey(name: 'user_id')
      final String userId;
      final String email;
      final String? nickname;
      final String? avatar;

      const ProfileResponse({required this.userId, required this.email, this.nickname, this.avatar});

      User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
    }

    @ApiRequest(
      path: ApiPaths.userProfile,
      method: HttpMethod.get,                  // ← GET 请求，toJson() 结果作为 query string
      responseType: ProfileResponse,
    )
    class GetProfileRequest extends ApiRequestable<ProfileResponse>
        with _$GetProfileRequestApi {
      GetProfileRequest();  // 无参数 — token 标识当前用户，无需显式传 user_id
      // toJson 自动生成空 map {}
    }

###### 无响应数据的接口（POST /auth/logout）

有些接口不返回 data 字段，只有 `{"code": 0, "message": "ok"}`。这种情况用 `ApiRequestable<void>`：

    // lib/data/remote/logout_request.dart

    import 'package:networks_sdk/networks_sdk.dart';

    // 无 Response DTO → 泛型写 void
    // 不需要 @ApiRequest 注解 → 直接实现 ApiRequestable（最简写法）
    class LogoutRequest extends ApiRequestable<void> {
      @override
      String get path => ApiPaths.authLogout;
      @override
      HttpMethod get method => HttpMethod.post;
      @override
      Map<String, dynamic> toJson() => {};  // 无请求体
    }

Repository 调用时直接 `await`：

    Future<void> logout() async {
      await _client.executeRequest(LogoutRequest());  // 返回 null，直接 await 即可
    }

###### 文件上传 — 两种模式

上传与普通请求的核心区别：

对比项 | 普通请求 | Upload 请求
--- | --- | ---
数据来源 | `toJson()` → JSON body | `uploadData` → FormData / Uint8List
requestType | `request`（默认） | `upload`
parameters | 有值（自动序列化） | 返回 null（SDK 自动跳过）
响应解码 | 标准 `{ code, msg, data }` | 可能需要 override `decodeResponse`

模式 A：FormData 上传到自有后端

    // lib/data/remote/upload_file_request.dart

    @ApiRequest(
      path: ApiPaths.uploadFile,
      method: HttpMethod.post,
      responseType: UploadResult,
      requestType: ApiRequestType.upload,  // ← 关键：标记为 upload
    )
    class UploadFileRequest extends ApiRequestable<UploadResult>
        with _$UploadFileRequestApi {
      final String filePath;
      final String? fileName;

      UploadFileRequest({required this.filePath, this.fileName});

      @override
      Map<String, dynamic> toJson() => {};  // upload 不走 toJson

      /// FormData — SDK 通过 uploadData 获取上传数据
      @override
      Object? get uploadData => FormData.fromMap({
        'file': MultipartFile.fromFileSync(filePath, filename: fileName),
      });
    }

模式 B：二进制上传到 S3 presigned URL

先向后端获取 presigned URL，再直接上传到 S3：

    class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
      final Uint8List data;        // 二进制文件数据
      final String presignedURL;   // 后端返回的 S3 签名 URL

      S3UploadRequest({required this.data, required this.presignedURL});

      @override
      String get path => presignedURL;   // ← 完整 URL，SDK 检测到 http 开头不拼 baseURL
      @override
      HttpMethod get method => HttpMethod.put;
      @override
      ApiRequestType get requestType => ApiRequestType.upload;
      @override
      Map<String, String>? get customHeaders => {'Content-Type': 'application/octet-stream'};
      @override
      Map<String, dynamic> toJson() => {};
      @override
      Object? get uploadData => data;  // Uint8List 直接作为 body

      /// S3 返回 204 No Content 或 XML，不是标准 { code, msg, data } 响应格式
      /// 必须 override decodeResponse
      @override
      S3UploadResponse? decodeResponse(Response response) {
        if (response.statusCode != null &&
            response.statusCode! >= 200 &&
            response.statusCode! < 300) {
          return const S3UploadResponse(success: true);
        }
        return const S3UploadResponse(success: false);
      }
    }

###### HTTP 方法速查表

方法 | 示例接口 | 参数传递方式 | 注解 / 手写
--- | --- | --- | ---
GET | `GET /user/profile?user_id=123` | `toJson()` → URL query parameters | @ApiRequest
POST | `POST /auth/login` | `toJson()` → JSON body | @ApiRequest
POST（无响应） | `POST /auth/logout` | `toJson()` → JSON body → 返回 null | 手写（简单场景）
Upload（FormData） | `POST /upload/file` | `uploadData` → FormData | @ApiRequest + override uploadData
Upload（S3） | `PUT presigned-url` | `uploadData` → Uint8List | 手写 + override decodeResponse
PUT / PATCH | `PUT /user/profile` | `toJson()` → JSON body | @ApiRequest（同 POST）
DELETE | `DELETE /message/:id` | `toJson()` → JSON body 或 query | @ApiRequest 或手写

##### App 层初始化配置（已由脚手架创建，通常不需要修改）

文件位置：`app/di/network_provider.dart`（本文件只提供 SDK 基础设施，不放业务 Provider）

    /// API 配置 Provider（全局单例）
    /// baseURL 来自 config.json → --dart-define-from-file 编译注入
    final apiConfigProvider = Provider<ApiConfig>((ref) {
      return ApiConfig(
        baseURL: AppConfig.apiBaseUrl,
        platformHeaders: {
          'platform': DeviceInfo.platform,        // 运行时同步读取（DeviceInfo.init() 预取）
          'os-type': DeviceInfo.osType.toString(), // Android=1 iOS=2 Windows=3 macOS=4 Linux=5
          'client-version': AppConfig.appVersion,
          'channel': AppConfig.channel,
          'lang': DeviceInfo.lang,
          'device-id': DeviceInfo.deviceId,
          'device-name': DeviceInfo.deviceName,
        },
        tokenExpiredCodes: {30002, 30003, 30124}, // 后端约定的 Token 过期错误码
        forceLogoutCodes: {30125},                // 后端约定的强制登出错误码
        onForceLogout: () { /* 清除登录态，跳转登录页 */ },
        onTokenRefresh: () async { /* 调刷新 token 接口 */ return null; },
        onLog: (message, {tag}) { print('[${tag ?? 'Network'}] $message'); },
      );
    });

    /// Networks SDK API Provider（全局单例，Facade 接口）
    /// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
    final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
      final config = ref.read(apiConfigProvider);
      return NetworksSdkWiring.build(config: config);
    });

##### DI 装配总览

    app/di/                                ← 手动装配：SDK 基础设施
    └── network_provider.dart              → apiConfigProvider + networkSdkApiProvider

    features/{模块}/di/                    ← 手动装配：业务模块 DI 链路（Repository → UseCase 按需）
    ├── auth/di/auth_providers.dart        → authRepositoryProvider
    │                                        → loginUseCaseProvider（按需）
    ├── chat/di/chat_providers.dart        → messageRepositoryProvider
    │                                        → sendMessageUseCaseProvider（按需）
    └── ...                                   （需要时才创建，不提前占位）

    features/{模块}/presentation/          ← @riverpod 自动生成：ViewModel Provider
    └── login_view_model.dart              → loginViewModelProvider（.g.dart 自动生成）

di/ 目录的定位：只放需要手动装配的 Provider（构造注入、回调组合等）。ViewModel Provider 由 `@riverpod` 注解自动生成（写在 `presentation/` 下），不在 `di/` 中。

最小化原则：`app/di/` 只提供 SDK 能力（ApiConfig / NetworksSdkApi），不放业务模块的 Provider。每个业务模块的手动装配 Provider 内聚在 `features/{模块}/di/{模块}_providers.dart` 中，需要时才创建。

##### SDK 间解耦：回调注入模式

Repository 不直接依赖 SDK 类型（如 `ApiConfig`）。需要 SDK 能力（如写 Token）时，通过回调注入，由 Provider 层组合多个 SDK：

    // features/auth/di/auth_providers.dart — App 层是唯一知道两个 SDK 的地方
    final authRepositoryProvider = Provider<AuthRepository>((ref) {
      final apiConfig = ref.read(apiConfigProvider);
      // final secureStorage = ref.read(secureStorageProvider);  // storage_sdk（待接入）

      return AuthRepositoryImpl(
        client: ref.read(networkSdkApiProvider),       // 注入 Facade 接口
        onTokenUpdate: (token) {
          apiConfig.updateToken(token);               // 内存（networks_sdk）
          // secureStorage.saveToken(token);          // 持久化（storage_sdk，待接入）
        },
      );
    });

好处：`networks_sdk` 不知道 `storage_sdk` 的存在，`AuthRepositoryImpl` 也不依赖任何 SDK 类型——只接收一个 `void Function(String?)` 回调。各 SDK 保持独立，App 层负责组合。

##### 错误处理

`ApiError` 是 Freezed 联合类型，覆盖所有网络错误场景。在 ViewModel 的 `catch` 中使用：

    try {
      final user = await ref.read(loginUseCaseProvider).execute(...);
    } on ApiError catch (e) {
      // 方式 A：用 displayMessage 一行搞定（ApiError 扩展方法，已内置中文提示）
      showToast(e.displayMessage);

      // 方式 B：精细处理每种错误
      e.when(
        noNetworkConnection: () => showToast('无网络连接'),
        timeout: ()            => showToast('请求超时，请重试'),
        networkError: (msg)    => showToast('网络错误: $msg'),
        decodingError: (msg)   => showToast('数据解析失败'),
        apiError: (code, msg)  => showToast('服务端错误[$code]: $msg'),
        cancelled: ()          => {}, // 用户主动取消，通常不提示
        unknown: (msg)         => showToast('未知错误'),
      );
    }

##### 代码生成（新人必读）

`@ApiRequest` 与 `@JsonSerializable` 共享同一个 `.g.dart` 文件（SharedPartBuilder），无需额外配置。

开发流程（3 步）

第 0 步：启动监听（整个开发期间只需执行一次，在独立终端窗口常驻）

    # ⚠️ 强制要求：开发期间必须常驻此命令
    # 在项目根目录打开一个独立终端窗口，执行：
    melos run gen:watch

启动后，每次保存 .dart 文件都会自动重新生成 .g.dart，无需手动操作。

第 1 步：手写源文件

    // data/remote/login_request.dart

    import 'package:networks_sdk/networks_sdk.dart';

    part 'login_request.g.dart';  // ← 必须写，指向即将生成的文件

    // ── Response DTO（纯 Dart 类，零注解，零样板）──
    // _$LoginResponseFromJson 由 ApiResponseGenerator 从 @ApiRequest(responseType: LoginResponse) 自动推导生成
    class LoginResponse {
      final String token;
      final String email;
      const LoginResponse({required this.token, required this.email});
      // 不需要 factory fromJson，不需要任何注解
    }

    // ── Request ──
    @ApiRequest(
      path: ApiPaths.authLogin,
      method: HttpMethod.post,
      responseType: LoginResponse,
      requestType: ApiRequestType.login,
    )
    class LoginRequest extends ApiRequestable<LoginResponse>
        with _$LoginRequestApi {                // ← 短暂报红，保存后自动消失
      final String email;
      final String password;

      LoginRequest({required this.email, required this.password});
      // toJson 由 mixin 自动生成，保存后红线自动消失
    }

第 2 步：保存文件 → 自动生成

保存后，后台的 `melos run gen:watch` 自动检测到文件变化，生成 `login_request.g.dart`。
所有红线自动消失，无需任何手动操作。

命名规则（写之前就能确定引用名）

来源 | 生成的符号 | 示例
--- | --- | ---
`@ApiRequest(responseType: T)` （ApiResponseGenerator 推导） | `_$类名FromJson`（私有函数，.g.dart 内部使用） | `_$LoginResponseFromJson(json)`
`@ApiRequest(...)` on Request 类 （ApiRequestGenerator 生成 mixin） | `_$类名Api`（mixin） | `_$LoginRequestApi`

规则固定，先写引用再保存，watch 模式下几秒后红线自动消失。如果红线没消失，检查 watch 终端是否在运行。

常见问题

- 忘了启动 watch：保存后红线不消失 → 检查终端是否有 `melos run gen:watch` 在运行
- 生成报错：`melos run gen` 重新全量生成
- .g.dart 冲突：多人协作时 .g.dart 冲突直接删除后重新生成即可，不要手动合并
- 新增依赖后：先 `dart pub get`，再重启 watch

#### Storage SDK（packages/storage_sdk/）

纯基础设施 SDK，不感知业务表结构。遵循 Facade + Wiring 模式，结构同 cipher_guard_sdk。

职责边界：

- storage_sdk 负责：数据库连接生命周期（按 uid 隔离文件）、通用泛型 CRUD（insert / select / watch / rawQuery 等）
- im_app 负责：AppDatabase 定义（含表声明和迁移策略）、各业务表（`data/local/drift/tables/`）

使用方式：

    // app/di/db_provider.dart
    final storageSdkProvider = Provider<StorageSdkApi>((ref) {
      return StorageSdkApi(
        databaseFactory: (executor) => AppDatabase(executor),
      );
    });

    // 登录后开库
    await ref.read(storageSdkProvider).openDatabase(userId);

    // CRUD
    final db = ref.read(storageSdkProvider);
    await db.insertOrReplace(appDb.users, companion);
    final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));

公开接口（StorageSdkApi）：生命周期（openDatabase / closeDatabase / isDatabaseOpen）+ 泛型 CRUD（insertOrReplace / batchInsertOrReplace / updateWhere / deleteWhere / deleteAll / selectAll / selectWhere / selectFirst / watchAll / watchWhere / watchFirst / rawQuery / rawExecute / count）。

build.yaml（im_app）：apps/im_app/build.yaml 配置 `drift_dev|preparing_builder: generate_for: [lib/**]`，确保 gen:watch 在修改表文件（无注解的 .dart）时正确触发 app_database.g.dart 重新生成。

#### Media SDK（packages/media_sdk/）

遵循 Facade + Wiring 模式。负责图片/视频处理，具体功能实现待开发。

#### RTC SDK（packages/rtc_sdk/）

遵循 Facade + Wiring 模式。负责实时音视频（WebRTC），具体功能实现待开发。

#### Push SDK（packages/notification_sdk/）

遵循 Facade + Wiring 模式。负责推送通知（FCM / APNs），具体功能实现待开发。

#### Protocol SDK（packages/protocol_sdk/）

遵循 Facade + Wiring 模式。负责消息协议（Protobuf 序列化），具体功能实现待开发。

#### CipherGuard SDK（packages/cipher_guard_sdk/）—— Flutter Plugin

端对端加密 SDK，同时处理 Dart 侧加解密和 Native 侧密钥同步（iOS App Group 用于推送通知解密）：

- `cipher_guard_sdk_api.dart`：公开 API 接口（Facade）
- `encryption_flutter_service.dart`：RSA/AES 双层加解密（纯 Dart 实现，基于 pointycastle + encrypt），含性能优化
- `encryption_method_channel.dart`：Native 密钥同步通道（iOS App Group 共享密钥供 Notification Extension 解密）
- Domain 实体：`RsaKeyPair` / `SessionKey` / `EncryptedMessage` / `ChatEncryptionKey`
- `android/` + `ios/`：Plugin 注册入口，原生侧实现密钥写入 App Group

##### 加解密性能优化

`encryption_flutter_service.dart` 针对 IM 高频加解密场景做了四项优化：

优化项 | 方案 | 效果
--- | --- | ---
RSA 密钥生成异步化 | `generateRsaKeyPairAsync` 使用 `Isolate.run()` 在独立线程生成 | 主线程零阻塞，不卡 UI（2048-bit 约 200-500ms）
派生密钥 LRU 缓存 | `_derivedKeyCache`（Map，上限 64 条），缓存键 = `sessionKey:round:mode`，满时淘汰最早条目 | 同一 round 的加解密只算一次 KDF，后续直接命中缓存
Random.secure() 复用 | 静态 `_secureRandom` 单例，所有 IV / 随机数生成共用 | 避免每次 `Random.secure()` 构造开销
KDF 双模式 | `KdfMode.md5`（默认，兼容既有数据）和 `KdfMode.pbkdf2`（PBKDF2-HMAC-SHA256，可配迭代次数） | 默认快速兼容，可选安全增强（防暴力破解）

构造时可配置 KDF 模式和 PBKDF2 迭代次数：

    EncryptionFlutterService(
      kdfMode: KdfMode.md5,        // 默认，兼容既有数据
      pbkdf2Iterations: 10000,     // PBKDF2 模式下的迭代次数
    )

`clearDerivedKeyCache()` 可在 session key 轮换时手动清空缓存。

### 7.4 多语言国际化（packages/l10n_sdk/）

已提取为独立 Package，被 core/ui 和 Feature 层单向引用（foundation 不依赖它）。

核心策略：远端动态下载，一劳永逸

翻译文案不随包发布，App 启动后自动从远端 URL 拉取最新版本并写入共享空间，Dart 层和原生层（Android / iOS）均从共享空间读取。后续只需在多语言后台追加/修改文案，无需发版，立即生效。

#### 启动流程

1. 拉取远端翻译：`l10n_loader.dart` 在 App 初始化阶段请求远端 URL，下载当前语言的翻译 JSON
2. 写入共享空间：将下载结果持久化到跨层共享目录（Documents / App Group Container），Dart 层和原生层均可读取
3. 加载入口：`l10n.dart` 优先从共享空间读取翻译，命中则使用；未命中则回退到内置兜底文件
4. 原生层同步：Android / iOS 原生代码同样从共享空间读取，与 Dart 层使用同一份翻译数据

#### 兜底机制

- 首次启动（远端尚未下载）：使用 `assets/fallback_*.json` 内置兜底翻译，保证界面不出空白
- 网络异常：继续使用上次成功缓存的翻译，下次启动重试
- 远端返回格式错误：忽略，保持当前缓存

#### 模块职责

- `l10n_loader.dart`：远端拉取 + 写入共享空间，App 启动时调用一次
- `l10n.dart`：多语言访问入口，优先读共享空间，回退至内置兜底
- `locale_provider.dart`：语言切换管理 —— 当前 Locale 状态、用户偏好持久化、跟随系统语言
- `assets/fallback_*.json`：内置兜底翻译，随包发布，仅作离线保障

> 为什么独立 Package：国际化服务于 core/ui（组件内置文案）和 Feature 层（页面文案、错误提示展示），作为独立 SDK 可跨项目复用翻译基础设施。注意：foundation 本身不依赖 l10n_sdk —— 错误映射仅产出错误码/错误键，由 Presentation / UI 层通过 l10n_sdk 转为本地化文案，从而避免 foundation ↔ l10n 双向依赖。

### 7.5 Core UI（core/ui/）最后更新 2026-03-10-10-17

UI 基础设施，为所有 Feature 提供统一的视觉规范和可复用组件。三层结构自底向上构建：

#### 第一层：基础定义（core/ui/base/）

最底层的视觉规范定义，不含任何 Widget，只输出颜色/字体常量和 ThemeData：

- `colors.dart`（已实现）：5 层颜色体系 —— ColorBases（颜色常量）+ AppColors（语义接口）+ LightColors / DarkColors（亮暗实现）
- `font.dart`（已实现）：字体 —— TextStyle 定义 + `textTheme(brightness)`（统一字族/字号/行高）
- `shadows.dart`（已实现）：阴影常量 —— bs4 / bs8 / bs12 / bs16，颜色走 AppColors.shadow 自动适配亮暗
- `spacing.dart`（已实现）：间距常量 —— s4 / s8 / s12 / s16 / s24 / s32
- `radius.dart`（已实现）：圆角常量 —— 基础 r4~r20 + 组件级 card / button / dialog
- `app_theme.dart`（已实现）：主题组装 —— `_build(AppColors c)` 零 isDark 分支
- `context_theme_ext.dart`（已实现）：统一 context 扩展 —— context.colors + context.styles + context.shadows

#### 第二层：基础组件（core/ui/components/）

原子级 Widget，只依赖第一层 base，不含任何业务逻辑：

- `app_button.dart`（已实现）：按钮（Primary / Secondary / Text 变体）
- app_text_field / app_avatar / app_badge 等（待开发）

#### 第三层：业务组合组件（core/ui/composites/）

由 base + components 组合而成的高阶 Widget，封装通用业务交互模式：

- `app_dialog.dart`（已实现）：确认弹窗（title / content / 确认 / 取消按钮）
- app_action_sheet / app_toast / app_empty_state / app_error_view 等（待开发）

> 依赖方向：composites → components → base → core/foundation/（颜色/字体等可以引用 foundation 的 config）。composites 可引用 l10n_sdk（组件内置文案）。Feature 层只引用 core/ui/，不直接使用 Flutter 原生 Material 组件。

> 依赖链：core/ui/ → l10n_sdk → core/foundation/，严格单向，不可反向依赖。

### SDK 约束与管理

#### 使用 Melos 实现 Mono-Repo

为了有效管理多个 SDK 和保证版本一致性，我们使用 Melos + mono-repo 架构。

#### Melos + mono-repo 的优势

维度 | Melos + mono-repo | 传统 multi-repo
--- | --- | ---
版本一致性 | 同一个 commit 保证所有 package 兼容 | 版本靠人同步，常出现 mismatch
API 变更 | 编译期立即发现，马上修复 | 发版后才发现，debug 成本高
Refactor 成本 | 一次性全 repo refactor | 需要跨 repo、分批跟进
依赖关系管理 | Melos 自动解析、link 本地套件 | pub / git tag 人工管理
SDK 开发体验 | 改 SDK → example app 立即验证 | 必须先发版才能验证
CI / 指令一致性 | 一套 melos 指令走天下 | 每个 repo 一套 script
测试策略 | 可只测受影响的 packages | 常常只能全测或凭感觉
Debug 效率 | 问题可回溯到单一 commit | 问题横跨多个 repo / 版本
新人上手 | clone 一个 repo 就全到位 | 要 clone 多个 repo 才能跑
技术债累积 | 缓慢、可控 | 指数型成长

#### SDK 约束规则

1. 位置约束：所有可复用 SDK 在 `packages/` 独立 Package，应用级基础设施在 `core/foundation/`，UI 基础设施在 `core/ui/`
2. 依赖约束：SDK 之间不能相互依赖（除非明确声明）
3. 职责约束：SDK 只提供纯技术能力，不包含业务逻辑
4. 版本管理：使用 Melos 统一管理版本和依赖
5. 独立性：每个 SDK 可独立测试、发布

环境配置、初始化步骤和 Melos 命令速查表见文档顶部 Part 0：开发环境配置。

## 扩展性设计

### 8.1 新增 Feature

添加新功能的标准流程：

1. 在 `features/` 下创建新目录
2. 创建 UI 层页面
3. 创建 Presentation 层 ViewModel
4. 创建 Domain 层 UseCase
5. 在 Domain 层定义 Repository 接口
6. 在 Data 层实现 Repository
7. 在 `features/{模块}/di/{模块}_providers.dart` 中注册模块 Provider

### 8.2 标准 Feature 结构模板

每个新 Feature 都应遵循以下标准结构：

#### 两档模板选择指南

复杂度 | 适用场景 | 必须有 | 可选添加
--- | --- | --- | ---
简单 | search、settings、profile 等 | view/ + presentation/（vm + state） | di/（需要自定义 Provider 时）
标准 | chat、call、auth 等复杂功能 | view/ + presentation/ + di/ | usecases/（多步编排、跨模块协调时按需添加）

#### 简单模板（search / settings / profile 类）

    features/[feature]/
    ├── view/
    │   ├── [feature]_page.dart
    │   └── widgets/
    └── presentation/
        ├── [feature]_view_model.dart        # ViewModel（直接调 Repository）
        ├── [feature]_view_model.g.dart      # 代码生成
        ├── [feature]_state.dart             # State（@freezed）
        └── [feature]_state.freezed.dart     # 代码生成

#### 标准模板（chat / call / auth 类）

    features/[feature]/
    ├── di/
    │   └── [feature]_providers.dart         # DI 装配（Repository → UseCase 按需）
    ├── view/
    │   ├── [feature]_page.dart
    │   └── widgets/
    ├── presentation/
    │   ├── [feature]_view_model.dart
    │   ├── [feature]_view_model.g.dart
    │   ├── [feature]_state.dart
    │   └── [feature]_state.freezed.dart
    └── usecases/                            # 按需 — 有多步编排时才添加
        └── [action]_usecase.dart

Domain Entity 说明：共享实体（Message、Contact、User 等）统一放在全局 `domain/entities/`，不在 feature 内部定义。

#### 完整示例：创建 Profile Feature（简单模板）

flowchart TD Step1[1. 创建目录结构
features/profile/] Step2[2. 创建 UI 层
profile/view/profile_page.dart] Step3[3. 创建 Presentation 层
profile/presentation/profile_view_model.dart] Step4[4. 定义 Repository 接口
domain/repositories/profile_repository.dart] Step5[5. 实现 Repository
data/repositories/profile_repository_impl.dart] Step6[6. 注册 Provider
features/profile/di/profile_providers.dart] StepOpt[可选：提取 UseCase
profile/usecases/] Step1 --> Step2 Step2 --> Step3 Step3 --> Step4 Step4 --> Step5 Step5 --> Step6 Step3 -.-> StepOpt StepOpt -.-> Step4 style Step1 fill:#e1f5ff,stroke:#0288d1 style Step2 fill:#e1f5ff,stroke:#0288d1 style Step3 fill:#fff4e6,stroke:#f57c00 style Step4 fill:#e8f5e9,stroke:#388e3c style Step5 fill:#e8f5e9,stroke:#388e3c style Step6 fill:#fff9c4,stroke:#f57f17 style StepOpt fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5

#### 具体步骤

步骤 1：创建 Feature 目录

    lib/features/profile/
    ├── view/
    └── presentation/

步骤 2：创建 UI 层页面（使用 ConsumerWidget）

    // features/profile/view/profile_page.dart
    class ProfilePage extends ConsumerWidget {
      const ProfilePage({super.key});

      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // 监听状态
        final state = ref.watch(profileViewModelProvider);
        final viewModel = ref.read(profileViewModelProvider.notifier);

        return Scaffold(
          appBar: AppBar(title: const Text('个人资料')),
          body: state.isLoading
              ? const Center(child: CircularProgressIndicator())
              : ProfileContent(
                  profile: state.profile,
                  onUpdate: viewModel.updateProfile,
                ),
        );
      }
    }

步骤 3：创建 Presentation 层 ViewModel（直接调 Repository）

    // features/profile/presentation/profile_state.dart
    @freezed
    class ProfileState with _$ProfileState {
      const factory ProfileState({
        Profile? profile,
        @Default(false) bool isLoading,
        @Default('') String error,
      }) = _ProfileState;
    }

    // features/profile/presentation/profile_view_model.dart
    @riverpod
    class ProfileViewModel extends _$ProfileViewModel {
      @override
      ProfileState build() => const ProfileState();

      Future<void> loadProfile() async {
        state = state.copyWith(isLoading: true, error: '');

        try {
          // 直接调 Repository，无需 UseCase 中间层
          final profile = await ref.read(profileRepositoryProvider).getProfile();
          state = state.copyWith(profile: profile);
        } catch (e) {
          state = state.copyWith(error: e.toString());
        } finally {
          state = state.copyWith(isLoading: false);
        }
      }

      Future<void> updateProfile(Profile profile) async {
        state = state.copyWith(isLoading: true);

        try {
          await ref.read(profileRepositoryProvider).updateProfile(profile);
          state = state.copyWith(profile: profile);
        } catch (e) {
          state = state.copyWith(error: e.toString());
        } finally {
          state = state.copyWith(isLoading: false);
        }
      }
    }

步骤 4：在全局 Domain 层定义 Repository 接口

    // domain/repositories/profile_repository.dart
    abstract class ProfileRepository {
      Future<Profile> getProfile();
      Future<void> updateProfile(Profile profile);
    }

步骤 5：在 Data 层实现 Repository

    // data/repositories/profile_repository_impl.dart
    class ProfileRepositoryImpl implements ProfileRepository {
      final NetworksSdkApi _client;

      ProfileRepositoryImpl({required NetworksSdkApi client})
          : _client = client;

      @override
      Future<Profile> getProfile() async {
        final data = await _client.executeRequest(GetProfileRequest());
        return data!.toEntity();
      }

      @override
      Future<void> updateProfile(Profile profile) async {
        // 实现逻辑
      }
    }

步骤 6：在 Feature 目录下注册模块 Provider

创建 `features/profile/di/profile_providers.dart`，注册 DI 链路：

    // features/profile/di/profile_providers.dart

    import '../../../app/di/network_provider.dart';

    // ── Repository ──
    final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
      return ProfileRepositoryImpl(
        client: ref.read(networkSdkApiProvider),    // 注入 Facade 接口
      );
    });

    // Profile 是简单 CRUD，不需要 UseCase。
    // ViewModel 通过 @riverpod 注解自动生成 Provider，无需额外注册。

说明：Profile 属于简单模板，ViewModel 直接调 Repository，无需 UseCase 中间层。`app/di/` 只提供 SDK 基础设施（ApiConfig / NetworksSdkApi），业务模块的 DI 链路内聚在 Feature 目录下。

#### Feature 结构图示

flowchart TD subgraph ProfileFeature[Profile Feature - 垂直切片] UI[UI Layer
profile/view/profile_page.dart] Presentation[Presentation Layer
profile/presentation/profile_view_model.dart] DI[DI 装配
profile/di/profile_providers.dart] end subgraph GlobalLayers[全局层] RepoInterface[Repository Interface
domain/repositories/profile_repository.dart] RepoImpl[Repository Implementation
data/repositories/profile_repository_impl.dart] SDKs[SDK Packages
networks_sdk / storage_sdk] end UI --> Presentation Presentation -->|直接调用| RepoInterface DI -.装配.-> RepoInterface RepoInterface -.实现.-> RepoImpl RepoImpl --> SDKs style ProfileFeature fill:#e3f2fd,stroke:#0288d1,stroke-width:3px style GlobalLayers fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px

> 关键要点：

> 1. 垂直切片：每个 Feature 包含 UI → Presentation（→ UseCase 按需）的链路

> 2. 高内聚：Feature 内部的代码都在同一目录下，便于维护

> 3. 低耦合：ViewModel 通过 Repository 接口与数据层解耦

> 4. 渐进式：从简单模板起步，业务复杂时按需升级为标准模板

### 8.3 替换底层实现

由于依赖倒置原则，可以轻松替换底层实现：

- 替换数据库：只需修改 Drift SDK
- 替换网络库：只需修改 Network SDK
- 替换加密算法：只需修改 Crypto SDK

### 8.4 跨平台扩展

- Platform Adapters 处理平台差异
- SDK Packages 提供统一接口
- Platform-specific 实现在各自的 SDK Package 中（如 cipher_guard_sdk 的 android/ / ios/）

## 项目配置

### 10.1 pubspec.yaml 依赖

#### 核心依赖

    name: im_app
    description: IM Application with Clean Architecture
    version: 1.0.0+1

    environment:
      sdk: '>=3.0.0 <4.0.0'

    dependencies:
      flutter:
        sdk: flutter

      # 状态管理 - Riverpod
      flutter_riverpod: ^2.4.0
      riverpod_annotation: ^2.3.0

      # 不可变状态 - Freezed
      freezed_annotation: ^2.4.1

      # JSON 序列化
      json_annotation: ^4.8.1

      # 依赖注入（可选，Riverpod 已包含依赖管理）
      # get_it: ^7.6.0
      # injectable: ^2.3.0

      # 路由导航
      go_router: ^12.0.0

      # 网络请求
      dio: ^5.4.0
      web_socket_channel: ^2.4.0

      # 本地存储
      drift: ^2.15.0
      sqlite3_flutter_libs: ^0.5.0
      flutter_secure_storage: ^9.0.0
      shared_preferences: ^2.2.0

      # 加密
      encrypt: ^5.0.3
      crypto: ^3.0.3

      # 媒体处理
      image_picker: ^1.0.4
      video_player: ^2.8.0
      cached_network_image: ^3.3.0

      # RTC
      agora_rtc_engine: ^6.3.0
      # 或者 flutter_webrtc: ^0.9.0

      # 推送通知
      firebase_messaging: ^14.7.0
      flutter_local_notifications: ^16.3.0

      # Protocol Buffers
      protobuf: ^3.1.0

      # 工具库
      equatable: ^2.0.5
      dartz: ^0.10.1
      intl: ^0.18.1

    dev_dependencies:
      flutter_test:
        sdk: flutter

      # 代码生成
      build_runner: ^2.4.6
      riverpod_generator: ^2.3.0
      freezed: ^2.4.5
      json_serializable: ^6.7.1
      drift_dev: ^2.15.0

      # 代码检查
      flutter_lints: ^3.0.0
      very_good_analysis: ^5.1.0

      # 测试
      mocktail: ^1.0.1
      integration_test:
        sdk: flutter

#### 依赖说明

类别 | 包名 | 用途
--- | --- | ---
状态管理 | flutter_riverpod | Riverpod 核心库
| riverpod_annotation | Riverpod 注解，用于代码生成
不可变状态 | freezed_annotation | Freezed 注解，生成不可变类
代码生成 | build_runner | Dart 代码生成工具
| riverpod_generator | Riverpod Provider 代码生成
| freezed | Freezed 代码生成
| json_serializable | JSON 序列化代码生成
网络 | dio | HTTP 客户端
| web_socket_channel | WebSocket 通信
存储 | drift | 类型安全的响应式数据库（基于 SQLite）
| flutter_secure_storage | 安全存储（加密）
测试 | mocktail | Mock 测试工具

### 10.2 代码生成命令

项目使用 Melos 统一管理，所有代码生成命令通过 `melos run` 执行，会自动作用于所有依赖 `build_runner` 的 Package。

#### 一次性生成

用于 CI 流水线，或首次 clone 后手动触发一次：

    melos run gen

#### 监听模式（开发期间必开）

开发时需要在一个独立的 Terminal 窗口中启动，全程保持运行。每次保存 `.dart` 文件后，build_runner 会自动检测变化并重新生成对应的 `.g.dart` / `.freezed.dart` 文件，无需手动执行。

    # 在独立 Terminal 窗口执行，不要关闭
    melos run gen:watch

⚠️ 若忘记开启，修改了 `@freezed` / `@JsonSerializable` 等注解后不会自动生成，编译时会报找不到对应文件的错误。

#### 底层等价命令（参考）

Melos 实际代为执行的命令，无需手动调用：

    dart run build_runner build --delete-conflicting-outputs
    dart run build_runner watch --delete-conflicting-outputs

### 10.3 环境配置与启动入口

#### 设计思路

采用 单一配置文件 + CI 脚本写入 的方式管理多环境配置：

- `config/config.json` 提交到 Git，默认存 dev 值（`IS_DEV=true`）
- CI 打包线上版本时，脚本直接改写此文件写入 prod 值，再执行 `flutter build`
- Dart 通过 `--dart-define-from-file` 在编译期将 JSON 字段注入二进制，运行时零开销读取

#### config/config.json（提交到 Git，默认 dev）

    {
      "IS_DEV": true,
      "API_BASE_URL": "https://dev-api.example.com"
    }

后续新增字段（WebSocket 地址、Sentry DSN、第三方 SDK Key 等）直接在此文件加 Key 即可，无需改启动逻辑。

#### core/foundation/config.dart

    // 编译期从 --dart-define-from-file=config/config.json 注入
    // CI 打包时脚本修改 config.json 写入线上值，本地开发保持默认（IS_DEV=true）
    const _kIsDebug = bool.fromEnvironment('IS_DEV', defaultValue: true);
    const _kApiBaseUrl = String.fromEnvironment(
      'API_BASE_URL',
      defaultValue: 'https://dev-api.example.com',
    );

    class AppConfig {
      const AppConfig({
        required this.isDebug,
        required this.apiBaseUrl,
      });

      /// 根据注入的编译期常量构建配置，main.dart 唯一入口
      static AppConfig get current => const AppConfig(
            isDebug: _kIsDebug,
            apiBaseUrl: _kApiBaseUrl,
          );

      final bool isDebug;
      final String apiBaseUrl;

      bool get isProd => !isDebug;
    }

#### app/bootstrap.dart

    void bootstrap(AppConfig config) {
      WidgetsFlutterBinding.ensureInitialized();
      runApp(IMApp(config: config));
    }

#### main.dart

    void main() {
      bootstrap(AppConfig.current);
    }

#### Android Studio / VSCode 运行配置

在 Android Studio 的 Run/Debug Configurations → Additional run args 中配置：

配置名 | Additional run args | 说明
--- | --- | ---
`im-debug` | `--dart-define-from-file=config/config.json --debug` | 本地开发调试
`im-release` | `--dart-define-from-file=config/config.json --release` | 本地 Release 模式验证

路径 `config/config.json` 相对于 Flutter 模块根目录（`apps/im_app/`）。配置已保存在 `.idea/runConfigurations/` 中，clone 后 Android Studio 可直接使用。

#### 打包脚本

每个平台均有独立的打包脚本，统一放在 `scripts/` 目录下，并通过 melos 命令调用。所有脚本均启用 `--split-debug-info` + `--obfuscate` 以减少产物体积。

平台 | 脚本 | melos 命令 | 用途 | 产物
--- | --- | --- | --- | ---
Android | `scripts/build_android.sh` | `melos run build:android:apk` | 本地测试 / 内部分发 | `build/app/outputs/flutter-apk/app-release.apk`
`melos run build:android:aab` | Google Play 上架 | `build/app/outputs/bundle/release/app-release.aab`
iOS | `scripts/build_ios.sh` | `melos run build:ios` | App Store / 内部分发 | `build/ios/ipa/im_app.ipa`
macOS | `scripts/build_macos.sh` | `melos run build:macos` | — | `build/macos/Build/Products/Release/im_app.app`
Windows | `scripts/build_windows.sh` | `melos run build:windows` | — | `build/windows/x64/runner/Release/`

melos 调用方式

    melos run build:android:apk       # APK（本地测试）
    melos run build:android:aab       # AAB（Google Play）
    melos run build:ios
    melos run build:macos
    melos run build:windows

    # prod 打包（需设置环境变量）
    PROD_API_BASE_URL=https://api.example.com melos run build:android:apk -- apk prod
    PROD_API_BASE_URL=https://api.example.com melos run build:android:aab -- aab prod
    PROD_API_BASE_URL=https://api.example.com melos run build:ios -- prod

体积优化说明

- `--split-debug-info`：将 Dart 调试符号从主产物中剥离，存入 `build/debug-info/<platform>/`，可减少 10~20 MB
- `--obfuscate`：混淆 Dart 符号名称，需配合符号表还原线上崩溃堆栈
- Android 额外优化：仅编译 `arm64-v8a`、R8 代码压缩（`isMinifyEnabled`）、资源压缩（`isShrinkResources`）
- 符号表请妥善保存，与对应版本一一对应，用于线上崩溃堆栈还原

### 10.4 analysis_options.yaml

    include: package:flutter_lints/flutter.yaml

    analyzer:
      exclude:
        - "**/*.g.dart"
        - "**/*.freezed.dart"
      language:
        strict-casts: true
        strict-inference: true
        strict-raw-types: true
      errors:
        missing_required_param: error
        missing_return: error
        todo: ignore

    linter:
      rules:
        # 架构规则
        avoid_classes_with_only_static_members: true
        prefer_final_fields: true

        # 代码风格
        prefer_single_quotes: true
        require_trailing_commas: true
        sort_child_properties_last: true
        prefer_const_constructors: true
        prefer_const_declarations: true
        prefer_const_literals_to_create_immutables: true
        prefer_final_locals: true
        always_use_package_imports: true   # 禁止相对路径 import，一律用 package: 全路径

        # 命名规则
        camel_case_types: true
        non_constant_identifier_names: true
        constant_identifier_names: true

        # 代码质量
        avoid_print: true
        avoid_empty_else: true
        no_duplicate_case_values: true
        unawaited_futures: true
        avoid_unnecessary_containers: true

        # 性能
        avoid_function_literals_in_foreach_calls: true
        prefer_collection_literals: true

        # 安全性
        avoid_web_libraries_in_flutter: true

### 10.5 CI Workflow（Gitea Actions）

文件：`.github/workflows/ci.yml`

    name: CI

    on:
      # 合并 PR 后触发（branch protection 保证只有 merge 能到达这里）
      push:
        branches: [main, dev]
      # PR 提交/更新时触发，main 和 dev 都接受 PR
      pull_request:
        branches: [main, dev]

    jobs:
      lint:
        name: Lint
        runs-on: self-hosted

        steps:
          - name: Checkout
            uses: actions/checkout@v4

          - name: Setup Flutter (stable)
            uses: subosito/flutter-action@v2
            with:
              channel: stable
              cache: true

          - name: Install Melos
            run: dart pub global activate melos

          - name: Deep clean
            run: melos run clean:deep

          - name: Install dependencies
            run: dart pub get

          - name: Generate code
            run: melos run gen

          - name: Analyze
            run: melos run analyze

步骤说明

步骤 | 命令 | 说明
--- | --- | ---
Deep clean | `melos run clean:deep` | 清除全平台缓存（Flutter / Android Gradle / iOS Pods / macOS Pods / Windows CMake）及所有生成文件，确保 CI 环境干净
Install dependencies | `dart pub get` | 在根目录统一解析所有 package 依赖，生成单一 pubspec.lock（Dart pub workspace）
Generate code | `melos run gen` | 生成 `.g.dart` / `.freezed.dart`（`*.g.dart` 不提交，CI 每次重新生成）
Analyze | `melos run analyze` | 对所有 package 执行静态分析，lint 不通过则 PR 不可合并

触发规则

- `pull_request` 到 `main` / `dev`：PR 提交或更新时运行，必须通过才能合并
- `push` 到 `main` / `dev`：PR 合并后触发（两个分支均开启 branch protection，不允许直接 push）

打包策略

打包不在自动 CI 中触发，通过 IM 管理后台手动触发打包任务。打包 workflow 单独维护，与 lint/analyze 流水线解耦。

预留 CI 能力

能力 | 触发时机 | 状态 | 说明
--- | --- | --- | ---
AI 代码 Review | PR 提交 / 更新时 | 🔜 预留 | 对每个 PR 的 diff 调用 AI 接口，自动输出可读性、架构合规性、潜在问题等 Review 意见，以 PR 评论形式呈现

分支保护建议（Gitea → Settings → Branches）

- Require a pull request before merging
- Require status checks to pass：`Lint`
- Do not allow bypassing the above settings

## 第五部分：数据流转示例

### 9.1 发送消息流程（Feature 驱动）

下面展示一个完整的发送消息流程，说明数据如何在 Feature 驱动的架构中流转：

sequenceDiagram participant UI as features/chat/view/
chat_page.dart participant VM as features/chat/presentation/
chat_view_model.dart participant UC as features/chat/usecases/
send_message_usecase.dart participant Repo as domain/repositories/
message_repository.dart participant RepoImpl as data/repositories/
message_repository_impl.dart participant LocalDS as data/local/
message_local_ds.dart participant SDK as networks_sdk/
NetworksSdkApi / SocketClient participant WS as WebSocket Server UI->>VM: 1. 用户点击发送按钮 VM->>UC: 2. 调用 SendMessageUseCase UC->>Repo: 3. 调用 Repository 接口 Repo->>RepoImpl: 4. Data 层实现接口 RepoImpl->>LocalDS: 5. 先保存到本地数据库 LocalDS-->>RepoImpl: 6. 本地保存成功（设置状态：发送中） RepoImpl->>SDK: 7. 直接调 SDK 发送 SDK->>WS: 8. 发送消息到服务器 WS-->>SDK: 9. 服务器确认收到 SDK-->>RepoImpl: 10. 返回发送结果 RepoImpl->>LocalDS: 11. 更新本地消息状态（已发送） LocalDS-->>RepoImpl: 12. 更新成功 RepoImpl-->>UC: 13. 返回 Message Entity UC-->>VM: 14. 返回结果 VM-->>UI: 15. 更新 UI 状态 UI->>UI: 16. 显示消息已发送 Note over UI,WS: Feature 垂直切片：chat Feature 的完整数据流

### 9.2 流程说明

1. 用户操作：用户在 `features/chat/view/chat_page.dart` 点击发送按钮
2. ViewModel 响应：`features/chat/presentation/chat_view_model.dart` 处理发送逻辑
3. 调用 UseCase：ViewModel 调用 `features/chat/usecases/send_message_usecase.dart`
4. Repository 接口：UseCase 通过 `domain/repositories/message_repository.dart` 接口调用
5. Repository 实现：`data/repositories/message_repository_impl.dart` 实现具体逻辑
6. 本地优先：先保存到 `data/local/message_local_ds.dart`
7. 网络发送：Repository 调 SDK（NetworksSdkApi / SocketClient）发送
8. 服务器确认：WebSocket 服务器确认接收
9. 状态更新：更新本地数据库中的消息状态
10. 数据返回：层层返回，最终更新 UI

#### 完整代码示例（Riverpod 实现）

1. UI 层（ConsumerWidget）

    // features/chat/view/chat_page.dart
    class ChatPage extends ConsumerWidget {
      const ChatPage({super.key});

      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final state = ref.watch(chatViewModelProvider);
        final viewModel = ref.read(chatViewModelProvider.notifier);

        return Scaffold(
          body: ChatInputArea(
            onSend: (content) => viewModel.sendMessage(content),
          ),
        );
      }
    }

2. Presentation 层（StateNotifier）

    // features/chat/presentation/chat_view_model.dart
    class ChatViewModel extends StateNotifier<ChatState> {
      ChatViewModel(this._sendMessageUseCase) : super(const ChatState());

      final SendMessageUseCase _sendMessageUseCase;

      Future<void> sendMessage(String content) async {
        state = state.copyWith(isLoading: true);
        try {
          await _sendMessageUseCase(content);
        } finally {
          state = state.copyWith(isLoading: false);
        }
      }
    }

    // Provider
    final chatViewModelProvider =
        StateNotifierProvider.autoDispose<ChatViewModel, ChatState>((ref) {
      return ChatViewModel(ref.watch(sendMessageUseCaseProvider));
    });

3. Domain 层（UseCase + Provider）

    // features/chat/usecases/send_message_usecase.dart
    class SendMessageUseCase {
      final MessageRepository _repository;

      SendMessageUseCase(this._repository);

      Future<Message> call(String content) {
        return _repository.sendMessage(content);
      }
    }

    // Provider
    @riverpod
    SendMessageUseCase sendMessageUseCase(SendMessageUseCaseRef ref) {
      return SendMessageUseCase(ref.watch(messageRepositoryProvider));
    }

    // domain/repositories/message_repository.dart
    abstract class MessageRepository {
      Future<Message> sendMessage(String content);
      Future<List<Message>> getMessages(String chatId);
    }

4. Data 层（Repository 实现 + Providers）

    // data/repositories/message_repository_impl.dart
    class MessageRepositoryImpl implements MessageRepository {
      final MessageLocalDataSource _localDS;
      final NetworksSdkApi _client;      // 注入 Facade 接口

      MessageRepositoryImpl(this._localDS, this._client);

      @override
      Future<Message> sendMessage(String content) async {
        // 1. 先保存到本地
        final localMessage = await _localDS.saveMessage(content, status: 'sending');

        // 2. 直接调 SDK 发送到服务器
        try {
          final serverMessage = await _client.executeRequest(
            SendMessageRequest(chatId: localMessage.chatId, content: content),
          );

          // 3. 更新本地状态
          await _localDS.updateMessageStatus(localMessage.id, 'sent');

          return serverMessage!.toEntity();
        } catch (e) {
          await _localDS.updateMessageStatus(localMessage.id, 'failed');
          rethrow;
        }
      }
    }

    // Provider
    @riverpod
    MessageRepository messageRepository(MessageRepositoryRef ref) {
      return MessageRepositoryImpl(
        ref.watch(messageLocalDataSourceProvider),
        ref.watch(networkSdkApiProvider),     // 注入 Facade 接口
      );
    }

5. Local DataSource + Provider

    // data/local/message_local_ds.dart
    class MessageLocalDataSource {
      final AppDatabase _db;

      Future<MessageDTO> saveMessage(String content, {required String status}) {
        // Drift 操作
      }
    }

    // Provider
    @riverpod
    MessageLocalDataSource messageLocalDataSource(MessageLocalDataSourceRef ref) {
      return MessageLocalDataSourceImpl(ref.watch(databaseProvider));
    }

### 9.3 加载会话列表流程

sequenceDiagram participant UI as features/chat_list/view/
chat_list_page.dart participant VM as features/chat_list/presentation/
chat_list_view_model.dart participant UC as features/chat_list/usecases/
load_chat_list_usecase.dart participant Repo as domain/repositories/
chat_repository.dart participant RepoImpl as data/repositories/
chat_repository_impl.dart participant Cache as data/cache/
cache_manager.dart participant LocalDS as data/local/
chat_local_ds.dart participant SDK as networks_sdk/
NetworksSdkApi UI->>VM: 1. 页面初始化 VM->>UC: 2. 调用 LoadChatListUseCase UC->>Repo: 3. 调用 Repository 接口 Repo->>RepoImpl: 4. Repository 实现 RepoImpl->>Cache: 5. 检查缓存 alt 缓存命中 Cache-->>RepoImpl: 6a. 返回缓存数据 else 缓存未命中 RepoImpl->>LocalDS: 6b. 读取本地数据库 LocalDS-->>RepoImpl: 7. 返回本地数据 RepoImpl->>SDK: 8. 调 NetworksSdkApi 请求远程数据 SDK-->>RepoImpl: 9. 返回最新数据 RepoImpl->>LocalDS: 10. 更新本地数据库 RepoImpl->>Cache: 11. 更新缓存 end RepoImpl-->>UC: 12. 返回 Chat Entity 列表 UC-->>VM: 13. 返回结果 VM-->>UI: 14. 更新 UI 状态 UI->>UI: 15. 显示会话列表 Note over UI,SDK: 缓存优先策略：缓存 → 本地 → 远程

### 9.4 跨 Feature 交互

不同 Feature 之间通过共享的 Repository 接口交互：

flowchart LR subgraph ChatFeature[Chat Feature] ChatUI[UI] ChatVM[ViewModel] ChatUC[UseCase] end subgraph ContactFeature[Contact Feature] ContactUI[UI] ContactVM[ViewModel] ContactUC[UseCase] end subgraph SharedDomain[共享 Domain] MessageRepo[MessageRepository] ContactRepo[ContactRepository] end subgraph DataLayer[Data Layer] RepoImpl[Repository 实现] end ChatUC --> MessageRepo ContactUC --> ContactRepo MessageRepo -.实现.-> RepoImpl ContactRepo -.实现.-> RepoImpl style ChatFeature fill:#e1f5ff,stroke:#0288d1 style ContactFeature fill:#e8f5e9,stroke:#388e3c style SharedDomain fill:#f3e5f5,stroke:#7b1fa2 style DataLayer fill:#e8f5e9,stroke:#388e3c

关键原则：

- Feature 之间不直接依赖
- 通过共享的 Repository 接口通信
- Repository 实现在 Data 层统一管理
- 保持 Feature 的独立性和可测试性

### 9.5 数据同步策略

Repository 层负责协调本地和远程数据：

- 读取数据：缓存 → 本地数据库 → 远程服务器（三级缓存）
- 写入数据：先写入本地，再同步到远程（本地优先）
- 冲突解决：使用时间戳或版本号解决冲突
- 离线支持：本地数据库支持离线操作，网络恢复后自动同步
- 增量同步：只同步变更的数据，减少网络传输

### 9.6 层级依赖总结

flowchart TD subgraph FeatureLayer[Feature Layer - 按页面垂直切片] direction TB FeatureView[features/*/view/
页面 + 组件] FeaturePresentation[features/*/presentation/
ViewModel + 状态] FeatureDomain[features/*/domain/
UseCase + Entity] end subgraph GlobalDomain[Global Domain - 共享接口] direction TB RepoInterfaces[domain/repositories/
Repository 接口] ValueObjects[domain/value_objects/
值对象] end subgraph DataLayer[Data Layer - 全局实现] direction TB RepoImpl[data/repositories/
Repository 实现] LocalDS2[data/local/
本地数据源] DTOs[data/models/
DTO 模型] end subgraph CoreLayer[Core Layer - 主 App 内部] subgraph Foundation[core/foundation/ - 应用级基础设施] direction TB AppInfra[Constants / Config / Errors
Logger / Types / Utils / Extensions] end subgraph CoreUILayer[core/ui/ - UI 基础设施] direction TB DesignBase[base/] UIComponents[components/] UIComposites[composites/] end end subgraph PackagesLayer[SDK Packages - Melos 管理] direction TB SDKPkgs2[networks_sdk / storage_sdk / cipher_guard_sdk / l10n_sdk
media_sdk / rtc_sdk / notification_sdk
protocol_sdk] end FeatureView --> FeaturePresentation FeatureView -->|UI 复用| UIComposites FeatureView -->|本地化文案| SDKPkgs2 FeaturePresentation --> FeatureDomain FeatureDomain --> RepoInterfaces RepoInterfaces -.实现.-> RepoImpl RepoImpl --> LocalDS2 RepoImpl --> SDKPkgs2 LocalDS2 --> SDKPkgs2 UIComposites --> UIComponents UIComposites -->|组件内置文案| SDKPkgs2 UIComponents --> DesignBase style FeatureLayer fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style GlobalDomain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px style DataLayer fill:#e8f5e9,stroke:#388e3c,stroke-width:3px style CoreLayer fill:#f5f5f5,stroke:#9e9e9e,stroke-width:3px style PackagesLayer fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px style Foundation fill:#fce4ec,stroke:#c2185b,stroke-width:2px style CoreUILayer fill:#fff4e6,stroke:#f57c00,stroke-width:2px

> 架构核心：

> 1. Feature 垂直切片：每个 Feature 包含 UI → Presentation → Domain 的完整链路

> 2. 全局 Repository 接口：domain/repositories/ 定义数据访问接口

> 3. 统一 Data 实现：data/ 实现所有 Repository，管理所有数据源

> 4. Core Foundation 支撑：core/foundation/ 提供应用级基础设施；SDK 能力由 packages/ 独立 Package 提供

> 5. L10n 国际化：packages/l10n_sdk 提供翻译资源和语言切换，被 core/ui 和 Feature 层单向引用

> 6. Core UI 统一视觉：core/ui/ 提供基础定义、基础组件和业务组合组件，Feature 层统一复用

> 7. 严格单向依赖：core/ui/ → l10n_sdk → core/foundation/，任何层级不可反向依赖

## 第六部分：企业级架构关键考虑因素

在实际的企业级 IM 应用开发中，除了基础架构设计，还需要考虑以下关键因素，这些因素直接影响系统的可扩展性、性能、可维护性和长期收益。

### 5.1 跨平台交互 Bridge 能力

核心理念：提前规划与其他平台的交互能力，确保 IM App 可以嵌入到各种宿主环境（企业内部平台、第三方应用等）。

#### Bridge 架构设计

flowchart TD App[IM App] --> Bridge[Bridge 层] Bridge --> Capabilities[能力检测] Bridge --> Protocol[协议转换] Bridge --> Adapter[平台适配器] Adapter --> Enterprise[企业平台] Adapter --> ThirdParty[第三方应用] Adapter --> H5[H5 WebView] Adapter --> Native[原生应用] Capabilities --> Check1[网络能力] Capabilities --> Check2[存储能力] Capabilities --> Check3[媒体能力] Capabilities --> Check4[推送能力] style Bridge fill:#fff4e6,stroke:#f57c00,stroke-width:3px style Capabilities fill:#e8f5e9,stroke:#388e3c style Adapter fill:#e3f2fd,stroke:#2196f3

#### Bridge 能力接口定义

    /// Bridge 能力接口 - 抽象层
    abstract class BridgeCapability {
      /// 检测能力是否可用
      Future<bool> isAvailable();

      /// 初始化能力
      Future<void> initialize();

      /// 能力名称
      String get name;

      /// 能力版本
      String get version;
    }

    /// Bridge 管理器 - 统一管理所有 Bridge 能力
    class BridgeManager {
      static final BridgeManager _instance = BridgeManager._internal();
      factory BridgeManager() => _instance;
      BridgeManager._internal();

      final Map<String, BridgeCapability> _capabilities = {};

      /// 注册能力
      void registerCapability(BridgeCapability capability) {
        _capabilities[capability.name] = capability;
      }

      /// 检测能力是否可用
      Future<bool> hasCapability(String name) async {
        final capability = _capabilities[name];
        if (capability == null) return false;
        return await capability.isAvailable();
      }

      /// 获取能力
      T? getCapability<T extends BridgeCapability>(String name) {
        return _capabilities[name] as T?;
      }
    }

    /// 网络 Bridge 能力
    class NetworkBridgeCapability implements BridgeCapability {
      @override
      String get name => 'network';

      @override
      String get version => '1.0.0';

      @override
      Future<bool> isAvailable() async {
        // 检测宿主环境是否支持网络请求
        return true;
      }

      @override
      Future<void> initialize() async {
        // 初始化网络能力
      }

      /// 通过 Bridge 发送网络请求
      Future<Response> request(String url, {
        required HTTPMethod method,
        Map<String, dynamic>? data,
      }) async {
        // 调用宿主环境的网络能力
        return await _callHost('network.request', {
          'url': url,
          'method': method.name,
          'data': data,
        });
      }
    }

    /// 存储 Bridge 能力
    class StorageBridgeCapability implements BridgeCapability {
      @override
      String get name => 'storage';

      @override
      String get version => '1.0.0';

      @override
      Future<bool> isAvailable() async {
        return true;
      }

      @override
      Future<void> initialize() async {}

      /// 存储数据
      Future<void> setItem(String key, String value) async {
        await _callHost('storage.set', {'key': key, 'value': value});
      }

      /// 读取数据
      Future<String?> getItem(String key) async {
        return await _callHost('storage.get', {'key': key});
      }
    }

#### 能力检测与降级策略

    /// 能力检测与降级
    class BridgeCapabilityChecker {
      /// 检测所有必需能力
      static Future<Map<String, bool>> checkRequiredCapabilities() async {
        final manager = BridgeManager();

        return {
          'network': await manager.hasCapability('network'),
          'storage': await manager.hasCapability('storage'),
          'media': await manager.hasCapability('media'),
          'push': await manager.hasCapability('push'),
          'payment': await manager.hasCapability('payment'),
        };
      }

      /// 根据能力启用功能
      static Future<void> enableFeaturesBasedOnCapabilities() async {
        final capabilities = await checkRequiredCapabilities();

        // 网络能力不可用 - 降级到离线模式
        if (!capabilities['network']!) {
          FeatureToggle.enable('offline_mode');
        }

        // 推送能力不可用 - 使用轮询
        if (!capabilities['push']!) {
          FeatureToggle.enable('polling_mode');
        }

        // 支付能力不可用 - 隐藏支付功能
        if (!capabilities['payment']!) {
          FeatureToggle.disable('payment_feature');
        }
      }
    }

关键价值

- 提前规划：在架构设计初期就考虑 Bridge 能力，避免后期重构
- 能力检测：运行时动态检测宿主环境能力，自动适配
- 优雅降级：能力不可用时自动降级，不影响核心功能
- 统一接口：通过 Bridge 层统一调用，屏蔽平台差异

### 5.2 数据获取多层策略

核心理念：通过内存缓存、热表、冷表、网络请求的多层数据获取策略，优化性能和用户体验。

#### 数据分层架构

flowchart TD Request[数据请求] --> Memory[L1: 内存缓存] Memory -->|未命中| Hot[L2: 热表 Hot Table] Hot -->|未命中| Cold[L3: 冷表 Cold Table] Cold -->|未命中| Network[L4: 网络请求] Network --> Sync[数据同步] Sync --> UpdateCold[更新冷表] Sync --> UpdateHot[更新热表] Sync --> UpdateMemory[更新内存] style Memory fill:#e8f5e9,stroke:#388e3c,stroke-width:3px style Hot fill:#fff4e6,stroke:#f57c00,stroke-width:2px style Cold fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style Network fill:#fce4ec,stroke:#c2185b,stroke-width:2px

#### 数据层定义

层级 | 存储位置 | 数据特点 | 访问速度 | 容量 | 生命周期
--- | --- | --- | --- | --- | ---
L1: 内存缓存 | RAM | 最近访问、高频访问 | 极快（<1ms） | 小（100MB） | 应用运行期间
L2: 热表 | Drift + 索引 | 频繁访问、近期活跃 | 快（1-10ms） | 中（1GB） | 30 天
L3: 冷表 | Drift | 历史数据、低频访问 | 中（10-50ms） | 大（10GB） | 永久
L4: 网络 | 远程服务器 | 最新数据、全量数据 | 慢（100-1000ms） | 无限 | 永久

#### 数据获取策略实现

    /// 多层数据获取策略
    class DataFetchStrategy<T> {
      final MemoryCache memoryCache;
      final HotTableDataSource hotTable;
      final ColdTableDataSource coldTable;
      final NetworkDataSource network;

      const DataFetchStrategy({
        required this.memoryCache,
        required this.hotTable,
        required this.coldTable,
        required this.network,
      });

      /// 获取数据 - 自动多层查找
      Future<T?> fetch(String key) async {
        // L1: 检查内存缓存
        final cached = memoryCache.get<T>(key);
        if (cached != null) {
          _recordHit('memory', key);
          return cached;
        }

        // L2: 检查热表
        final hot = await hotTable.query<T>(key);
        if (hot != null) {
          _recordHit('hot_table', key);
          memoryCache.set(key, hot); // 回填内存
          return hot;
        }

        // L3: 检查冷表
        final cold = await coldTable.query<T>(key);
        if (cold != null) {
          _recordHit('cold_table', key);
          // 提升到热表（如果访问频率高）
          if (await _shouldPromoteToHot(key)) {
            await hotTable.insert(key, cold);
          }
          memoryCache.set(key, cold); // 回填内存
          return cold;
        }

        // L4: 网络请求
        try {
          final data = await network.fetch<T>(key);
          if (data != null) {
            _recordHit('network', key);
            // 同步到各层缓存
            await _syncToCache(key, data);
            return data;
          }
        } catch (e) {
          _recordError('network', key, e);
        }

        return null;
      }

      /// 同步数据到缓存
      Future<void> _syncToCache(String key, T data) async {
        // 写入内存
        memoryCache.set(key, data);

        // 写入热表
        await hotTable.insert(key, data);

        // 写入冷表（持久化）
        await coldTable.insert(key, data);
      }

      /// 判断是否应该提升到热表
      Future<bool> _shouldPromoteToHot(String key) async {
        final accessCount = await _getAccessCount(key);
        return accessCount > 5; // 访问超过 5 次提升到热表
      }
    }

    /// 内存缓存（LRU）
    class MemoryCache {
      final int maxSize;
      final Map<String, dynamic> _cache = {};
      final List<String> _accessOrder = [];

      MemoryCache({this.maxSize = 1000});

      T? get<T>(String key) {
        if (!_cache.containsKey(key)) return null;

        // 更新访问顺序
        _accessOrder.remove(key);
        _accessOrder.add(key);

        return _cache[key] as T?;
      }

      void set(String key, dynamic value) {
        // LRU 淘汰
        if (_cache.length >= maxSize) {
          final oldest = _accessOrder.removeAt(0);
          _cache.remove(oldest);
        }

        _cache[key] = value;
        _accessOrder.add(key);
      }
    }

    /// 热表数据源（高频访问）
    class HotTableDataSource {
      /// 查询热表
      Future<T?> query<T>(String key) async {
        // SELECT * FROM hot_table WHERE key = ? AND last_access > (NOW() - 30 days)
        return await _database.query('hot_table', where: 'key = ?', whereArgs: [key]);
      }

      /// 插入热表
      Future<void> insert(String key, dynamic value) async {
        await _database.insert('hot_table', {
          'key': key,
          'value': jsonEncode(value),
          'last_access': DateTime.now().toIso8601String(),
        });
      }
    }

    /// 冷表数据源（历史数据）
    class ColdTableDataSource {
      /// 查询冷表
      Future<T?> query<T>(String key) async {
        // SELECT * FROM cold_table WHERE key = ?
        return await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
      }

      /// 插入冷表
      Future<void> insert(String key, dynamic value) async {
        await _database.insert('cold_table', {
          'key': key,
          'value': jsonEncode(value),
          'created_at': DateTime.now().toIso8601String(),
        });
      }
    }

#### 数据迁移策略

    /// 数据迁移服务 - 热表与冷表之间的数据流动
    class DataMigrationService {
      /// 定期清理过期的热表数据
      Future<void> cleanupHotTable() async {
        // 删除 30 天未访问的数据
        await _database.delete(
          'hot_table',
          where: 'last_access < ?',
          whereArgs: [DateTime.now().subtract(Duration(days: 30))],
        );
      }

      /// 将热数据迁移到冷表
      Future<void> migrateHotToCold() async {
        final oldData = await _database.query(
          'hot_table',
          where: 'last_access < ?',
          whereArgs: [DateTime.now().subtract(Duration(days: 30))],
        );

        for (final row in oldData) {
          await _database.insert('cold_table', row);
        }
      }

      /// 提升冷数据到热表
      Future<void> promoteColdToHot(String key) async {
        final data = await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
        if (data != null) {
          await _database.insert('hot_table', data);
        }
      }
    }

性能收益

- 响应速度：内存缓存命中率高，响应时间极快
- 网络流量：大幅减少网络请求，节省流量和电量
- 离线可用：冷表保存历史数据，离线时仍可访问
- 自动优化：根据访问频率自动调整数据存储位置

### 5.3 大量中间层的价值

核心理念：通过大量的中间层（抽象层、适配层、转换层），实现高度解耦、易于测试、便于替换。

#### 中间层架构

flowchart TD UI[UI Layer] --> ViewModelAdapter[ViewModel 适配层] ViewModelAdapter --> UseCase[UseCase 抽象层] UseCase --> RepositoryInterface[Repository 接口层] RepositoryInterface --> RepositoryImpl[Repository 实现层] RepositoryImpl --> NetworkSDK[Network SDK
packages/networks_sdk] RepositoryImpl --> StorageSDK[Storage SDK
packages/storage_sdk] style ViewModelAdapter fill:#fff4e6,stroke:#f57c00 style UseCase fill:#e8f5e9,stroke:#388e3c style RepositoryInterface fill:#e3f2fd,stroke:#2196f3 style RepositoryImpl fill:#f3e5f5,stroke:#7b1fa2

#### 中间层的类型与作用

中间层类型 | 位置 | 作用 | 示例
--- | --- | --- | ---
抽象层 | Domain | 定义业务接口，隔离实现细节 | Repository 接口、UseCase 抽象类
适配层 | Data | 适配不同数据源，统一接口 | LocalDataSource、CacheManager
转换层 | Data | DTO ↔ Entity 数据转换 | Mapper、Converter
协议层 | Core | 封装通信协议，屏蔽底层细节 | APIRequestable、WebSocketProtocol
策略层 | Core/Data | 封装算法和策略，易于替换 | CacheStrategy、RetryStrategy

#### 中间层实践示例

    // 1. Repository 接口层（抽象层）
    abstract class ChatRepository {
      Future<List<Message>> getMessages(String chatId);
      Future<void> sendMessage(Message message);
    }

    // 2. Repository 实现层（注入 Facade 接口）
    class ChatRepositoryImpl implements ChatRepository {
      final NetworksSdkApi client;
      final LocalDataSource localDataSource;
      final MessageMapper mapper;

      ChatRepositoryImpl({
        required this.client,
        required this.localDataSource,
        required this.mapper,
      });

      @override
      Future<List<Message>> getMessages(String chatId) async {
        // 先从本地获取
        final localDTOs = await localDataSource.getMessages(chatId);
        if (localDTOs.isNotEmpty) {
          return localDTOs.map(mapper.toEntity).toList();
        }

        // 调 NetworksSdkApi 从远程获取
        final response = await client.executeRequest(
          GetMessagesRequest(chatId: chatId),
        );

        // 保存到本地
        await localDataSource.saveMessages(response?.messages ?? []);

        // 转换为 Entity
        return (response?.messages ?? []).map(mapper.toEntity).toList();
      }
    }

    // 4. 转换层（Mapper）
    class MessageMapper {
      Message toEntity(MessageDTO dto) {
        return Message(
          id: dto.id,
          content: dto.content,
          senderId: dto.senderId,
          timestamp: DateTime.parse(dto.timestamp),
        );
      }

      MessageDTO toDTO(Message entity) {
        return MessageDTO(
          id: entity.id,
          content: entity.content,
          senderId: entity.senderId,
          timestamp: entity.timestamp.toIso8601String(),
        );
      }
    }

    // 5. 策略层
    abstract class CacheStrategy {
      bool shouldCache(String key);
      Duration getCacheDuration(String key);
    }

    class MessageCacheStrategy implements CacheStrategy {
      @override
      bool shouldCache(String key) {
        // 最近 100 条消息缓存
        return true;
      }

      @override
      Duration getCacheDuration(String key) {
        return Duration(hours: 24);
      }
    }

中间层的价值

- 解耦：各层通过接口通信，修改实现不影响调用方
- 可测试：每层可独立测试，易于 Mock
- 可替换：底层实现可随时替换（如换数据库、换 API）
- 可扩展：新增功能只需新增实现，不修改接口
- 可维护：职责清晰，问题定位快速

### 5.4 系统能力划分

核心理念：将系统能力划分为基础能力、业务能力、快速响应机制、部署策略，实现高内聚低耦合。

#### 能力划分架构

flowchart TD System[IM 系统] --> Basic[基础能力层] System --> Business[业务能力层] System --> FastResponse[快速响应层] System --> Container[容器化部署？] Basic --> Network[网络通信] Basic --> Storage[数据存储] Basic --> Crypto[加密解密] Basic --> Media[媒体处理] Business --> Chat[聊天功能] Business --> Contact[联系人] Business --> Group[群组] Business --> Call[音视频通话] FastResponse --> Cache[智能缓存] FastResponse --> Preload[预加载] FastResponse --> Optimize[性能优化] Container --> Docker[Docker 容器] Container --> K8S[Kubernetes] Container --> CI[CI/CD 流水线] style Basic fill:#e8f5e9,stroke:#388e3c,stroke-width:3px style Business fill:#e3f2fd,stroke:#2196f3,stroke-width:3px style FastResponse fill:#fff4e6,stroke:#f57c00,stroke-width:3px style Container fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px

#### 基础能力层

定义：与业务无关的通用技术能力，可复用到任何项目。

能力 | 说明 | SDK | 可复用性
--- | --- | --- | ---
网络通信 | HTTP/WebSocket/gRPC | NetworkSDK | 完全通用
数据存储 | Drift/SharedPreferences/SecureStorage | StorageSDK | 完全通用
端对端加密 | RSA/AES 双层加密 + Native 密钥同步 | CipherGuardSDK | 完全通用
媒体处理 | 图片/视频/音频压缩 | MediaSDK | 高度通用
音视频通话 | WebRTC/Agora | RTCSDK | 较为通用
推送通知 | FCM/APNs/本地通知 | NotificationSDK | 高度通用
推送解密 | iOS App Group 密钥同步（Notification Extension） | CipherGuardSDK | 高度通用
协议序列化 | Protocol Buffers/JSON | ProtocolSDK | 完全通用

#### 业务能力层

定义：IM 领域特定的业务能力，基于基础能力层构建。

能力 | 说明 | 依赖基础能力 | 可复用性
--- | --- | --- | ---
聊天功能 | 单聊/群聊/消息管理 | Network + Storage + Protocol | IM 领域高度复用
联系人管理 | 好友/黑名单/通讯录 | Network + Storage | IM 领域高度复用
群组管理 | 创建群/成员管理/权限 | Network + Storage | IM 领域高度复用
音视频通话 | 一对一/多人通话 | RTC + Network | IM 领域较为复用
消息推送 | 离线推送/在线推送 | Notification + Network | IM 领域高度复用

#### 快速响应机制

目标：通过智能缓存、预加载、性能优化，实现极速响应用户操作。

    /// 快速响应管理器
    class FastResponseManager {
      /// 智能缓存 - 预测用户行为并缓存
      Future<void> smartCache() async {
        // 预缓存最近联系人的头像
        final recentContacts = await _getRecentContacts();
        for (final contact in recentContacts) {
          await ImageCache.precache(contact.avatar);
        }

        // 预缓存最近会话的最后几条消息
        final recentChats = await _getRecentChats();
        for (final chat in recentChats) {
          await MessageCache.precache(chat.id, limit: 20);
        }
      }

      /// 预加载 - 提前加载可能访问的数据
      Future<void> preload() async {
        // 预加载用户资料
        await UserProfilePreloader.preload();

        // 预加载表情包
        await EmojiPreloader.preload();

        // 预加载常用设置
        await SettingsPreloader.preload();
      }

      /// 性能优化 - 优化关键路径
      Future<void> optimize() async {
        // 延迟加载非关键资源
        await LazyLoader.load();

        // 分帧渲染大列表
        await ListOptimizer.optimize();

        // 图片懒加载
        await ImageLazyLoader.setup();
      }
    }

    /// 响应时间监控
    class ResponseTimeMonitor {
      static void track(String operation, Duration duration) {
        final ms = duration.inMilliseconds;

        // 记录慢操作
        if (ms > 100) {
          Logger.warn('Slow operation: $operation took ${ms}ms');
        }

        // 上报性能数据
        Analytics.track('response_time', {
          'operation': operation,
          'duration_ms': ms,
        });
      }
    }

#### 容器化部署？

潜在方向：可考虑通过容器化实现快速部署、弹性扩展、版本管理。

- Docker 容器：应用容器化打包
- Kubernetes 编排：容器编排和管理
- CI/CD 流水线：自动化部署流程
- 弹性扩展：根据负载自动扩容缩容

注：容器化部署更多适用于后端服务，对于移动端 App 的应用场景需要进一步评估。

系统能力划分的价值

- 清晰边界：基础能力与业务能力分离，职责明确
- 高复用性：基础能力可复用到其他项目
- 快速响应：智能缓存和预加载提升用户体验
- 灵活部署：支持多种部署方式，适应不同场景
- 易于维护：能力独立开发和测试

### 5.5 严格的 Code Review 机制

核心理念：通过严格的代码审查机制，保证代码质量、一致性和可维护性。

#### Code Review 流程

flowchart TD Start[开发完成] --> SelfReview[自我审查] SelfReview --> Lint[代码检查工具] Lint --> UnitTest[单元测试] UnitTest --> PR[提交 PR] PR --> AutoCheck[自动检查] AutoCheck --> PeerReview[同行评审] PeerReview -->|不通过| Revise[修改代码] Revise --> SelfReview PeerReview -->|通过| ArchReview[架构师审查] ArchReview -->|不通过| Revise ArchReview -->|通过| Merge[合并到主分支] Merge --> Deploy[部署] style SelfReview fill:#e8f5e9,stroke:#388e3c style PeerReview fill:#e3f2fd,stroke:#2196f3 style ArchReview fill:#fff4e6,stroke:#f57c00 style Merge fill:#f3e5f5,stroke:#7b1fa2

#### Code Review 检查清单

类别 | 检查项 | 重要性
--- | --- | ---
架构合规 | 是否遵循分层架构？是否违反依赖规则？ | 必须
代码规范 | 是否符合命名规范？是否通过 Lint 检查？ | 必须
设计原则 | 是否符合 SOLID 原则？是否高内聚低耦合？ | 必须
测试覆盖 | 是否编写单元测试？测试覆盖率是否充分？ | 必须
性能 | 是否有性能问题？是否有内存泄漏？ | 重要
安全性 | 是否有安全漏洞？敏感数据是否加密？ | 必须
可读性 | 代码是否易于理解？是否有必要的注释？ | 重要
可维护性 | 是否易于修改？是否有重复代码？ | 重要

#### 自动化检查工具

    # analysis_options.yaml - Dart 代码检查配置（同 10.4 节）
    include: package:flutter_lints/flutter.yaml

    analyzer:
      exclude:
        - "**/*.g.dart"
        - "**/*.freezed.dart"
      language:
        strict-casts: true
        strict-inference: true
        strict-raw-types: true
      errors:
        missing_required_param: error
        missing_return: error
        todo: ignore

    linter:
      rules:
        # 架构规则
        avoid_classes_with_only_static_members: true
        prefer_final_fields: true
        # 代码风格
        prefer_single_quotes: true
        require_trailing_commas: true
        prefer_const_constructors: true
        prefer_const_declarations: true
        prefer_final_locals: true
        # 命名规则
        camel_case_types: true
        non_constant_identifier_names: true
        constant_identifier_names: true
        # 代码质量
        avoid_print: true
        avoid_empty_else: true
        no_duplicate_case_values: true
        unawaited_futures: true
        # 性能
        avoid_function_literals_in_foreach_calls: true
        prefer_collection_literals: true
        # 安全性
        avoid_web_libraries_in_flutter: true

#### Code Review 最佳实践

    /// Code Review 检查工具
    class CodeReviewChecker {
      /// 架构合规检查
      static List<String> checkArchitectureCompliance(String filePath) {
        final issues = <String>[];

        // 检查分层依赖
        if (_hasReverseDependency(filePath)) {
          issues.add('发现反向依赖：Domain 层不能依赖 Data 层');
        }

        // 检查跨层调用
        if (_hasCrossLayerCall(filePath)) {
          issues.add('发现跨层调用：UI 层不能直接调用 Repository');
        }

        return issues;
      }

      /// 代码质量检查
      static List<String> checkCodeQuality(String filePath) {
        final issues = <String>[];

        // 检查类复杂度
        if (_getClassComplexity(filePath) > 10) {
          issues.add('类复杂度过高，建议拆分');
        }

        // 检查方法长度
        if (_getMaxMethodLength(filePath) > 50) {
          issues.add('方法过长，建议拆分');
        }

        // 检查重复代码
        if (_hasDuplicateCode(filePath)) {
          issues.add('发现重复代码，建议抽取');
        }

        return issues;
      }
    }

Code Review 的价值

- 提前发现问题：在代码合并前发现 Bug 和设计问题
- 保证质量：确保代码符合架构规范和编码标准
- 知识共享：团队成员相互学习和成长
- 统一风格：保持代码库的一致性
- 降低维护成本：高质量代码更易于维护

### 5.6 长期收益分析

核心理念：架构设计不是为了短期收益，而是为了长期可持续发展。

#### 短期 vs 长期对比

维度 | 短期方案 | 长期方案（本架构） | 长期对比
--- | --- | --- | ---
初期开发 | 快速 | 需要更多时间 | -
维护成本 | 高（大量时间修 Bug） | 低（少量维护） | 大幅降低
Bug 率 | 频繁出现 Bug | Bug 很少 | 显著减少
新功能开发 | 缓慢 | 快速 | 明显提速
技术债务 | 严重 | 很少 | 避免重构
团队效率 | 低（大量救火） | 高（专注开发） | 显著提升
代码质量 | 差（难以维护） | 优（易于维护） | 可持续发展

#### 长期收益体现

初期投入 vs 长期回报：

- 初期阶段：需要更多时间进行架构设计和基础设施建设
- 维护阶段：维护成本大幅降低，团队可以专注新功能开发
- 迭代阶段：新功能开发速度显著提升，架构优势逐渐显现
- 长期发展：技术债务少，代码库健康，可持续发展

收益来源：

- 维护成本降低：良好的架构设计减少维护工作量
- Bug 率降低：严格的分层和测试机制减少 Bug 数量
- 开发效率提升：清晰的架构和丰富的基础组件加速开发
- 避免重构：提前规划避免后期大规模重构

#### 长期收益的关键指标

flowchart LR Architecture[良好架构] --> LowMaintenance[低维护成本] Architecture --> HighQuality[高代码质量] Architecture --> FastDevelopment[快速开发] LowMaintenance --> MoreTime[更多时间] HighQuality --> FewerBugs[更少 Bug] FastDevelopment --> MoreFeatures[更多功能] MoreTime --> BusinessValue[业务价值] FewerBugs --> BusinessValue MoreFeatures --> BusinessValue BusinessValue --> CompetitiveAdvantage[竞争优势] style Architecture fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style BusinessValue fill:#10b981,stroke:#059669,stroke-width:2px style CompetitiveAdvantage fill:#f59e0b,stroke:#d97706,stroke-width:2px

#### 长期收益总结

- 投资回报：初期投入更多时间，长期回报远超初期投入
- 维护成本：大幅降低，团队可以专注创新而非救火
- Bug 率：显著减少，用户体验持续提升
- 开发效率：新功能开发明显提速，快速响应市场
- 技术债务：避免大规模重构，保持代码库健康
- 团队成长：规范的架构让团队成员快速成长
- 竞争优势：更快的迭代速度，更高的产品质量

### 5.7 日志与监控系统

核心理念：通过完善的日志系统和运行监控，实现问题快速定位、性能实时追踪、用户行为分析。

#### 5.7.1 日志系统设计

日志分级策略：

级别 | 用途 | 输出位置 | 保留时间
--- | --- | --- | ---
Debug | 开发调试信息 | 仅开发环境控制台 | 不保存
Info | 正常运行信息、关键操作 | 本地文件 | 7 天
Warning | 潜在问题、异常情况 | 本地文件 + 远程上报 | 30 天
Error | 错误信息、异常堆栈 | 本地文件 + 远程立即上报 | 90 天
Fatal | 严重错误、崩溃 | 本地文件 + 远程立即上报 + 告警 | 永久

日志分类：

flowchart TD Logger[日志系统] --> UILog[UI 日志] Logger --> NetworkLog[网络日志] Logger --> BusinessLog[业务日志] Logger --> PerformanceLog[性能日志] Logger --> SecurityLog[安全日志] UILog --> UIAction[用户操作] UILog --> UIError[UI 错误] NetworkLog --> Request[请求记录] NetworkLog --> Response[响应记录] NetworkLog --> NetworkError[网络错误] BusinessLog --> BizFlow[业务流程] BusinessLog --> DataChange[数据变更] PerformanceLog --> LoadTime[加载时间] PerformanceLog --> MemUsage[内存占用] PerformanceLog --> FPS[帧率] SecurityLog --> Auth[认证行为] SecurityLog --> Privacy[隐私操作] style Logger fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style UILog fill:#e3f2fd,stroke:#2196f3 style NetworkLog fill:#e8f5e9,stroke:#388e3c style BusinessLog fill:#fff4e6,stroke:#f57c00 style PerformanceLog fill:#f3e5f5,stroke:#7b1fa2 style SecurityLog fill:#fce4ec,stroke:#c2185b

#### 5.7.2 日志系统实现

    /// 日志系统 - 统一日志管理
    class LoggerService {
      static final LoggerService _instance = LoggerService._internal();
      factory LoggerService() => _instance;
      LoggerService._internal();

      /// 日志级别
      void debug(String message, {Map<String, dynamic>? data}) {
        if (!kReleaseMode) {
          _log(LogLevel.debug, message, data);
        }
      }

      void info(String message, {Map<String, dynamic>? data}) {
        _log(LogLevel.info, message, data);
        _saveToLocal(LogLevel.info, message, data);
      }

      void warning(String message, {Map<String, dynamic>? data}) {
        _log(LogLevel.warning, message, data);
        _saveToLocal(LogLevel.warning, message, data);
        _uploadToRemote(LogLevel.warning, message, data);
      }

      void error(String message, {
        Map<String, dynamic>? data,
        StackTrace? stackTrace,
      }) {
        _log(LogLevel.error, message, data);
        _saveToLocal(LogLevel.error, message, data, stackTrace);
        _uploadToRemote(LogLevel.error, message, data, stackTrace, immediate: true);
      }

      void fatal(String message, {
        Map<String, dynamic>? data,
        StackTrace? stackTrace,
      }) {
        _log(LogLevel.fatal, message, data);
        _saveToLocal(LogLevel.fatal, message, data, stackTrace);
        _uploadToRemote(LogLevel.fatal, message, data, stackTrace, immediate: true);
        _triggerAlert(message, data, stackTrace);
      }

      /// 网络日志
      void logNetworkRequest(String url, {
        required HTTPMethod method,
        Map<String, dynamic>? headers,
        Map<String, dynamic>? params,
      }) {
        info('Network Request', data: {
          'url': url,
          'method': method.name,
          'headers': _sanitizeHeaders(headers),
          'params': _sanitizeData(params),
          'timestamp': DateTime.now().toIso8601String(),
        });
      }

      void logNetworkResponse(String url, {
        required int statusCode,
        required Duration duration,
        String? error,
      }) {
        final level = statusCode >= 400 ? LogLevel.warning : LogLevel.info;
        _log(level, 'Network Response', {
          'url': url,
          'statusCode': statusCode,
          'duration': '${duration.inMilliseconds}ms',
          'error': error,
        });
      }

      /// 用户操作日志
      void logUserAction(String action, {Map<String, dynamic>? data}) {
        info('User Action: $action', data: data);
      }

      /// 性能日志
      void logPerformance(String operation, Duration duration) {
        if (duration.inMilliseconds > 100) {
          warning('Slow Operation: $operation', data: {
            'duration': '${duration.inMilliseconds}ms',
          });
        }
      }

      /// 隐私数据脱敏
      Map<String, dynamic>? _sanitizeData(Map<String, dynamic>? data) {
        if (data == null) return null;
        final sanitized = Map<String, dynamic>.from(data);

        // 脱敏敏感字段
        final sensitiveKeys = ['password', 'token', 'secret', 'phone', 'email'];
        for (final key in sensitiveKeys) {
          if (sanitized.containsKey(key)) {
            sanitized[key] = '***';
          }
        }

        return sanitized;
      }
    }

    enum LogLevel { debug, info, warning, error, fatal }

#### 5.7.3 运行监控系统

监控维度：

监控类型 | 监控指标 | 告警阈值 | 采集频率
--- | --- | --- | ---
性能监控 | CPU 占用率、内存占用、帧率（FPS） | 内存 > 500MB FPS < 50 | 实时
错误监控 | Crash 率、ANR 率、异常捕获 | Crash 率 > 0.1% | 实时
网络监控 | 请求成功率、响应时间、流量消耗 | 成功率 < 95% 响应时间 > 3s | 每次请求
业务监控 | 消息发送成功率、登录成功率 | 成功率 < 98% | 每次操作
用户行为 | 页面访问路径、功能使用频率 | - | 每次操作

监控系统架构：

flowchart TD App[IM App] --> Monitor[监控 SDK] Monitor --> Performance[性能监控] Monitor --> Error[错误监控] Monitor --> Network[网络监控] Monitor --> Business[业务监控] Monitor --> Behavior[行为监控] Performance --> Collector[数据采集器] Error --> Collector Network --> Collector Business --> Collector Behavior --> Collector Collector --> LocalCache[本地缓存] LocalCache --> Uploader[批量上报] Uploader --> Backend[监控后端] Backend --> Analysis[数据分析] Backend --> Alert[实时告警] Backend --> Dashboard[监控大盘] style Monitor fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style Backend fill:#10b981,stroke:#059669,stroke-width:2px style Alert fill:#ef4444,stroke:#dc2626,stroke-width:2px

#### 5.7.4 监控系统实现

    /// 监控服务
    class MonitorService {
      static final MonitorService _instance = MonitorService._internal();
      factory MonitorService() => _instance;
      MonitorService._internal();

      /// 性能监控
      void trackPerformance() {
        // 监控内存占用
        final memoryUsage = _getMemoryUsage();
        if (memoryUsage > 500 * 1024 * 1024) {  // 500MB
          LoggerService().warning('High Memory Usage', data: {
            'memory': '${memoryUsage ~/ (1024 * 1024)}MB',
          });
        }

        // 监控帧率
        WidgetsBinding.instance.addTimingsCallback((timings) {
          for (final timing in timings) {
            final fps = 1000 / timing.totalSpan.inMilliseconds;
            if (fps < 50) {
              LoggerService().warning('Low FPS', data: {'fps': fps.toStringAsFixed(1)});
            }
          }
        });
      }

      /// 错误监控 - Crash 捕获
      void setupErrorMonitoring() {
        // Flutter 错误捕获
        FlutterError.onError = (details) {
          LoggerService().fatal('Flutter Error', data: {
            'exception': details.exception.toString(),
            'stackTrace': details.stack.toString(),
          });
        };

        // Dart 错误捕获
        PlatformDispatcher.instance.onError = (error, stack) {
          LoggerService().fatal('Dart Error', data: {
            'error': error.toString(),
            'stackTrace': stack.toString(),
          });
          return true;
        };
      }

      /// 网络监控
      void trackNetworkRequest({
        required String url,
        required DateTime startTime,
        required DateTime endTime,
        required int statusCode,
        String? error,
      }) {
        final duration = endTime.difference(startTime);

        // 记录请求
        _recordMetric('network.request', {
          'url': url,
          'duration': duration.inMilliseconds,
          'statusCode': statusCode,
          'success': statusCode >= 200 && statusCode < 300,
          'error': error,
        });

        // 慢请求告警
        if (duration.inSeconds > 3) {
          LoggerService().warning('Slow Network Request', data: {
            'url': url,
            'duration': '${duration.inSeconds}s',
          });
        }

        // 请求失败告警
        if (statusCode >= 400) {
          LoggerService().error('Network Request Failed', data: {
            'url': url,
            'statusCode': statusCode,
            'error': error,
          });
        }
      }

      /// 业务监控
      void trackBusinessEvent(String event, {
        bool success = true,
        Map<String, dynamic>? data,
      }) {
        _recordMetric('business.$event', {
          'success': success,
          ...?data,
        });

        if (!success) {
          LoggerService().warning('Business Event Failed: $event', data: data);
        }
      }

      /// 用户行为监控
      void trackUserBehavior(String page, String action, {Map<String, dynamic>? data}) {
        _recordMetric('behavior', {
          'page': page,
          'action': action,
          'timestamp': DateTime.now().toIso8601String(),
          ...?data,
        });
      }

      /// 记录指标
      void _recordMetric(String metric, Map<String, dynamic> data) {
        // 本地缓存
        _cacheMetric(metric, data);

        // 批量上报（每 1 分钟或累计 100 条）
        _scheduleUpload();
      }

      /// 批量上报
      Future<void> _scheduleUpload() async {
        // 实现批量上报逻辑
      }
    }

    /// 使用示例
    class ChatViewModel extends StateNotifier<ChatState> {
      Future<void> sendMessage(String content) async {
        final startTime = DateTime.now();

        try {
          await _sendMessageUseCase(content);

          // 监控业务成功
          MonitorService().trackBusinessEvent('send_message', success: true, data: {
            'messageLength': content.length,
            'duration': DateTime.now().difference(startTime).inMilliseconds,
          });

          // 监控用户行为
          MonitorService().trackUserBehavior('chat', 'send_message');

        } catch (e, stackTrace) {
          // 监控业务失败
          MonitorService().trackBusinessEvent('send_message', success: false, data: {
            'error': e.toString(),
          });

          LoggerService().error('Send Message Failed', data: {
            'error': e.toString(),
          }, stackTrace: stackTrace);

          rethrow;
        }
      }
    }

#### 5.7.5 日志与监控最佳实践

实践 | 说明 | 价值
--- | --- | ---
结构化日志 | 使用 JSON 格式记录日志，便于检索和分析 | 快速定位问题
隐私保护 | 敏感数据脱敏（密码、Token、手机号） | 符合隐私合规
批量上报 | 本地缓存，定时批量上报，减少网络请求 | 节省流量和电量
实时告警 | Fatal 级别错误立即上报并触发告警 | 及时发现严重问题
性能优化 | 日志写入异步化，不阻塞主线程 | 不影响用户体验
存储管理 | 本地日志定期清理，避免占用过多空间 | 节省存储空间

日志与监控的核心价值

- 问题定位：通过完整的日志链路，快速定位问题根因
- 性能优化：实时监控性能指标，及时发现和解决性能瓶颈
- 用户体验：监控用户行为，优化产品功能和交互
- 故障预警：实时告警机制，在问题扩散前及时处理
- 数据驱动：基于监控数据做决策，而非主观猜测

## 第七部分：总结

### 架构优势

- Feature 驱动的垂直切片：每个功能页面独立成模块，包含完整的 UI → Presentation → Domain 链路
- 清晰的职责划分：四层架构（Feature/Domain/Data/Core），每层有明确的职责边界
- 高度可测试：每层可独立测试，Feature 可独立验证
- 易于维护：模块化设计，修改影响范围小，功能高度内聚
- 可扩展性强：添加新 Feature 不影响现有代码，遵循标准模板
- 技术栈独立：底层实现可随时替换，业务逻辑不受影响
- 团队协作友好：Feature 驱动便于并行开发，减少代码冲突
- 低耦合高内聚：Feature 之间通过 Repository 接口解耦

### 核心架构设计

flowchart TD subgraph Principles[设计原则] FeatureDriven[Feature 驱动] CleanArch[Clean Architecture] MVVM[MVVM 状态管理] end subgraph Structure[目录结构] Features[features/ - 按页面垂直切片] GlobalDomain[domain/ - 全局接口] Data[data/ - 统一数据层] CoreFoundation[core/foundation/ - 应用级基础设施] L10nSDK[packages/l10n_sdk - 多语言国际化] CoreUI[core/ui/ - UI 基础设施] SDKPackages[packages/ - 独立 SDK Packages] end subgraph Benefits[核心优势] Isolation[功能隔离] Testable[可测试性] Scalable[可扩展性] Maintainable[可维护性] end Principles --> Structure Structure --> Benefits style Principles fill:#e1f5ff,stroke:#0288d1,stroke-width:2px style Structure fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Benefits fill:#e8f5e9,stroke:#388e3c,stroke-width:2px

### 最佳实践

1. 严格遵守分层依赖规则：单向向下依赖，禁止反向依赖和跨层调用
2. Feature 垂直切片：每个 Feature 包含 view/presentation/domain 的完整链路
3. 全局 Repository 接口：所有 Repository 接口定义在 domain/repositories/
4. 统一 Data 实现：所有 Repository 实现在 data/repositories/
5. UseCase 单一职责：每个 UseCase 只处理一个业务场景
6. Riverpod 状态管理：使用 StateNotifier 管理 UI 状态，Providers 管理依赖
7. 使用代码生成：利用 riverpod_generator 和 freezed 减少样板代码
8. 使用 Melos 管理依赖：Mono-Repo 保证版本一致性
9. 编写完整测试：单元测试、集成测试、端到端测试
10. UI 层使用 ConsumerWidget：通过 ref.watch 监听状态，ref.read 读取 Provider
11. import 一律使用 package 全路径：禁止相对路径（`../base/colors.dart`），统一写 `package:im_app/core/ui/base/colors.dart`；相对路径文件移动后静默失效，全路径重构安全、跨包引用统一，已由 `always_use_package_imports: true` 强制执行

### 关键原则

> Feature 驱动：以页面为单位组织代码，每个 Feature 是完整的垂直切片

> 依赖倒置：高层模块不依赖低层模块，两者都依赖抽象（Repository 接口）

> 单一职责：每个模块只做一件事，UseCase/ViewModel/Repository 各司其职

## 第八部分：UI 设计规范

本章定义颜色、字体、组件、弹框、图标的命名与使用规则，明确设计与研发的协作约定。Figma 按此命名，代码按此封装，两端名称一一对应。

### 8.0 核心约定最后更新 2026-03-10-10-17

全局只有一份

颜色、字体、基础组件、业务弹框、图片、图标——Figma 里每种元素只有一个定义。没有"备用版本"，没有"临时副本"，不允许两个"差不多一样"的组件并存。

1. Figma 命名是重中之重：点中任何元素都必须看到抽象名称。六类无例外：
   - `颜色` — 如 `primary`、`surface`
   - `字体` — 如 `Body/Medium`、`Label/Small`
   - `基础组件` — 如 `Button/Primary`、`Input/Default`，全局只有一个版本
   - `业务弹框` — 如 `Dialog/Confirm`
   - `图片` — Figma 统一导出，代码侧 `AppAssets` 注册，不硬编码路径
   - `图标` — 如 `send`、`more_options`，代码侧 `AppIcons` 调用
2. 基础组件定稿后不随意改动：需改动时必须先告知研发，评估影响范围，双方同步后再执行
3. UI 团队自主维护 UI 基建体系：研发照着 Figma 名字封装，名称必须完全一致
4. 所有元素遵循同一套命名规则：新增任何元素先在 Figma 定名，研发用相同名字注册

图片和组件是重灾区：没有统一来源时，不同研发各自导出同一张图，文件名不同、尺寸不同，最终项目里堆满重复文件。Figma 统一命名、代码统一注册，才能从源头堵住。

### 8.1 颜色体系最后更新 2026-03-10-10-17

颜色采用 5 层架构：ColorBases（颜色常量）→ AppColors（语义接口）→ LightColors/DarkColors（亮暗实现）→ AppTheme._build(AppColors c)（主题组装）→ context.colors（Widget 消费）。AppTheme 内部零 isDark 分支，亮暗差异完全由实现类决定。

#### Layer 1: ColorBases — 颜色常量

色相 | 常量名 | 色值
--- | --- | ---
Brand | primary400 | #5BA3F5
primary500 | #2F80ED
primary700 | #1A6BD4
Neutral | neutral0 | #FFFFFF
neutral50 | #F8F9FA
neutral100 | #F1F3F4
neutral200 | #E8EAED
neutral400 | #BDC1C6
neutral600 | #80868B
neutral800 | #3C4043
neutral850 | #2C2C2E
neutral900 | #202124
neutral950 | #000000
Neutral Alpha | black12 | 12% 透明度黑色
black60 | 60% 透明度黑色
Status | error500 | #EB5757
success500 | #27AE60
warning500 | #F2C94C

#### Layer 2-3: AppColors — 语义接口 + 亮暗实现

语义名 | 用途 | 亮色 | 暗色
--- | --- | --- | ---
`bgPrimary` | Scaffold 底色 | neutral50 | neutral900
`bgSecondary` | Card / Surface | neutral0 | neutral800
`bgTertiary` | 次要区块 | neutral100 | neutral850
`textPrimary` | 主文本 | neutral900 | neutral0
`textSecondary` | 辅助文本 | neutral600 | neutral400
`textDisabled` | 禁用态 | neutral400 | neutral600
`textOnPrimary` | 品牌色上的文字 | neutral0
`borderDefault` | 默认边框 | neutral200 | neutral800
`borderFocused` | 聚焦边框 | primary500 | primary400
`brandPrimary` | 主品牌色 | primary500 | primary400
`brandPrimaryHover` | Hover 态 | primary700 | primary500
`statusError` | 错误 | error500
`statusSuccess` | 成功 | success500
`statusWarning` | 警告 | warning500
`navBarBg` | 底部导航背景 | neutral0 | neutral900
`navBarSelected` | 导航选中色 | primary500 | primary400
`divider` | 分割线 | neutral100 | neutral800
`shadow` | 阴影颜色 | black12 | black60

#### Widget 侧使用（双入口）

三个 context 扩展：`context.colors` — 语义颜色；`context.styles` — 字体 + 预组合样式；`context.shadows` — 阴影常量。

    final c = context.colors;
    final s = context.styles;

    Container(color: c.bgSecondary)          // 语义颜色
    Text('标题', style: s.headlineMedium)    // 字体
    Text('分组', style: s.sectionLabel)      // 预组合：字体 + 品牌色
    Icon(Icons.check, color: c.brandPrimary) // 图标颜色
    Container(decoration: BoxDecoration(boxShadow: context.shadows.bs8)) // 阴影

使用原则：颜色一律用 `context.colors.xxx`，禁止写 `Color(0xFF...)` 或 `Theme.of(context).colorScheme.xxx`。新增语义色先在 `AppColors` 加 getter，再在 LightColors / DarkColors 各实现一遍。

### 8.2 字体体系

字体按层级分五档：Display、Headline、Title、Body、Label，每档三个尺寸。Figma 中按 层级/尺寸 格式命名（如 `Body/Large`），开发用同名变量调用。

Figma 名称 | 字号 | 字重 | 行高 | 字距 | 典型用途
--- | --- | --- | --- | --- | ---
DISPLAY
Display/Large | 57 | 400 | 64 | -0.25 | 启动页大标题、空状态
Display/Medium | 45 | 400 | 52 | — | —
Display/Small | 36 | 400 | 44 | — | —
HEADLINE
Headline/Large | 32 | 400 | 40 | — | 页面主标题、导航栏
Headline/Medium | 28 | 400 | 36 | — | —
Headline/Small | 24 | 400 | 32 | — | —
TITLE
Title/Large | 22 | 500 | 28 | — | 会话列表名称、设置项标题
Title/Medium | 16 | 500 | 24 | 0.15 | 卡片标题、列表主行
Title/Small | 14 | 500 | 20 | 0.1 | —
BODY
Body/Large | 16 | 400 | 24 | 0.5 | 聊天气泡、表单输入
Body/Medium | 14 | 400 | 20 | 0.25 | 正文说明、列表副行
Body/Small | 12 | 400 | 16 | 0.4 | 辅助信息、提示文字
LABEL
Label/Large | 14 | 500 | 20 | 0.1 | 按钮文字、Tab 标签、Badge
Label/Medium | 12 | 500 | 16 | 0.5 | 次要标签、徽标文字
Label/Small | 11 | 500 | 16 | 0.5 | 最小粒度标签
语义样式
Section Label | 13 | 600 | — | 0.5 | 列表分组标题、设置分区
Body/Muted | 12 | 400 | 16 | — | 说明文字（灰色，低对比度）
Body/Error | 12 | 400 | 16 | — | 表单错误提示（红色）
Label/Muted | 12 | 500 | 16 | — | 时间戳、元数据（低对比度）

### 8.3 组件 — Button

按钮共四种变体，每种有明确使用场景和 Figma 组件名。每个页面上主操作只用一个 Primary。

Figma 组件名 | 用途 | 亮色样式 | 暗色样式 | 状态
--- | --- | --- | --- | ---
`Button/Primary` | 主操作（登录、发送、确认），每屏最多一次 | 背景 #2F80ED，白字 | 背景 #5BA3F5，白字 | 默认 / Loading / 禁用（#BDC1C6）
`Button/Secondary` | 次要操作（注册、稍后再说），描边样式 | 描边 #2F80ED，蓝字 | 描边 #5BA3F5，蓝字 | 默认 / 禁用
`Button/Text` | 辅助链接（忘记密码、查看全部、弹框取消） | 无背景，蓝字 | 无背景，蓝字 | 默认
`Button/Inverse` | 反色按钮（深色背景高亮），支持左侧图标 | 背景 #202124，白字 | 背景 #FFFFFF，黑字 | 默认

### 8.4 业务弹框 — Dialog

当前封装一种通用确认弹框 `Dialog/Confirm`，后续新增先在 Figma 以 `Dialog/` 前缀命名。

项目 | 说明
--- | ---
结构 | 标题（不超 15 字） + 内容 + 操作区（取消 Text 样式 | 确认 Primary 样式）
可配置 | 标题文字、内容文字、确认/取消标签、点击背景是否可关闭
返回值 | 确认 / 取消 / 关闭（点击背景）

### 8.5 图标规范

1. UI 先命名，开发跟随：Figma 确定名称，开发用完全相同的名称封装到 `AppIcons`
2. 名称有实际语义：全小写下划线，如 `send`、`add_contact`、`more_options`。不用拼音，不缩写
3. 统一用 AppIcons 调用：不允许直接用裸 icon 库变量，替换时改一处全局生效
4. 同义图标只保留一个：同功能图标在整个产品内只存在一种

新增图标流程：设计师 Figma 确认名称 → 告知开发 → 开发用相同名称在 AppIcons 注册。两端名称必须完全一致。