docs: 新增完整文档体系(setup-project / setup-android / setup-ios / CLAUDE.md)

This commit is contained in:
SkyJourney
2026-05-14 13:04:51 +08:00
parent bb07828234
commit a266323905
5 changed files with 1065 additions and 169 deletions
+217 -64
View File
@@ -1,89 +1,242 @@
# CLAUDE.md — sunny_mochi 脚手架项目规范
# CLAUDE.md — Flutter 企业级脚手架(sunny_mochi
## 项目简介
> 本文件为 Claude Code 提供项目上下文。**所有 AI 辅助开发会话必须首先读取本文件**,
> 再按需加载 `docs/` 下的补充文档。
`sunny_mochi` 是企业级 Flutter APP 脚手架模板,包含:
- Flavor 三包体系(dev/staging/prod
- 完整 Core 基础设施层(网络/存储/崩溃/主题/观测性)
- 认证骨架(Auth Feature
- 开发者工具面板(Dev Panel)
- 本地错误日志 + 上报
---
## 项目身份信息
> **以下带 `TODO` 的字段由业务项目接手时填入,脚手架默认保留占位符。**
| 字段 | 当前值(脚手架默认) | 说明 |
|------|-----------------|------|
| 项目名称 | sunny_mochi | TODO: 替换为实际项目名 |
| Application ID (Android) | com.example.sunny_mochi | TODO: 如 com.company.projectname |
| Bundle ID (iOS) | com.example.sunny-mochi | TODO: 与 Android 对应 |
| API Base URL (dev) | http://localhost:8080 | TODO: 填入 `dart-defines/dev.json` |
| API Base URL (staging) | https://staging.example.com | TODO: 填入 `dart-defines/staging.json` |
| API Base URL (prod) | https://api.example.com | TODO: 填入 `dart-defines/prod.json` |
| 后端鉴权框架 | sa-token(动态 header tokenName | TODO: 若使用 Bearer/JWT 修改 auth_interceptor |
| Sentry DSN | 空(未启用) | TODO: 填入 `dart-defines/prod.json` |
| RSA 公钥 | REPLACE_WITH_RSA_PUBLIC_KEY | TODO: 填入三个 dart-defines JSON |
---
## 技术栈(脚手架锁定版本)
| 分类 | 技术 | 版本 |
|------|------|------|
| Flutter SDK | Flutter | ≥ 3.41.0 |
| 状态管理 | Riverpod + riverpod_annotation | ^3.3.0 / ^4.0.0 |
| 路由 | GoRouter + go_router_builder | ^17.0.0 / ^4.3.0 |
| 网络 | Dio | ^5.9.0 |
| 本地数据库 | Drift + SQLCipher | 2.31.0(锁定)|
| 安全存储 | flutter_secure_storage | ^10.0.0 |
| 序列化 | Freezed + json_serializable | ^3.x / 6.13.0(锁定)|
| 日志 | Talker + talker_flutter | ^5.x |
| 错误追踪 | Sentry Flutter | ^9.0.0 |
| i18n | Slang | ^4.0.0 |
| UI 自适应 | flutter_screenutil(基准 375×812| ^5.9.0 |
| 图片缓存 | cached_network_image | ^3.4.0 |
| 字体 | HarmonyOS Sans SCRegular/Medium/Bold/Black| GB2312 子集 |
**版本锁定原因(勿随意升级):**
- `drift: 2.31.0` — 2.32+ 需要 analyzer ^10.x,与 riverpod_generator 的 analyzer ^9.x 冲突
- `json_serializable: 6.13.0` — 6.13.1+ 同样需要 analyzer ^10.x
---
## 目录结构
```
lib/
├── main.dart # 6 步启动序列
├── app.dart # AppRoot → MaterialApp.router
├── i18n/ # Slang 翻译文件
├── main.dart # 6步启动序列(顺序固定,勿调整)
├── app.dart # AppRoot → ScreenUtilInit → App → MaterialApp.router
├── i18n/ # Slang 翻译zh-CN 基准 + en 备用)
├── core/
│ ├── config/ # Env / ApiConfig / ApiPaths
│ ├── router/ # GoRouter + TypedRoutes
│ ├── network/ # Dio 7 拦截器 + MockAdapter
├── storage/ # Drift + SQLCipher + SecureStorage
│ ├── crash/ # CrashReporter 3 步生命周期
│ ├── error/ # sealed Failure 层级
├── theme/ # 5 色板 + 深色模式
│ ├── observability/ # Talker + Sentry
│ ├── sync/ # Offline-First 同步框架
│ ├── crypto/ # RSA 加密工具
│ ├── extensions/ # BuildContext / String / num / DateTime
│ ├── utils/ # 验证器 / 密码强度 / SMS 倒计时
├── data/ # FreshnessPolicy 缓存策略
── widgets/ # 通用 UI 组件库
│ ├── config/
│ ├── env.dart # 从 dart-defines 读取环境变量(只读,勿改结构)
│ ├── api_config.dart # baseUrl 按 Flavor 决策
│ └── api_paths.dart # ★ 新项目必填:所有 API 路径常量
│ ├── router/
│ ├── app_router.dart # GoRouter providerkeepAlive
│ └── routes.dart # ★ 新项目扩展:添加 @TypedGoRoute
│ ├── network/
│ ├── dio_client.dart # 7 拦截器注册(顺序固定)
│ ├── api_response.dart # 统一 envelope 解析(parseEnvelope / unwrapVoid
│ ├── response_code.dart# 业务状态码(成功=00000token 类码分类)
│ ├── interceptors/ # 7 个拦截器(勿修改拦截顺序)
│ └── mock/ # MockAdapter + fixture JSONdev 调试用)
── storage/
│ │ ├── app_database.dart # Drift @DriftDatabaseschemaVersion 从 1 开始)
│ │ ├── secure_storage.dart# token / userId / tokenName 的唯一真源
│ │ ├── db_key_provider.dart# AES-256 密钥派生(勿修改策略)
│ │ ├── tables/ # Drift 表定义(Users + ErrorLogs + 业务表)
│ │ └── daos/ # DAO(只放纯 DB 操作)
│ ├── crash/ # CrashReporterpreInit/consumePending/installHooks
│ ├── error/
│ │ ├── failures.dart # Sealed Failure 层级(唯一错误分类)
│ │ └── exception_mapper.dart # 异常 → Failure 映射
│ ├── theme/ # 5 色板 + 深色模式(ThemeNotifier + SharedPreferences
│ ├── observability/ # Talker(全局 appTalker+ SentryRelease 上报)
│ ├── sync/ # Offline-First 同步框架骨架(SyncService
│ ├── crypto/ # RSA PKCS#1 v1.5RsaHelper.encrypt
│ ├── extensions/ # BuildContextX / StringX / NumX / DateTimeX
│ ├── utils/ # Validator / PasswordValidator / SmsCountdownMixin
│ ├── data/ # FreshnessPolicy(缓存新鲜度判断)
│ └── widgets/ # 通用 UI 组件(AppToast / AppButton / EmptyView 等)
└── features/
├── auth/ # 认证 Feature(完整 Clean Architecture
├── dev_panel/ # Talker 调试面板(Internal 包)
└── error_report/ # 本地错误日志查看 + 上报
├── auth/ # ★ 认证(完整骨架,业务项目修改 LoginPage UI
├── dev_panel/ # Talker 面板(Internal 包,勿在 release 中暴露入口
└── error_report/ # 本地错误日志 + 上报(完整实现)
```
## 新项目初始化清单
---
1. **替换包名**`sunny_mochi` → 你的包名(android/app/build.gradle.kts + pubspec.yaml + import
2. **填入 API 路径**`lib/core/config/api_paths.dart` 中所有空字符串
3. **配置 Flavor**`android/app/build.gradle.kts` applicationId + 签名配置
4. **填入 Sentry DSN**`dart-defines/prod.json`
5. **填入 RSA 公钥**`dart-defines/dev.json``rsaPublicKey` 字段
6. **替换 i18n 字符串**`lib/i18n/strings_zh_CN.i18n.json` 按需扩展
7. **添加业务路由**`lib/core/router/routes.dart` 新增 TypedGoRoute
8. **实现 Tab 页面**:替换 HomeRoute / MineRoute 的 placeholder build
## 开发命令
## 零容忍规则
```bash
# 代码生成(修改 Dart 文件后运行)
make gen
# i18n 生成(修改 JSON 翻译文件后运行)
make gen-i18n
# 运行
make run-dev # dev 包(连 dev 服务器,mock 可开)
make run-staging # staging 包(连 test 服务器,release 模式)
# 构建
make build-apk-dev # dev APK(快速真机验证)
make build-staging # staging APK(交测试团队)
make build-prod # prod AAB(需 prod.json + key.properties
# 质量检查
flutter analyze # 目标 0 errors103 条 info 为正常)
flutter test # 全量测试
```
---
## 常用开发范式
### 新增 Feature
```
lib/features/{feature_name}/
├── domain/
│ ├── entities/{name}_entity.dart # @freezed,纯 Dart 字段
│ └── repositories/{name}_repository.dart # abstract interface
├── data/
│ ├── models/{name}_model.dart # @freezed + fromJson,含 toEntity()
│ ├── datasources/{name}_remote_datasource.dart # @riverpod,只调 Dio
│ └── repositories/{name}_repository_impl.dart # @riverpod,实现 domain 接口
└── presentation/
├── notifiers/{name}_notifier.dart # @riverpod class XxxNotifier
└── pages/{name}_page.dart # ConsumerWidget / ConsumerStatefulWidget
```
### 新增路由
`lib/core/router/routes.dart` 添加(然后运行 `make gen`):
```dart
@TypedGoRoute<MyNewRoute>(path: '/my-new-path')
class MyNewRoute extends GoRouteData with $MyNewRoute {
const MyNewRoute();
@override
Widget build(BuildContext context, GoRouterState state) => const MyNewPage();
}
```
### 新增 API 路径
`lib/core/config/api_paths.dart` 添加:
```dart
// ============== My Feature ==============
static const String myFeatureList = '/api/v1/my-feature/list';
```
### 新增 DB 表
1.`lib/core/storage/tables/` 新建表文件
2.`lib/core/storage/app_database.dart``@DriftDatabase(tables: [...])` 添加
3. 递增 `schemaVersion` 并在 `MigrationStrategy.onUpgrade` 中添加迁移
4. 运行 `make gen`
### 错误处理标准模式
```dart
// Notifier 层:捕获 + 映射
} on Object catch (e, st) {
final failure = ExceptionMapper().fromUnknown(e, st);
state = MyState.error(failure.message);
}
// UI 层:监听 + 显示
ref.listen(myProvider, (_, next) {
if (next is AsyncError) context.showError(ref, next.error!, next.stackTrace!);
});
```
---
## 零容忍规则(AI 必须遵守)
| 规则 | 正确 | 禁止 |
|------|------|------|
| API 路径 | `ApiPaths.xxx` | 字面量字符串 |
| 错误处理 | `ExceptionMapper.fromUnknown()` | 散落的 `catch(e)` 直接显示 |
| Token 存储 | `SecureStorage`SoT | 普通 SharedPreferences |
| DB 加密 | SQLCipher(默认) | 无加密 sqflite |
| 日志 | `appTalker.xxx()` | `print()` / `debugPrint()` |
| 路由跳转 | `LoginRoute().go(context)` | `Navigator.push()` |
| API 路径 | `ApiPaths.xxx` 常量 | 字符串字面量散落在代码中 |
| 错误处理 | `ExceptionMapper().fromUnknown(e, st)` | `catch` 直接 `toString()` 显示 |
| Token 存储 | `SecureStorage`Keychain/EncryptedPrefs| 普通 `SharedPreferences` |
| 数据库加密 | SQLCipher(默认) | 无加密 `sqflite` |
| 日志输出 | `appTalker.info/warning/error()` | `print()` / `debugPrint()` |
| 路由跳转 | `XxxRoute().go(context)` / `XxxRoute().push(context)` | `Navigator.push()` |
| 状态管理 | Riverpod `@riverpod` / `@Riverpod(keepAlive: true)` | `setState` 跨组件共享 / `Provider` 包 |
| Riverpod 命名 | 生成 provider 名(`AuthNotifier``authProvider`| 手写 `authNotifierProvider` |
| 代码生成文件 | 只读,不手写 `.g.dart` / `.freezed.dart` | 手动修改生成文件 |
| Domain 层 | 纯 Dart,无 Flutter / Drift / Dio 依赖 | domain 层 import `package:dio` |
| Mock fixture | `assets/fixtures/{group}/{name}.json` | 在代码中硬编码 mock 数据 |
| 注释语言 | 中文(团队约定) | 英文注释(除公共 API doc)|
## 常用命令
---
```bash
# 代码生成
make gen
## 关键架构决策(勿重议)
# i18n 生成
make gen-i18n
1. **SQLCipher 不可替换**:企业合规要求,部署后无法无损切换到无加密数据库
2. **7 拦截器顺序固定**CertPinning→Auth→TokenRefresh→Retry→Error→AuthLogout→Log,错误处理链依赖此顺序
3. **TokenRefresh 使用 Completer 互斥**:防止并发 401 重复消耗 RefreshToken,不可改为简单 flag
4. **SecureStorage 是 token 唯一真源**DB 中的 refreshToken 仅作可观测性镜像
5. **GoRouter 用 `ref.read`(非 `ref.watch`)构建**router 是 keepAlive 单例,通过 `refreshListenable` 响应 auth 变化
6. **Failure 不可绕过**:所有网络/DB 异常必须经 ExceptionMapper 映射,UI 只看 Failure.message
# 运行(dev 包)
make run-dev
---
# 构建 staging APK
make build-staging
## 新项目接手 Checklist
# 静态分析
flutter analyze
> 完成后删除本节或移至项目 wiki
# 测试
flutter test --coverage
```
- [ ] 更新本文件"项目身份信息"表格
- [ ] 填入 `lib/core/config/api_paths.dart` 所有路径
- [ ] 配置三个 `dart-defines/*.json`(含 RSA 公钥、Sentry DSN
- [ ] 修改 `android/app/build.gradle.kts` 中的 `applicationId`
- [ ] 修改 `pubspec.yaml` 中的 `name`(含相关 import 路径)
- [ ] 完成 `docs/setup-android.md` 中的 Android 签名配置
- [ ] 完成 `docs/setup-ios.md` 中的 iOS Scheme / Signing 配置
- [ ] 替换登录页 UI 品牌(`lib/features/auth/presentation/pages/login_page.dart`
- [ ] 替换 App IconAndroid mipmap / iOS AppIcon.appiconset
- [ ] 实现业务 Tab 页面(替换 `routes.dart` 中的 placeholder build
- [ ] 更新 README.md 项目描述
## 关键技术决策
---
- **SQLCipher**:企业合规要求加密存储,部署后无法无损迁移到无加密
- **7 拦截器栈**Token 刷新互斥(Completer)+ 证书绑定是安全基础,不可简化
- **sealed Failure**:统一错误分类,UI / 日志 / Sentry 三层复用同一模型
- **schemaVersion=1**:脚手架全新 DB,不携带任何历史迁移负担
## 参考文档
| 文档 | 内容 |
|------|------|
| [README.md](README.md) | 项目概述、技术栈、快速开始 |
| [docs/setup-project.md](docs/setup-project.md) | 完整项目配置指南(所有开发者必读)|
| [docs/setup-android.md](docs/setup-android.md) | Android Flavor / 签名 / 构建详细配置 |
| [docs/setup-ios.md](docs/setup-ios.md) | iOS Scheme / CocoaPods / 签名详细配置 |
+138 -105
View File
@@ -1,123 +1,156 @@
# sunny_mochi — Flutter 企业级脚手架
基于 Flutter 3.x 的企业级 APP 脚手架,开箱即用的基础设施底座。
> **新项目接手后,请将本文件第一行替换为实际项目名和简介。**
## 包含能力
| 模块 | 说明 |
|------|------|
| Flavor 三包 | dev / staging / prod 环境共存 |
| 加密数据库 | Drift + SQLCipher AES-256 |
| 网络层 | 7 拦截器 Dio(Token 刷新互斥、证书绑定、错误映射) |
| 崩溃日志 | 同步写文件 + 启动时弹窗上报 |
| 错误分类 | Sealed Failure 体系(Network / Auth / Server / Cache |
| 主题系统 | 5 套色板 + 深色模式 + Riverpod 持久化 |
| 开发者面板 | Talker UIInternal 包可见,Release 编译期关闭) |
| 认证骨架 | Clean Architecture:手机号 + 短信 / 密码双模式登录 |
| 本地错误日志 | 查看 + 一键上报 |
| i18n | Slang 4.xzh-CN 基准 + en 备用) |
基于 Flutter 3.x 的企业级 APP 模板底座,涵盖开发 / 测试 / 生产三包共存体系、加密本地数据库、完整网络安全栈、崩溃与错误日志、主题系统和认证骨架,让新项目从第一天就拥有生产级基础设施。
---
## 克隆后首次设置
## 已实现的基础设施
### 构建体系
- **Flavor 三包**devMock 可开)/ staging(连测试服)/ prod(正式签名)
- **dart-defines JSON**:环境变量与代码完全解耦,CI/CD 通过 secret 注入 prod 配置
- **Android productFlavors + iOS xcconfig**:三套应用名、ID、签名互不干扰
### 网络层(Dio,7 拦截器固定顺序)
| 顺序 | 拦截器 | 作用 |
|------|--------|------|
| 1 | CertPinning | SSL 证书绑定,防 MITM |
| 2 | Auth | 注入 sa-token 动态 headertokenName 从 SecureStorage 读取)|
| 3 | TokenRefresh | 401 时静默刷新,Completer 互斥防并发重复消耗 |
| 4 | Retry | 网络超时 / 5xx 指数退避重试(最多 3 次)|
| 5 | Error | DioException → sealed Failure 映射 |
| 6 | AuthLogout | AuthFailure(unauthorized/refreshFailed) → 清 token + 跳登录 |
| 7 | Log | TalkerDioLogger(仅 dev/Internal 包输出)|
### 本地存储
- **Drift 2.31.0 + SQLCipher**AES-256 加密 SQLite,密钥由 `DbKeyProvider` 派生并存入 Keychain
- **SecureStorage**token / refreshToken / tokenName / userId 的唯一真源(Keychain on iOSEncryptedSharedPreferences on Android
- **Offline-First 同步框架**`SyncService` + `SyncColumns` mixin4 字段脏数据追踪骨架
### 错误体系
```
Failure (sealed)
├── NetworkFailure → 超时 / 无网络 / 证书错误 / 请求取消
├── AuthFailure → 未授权 / token 过期 / 刷新失败 / 无权限 / 加密失败
├── ServerFailure → HTTP 4xx/5xx / 业务码非 00000
├── CacheFailure → Drift / IO 异常
└── UnknownFailure → 兜底
```
### 崩溃日志
三步生命周期:`preInit()` → 启动 → `consumePending()`(读取上次崩溃)→ `installHooks()`(接管 Flutter/Zone 全局异常)。崩溃数据同步写文件(不依赖异步),重启后弹窗展示并提供上报入口。
### 主题系统
5 套色板(blue / red / green / purple / teal)× 浅色/深色模式,通过 `ThemeNotifier`SharedPreferences 持久化)+ Riverpod 全局响应式切换。
### 认证骨架(Clean Architecture
```
domain/entities/UserEntity ← 纯 Dart,无框架依赖
domain/repositories/AuthRepository ← abstract interface
data/models/UserModel ← Freezed + JSON,含 toEntity()
data/datasources/AuthRemoteDatasource ← Dio 调用
data/repositories/AuthRepositoryImpl ← 接口实现
presentation/notifiers/AuthNotifier ← @riverpodgeneration 计数防竞态
presentation/notifiers/AuthStatusController ← ValueNotifier<bool?> 三态门面
presentation/pages/LoginPage ← 手机号 + SMS/密码双模式骨架
```
### 可观测性
- **Talker**:全局 `appTalker`dev/Internal 包输出,Release 编译期关闭
- **Sentry**`SentrySetup.init()` 包裹 `runApp`,空 DSN 时自动跳过,Release 才上报
- **DevPanel Feature**Internal 包内 TalkerScreen 入口,方便调试网络 / 状态变化
### 通用 Widget 库
`AppToast`Overlay 动画条)、`AppButton`(带 loading)、`AppTextField``EmptyView``ErrorView``LoadingOverlay``ConfirmDialog``SectionCard``SectionTitle``Skeleton``InfoRow``CountDownButton``AvatarWidget``TagChip`
---
## 技术栈版本
| 技术 | 版本 | 说明 |
|------|------|------|
| Flutter | ≥ 3.41.0 | |
| Dart | ≥ 3.7.0 | |
| flutter_riverpod | ^3.3.0 | |
| go_router | ^17.0.0 | TypedRoutes + StatefulShellRoute |
| drift | 2.31.0 | **锁定**2.32+ analyzer 冲突 |
| sqlcipher_flutter_libs | ^0.6.0 | |
| freezed_annotation | ^3.0.0 | |
| json_serializable | 6.13.0 | **锁定**6.13.1+ analyzer 冲突 |
| dio | ^5.9.0 | |
| flutter_secure_storage | ^10.0.0 | |
| slang | ^4.0.0 | i18nzh-CN 基准 |
| talker_flutter | ^5.0.0 | |
| sentry_flutter | ^9.0.0 | |
| flutter_screenutil | ^5.9.0 | 设计基准 375×812pt |
| pointycastle + asn1lib | ^4.0.0 / ^1.5.0 | RSA PKCS#1 v1.5 |
---
## 快速开始
**前提**:已安装 Flutter ≥ 3.41.0、Android StudioAndroid SDK)、Xcode 16+iOS 开发)。
### 1. 安装依赖
```bash
# 1. 克隆(替换为业务项目实际地址)
git clone https://gitea.example.com/your-org/your-project.git
cd your-project
# 2. 安装依赖
flutter pub get
# 3. 代码生成
make gen # 生成 .g.dart / .freezed.dart
make gen-i18n # 生成 lib/i18n/strings.g.dart
# 4. 运行(dev 包,Mock 模式)
make run-dev
```
### 2. 代码生成
```bash
make gen # build_runner → .g.dart / .freezed.dart
make gen-i18n # slang → lib/i18n/strings.g.dart
```
### 3. 填入项目 API 路径
编辑 `lib/core/config/api_paths.dart`,将所有空字符串替换为实际路径。
### 4. 配置 Android 签名(发布版必须)
```bash
cp android/key.properties.template android/key.properties
# 编辑 key.properties,填入 keystore 路径和密码
```
### 5. 配置生产环境变量
```bash
cp dart-defines/prod.json.template dart-defines/prod.json
# 编辑 prod.json,填入 Sentry DSN、RSA 公钥等
```
### 6. iOS(首次或 Pod 变更后)
```bash
cd ios && pod install && cd ..
```
---
## 常用命令
```bash
make run-dev # 开发版调试运行
make run-staging # 测试版 Release 运行
make build-staging # 打测试包 APK
make build-prod # 打正式版 AAB(需先配置 prod.json + key.properties
make gen # 代码生成
make gen-i18n # i18n 生成
flutter analyze # 静态分析(目标 0 errors
flutter test # 单元测试
```
---
## 新项目初始化清单
- [ ] 替换包名 `com.example.sunny_mochi` → 项目包名(`android/app/build.gradle.kts` + `pubspec.yaml`
- [ ] 填入 API 路径(`lib/core/config/api_paths.dart`
- [ ] 配置 `android/key.properties`(从 `key.properties.template` 复制)
- [ ] 配置 `dart-defines/prod.json`(从 `prod.json.template` 复制)
- [ ] 替换 App 名称(`android/app/build.gradle.kts` `resValue``ios/Runner/Info.plist`
- [ ] 替换 App Icon`android/app/src/main/res/mipmap-*/``ios/Runner/Assets.xcassets/AppIcon.appiconset/`
- [ ] 实现 Tab 页面(替换 `lib/core/router/routes.dart` 中的 placeholder build
- [ ] 添加业务路由(`lib/core/router/routes.dart` 新增 `@TypedGoRoute`
- [ ] 配置 Sentry DSN`dart-defines/prod.json`
- [ ] 配置 RSA 公钥(`dart-defines/dev.json``staging.json``prod.json`
> 首次运行前请阅读 [docs/setup-project.md](docs/setup-project.md) 完成环境配置。
---
## 项目结构
```
lib/
├── main.dart # 6 步启动序列
├── app.dart # AppRoot → MaterialApp.router
├── i18n/ # Slang 翻译
├── core/
│ ├── config/ # Env / ApiConfig / ApiPaths(填入 API 路径)
── router/ # GoRouter + TypedRoutes(添加业务路由)
│ ├── network/ # Dio 7 拦截器 + MockAdapter
│ ├── storage/ # Drift + SQLCipher + SecureStorage
│ ├── crash/ # CrashReporter 生命周期
│ ├── error/ # Sealed Failure 分类
── theme/ # 5 色板 + 深色模式
│ ├── observability/ # Talker + Sentry
│ ├── sync/ # Offline-First 同步框架骨架
│ ├── crypto/ # RSA 加密
── extensions/ # BuildContext / String / num / DateTime
│ ├── utils/ # 验证器 / 密码强度 / SMS 倒计时
│ └── widgets/ # 通用 UI 组件库
└── features/
├── auth/ # 认证(完整 Clean Architecture,替换登录 UI 品牌)
── dev_panel/ # Talker 调试面板(Internal 包)
└── error_report/ # 本地错误日志 + 上报
android/
── app/build.gradle.kts # Flavor 三包 + SQLCipher + 签名配置
├── key.properties.template # 签名配置模板(复制为 key.properties 并填入密码)
└── keystore/ # 放置 .jks 文件(.gitignore 中,勿提交)
dart-defines/
├── dev.json # 开发环境变量(占位符,可提交)
├── staging.json # 测试环境变量(占位符,可提交)
├── prod.json.template # 生产环境变量模板
└── prod.json # 生产真实配置(.gitignore 中,勿提交)
├── android/
│ ├── app/build.gradle.kts # Flavor 三包 + SQLCipher + 签名配置
│ ├── key.properties.template # 签名配置模板(复制为 key.properties 并填入密码)
│ └── keystore/ # 存放 .jks 文件(gitignore,勿提交)
├── ios/
│ ├── Flutter/flavors/ # dev / staging / prod xcconfig
── Podfile # iOS 15.0+pod install 后生成 Pods/
├── dart-defines/
│ ├── dev.json # 开发环境(占位符,可提交)
│ ├── staging.json # 测试环境(占位符,可提交)
│ ├── prod.json.template # 生产模板(复制为 prod.json 并填入真实值)
── prod.json # 生产真实配置(gitignore,勿提交)
├── assets/
│ ├── fonts/ # HarmonyOS Sans SC4 字重)
│ ├── fixtures/ # Mock JSONdev 调试用)
── themes/ # 主题图片(业务项目按 AppColorScheme 放置)
├── lib/ # 详见 CLAUDE.md 目录结构
├── docs/
│ ├── setup-project.md # 完整项目配置指南
├── setup-android.md # Android Flavor / 签名详细配置
── setup-ios.md # iOS Scheme / CocoaPods / 签名详细配置
├── CLAUDE.md # AI 辅助开发上下文(技术栈 / 规范 / 零容忍规则)
├── Makefile # 常用命令
├── pubspec.yaml # 依赖(含版本锁定 dependency_overrides
── slang.yaml # i18n 配置(base_locale: zh-CN
```
---
## 文档导航
| 文档 | 适用人群 | 内容 |
|------|---------|------|
| **本文(README.md** | 所有成员 | 项目概述、技术栈、快速开始 |
| [docs/setup-project.md](docs/setup-project.md) | 所有开发者 | 环境配置、API 路径、dart-defines 填写 |
| [docs/setup-android.md](docs/setup-android.md) | Android / 全栈 | Flavor、签名、keystore、构建 |
| [docs/setup-ios.md](docs/setup-ios.md) | iOS / 全栈 | Scheme、CocoaPods、证书、Archive |
| [CLAUDE.md](CLAUDE.md) | AI 辅助开发 | 架构规范、零容忍规则、开发范式 |
+233
View File
@@ -0,0 +1,233 @@
# Android 配置指南
本文档覆盖 Android 端从 Flavor 验证到正式签名发布包所需的全部配置步骤。
---
## 前提
- Android Studio Meerkat (2024.3) 或更高版本
- Android SDKAPI 34compileSdk/ API 21minSdk
- JDK 17Android Gradle Plugin 要求)
- 已完成 [setup-project.md](setup-project.md) 的 Step 14
---
## 1. 验证 Flavor 构建
脚手架预配置了三个 Flavor`dev` / `staging` / `prod`,对应三套应用名和包名后缀。
```bash
# 确认三个 Flavor 都能构建(debug 模式,速度最快)
make build-apk-dev # com.example.sunny_mochi.dev
flutter build apk --flavor staging --debug \
--dart-define-from-file=dart-defines/staging.json
# com.example.sunny_mochi.staging
flutter build apk --flavor prod --debug \
--dart-define-from-file=dart-defines/prod.json
# com.example.sunny_mochi
```
若构建失败,检查 `android/app/build.gradle.kts` 中的 `flavorDimensions``productFlavors` 配置。
---
## 2. 修改包名(业务项目必须)
编辑 `android/app/build.gradle.kts`,找到 `productFlavors``defaultConfig`
```kotlin
defaultConfig {
applicationId = "com.your_company.your_app" // ← 修改
// ...
}
productFlavors {
create("dev") {
applicationIdSuffix = ".dev"
// applicationId 实际为 com.your_company.your_app.dev
resValue("string", "app_name", "YourApp Dev") // ← 修改应用名
}
create("staging") {
applicationIdSuffix = ".staging"
resValue("string", "app_name", "YourApp Beta") // ← 修改应用名
}
create("prod") {
resValue("string", "app_name", "YourApp") // ← 修改应用名
}
}
```
同步修改 `android/app/src/main/AndroidManifest.xml` 确认使用 `@string/app_name`(脚手架默认已使用)。
---
## 3. 配置 Release 签名
### 3.1 生成 Keystore(首次)
```bash
mkdir -p android/keystore
keytool -genkey -v \
-keystore android/keystore/release.jks \
-alias your-key-alias \
-keyalg RSA \
-keysize 2048 \
-validity 10000
```
> ⚠️ **Keystore 丢失无法找回,必须妥善保管。** 建议:
> - 备份到加密云存储(1Password / Bitwarden Vault
> - CI/CD 系统通过 secret 变量注入,不放在仓库中
> - `android/keystore/` 目录已在 `.gitignore` 中
### 3.2 填写 key.properties
```bash
cp android/key.properties.template android/key.properties
```
编辑 `android/key.properties`(相对于 `android/app/` 目录):
```properties
storePassword=your-keystore-password
keyPassword=your-key-password
keyAlias=your-key-alias
storeFile=../keystore/release.jks
```
> `key.properties` 已在 `android/.gitignore` 中,不会被提交。
### 3.3 验证签名配置
```bash
# 构建 staging release APK(使用 release 签名)
make build-staging
# 查看签名信息
apksigner verify --print-certs \
build/app/outputs/flutter-apk/app-staging-release.apk
```
---
## 4. 配置 Firebase(可选)
若业务项目使用 Firebase(推送通知 / Analytics):
1. 在 [Firebase Console](https://console.firebase.google.com) 为每个 Flavor 创建应用(不同包名)
2. 下载各包名对应的 `google-services.json`,放置到对应 Flavor 目录:
```
android/app/src/
├── dev/google-services.json
├── staging/google-services.json
└── main/google-services.json # prod 用 main
```
3.`android/app/build.gradle.kts` 添加 Google Services 插件:
```kotlin
plugins {
// ...
id("com.google.gms.google-services")
}
```
4.`google-services.json` 加入 `.gitignore`(含 API key,勿提交):
```
# 在根 .gitignore 中添加
**/google-services.json
```
> `google-services.json` 已在 `android/.gitignore` 中有注释行,取消注释即可。
---
## 5. 配置 ProGuard / R8Release 混淆)
脚手架的 `android/app/build.gradle.kts` 已预启用 R8
```kotlin
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
```
`android/app/proguard-rules.pro` 中已预留 Drift / Sentry / Riverpod 保留规则注释,按需取消注释。
---
## 6. SQLCipher 注意事项
脚手架已在 `build.gradle.kts` 中处理 SQLCipher 的 native library 冲突:
```kotlin
packaging {
jniLibs {
pickFirsts += setOf("**/libsqlite3.so")
}
}
```
若后续新增包时出现 `libsqlite3.so` 重复错误,在此处追加相同模式即可。
---
## 7. CI/CD 构建(示例:GitHub Actions
以下为生产包构建的 workflow 关键步骤:
```yaml
- name: Decode keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode \
> android/keystore/release.jks
- name: Write key.properties
run: |
cat > android/key.properties <<EOF
storePassword=${{ secrets.KEYSTORE_PASSWORD }}
keyPassword=${{ secrets.KEY_PASSWORD }}
keyAlias=${{ secrets.KEY_ALIAS }}
storeFile=../keystore/release.jks
EOF
- name: Write prod.json
run: |
echo '${{ secrets.PROD_DART_DEFINES }}' > dart-defines/prod.json
- name: Build AAB
run: make build-prod
```
Secrets 清单:`KEYSTORE_BASE64``KEYSTORE_PASSWORD``KEY_PASSWORD``KEY_ALIAS``PROD_DART_DEFINES`
---
## 常见问题
### Gradle sync 失败,报找不到 compileSdk
确认 Android SDK Platform 34 已安装(Android Studio → SDK Manager → Android 14)。
### `libsqlite3.so` duplicate 错误
`build.gradle.kts``packaging.jniLibs.pickFirsts` 中追加冲突的 .so 路径。
### Release APK 运行时崩溃,Debug APK 正常
通常是 ProGuard 混淆了不应混淆的类。查看 `build/outputs/mapping/` 下的 `seeds.txt` 和 Logcat,在 `proguard-rules.pro` 添加对应的 `-keep` 规则。
### 签名 APK 安装时提示"应用已安装但签名不同"
设备上已安装其他签名版本,需先卸载再安装,或换测试设备。
+243
View File
@@ -0,0 +1,243 @@
# iOS 配置指南
本文档覆盖 iOS 端从 CocoaPods 安装到 Archive 发布所需的全部配置步骤。
> **平台要求**macOS 系统 + Xcode 16+。Windows / Linux 无法进行 iOS 开发。
---
## 前提
- macOS 14 (Sonoma) 或更高版本
- Xcode 16.0(从 App Store 安装,包含 iOS 18 SDK
- CocoaPods 1.15+`sudo gem install cocoapods`
- Apple Developer 账户(真机运行需 Free 账户,发布需 Paid 账户 $99/年)
- 已完成 [setup-project.md](setup-project.md) 的 Step 14
---
## 1. 安装 CocoaPods 依赖
```bash
cd ios
pod install
cd ..
```
首次执行会拉取依赖,耗时 5–15 分钟(取决于网络)。成功后生成:
- `ios/Pods/` 目录(gitignore,不提交)
- `ios/Podfile.lock`(版本锁定,**应提交**
> 如果 `pod install` 卡住,检查 CocoaPods 源:
> ```bash
> pod repo update
> ```
---
## 2. 验证基础构建
```bash
# 无签名编译验证(不需要 Apple 账户)
flutter build ios --no-codesign --flavor dev \
--dart-define-from-file=dart-defines/dev.json
# 预期输出:
# ✓ Built build/ios/iphoneos/Runner.app
```
---
## 3. 配置 Bundle Identifier
打开 Xcode**必须通过 .xcworkspace 打开**):
```bash
open ios/Runner.xcworkspace
```
在 Xcode 中:
1. 左侧导航选择 **Runner** 项目
2. 选择 **Runner** Target → **General** 选项卡
3. **Bundle Identifier** 改为业务项目实际 ID
| Build Configuration | Bundle ID |
|---------------------|-----------|
| Debugdev 开发调试)| `com.your_company.your_app.dev` |
| Debug-staging | `com.your_company.your_app.staging` |
| Releaseprod 发布)| `com.your_company.your_app` |
> 脚手架默认使用单个 Bundle IDFlavor 区分由 xcconfig 控制。若需要三个独立 Bundle ID,在 `ios/Flutter/flavors/` 的 xcconfig 文件中添加 `PRODUCT_BUNDLE_IDENTIFIER` 覆盖。
---
## 4. 配置 Xcode Build Configuration 和 Scheme
脚手架已在 `ios/Flutter/flavors/` 中提供三套 xcconfig
| 文件 | 对应 Flavor |
|------|-----------|
| `dev.xcconfig` | 开发包(Debug-dev|
| `staging.xcconfig` | 测试包(Debug-staging / Release-staging|
| `prod.xcconfig` | 正式包(Release|
### 关联 xcconfig 到 Build Configuration
1. Xcode → **Runner** 项目 → **Info** 选项卡 → **Configurations**
2. 展开每个 Configuration,点击 Runner 旁的下拉:
| Configuration 名称 | 关联 xcconfig |
|--------------------|---------------|
| Debug | `Flutter/flavors/dev.xcconfig` |
| Release | `Flutter/flavors/prod.xcconfig` |
| Profile | `Flutter/flavors/prod.xcconfig` |
> 若需要 staging 独立 Configuration,在此添加 `Debug-staging` / `Release-staging`。
### 添加 dev / staging Scheme(推荐)
1. Xcode → **Product****Scheme****Manage Schemes**
2. 复制 `Runner` Scheme,重命名为 `dev`
3. 编辑 `dev` Scheme
- **Build Configuration**Run)→ `Debug`
-**Arguments** 中确认无硬编码的 dart-definesFlutter 通过 `--dart-define-from-file` 注入)
---
## 5. 配置代码签名
### 5.1 自动签名(开发调试,推荐)
1. Xcode → **Runner** Target → **Signing & Capabilities**
2. 勾选 **Automatically manage signing**
3. **Team** 选择你的 Apple Developer 账户
4. Xcode 会自动创建 Provisioning Profile 和 Signing Certificate
### 5.2 手动签名(CI/CD 或发布版)
1. 在 [Apple Developer Portal](https://developer.apple.com/account) 创建:
- Distribution Certificate(可签发 App Store / Ad Hoc 包)
- App ID(对应 Bundle Identifier
- Provisioning ProfileDistribution 类型)
2. 下载 `.mobileprovision` 文件,双击安装到 Xcode
3. 取消勾选 **Automatically manage signing**
4. 选择对应的 **Signing Certificate****Provisioning Profile**
---
## 6. 真机运行
```bash
# 确认设备已连接
flutter devices
# 运行到真机(dev 包)
flutter run --flavor dev \
--dart-define-from-file=dart-defines/dev.json \
-d <device-id>
# 或通过 Xcode 直接运行(选择 dev Scheme + 目标设备)
```
首次在设备上运行需要:
- 设备 → **设置 → 通用 → VPN 与设备管理** → 信任开发者证书
---
## 7. 构建 TestFlight / App Store Archive
```bash
# 1. 确保 prod.json 已配置正确
cat dart-defines/prod.json
# 2. 构建 iOS Release
flutter build ios --flavor prod --release \
--dart-define-from-file=dart-defines/prod.json
# 3. 在 Xcode 中 Archive(需要 Distribution Certificate
# Product → Archive → Distribute App → App Store Connect
```
### CI/CD ArchiveFastlane 示例)
```ruby
# Fastfile
lane :beta do
build_app(
workspace: "ios/Runner.xcworkspace",
scheme: "Runner", # 或 prod scheme
configuration: "Release",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.your_company.your_app" => "Your App Store Profile"
}
}
)
upload_to_testflight
end
```
---
## 8. 常用 xcconfig 字段说明
`ios/Flutter/flavors/dev.xcconfig`
```xcconfig
// 覆盖 Display Name(桌面图标显示的应用名)
DISPLAY_NAME=YourApp Dev
// 覆盖 Bundle ID(若三包使用不同 ID
// PRODUCT_BUNDLE_IDENTIFIER=com.your_company.your_app.dev
// 覆盖 App Icon(若三包使用不同图标)
// ASSETCATALOG_COMPILER_APPICON_NAME=AppIconDev
```
---
## 9. 最低 iOS 版本
`Podfile` 已锁定 iOS 15.0
```ruby
platform :ios, '15.0'
```
若业务需要支持更低版本,修改此行并运行 `pod install`。同时在 Xcode → Target → **Deployment Info****iOS Deployment Target** 同步修改。
---
## 常见问题
### `pod install` 报 `CocoaPods could not find compatible versions`
```bash
pod repo update # 更新本地 CocoaPods 源
pod install --repo-update
```
### Xcode 打开 `.xcodeproj` 而非 `.xcworkspace`
必须打开 `.xcworkspace`,否则 CocoaPods 的依赖不会加载:
```bash
open ios/Runner.xcworkspace # ✓ 正确
# 不要 open ios/Runner.xcodeproj ✗
```
### 真机运行报 `Untrusted Developer`
设备 → 设置 → 通用 → VPN 与设备管理 → 找到 Developer App → 信任。
### Archive 失败,报 `Provisioning profile doesn't include the entitlement`
在 Apple Developer Portal 重新生成 Provisioning Profile(选中全部所需 Entitlements),重新下载安装。
### Flutter build 报 `The iOS deployment target is set to xxx, but the range of supported deployment targets is`
更新 `Podfile` 中的 `platform :ios` 版本,运行 `pod install`,并在 Xcode Target → Deployment Info 同步修改。
### 模拟器运行正常,真机崩溃(SecureStorage 相关)
iOS 模拟器的 Keychain 行为与真机不同。确认 Xcode → Target → **Signing & Capabilities** → 已添加 **Keychain Sharing** Capability(脚手架默认不需要,但某些系统版本可能要求)。
+234
View File
@@ -0,0 +1,234 @@
# 项目配置指南(所有开发者必读)
本文档覆盖从克隆脚手架到可以运行第一个 dev 包所需的全部步骤。
Android 和 iOS 的平台专属配置请分别参阅 [setup-android.md](setup-android.md) 和 [setup-ios.md](setup-ios.md)。
---
## 前提条件
| 工具 | 最低版本 | 安装方式 |
|------|---------|---------|
| Flutter SDK | 3.41.0 | [flutter.dev](https://docs.flutter.dev/get-started/install) |
| Dart SDK | 3.7.0 | 随 Flutter 一起安装 |
| Android Studio | Meerkat (2024.3) | [developer.android.com](https://developer.android.com/studio) |
| Xcode | 16.0 | Mac App Store(仅 iOS 开发需要)|
| CocoaPods | 1.15+ | `sudo gem install cocoapods` |
| make | 系统内置 | macOS/Linux 自带;Windows 用 Git Bash 或 WSL |
验证安装:
```bash
flutter --version # 应显示 ≥ 3.41.0
flutter doctor # 确认 Android toolchain + Xcode 全绿
```
---
## Step 1 — 克隆与初始化
```bash
git clone https://gitea.example.com/your-org/your-project.git # TODO: 替换实际地址
cd your-project
flutter pub get # 拉取所有依赖
make gen # 生成 .g.dart / .freezed.dart(约 1 分钟)
make gen-i18n # 生成 lib/i18n/strings.g.dart
```
> **注意**:如果 `make gen` 报错 `build_runner` 找不到,请先确认 `dart` 在 PATH 中:
> ```bash
> dart --version
> ```
---
## Step 2 — 填入项目 API 路径
编辑 `lib/core/config/api_paths.dart`,将所有空字符串替换为实际后端路径:
```dart
// 示例(sa-token + Spring Boot
static const String authLoginSms = '/sys/auth/sms-login';
static const String authLoginPwd = '/sys/auth/password-login';
static const String authSmsSend = '/sys/auth/send-sms';
static const String authLogout = '/sys/auth/logout';
static const String authRefresh = '/sys/auth/refresh-token';
static const String userProfile = '/sys/sys-user/app/userInfo';
// ... 其余路径按业务填写
```
所有 datasource 文件通过 `ApiPaths.xxx` 引用路径,**严禁字符串字面量散落**。
---
## Step 3 — 配置环境变量(dart-defines
### 3.1 开发环境(dev.json
```bash
# 直接编辑,此文件可提交(含占位符,无真实密钥)
nano dart-defines/dev.json
```
需填入的字段:
```json
{
"ENV": "dev",
"USE_MOCK": "true", // true = 使用 Mock Adapter(不调真实服务)
"INTERNAL_BUILD": "true", // true = 显示 Dev Panel 入口
"SENTRY_DSN": "", // dev 通常留空
"PINNED_FINGERPRINTS": "", // 证书指纹(dev 可留空跳过 SSL 绑定)
"API_BASE_URL": "http://192.168.1.100:8080", // TODO: 实际 dev 服务器
"RSA_PUBLIC_KEY": "MIIBIjANBg..." // TODO: 后端 RSA 公钥(Base64 DER
}
```
### 3.2 测试环境(staging.json
```json
{
"ENV": "test",
"USE_MOCK": "false",
"INTERNAL_BUILD": "true",
"SENTRY_DSN": "",
"PINNED_FINGERPRINTS": "",
"API_BASE_URL": "https://staging.your-domain.com", // TODO
"RSA_PUBLIC_KEY": "MIIBIjANBg..." // TODO
}
```
### 3.3 生产环境(prod.json
```bash
cp dart-defines/prod.json.template dart-defines/prod.json
nano dart-defines/prod.json
```
```json
{
"ENV": "prod",
"USE_MOCK": "false",
"INTERNAL_BUILD": "false",
"SENTRY_DSN": "https://xxx@sentry.io/yyy", // TODO: Sentry 项目 DSN
"PINNED_FINGERPRINTS": "AA:BB:CC:...", // TODO: 服务器证书 SHA-256 指纹
"API_BASE_URL": "https://api.your-domain.com",// TODO
"RSA_PUBLIC_KEY": "MIIBIjANBg..." // TODO
}
```
> **prod.json 已在 .gitignore 中**,不会被提交。CI/CD 系统通过 secret 变量注入。
### 获取 RSA 公钥
后端一般提供 RSA 公钥的 Base64 DER 编码。若后端使用 sa-token + RSA
```bash
# Java 后端:从 public.key 文件提取(去掉 PEM header/footer,拼接成单行)
cat public.key | grep -v "BEGIN\|END" | tr -d '\n'
```
### 获取证书 SHA-256 指纹
```bash
# 方法 1openssl(推荐)
echo | openssl s_client -connect api.your-domain.com:443 2>/dev/null \
| openssl x509 -fingerprint -sha256 -noout \
| sed 's/SHA256 Fingerprint=//'
# 方法 2:Chrome → 锁图标 → 证书 → 指纹
```
---
## Step 4 — 验证首次运行
```bash
# 方式 1:使用 Mock(推荐首次验证,不需要后端服务)
# 确保 dart-defines/dev.json 中 USE_MOCK=true
make run-dev
# 方式 2:连接真实后端
# 确保 dart-defines/dev.json 中 USE_MOCK=false + API_BASE_URL 已填入
make run-dev
```
**期望结果:**
- APP 启动,显示登录页(手机号 + SMS/密码双模式)
- 右上角(或侧滑)可进入 Dev Panel(Talker 日志面板)
- `flutter analyze` 输出 `103 issues found`(均为 info 级别,0 errors
---
## Step 5 — 更新包名(业务项目必须)
脚手架默认包名为 `com.example.sunny_mochi`,接手后必须替换:
### Android
编辑 `android/app/build.gradle.kts`
```kotlin
// dev Flavor
applicationId = "com.your_company.your_app.dev"
// staging Flavor
applicationId = "com.your_company.your_app.staging"
// prod / release
defaultConfig {
applicationId = "com.your_company.your_app"
}
```
### iOS
在 Xcode 中修改 Bundle Identifier(见 [setup-ios.md](setup-ios.md) Step 3)。
### pubspec.yaml + Dart 导入路径
```yaml
name: your_app_name # 修改后所有 import 路径也需要更新
```
批量替换导入:
```bash
# macOS / Linux
find lib -name "*.dart" -exec sed -i '' 's/package:sunny_mochi/package:your_app_name/g' {} \;
# Windows PowerShell
Get-ChildItem -Path lib -Recurse -Filter "*.dart" |
ForEach-Object { (Get-Content $_.FullName) -replace 'sunny_mochi', 'your_app_name' |
Set-Content $_.FullName }
```
运行 `make gen` 重新生成后确认无编译错误。
---
## Step 6 — 平台专属配置
- **Android**(签名 / Flavor 验证 / 发布包构建)→ [setup-android.md](setup-android.md)
- **iOS**Xcode Scheme / CocoaPods / 证书 / Archive)→ [setup-ios.md](setup-ios.md)
---
## 常见问题
### `build_runner` 生成失败,报 analyzer 版本冲突
确认 `pubspec.yaml` 中的 `dependency_overrides`
```yaml
dependency_overrides:
drift: 2.31.0
json_serializable: 6.13.0
```
若被修改,恢复后重跑 `flutter pub get && make gen`
### App 启动后 token 丢失 / 每次重启都跳登录
SecureStorage 在 iOS Simulator 上可能行为异常。请在真机测试,或检查 `Keychain Sharing` 是否开启。
### Mock 模式下请求崩溃 `Unable to load asset`
检查 `assets/fixtures/` 下是否有对应的 JSON 文件,且 `pubspec.yaml` 中的 `assets:` 包含了对应子目录。
### `flutter analyze` 报错(非 info 级别)
先运行 `make gen` 确保生成文件是最新的,再重新 analyze。