# CLAUDE.md — Flutter 企业级脚手架(sunny_mochi) > 本文件为 Claude Code 提供项目上下文。**每次新会话首先读取本文件**, > 需要时再按需加载 `docs/` 下的补充文档。 --- ## ⚡ 30 秒上手(开箱即用验证) ```bash flutter pub get && make gen && make gen-i18n # dev.json 默认 USE_MOCK=true,无需后端即可运行: make run-dev # 期望结果:App 启动 → 显示登录页 → 右上角可进 Dev Panel(Talker 日志) ``` **这是脚手架,不是完整 App**。以下内容"需要业务项目填写": - `lib/core/config/api_paths.dart` — 所有路径均为空字符串 - `lib/core/config/api_config.dart` — 三套 baseUrl 均为 localhost 占位符 - `dart-defines/*.json` — RSA 公钥 / Sentry DSN 均为占位符 --- ## 项目身份信息 > **业务项目接手后,将下表 TODO 字段替换为真实值,AI 才能给出精准建议。** | 字段 | 当前值(脚手架占位)| 说明 | |------|-----------------|------| | 项目名称 | sunny_mochi | TODO: 替换为实际项目名 | | Application ID | com.example.sunny_mochi | TODO: 如 com.company.appname | | API Base URL (dev) | http://localhost:8080 | TODO: 改 api_config.dart + dev.json | | API Base URL (staging) | https://staging.your-domain.com | TODO: 改 api_config.dart | | API Base URL (prod) | https://api.your-domain.com | TODO: 改 api_config.dart | | 后端鉴权框架 | sa-token(动态 tokenName header)| TODO: 若用 Bearer JWT 改 auth_interceptor | | Sentry DSN | 空(不上报)| TODO: prod.json | | RSA 公钥 | 空(加密跳过)| TODO: 三个 dart-defines JSON | --- ## 零容忍规则(必须遵守,不可商量) | 规则 | ✅ 正确 | ❌ 禁止 | |------|---------|---------| | API 路径 | `ApiPaths.xxx` 常量 | 字符串字面量散落代码中 | | 错误处理 | `ExceptionMapper().fromUnknown(e, st)` | 裸 `catch (e)` 直接 `toString()` 显示 | | Token 存储 | `SecureStorage`(Keychain / EncryptedPrefs)| 普通 `SharedPreferences` | | 数据库加密 | SQLCipher(默认,禁止替换)| 无加密 sqflite | | 日志 | `appTalker.info/warning/error()` | `print()` / `debugPrint()` | | 路由跳转 | `XxxRoute().go(context)` | `Navigator.push()` | | 状态管理 | `@riverpod` / `@Riverpod(keepAlive: true)` | `setState` 跨组件 / Provider 包 | | Riverpod provider 命名 | 生成名(`AuthNotifier` → `authProvider`)| 手写 `authNotifierProvider` | | 生成文件 | 只读,运行 `make gen` 刷新 | 手动修改 `.g.dart` / `.freezed.dart` | | Domain 层依赖 | 纯 Dart,无 Flutter/Drift/Dio | domain 层 `import 'package:dio'` | --- ## 禁止修改的文件(架构决策,除非全面评估影响) ``` lib/core/network/dio_client.dart # 7 拦截器顺序固定,顺序即语义 lib/core/storage/db_key_provider.dart # AES-256 密钥派生策略,改动会破坏已有 DB lib/core/error/failures.dart # 全局错误分类,改 sealed 影响所有 switch lib/core/network/response_code.dart # 业务码分类,影响 TokenRefresh 互斥逻辑 ``` --- ## dart-defines 字段完整对照表 | JSON 键 | `Env.dart` 读取字段 | 类型 | 说明 | |---------|-------------------|------|------| | `ENV` | `Env.name` | String | `dev` / `test` / `release` | | `API_BASE_URL` | `Env.apiBaseUrlOverride` | String | 留空则用 api_config.dart 中的固定 URL | | `USE_MOCK` | `Env.useMock` | bool | `true` = 走 MockAdapter,无需后端 | | `INTERNAL_BUILD` | `Env.isInternalBuild` | bool | `true` = 显示 Dev Panel 入口 | | `SENTRY_DSN` | `Env.sentryDsn` | String | 留空时 Sentry 自动跳过 | | `PINNED_FINGERPRINTS` | `Env.pinnedFingerprints` | List | 逗号分隔的 SHA-256 指纹,留空跳过 SSL 绑定 | | `RSA_PUBLIC_KEY` | `Env.rsaPublicKey` | String | DER-SPKI Base64,留空时密码不加密传输 | --- ## 技术栈(脚手架锁定版本) | 分类 | 技术 | 版本 | |------|------|------| | 状态管理 | flutter_riverpod + riverpod_annotation | ^3.3.0 / ^4.0.0 | | 路由 | go_router + 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(base_locale: zh-CN)| ^4.0.0 | | UI 自适应 | flutter_screenutil(基准 375×812)| ^5.9.0 | > `drift: 2.31.0` 和 `json_serializable: 6.13.0` 锁定原因: > 更新版本需要 analyzer ^10.x,与 riverpod_generator 的 analyzer ^9.x 冲突。**勿升级。** --- ## 目录结构与关键文件 ``` lib/ ├── main.dart # 启动序列(固定顺序:preInit→consumePending→SentryInit→installHooks) ├── app.dart # AppRoot → ScreenUtilInit(375×812) → App → MaterialApp.router ├── i18n/ # Slang 翻译(zh-CN 基准 + en 备用) ├── core/ │ ├── config/ │ │ ├── env.dart # ← 读取 dart-defines,只读 │ │ ├── api_config.dart # ★ 填入三套环境 baseUrl │ │ └── api_paths.dart # ★ 填入所有 API 路径常量 │ ├── router/ │ │ ├── app_router.dart # GoRouter provider(ref.read + refreshListenable) │ │ └── routes.dart # ★ 添加业务 @TypedGoRoute(然后 make gen) │ ├── network/ │ │ ├── dio_client.dart # 7 拦截器(禁止改顺序) │ │ ├── api_response.dart # envelope 解析(parseEnvelope / unwrapVoid) │ │ ├── response_code.dart # 业务状态码(成功=00000,token 类码分类) │ │ ├── interceptors/ # 7 个拦截器(禁止改顺序) │ │ └── mock/ # USE_MOCK=true 时生效,读 assets/fixtures/ │ ├── storage/ │ │ ├── app_database.dart # schemaVersion=1,@DriftDatabase([Users, ErrorLogs]) │ │ ├── secure_storage.dart # token / userId / tokenName 的唯一真源 │ │ ├── db_key_provider.dart # AES-256 密钥派生(禁止改策略) │ │ ├── tables/ # ★ 业务项目在此添加 Drift 表 │ │ └── daos/ # DAO(只放纯 DB 操作) │ ├── error/ │ │ ├── failures.dart # sealed Failure(禁止改结构) │ │ └── exception_mapper.dart # 异常 → Failure 映射 + Riverpod provider │ └── widgets/ # AppToast / AppButton / EmptyView / AvatarWidget 等 └── features/ ├── auth/ # ★ 修改 LoginPage UI 品牌,业务路由骨架已就绪 ├── dev_panel/ # Talker 面板(INTERNAL_BUILD=true 时可路由进入) └── error_report/ # 本地错误日志 + 批量上报(完整实现) ``` --- ## 常用开发范式 ### 新增 Feature(标准目录结构) ``` lib/features/{feature}/ ├── 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,实现接口 └── presentation/ ├── notifiers/{name}_notifier.dart # @riverpod class XxxNotifier └── pages/{name}_page.dart # ConsumerWidget ``` ### 新增路由(routes.dart → make gen) ```dart @TypedGoRoute(path: '/my-path') class MyRoute extends GoRouteData with $MyRoute { const MyRoute(); @override Widget build(BuildContext context, GoRouterState state) => const MyPage(); } ``` ### 标准错误处理 ```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!); }); ``` ### 新增 DB 表 1. `lib/core/storage/tables/{name}_table.dart` — 定义 `@DataClassName` + 字段 2. `app_database.dart` — `@DriftDatabase(tables: [..., NewTable])` + 递增 schemaVersion + 添加 onUpgrade 迁移 3. `lib/core/storage/daos/{name}_dao.dart` — `@DriftAccessor(tables: [NewTable])` 4. 运行 `make gen` --- ## 开发命令 ```bash make gen # build_runner → .g.dart / .freezed.dart(改 Dart 文件后必跑) make gen-i18n # slang → lib/i18n/strings.g.dart(改翻译 JSON 后必跑) make run-dev # dev 包(USE_MOCK=true,无需后端) make run-staging # staging 包(连 test 服务器) make build-staging# staging APK make build-prod # prod AAB(需先配置 prod.json + key.properties) flutter analyze # 目标 0 errors(当前正常 info 数约 103 条) flutter test # 全量测试 ``` --- ## 新项目接手 Checklist > 完成并删除本节(或移至项目 wiki) - [ ] 更新本文件"项目身份信息"表格(让 AI 上下文准确) - [ ] 修改 `lib/core/config/api_config.dart` 三套 baseUrl - [ ] 填入 `lib/core/config/api_paths.dart` 所有路径 - [ ] 配置三个 `dart-defines/*.json`(RSA 公钥 / Sentry DSN) - [ ] 修改 Android 包名:`android/app/build.gradle.kts` applicationId - [ ] 修改 iOS Bundle ID:Xcode → Runner Target → General - [ ] 修改 `pubspec.yaml` name 并批量替换 import 路径(见 setup-project.md Step 5) - [ ] Android 签名:`cp android/key.properties.template android/key.properties` 并填写 - [ ] iOS 签名:Xcode → Signing & Capabilities(见 setup-ios.md) - [ ] 替换登录页 UI 品牌(`lib/features/auth/presentation/pages/login_page.dart`) - [ ] 替换 App Icon(Android mipmap / iOS AppIcon.appiconset) - [ ] 实现 Tab 页面(`lib/core/router/routes.dart` HomeRoute / MineRoute 的 build) - [ ] 更新 README.md 项目描述和快速开始地址 --- ## 参考文档 | 文档 | 内容 | |------|------| | [README.md](../README.md) | 项目概述、技术栈、基础设施一览 | | [docs/setup-project.md](setup-project.md) | 完整配置指南(dart-defines / 包名 / 首次运行)| | [docs/setup-android.md](setup-android.md) | Android Flavor / 签名 / 构建 / CI | | [docs/setup-ios.md](setup-ios.md) | iOS Scheme / CocoaPods / 证书 / Archive | | [docs/architecture.md](architecture.md) | 数据流 / 认证流 / 错误链 ASCII 架构图 |