Template
fix: 清除 platform-flutter 残留代码,重构文档与开发者体验
P0 代码修复:删除 imSdkAppId、清除内网 IP 与原项目域名、修正 USE_MOCK 默认值 P1 CLAUDE.md 重组:零容忍规则前置,新增 dart-defines 字段对照表 P2 文档优化:先跑再配的 setup 流程,新增 architecture.md 架构图
This commit is contained in:
@@ -1,25 +1,82 @@
|
|||||||
# CLAUDE.md — Flutter 企业级脚手架(sunny_mochi)
|
# CLAUDE.md — Flutter 企业级脚手架(sunny_mochi)
|
||||||
|
|
||||||
> 本文件为 Claude Code 提供项目上下文。**所有 AI 辅助开发会话必须首先读取本文件**,
|
> 本文件为 Claude Code 提供项目上下文。**每次新会话首先读取本文件**,
|
||||||
> 再按需加载 `docs/` 下的补充文档。
|
> 需要时再按需加载 `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` 的字段由业务项目接手时填入,脚手架默认保留占位符。**
|
> **业务项目接手后,将下表 TODO 字段替换为真实值,AI 才能给出精准建议。**
|
||||||
|
|
||||||
| 字段 | 当前值(脚手架默认) | 说明 |
|
| 字段 | 当前值(脚手架占位)| 说明 |
|
||||||
|------|-----------------|------|
|
|------|-----------------|------|
|
||||||
| 项目名称 | sunny_mochi | TODO: 替换为实际项目名 |
|
| 项目名称 | sunny_mochi | TODO: 替换为实际项目名 |
|
||||||
| Application ID (Android) | com.example.sunny_mochi | TODO: 如 com.company.projectname |
|
| Application ID | com.example.sunny_mochi | TODO: 如 com.company.appname |
|
||||||
| Bundle ID (iOS) | com.example.sunny-mochi | TODO: 与 Android 对应 |
|
| API Base URL (dev) | http://localhost:8080 | TODO: 改 api_config.dart + dev.json |
|
||||||
| API Base URL (dev) | http://localhost:8080 | TODO: 填入 `dart-defines/dev.json` |
|
| API Base URL (staging) | https://staging.your-domain.com | TODO: 改 api_config.dart |
|
||||||
| API Base URL (staging) | https://staging.example.com | TODO: 填入 `dart-defines/staging.json` |
|
| API Base URL (prod) | https://api.your-domain.com | TODO: 改 api_config.dart |
|
||||||
| API Base URL (prod) | https://api.example.com | TODO: 填入 `dart-defines/prod.json` |
|
| 后端鉴权框架 | sa-token(动态 tokenName header)| TODO: 若用 Bearer JWT 改 auth_interceptor |
|
||||||
| 后端鉴权框架 | sa-token(动态 header tokenName) | TODO: 若使用 Bearer/JWT 修改 auth_interceptor |
|
| Sentry DSN | 空(不上报)| TODO: prod.json |
|
||||||
| Sentry DSN | 空(未启用) | TODO: 填入 `dart-defines/prod.json` |
|
| RSA 公钥 | 空(加密跳过)| TODO: 三个 dart-defines JSON |
|
||||||
| RSA 公钥 | REPLACE_WITH_RSA_PUBLIC_KEY | 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,留空时密码不加密传输 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -27,208 +84,146 @@
|
|||||||
|
|
||||||
| 分类 | 技术 | 版本 |
|
| 分类 | 技术 | 版本 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| Flutter SDK | Flutter | ≥ 3.41.0 |
|
| 状态管理 | flutter_riverpod + riverpod_annotation | ^3.3.0 / ^4.0.0 |
|
||||||
| 状态管理 | Riverpod + riverpod_annotation | ^3.3.0 / ^4.0.0 |
|
| 路由 | go_router + go_router_builder | ^17.0.0 / ^4.3.0 |
|
||||||
| 路由 | GoRouter + go_router_builder | ^17.0.0 / ^4.3.0 |
|
|
||||||
| 网络 | Dio | ^5.9.0 |
|
| 网络 | Dio | ^5.9.0 |
|
||||||
| 本地数据库 | Drift + SQLCipher | 2.31.0(锁定)|
|
| 本地数据库 | Drift + SQLCipher | **2.31.0(锁定)** |
|
||||||
| 安全存储 | flutter_secure_storage | ^10.0.0 |
|
| 安全存储 | flutter_secure_storage | ^10.0.0 |
|
||||||
| 序列化 | Freezed + json_serializable | ^3.x / 6.13.0(锁定)|
|
| 序列化 | Freezed + json_serializable | ^3.x / **6.13.0(锁定)** |
|
||||||
| 日志 | Talker + talker_flutter | ^5.x |
|
| 日志 | Talker + talker_flutter | ^5.x |
|
||||||
| 错误追踪 | Sentry Flutter | ^9.0.0 |
|
| 错误追踪 | sentry_flutter | ^9.0.0 |
|
||||||
| i18n | Slang | ^4.0.0 |
|
| i18n | Slang(base_locale: zh-CN)| ^4.0.0 |
|
||||||
| UI 自适应 | flutter_screenutil(基准 375×812)| ^5.9.0 |
|
| UI 自适应 | flutter_screenutil(基准 375×812)| ^5.9.0 |
|
||||||
| 图片缓存 | cached_network_image | ^3.4.0 |
|
|
||||||
| 字体 | HarmonyOS Sans SC(Regular/Medium/Bold/Black)| GB2312 子集 |
|
|
||||||
|
|
||||||
**版本锁定原因(勿随意升级):**
|
> `drift: 2.31.0` 和 `json_serializable: 6.13.0` 锁定原因:
|
||||||
- `drift: 2.31.0` — 2.32+ 需要 analyzer ^10.x,与 riverpod_generator 的 analyzer ^9.x 冲突
|
> 更新版本需要 analyzer ^10.x,与 riverpod_generator 的 analyzer ^9.x 冲突。**勿升级。**
|
||||||
- `json_serializable: 6.13.0` — 6.13.1+ 同样需要 analyzer ^10.x
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构与关键文件
|
||||||
|
|
||||||
```
|
```
|
||||||
lib/
|
lib/
|
||||||
├── main.dart # 6步启动序列(顺序固定,勿调整)
|
├── main.dart # 启动序列(固定顺序:preInit→consumePending→SentryInit→installHooks)
|
||||||
├── app.dart # AppRoot → ScreenUtilInit → App → MaterialApp.router
|
├── app.dart # AppRoot → ScreenUtilInit(375×812) → App → MaterialApp.router
|
||||||
├── i18n/ # Slang 翻译(zh-CN 基准 + en 备用)
|
├── i18n/ # Slang 翻译(zh-CN 基准 + en 备用)
|
||||||
├── core/
|
├── core/
|
||||||
│ ├── config/
|
│ ├── config/
|
||||||
│ │ ├── env.dart # 从 dart-defines 读取环境变量(只读,勿改结构)
|
│ │ ├── env.dart # ← 读取 dart-defines,只读
|
||||||
│ │ ├── api_config.dart # baseUrl 按 Flavor 决策
|
│ │ ├── api_config.dart # ★ 填入三套环境 baseUrl
|
||||||
│ │ └── api_paths.dart # ★ 新项目必填:所有 API 路径常量
|
│ │ └── api_paths.dart # ★ 填入所有 API 路径常量
|
||||||
│ ├── router/
|
│ ├── router/
|
||||||
│ │ ├── app_router.dart # GoRouter provider(keepAlive)
|
│ │ ├── app_router.dart # GoRouter provider(ref.read + refreshListenable)
|
||||||
│ │ └── routes.dart # ★ 新项目扩展:添加 @TypedGoRoute
|
│ │ └── routes.dart # ★ 添加业务 @TypedGoRoute(然后 make gen)
|
||||||
│ ├── network/
|
│ ├── network/
|
||||||
│ │ ├── dio_client.dart # 7 拦截器注册(顺序固定)
|
│ │ ├── dio_client.dart # 7 拦截器(禁止改顺序)
|
||||||
│ │ ├── api_response.dart # 统一 envelope 解析(parseEnvelope / unwrapVoid)
|
│ │ ├── api_response.dart # envelope 解析(parseEnvelope / unwrapVoid)
|
||||||
│ │ ├── response_code.dart# 业务状态码(成功=00000,token 类码分类)
|
│ │ ├── response_code.dart # 业务状态码(成功=00000,token 类码分类)
|
||||||
│ │ ├── interceptors/ # 7 个拦截器(勿修改拦截顺序)
|
│ │ ├── interceptors/ # 7 个拦截器(禁止改顺序)
|
||||||
│ │ └── mock/ # MockAdapter + fixture JSON(dev 调试用)
|
│ │ └── mock/ # USE_MOCK=true 时生效,读 assets/fixtures/
|
||||||
│ ├── storage/
|
│ ├── storage/
|
||||||
│ │ ├── app_database.dart # Drift @DriftDatabase(schemaVersion 从 1 开始)
|
│ │ ├── app_database.dart # schemaVersion=1,@DriftDatabase([Users, ErrorLogs])
|
||||||
│ │ ├── secure_storage.dart# token / userId / tokenName 的唯一真源
|
│ │ ├── secure_storage.dart # token / userId / tokenName 的唯一真源
|
||||||
│ │ ├── db_key_provider.dart# AES-256 密钥派生(勿修改策略)
|
│ │ ├── db_key_provider.dart # AES-256 密钥派生(禁止改策略)
|
||||||
│ │ ├── tables/ # Drift 表定义(Users + ErrorLogs + 业务表)
|
│ │ ├── tables/ # ★ 业务项目在此添加 Drift 表
|
||||||
│ │ └── daos/ # DAO(只放纯 DB 操作)
|
│ │ └── daos/ # DAO(只放纯 DB 操作)
|
||||||
│ ├── crash/ # CrashReporter(preInit/consumePending/installHooks)
|
|
||||||
│ ├── error/
|
│ ├── error/
|
||||||
│ │ ├── failures.dart # Sealed Failure 层级(唯一错误分类)
|
│ │ ├── failures.dart # sealed Failure(禁止改结构)
|
||||||
│ │ └── exception_mapper.dart # 异常 → Failure 映射
|
│ │ └── exception_mapper.dart # 异常 → Failure 映射 + Riverpod provider
|
||||||
│ ├── theme/ # 5 色板 + 深色模式(ThemeNotifier + SharedPreferences)
|
│ └── widgets/ # AppToast / AppButton / EmptyView / AvatarWidget 等
|
||||||
│ ├── observability/ # Talker(全局 appTalker)+ Sentry(Release 上报)
|
|
||||||
│ ├── sync/ # Offline-First 同步框架骨架(SyncService)
|
|
||||||
│ ├── crypto/ # RSA PKCS#1 v1.5(RsaHelper.encrypt)
|
|
||||||
│ ├── extensions/ # BuildContextX / StringX / NumX / DateTimeX
|
|
||||||
│ ├── utils/ # Validator / PasswordValidator / SmsCountdownMixin
|
|
||||||
│ ├── data/ # FreshnessPolicy(缓存新鲜度判断)
|
|
||||||
│ └── widgets/ # 通用 UI 组件(AppToast / AppButton / EmptyView 等)
|
|
||||||
└── features/
|
└── features/
|
||||||
├── auth/ # ★ 认证(完整骨架,业务项目修改 LoginPage UI)
|
├── auth/ # ★ 修改 LoginPage UI 品牌,业务路由骨架已就绪
|
||||||
├── dev_panel/ # Talker 面板(Internal 包,勿在 release 中暴露入口)
|
├── dev_panel/ # Talker 面板(INTERNAL_BUILD=true 时可路由进入)
|
||||||
└── error_report/ # 本地错误日志 + 上报(完整实现)
|
└── error_report/ # 本地错误日志 + 批量上报(完整实现)
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 开发命令
|
|
||||||
|
|
||||||
```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 errors(103 条 info 为正常)
|
|
||||||
flutter test # 全量测试
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常用开发范式
|
## 常用开发范式
|
||||||
|
|
||||||
### 新增 Feature
|
### 新增 Feature(标准目录结构)
|
||||||
|
|
||||||
```
|
```
|
||||||
lib/features/{feature_name}/
|
lib/features/{feature}/
|
||||||
├── domain/
|
├── domain/
|
||||||
│ ├── entities/{name}_entity.dart # @freezed,纯 Dart 字段
|
│ ├── entities/{name}_entity.dart # @freezed,纯 Dart
|
||||||
│ └── repositories/{name}_repository.dart # abstract interface
|
│ └── repositories/{name}_repository.dart # abstract interface
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── models/{name}_model.dart # @freezed + fromJson,含 toEntity()
|
│ ├── models/{name}_model.dart # @freezed + fromJson + toEntity()
|
||||||
│ ├── datasources/{name}_remote_datasource.dart # @riverpod,只调 Dio
|
│ ├── datasources/{name}_remote_datasource.dart # @riverpod,只用 Dio
|
||||||
│ └── repositories/{name}_repository_impl.dart # @riverpod,实现 domain 接口
|
│ └── repositories/{name}_repository_impl.dart # @riverpod,实现接口
|
||||||
└── presentation/
|
└── presentation/
|
||||||
├── notifiers/{name}_notifier.dart # @riverpod class XxxNotifier
|
├── notifiers/{name}_notifier.dart # @riverpod class XxxNotifier
|
||||||
└── pages/{name}_page.dart # ConsumerWidget / ConsumerStatefulWidget
|
└── pages/{name}_page.dart # ConsumerWidget
|
||||||
```
|
```
|
||||||
|
|
||||||
### 新增路由
|
### 新增路由(routes.dart → make gen)
|
||||||
|
|
||||||
在 `lib/core/router/routes.dart` 添加(然后运行 `make gen`):
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
@TypedGoRoute<MyNewRoute>(path: '/my-new-path')
|
@TypedGoRoute<MyRoute>(path: '/my-path')
|
||||||
class MyNewRoute extends GoRouteData with $MyNewRoute {
|
class MyRoute extends GoRouteData with $MyRoute {
|
||||||
const MyNewRoute();
|
const MyRoute();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) => const MyNewPage();
|
Widget build(BuildContext context, GoRouterState state) => const MyPage();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 新增 API 路径
|
### 标准错误处理
|
||||||
|
|
||||||
在 `lib/core/config/api_paths.dart` 添加:
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
// ============== My Feature ==============
|
// Notifier 层
|
||||||
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) {
|
} on Object catch (e, st) {
|
||||||
final failure = ExceptionMapper().fromUnknown(e, st);
|
final failure = ExceptionMapper().fromUnknown(e, st);
|
||||||
state = MyState.error(failure.message);
|
state = MyState.error(failure.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 层:监听 + 显示
|
// UI 层
|
||||||
ref.listen(myProvider, (_, next) {
|
ref.listen(myProvider, (_, next) {
|
||||||
if (next is AsyncError) context.showError(ref, next.error!, next.stackTrace!);
|
if (next is AsyncError) context.showError(ref, next.error!, next.stackTrace!);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 新增 DB 表
|
||||||
|
|
||||||
## 零容忍规则(AI 必须遵守)
|
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`
|
||||||
| 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)|
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 关键架构决策(勿重议)
|
## 开发命令
|
||||||
|
|
||||||
1. **SQLCipher 不可替换**:企业合规要求,部署后无法无损切换到无加密数据库
|
```bash
|
||||||
2. **7 拦截器顺序固定**:CertPinning→Auth→TokenRefresh→Retry→Error→AuthLogout→Log,错误处理链依赖此顺序
|
make gen # build_runner → .g.dart / .freezed.dart(改 Dart 文件后必跑)
|
||||||
3. **TokenRefresh 使用 Completer 互斥**:防止并发 401 重复消耗 RefreshToken,不可改为简单 flag
|
make gen-i18n # slang → lib/i18n/strings.g.dart(改翻译 JSON 后必跑)
|
||||||
4. **SecureStorage 是 token 唯一真源**:DB 中的 refreshToken 仅作可观测性镜像
|
make run-dev # dev 包(USE_MOCK=true,无需后端)
|
||||||
5. **GoRouter 用 `ref.read`(非 `ref.watch`)构建**:router 是 keepAlive 单例,通过 `refreshListenable` 响应 auth 变化
|
make run-staging # staging 包(连 test 服务器)
|
||||||
6. **Failure 不可绕过**:所有网络/DB 异常必须经 ExceptionMapper 映射,UI 只看 Failure.message
|
make build-staging# staging APK
|
||||||
|
make build-prod # prod AAB(需先配置 prod.json + key.properties)
|
||||||
|
flutter analyze # 目标 0 errors(当前正常 info 数约 103 条)
|
||||||
|
flutter test # 全量测试
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 新项目接手 Checklist
|
## 新项目接手 Checklist
|
||||||
|
|
||||||
> 完成后删除本节或移至项目 wiki
|
> 完成并删除本节(或移至项目 wiki)
|
||||||
|
|
||||||
- [ ] 更新本文件"项目身份信息"表格
|
- [ ] 更新本文件"项目身份信息"表格(让 AI 上下文准确)
|
||||||
|
- [ ] 修改 `lib/core/config/api_config.dart` 三套 baseUrl
|
||||||
- [ ] 填入 `lib/core/config/api_paths.dart` 所有路径
|
- [ ] 填入 `lib/core/config/api_paths.dart` 所有路径
|
||||||
- [ ] 配置三个 `dart-defines/*.json`(含 RSA 公钥、Sentry DSN)
|
- [ ] 配置三个 `dart-defines/*.json`(RSA 公钥 / Sentry DSN)
|
||||||
- [ ] 修改 `android/app/build.gradle.kts` 中的 `applicationId`
|
- [ ] 修改 Android 包名:`android/app/build.gradle.kts` applicationId
|
||||||
- [ ] 修改 `pubspec.yaml` 中的 `name`(含相关 import 路径)
|
- [ ] 修改 iOS Bundle ID:Xcode → Runner Target → General
|
||||||
- [ ] 完成 `docs/setup-android.md` 中的 Android 签名配置
|
- [ ] 修改 `pubspec.yaml` name 并批量替换 import 路径(见 setup-project.md Step 5)
|
||||||
- [ ] 完成 `docs/setup-ios.md` 中的 iOS Scheme / Signing 配置
|
- [ ] 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`)
|
- [ ] 替换登录页 UI 品牌(`lib/features/auth/presentation/pages/login_page.dart`)
|
||||||
- [ ] 替换 App Icon(Android mipmap / iOS AppIcon.appiconset)
|
- [ ] 替换 App Icon(Android mipmap / iOS AppIcon.appiconset)
|
||||||
- [ ] 实现业务 Tab 页面(替换 `routes.dart` 中的 placeholder build)
|
- [ ] 实现 Tab 页面(`lib/core/router/routes.dart` HomeRoute / MineRoute 的 build)
|
||||||
- [ ] 更新 README.md 项目描述
|
- [ ] 更新 README.md 项目描述和快速开始地址
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -236,7 +231,8 @@ ref.listen(myProvider, (_, next) {
|
|||||||
|
|
||||||
| 文档 | 内容 |
|
| 文档 | 内容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [README.md](README.md) | 项目概述、技术栈、快速开始 |
|
| [README.md](../README.md) | 项目概述、技术栈、基础设施一览 |
|
||||||
| [docs/setup-project.md](docs/setup-project.md) | 完整项目配置指南(所有开发者必读)|
|
| [docs/setup-project.md](setup-project.md) | 完整配置指南(dart-defines / 包名 / 首次运行)|
|
||||||
| [docs/setup-android.md](docs/setup-android.md) | Android Flavor / 签名 / 构建详细配置 |
|
| [docs/setup-android.md](setup-android.md) | Android Flavor / 签名 / 构建 / CI |
|
||||||
| [docs/setup-ios.md](docs/setup-ios.md) | iOS Scheme / CocoaPods / 签名详细配置 |
|
| [docs/setup-ios.md](setup-ios.md) | iOS Scheme / CocoaPods / 证书 / Archive |
|
||||||
|
| [docs/architecture.md](architecture.md) | 数据流 / 认证流 / 错误链 ASCII 架构图 |
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 30 秒可运行验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub get && make gen && make gen-i18n
|
||||||
|
make run-dev # USE_MOCK=true,无需后端,显示登录页即为成功
|
||||||
|
```
|
||||||
|
|
||||||
|
> 登录:任意手机号 + 验证码 `123456`(Mock 自动填入)→ 跳 Home 占位页 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 已实现的基础设施
|
## 已实现的基础设施
|
||||||
|
|
||||||
### 构建体系
|
### 构建体系
|
||||||
@@ -150,7 +161,8 @@ make run-dev
|
|||||||
| 文档 | 适用人群 | 内容 |
|
| 文档 | 适用人群 | 内容 |
|
||||||
|------|---------|------|
|
|------|---------|------|
|
||||||
| **本文(README.md)** | 所有成员 | 项目概述、技术栈、快速开始 |
|
| **本文(README.md)** | 所有成员 | 项目概述、技术栈、快速开始 |
|
||||||
| [docs/setup-project.md](docs/setup-project.md) | 所有开发者 | 环境配置、API 路径、dart-defines 填写 |
|
| [docs/setup-project.md](docs/setup-project.md) | 所有开发者 | 先跑起来、dart-defines 字段表、API 路径、包名替换 |
|
||||||
| [docs/setup-android.md](docs/setup-android.md) | Android / 全栈 | Flavor、签名、keystore、构建 |
|
| [docs/setup-android.md](docs/setup-android.md) | Android / 全栈 | Flavor、签名、keystore、构建 |
|
||||||
| [docs/setup-ios.md](docs/setup-ios.md) | iOS / 全栈 | Scheme、CocoaPods、证书、Archive |
|
| [docs/setup-ios.md](docs/setup-ios.md) | iOS / 全栈 | Scheme、CocoaPods、证书、Archive |
|
||||||
| [CLAUDE.md](CLAUDE.md) | AI 辅助开发 | 架构规范、零容忍规则、开发范式 |
|
| [docs/architecture.md](docs/architecture.md) | 所有开发者 + AI | 数据流 / 认证流 / 错误链 / 拦截器链 ASCII 图 |
|
||||||
|
| [CLAUDE.md](CLAUDE.md) | AI 辅助开发 | 零容忍规则、禁止改的文件、开发范式、新项目 Checklist |
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"ENV": "dev",
|
"ENV": "dev",
|
||||||
"USE_MOCK": "false",
|
"USE_MOCK": "true",
|
||||||
"INTERNAL_BUILD": "true",
|
"INTERNAL_BUILD": "true",
|
||||||
|
"API_BASE_URL": "",
|
||||||
"SENTRY_DSN": "",
|
"SENTRY_DSN": "",
|
||||||
"PINNED_FINGERPRINTS": "",
|
"PINNED_FINGERPRINTS": "",
|
||||||
"RSA_PUBLIC_KEY": "REPLACE_WITH_RSA_PUBLIC_KEY"
|
"RSA_PUBLIC_KEY": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"ENV": "release",
|
"ENV": "release",
|
||||||
"USE_MOCK": "false",
|
"USE_MOCK": "false",
|
||||||
"INTERNAL_BUILD": "false",
|
"INTERNAL_BUILD": "false",
|
||||||
|
"API_BASE_URL": "",
|
||||||
"SENTRY_DSN": "REPLACE_WITH_ACTUAL_SENTRY_DSN",
|
"SENTRY_DSN": "REPLACE_WITH_ACTUAL_SENTRY_DSN",
|
||||||
"PINNED_FINGERPRINTS": "REPLACE_WITH_ACTUAL_FINGERPRINTS_AA:BB:CC",
|
"PINNED_FINGERPRINTS": "REPLACE_WITH_CERT_SHA256_AA:BB:CC:DD",
|
||||||
"RSA_PUBLIC_KEY": "REPLACE_WITH_RSA_PUBLIC_KEY"
|
"RSA_PUBLIC_KEY": "REPLACE_WITH_RSA_PUBLIC_KEY_BASE64_DER"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"ENV": "test",
|
"ENV": "test",
|
||||||
"USE_MOCK": "false",
|
"USE_MOCK": "false",
|
||||||
"INTERNAL_BUILD": "true",
|
"INTERNAL_BUILD": "true",
|
||||||
|
"API_BASE_URL": "",
|
||||||
"SENTRY_DSN": "",
|
"SENTRY_DSN": "",
|
||||||
"PINNED_FINGERPRINTS": "",
|
"PINNED_FINGERPRINTS": "",
|
||||||
"RSA_PUBLIC_KEY": "REPLACE_WITH_RSA_PUBLIC_KEY"
|
"RSA_PUBLIC_KEY": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# 架构概览
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 整体分层
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Presentation 层 │
|
||||||
|
│ ConsumerWidget / ConsumerStatefulWidget │
|
||||||
|
│ ref.watch(provider) → UI 响应式更新 │
|
||||||
|
│ ref.listen(provider) → 副作用(Toast / 导航) │
|
||||||
|
└──────────────┬──────────────────────────────────┘
|
||||||
|
│ @riverpod Notifier
|
||||||
|
┌──────────────▼──────────────────────────────────┐
|
||||||
|
│ Domain 层 │
|
||||||
|
│ abstract interface Repository │
|
||||||
|
│ @freezed Entity(纯 Dart,无框架依赖) │
|
||||||
|
└──────────────┬──────────────────────────────────┘
|
||||||
|
│ @riverpod impl
|
||||||
|
┌──────────────▼──────────────────────────────────┐
|
||||||
|
│ Data 层 │
|
||||||
|
│ @riverpod RemoteDatasource(Dio) │
|
||||||
|
│ @riverpod RepositoryImpl(组合 Dio + Drift) │
|
||||||
|
│ @freezed Model(+ fromJson / toEntity) │
|
||||||
|
└──────────────┬──────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────┴───────┐
|
||||||
|
▼ ▼
|
||||||
|
┌─────────┐ ┌──────────┐
|
||||||
|
│ Dio 网络│ │ Drift DB│
|
||||||
|
│ 7拦截器 │ │ SQLCipher│
|
||||||
|
└─────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 认证流程(Auth Flow)
|
||||||
|
|
||||||
|
```
|
||||||
|
App 启动
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
CrashReporter.preInit() # 准备崩溃写入路径
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
CrashReporter.consumePending() # 读上次崩溃文件(同步)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
SentrySetup.init() # 包裹 runApp(DSN 空则直接 runApp)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AppDatabase.open() # AES-256 解锁 SQLite
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ProviderScope(注入 DB + pendingCrash)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
CrashReporter.installHooks() # 接管 FlutterError + Zone 异常
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
authStatusProvider._bootstrap() # 异步读 SecureStorage.getToken()
|
||||||
|
│ ├── token 非空 → markLoggedIn()
|
||||||
|
│ └── token 为空 → markLoggedOut()
|
||||||
|
▼
|
||||||
|
GoRouter.redirect() # 监听 authStatus.listenable(ValueNotifier)
|
||||||
|
│
|
||||||
|
├── loggedIn == null → 不跳转(等待 bootstrap 完成)
|
||||||
|
├── loggedIn == false → push /login
|
||||||
|
└── loggedIn == true → push /home(若当前在 /login)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 网络请求链(7 拦截器顺序固定)
|
||||||
|
|
||||||
|
```
|
||||||
|
Dio.request()
|
||||||
|
│
|
||||||
|
▼ [1] CertPinningInterceptor
|
||||||
|
│ ├── PINNED_FINGERPRINTS 为空 → 跳过(dev 环境)
|
||||||
|
│ └── 指纹不匹配 → throw NetworkFailure(badCertificate)
|
||||||
|
│
|
||||||
|
▼ [2] AuthInterceptor
|
||||||
|
│ ├── extra['skip_auth'] == true → 跳过(登录 / 刷新 Token 接口)
|
||||||
|
│ └── 读 SecureStorage.getToken() + getTokenName() → 注入 Header
|
||||||
|
│
|
||||||
|
▼ [3] TokenRefreshInterceptor(仅 onError)
|
||||||
|
│ ├── status != 401 → 透传
|
||||||
|
│ ├── retCode 不在 token 类码 → 标记 _auth_not_refreshable → 透传
|
||||||
|
│ ├── 已在刷新(_refreshing != null)→ await 同一 Completer(防并发)
|
||||||
|
│ └── 刷新成功 → 更新 token → 重放原请求
|
||||||
|
│ └── 刷新失败 → 标记 _auth_refresh_failed → 透传
|
||||||
|
│
|
||||||
|
▼ [4] RetryInterceptor(仅 onError)
|
||||||
|
│ └── 408/429/5xx 且未超过 3 次 → 指数退避重试(200ms→400ms→800ms)
|
||||||
|
│
|
||||||
|
▼ [5] ErrorInterceptor(仅 onError)
|
||||||
|
│ └── DioException → ExceptionMapper.fromDio() → sealed Failure
|
||||||
|
│ 写入 err.error(供下游拦截器识别)
|
||||||
|
│
|
||||||
|
▼ [6] AuthLogoutInterceptor(仅 onError)
|
||||||
|
│ └── err.error is AuthFailure(unauthorized|refreshFailed)
|
||||||
|
│ → SecureStorage.clearAll() + authStatus.markLoggedOut()
|
||||||
|
│ → GoRouter 自动跳 /login
|
||||||
|
│
|
||||||
|
▼ [7] LogInterceptor
|
||||||
|
└── Env.enableDevPanel 为 true → TalkerDioLogger 输出
|
||||||
|
否则 → 无操作(Release 包零日志)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误传播链
|
||||||
|
|
||||||
|
```
|
||||||
|
网络/DB 异常
|
||||||
|
│
|
||||||
|
▼ ExceptionMapper.fromUnknown(e, st)
|
||||||
|
│
|
||||||
|
▼ sealed Failure(一律通过此分类)
|
||||||
|
│ ├── NetworkFailure → 超时 / 无网络 / 证书错误
|
||||||
|
│ ├── AuthFailure → 未授权 / token 过期 / 加密失败
|
||||||
|
│ ├── ServerFailure → HTTP 4xx/5xx / 业务码非 00000
|
||||||
|
│ ├── CacheFailure → Drift / IO 异常
|
||||||
|
│ └── UnknownFailure → 兜底
|
||||||
|
│
|
||||||
|
├─→ UI 层:failure.message(用户可读文案,Notifier.state = error(msg))
|
||||||
|
├─→ ErrorLogger.log(failure)(写入 Drift error_logs 表,fire-and-forget)
|
||||||
|
└─→ context.showError(ref, e, st)(Toast 展示 + 自动写日志)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Token 刷新互斥(Completer 模式)
|
||||||
|
|
||||||
|
解决并发场景下多个 401 同时触发 refresh 消耗 RefreshToken 的问题:
|
||||||
|
|
||||||
|
```
|
||||||
|
请求 A ──401──▶ _refreshing == null
|
||||||
|
│ 创建 Completer,赋值 _refreshing
|
||||||
|
│ 调用 _runRefresh()
|
||||||
|
│ │
|
||||||
|
请求 B ──401──▶ │ _refreshing != null
|
||||||
|
│ await _refreshing.future(阻塞等待)
|
||||||
|
│ │
|
||||||
|
请求 C ──401──▶ │ _refreshing != null
|
||||||
|
│ await _refreshing.future(阻塞等待)
|
||||||
|
│ │
|
||||||
|
│ refresh 完成
|
||||||
|
│ _refreshing = null ← 先清空,再 complete
|
||||||
|
│ completer.complete(true)
|
||||||
|
│ │
|
||||||
|
└─────────┴──▶ B、C 收到结果,用新 token 重放请求
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库表结构(schemaVersion 1)
|
||||||
|
|
||||||
|
```
|
||||||
|
UsersTable(userId 主键)
|
||||||
|
├── userId TEXT NOT NULL PK
|
||||||
|
├── username TEXT nullable
|
||||||
|
├── realName TEXT nullable
|
||||||
|
├── phone TEXT nullable
|
||||||
|
├── avatar TEXT nullable
|
||||||
|
├── gender INT nullable
|
||||||
|
├── age INT nullable
|
||||||
|
├── refreshToken TEXT nullable ← 镜像,SoT 在 SecureStorage
|
||||||
|
├── email TEXT nullable
|
||||||
|
├── birthday DATETIME nullable
|
||||||
|
├── employeeNo TEXT nullable
|
||||||
|
├── company TEXT nullable
|
||||||
|
├── department TEXT nullable
|
||||||
|
└── [SyncColumns: syncStatus, localUpdatedAt, serverUpdatedAt, conflictPayload]
|
||||||
|
|
||||||
|
ErrorLogsTable(自增 id)
|
||||||
|
├── id INT PK AUTOINCREMENT
|
||||||
|
├── kind TEXT NOT NULL ← 'network'|'server'|'auth'|'cache'|'unknown'
|
||||||
|
├── message TEXT NOT NULL ← 技术细节(Sentry / Talker 用)
|
||||||
|
├── displayMessage TEXT NOT NULL ← 用户可读文案
|
||||||
|
├── code TEXT nullable ← 业务错误码(ServerFailure)
|
||||||
|
├── statusCode INT nullable ← HTTP 状态码
|
||||||
|
├── occurredAt DATETIME NOT NULL
|
||||||
|
├── reported BOOL DEFAULT false
|
||||||
|
├── deviceModel TEXT nullable
|
||||||
|
├── osVersion TEXT nullable
|
||||||
|
├── appVersion TEXT nullable
|
||||||
|
└── appBuild TEXT nullable
|
||||||
|
```
|
||||||
|
|
||||||
|
> **token 不存 DB**:accessToken 的唯一真源是 SecureStorage(Keychain / EncryptedSharedPrefs)。
|
||||||
|
> UsersTable.refreshToken 仅作可观测性镜像,TokenRefreshInterceptor 始终从 SecureStorage 读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Envelope 格式
|
||||||
|
|
||||||
|
脚手架假设后端使用统一 JSON 响应包装(sa-token 风格):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "00000", // 成功码(ResponseCode.success = "00000")
|
||||||
|
"msg": "success",
|
||||||
|
"data": { ... } // 业务数据(可为 null)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`parseEnvelope<T>(body, fromJsonT)` 解析此结构;`apiResp.unwrapVoid()` 用于无数据响应。
|
||||||
|
|
||||||
|
token 类错误码(触发 TokenRefreshInterceptor):
|
||||||
|
- `A0401`(unauthorized)
|
||||||
|
- `TOKEN_EXPIRED` / `TOKEN_INVALID`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 主题系统
|
||||||
|
|
||||||
|
```
|
||||||
|
AppColorScheme (enum) — 5 套色板
|
||||||
|
├── blue (默认)
|
||||||
|
├── red
|
||||||
|
├── green
|
||||||
|
├── purple
|
||||||
|
└── teal
|
||||||
|
|
||||||
|
ThemeNotifier (@Riverpod, keepAlive)
|
||||||
|
└── 读/写 SharedPreferences('theme_scheme' + 'theme_mode')
|
||||||
|
└── app.dart 的 MaterialApp.router 监听 themeProvider + themeModeProvider
|
||||||
|
```
|
||||||
+133
-142
@@ -1,234 +1,225 @@
|
|||||||
# 项目配置指南(所有开发者必读)
|
# 项目配置指南
|
||||||
|
|
||||||
本文档覆盖从克隆脚手架到可以运行第一个 dev 包所需的全部步骤。
|
> **阅读顺序建议**:先跑起来(Step 1–2),再按需配置后续步骤。
|
||||||
Android 和 iOS 的平台专属配置请分别参阅 [setup-android.md](setup-android.md) 和 [setup-ios.md](setup-ios.md)。
|
> 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) |
|
| Flutter SDK | 3.41.0 | [flutter.dev/get-started](https://docs.flutter.dev/get-started/install) |
|
||||||
| Dart SDK | 3.7.0 | 随 Flutter 一起安装 |
|
| Android Studio | Meerkat 2024.3 | 含 Android SDK API 34 |
|
||||||
| Android Studio | Meerkat (2024.3) | [developer.android.com](https://developer.android.com/studio) |
|
| Xcode | 16.0 | 仅 iOS 开发需要(macOS 专属)|
|
||||||
| Xcode | 16.0 | Mac App Store(仅 iOS 开发需要)|
|
|
||||||
| CocoaPods | 1.15+ | `sudo gem install cocoapods` |
|
| CocoaPods | 1.15+ | `sudo gem install cocoapods` |
|
||||||
| make | 系统内置 | macOS/Linux 自带;Windows 用 Git Bash 或 WSL |
|
|
||||||
|
|
||||||
验证安装:
|
|
||||||
```bash
|
```bash
|
||||||
flutter --version # 应显示 ≥ 3.41.0
|
flutter doctor # 确认 Android toolchain + Xcode 全绿后再继续
|
||||||
flutter doctor # 确认 Android toolchain + Xcode 全绿
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 1 — 克隆与初始化
|
## Step 1 — 克隆 + 安装依赖 + 代码生成
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitea.example.com/your-org/your-project.git # TODO: 替换实际地址
|
git clone https://gitea.example.com/your-org/your-project.git # TODO: 替换真实地址
|
||||||
cd your-project
|
cd your-project
|
||||||
|
|
||||||
flutter pub get # 拉取所有依赖
|
flutter pub get # 拉取所有依赖(约 1 分钟)
|
||||||
make gen # 生成 .g.dart / .freezed.dart(约 1 分钟)
|
make gen # 生成 .g.dart / .freezed.dart(约 1 分钟)
|
||||||
make gen-i18n # 生成 lib/i18n/strings.g.dart
|
make gen-i18n # 生成 lib/i18n/strings.g.dart(秒级)
|
||||||
```
|
```
|
||||||
|
|
||||||
> **注意**:如果 `make gen` 报错 `build_runner` 找不到,请先确认 `dart` 在 PATH 中:
|
> 如果 `make gen` 报错,确认 `dart` 在 PATH 中:`dart --version`
|
||||||
> ```bash
|
|
||||||
> dart --version
|
|
||||||
> ```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 2 — 填入项目 API 路径
|
## Step 2 — 立即运行(无需后端)
|
||||||
|
|
||||||
|
`dart-defines/dev.json` 默认开启 Mock 模式(`USE_MOCK=true`),App 使用 `assets/fixtures/` 下的预设 JSON 响应,**不需要任何后端服务**即可运行。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**期望结果:**
|
||||||
|
- ✅ App 启动,显示登录页(手机号 + SMS/密码双模式)
|
||||||
|
- ✅ 点击"发送验证码"→ 自动填入 `123456`(Mock 响应)
|
||||||
|
- ✅ 输入任意手机号 + `123456` 登录 → 跳转 Home 占位页
|
||||||
|
- ✅ 进入 Dev Panel(Talker 日志面板可正常显示请求日志)
|
||||||
|
|
||||||
|
> `flutter analyze` 输出约 103 条 `info` 提示,**这是正常的**(全为风格建议,0 errors)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — 填入 API 路径
|
||||||
|
|
||||||
编辑 `lib/core/config/api_paths.dart`,将所有空字符串替换为实际后端路径:
|
编辑 `lib/core/config/api_paths.dart`,将所有空字符串替换为实际后端路径:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
// 示例(sa-token + Spring Boot)
|
// 示例(sa-token 后端)
|
||||||
static const String authLoginSms = '/sys/auth/sms-login';
|
static const String authLoginSms = '/sys/auth/sms-login';
|
||||||
static const String authLoginPwd = '/sys/auth/password-login';
|
static const String authLoginPwd = '/sys/auth/password-login';
|
||||||
static const String authSmsSend = '/sys/auth/send-sms';
|
static const String authSmsSend = '/sys/auth/send-sms';
|
||||||
static const String authLogout = '/sys/auth/logout';
|
static const String authLogout = '/sys/auth/logout';
|
||||||
static const String authRefresh = '/sys/auth/refresh-token';
|
static const String authRefresh = '/sys/auth/refresh-token';
|
||||||
static const String userProfile = '/sys/sys-user/app/userInfo';
|
static const String userProfile = '/sys/user/profile';
|
||||||
|
static const String crashReport = '/sys/crash-report';
|
||||||
|
static const String errorReport = '/sys/error-report';
|
||||||
// ... 其余路径按业务填写
|
// ... 其余路径按业务填写
|
||||||
```
|
```
|
||||||
|
|
||||||
所有 datasource 文件通过 `ApiPaths.xxx` 引用路径,**严禁字符串字面量散落**。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 3 — 配置环境变量(dart-defines)
|
## Step 4 — 配置环境变量(连接真实后端)
|
||||||
|
|
||||||
### 3.1 开发环境(dev.json)
|
### dart-defines 字段说明
|
||||||
|
|
||||||
|
| JSON 键 | 作用 | dev 默认 | 必须填写时机 |
|
||||||
|
|---------|------|---------|------------|
|
||||||
|
| `ENV` | 环境名(dev/test/release)| `dev` | 通常不改 |
|
||||||
|
| `API_BASE_URL` | 临时覆盖 baseUrl | 空 | 见下方说明 |
|
||||||
|
| `USE_MOCK` | 开启 Mock 模式 | `true` | 连真实后端时改 `false` |
|
||||||
|
| `INTERNAL_BUILD` | 显示 Dev Panel | `true` | 通常不改 |
|
||||||
|
| `SENTRY_DSN` | Sentry 上报地址 | 空 | prod 上线前 |
|
||||||
|
| `PINNED_FINGERPRINTS` | SSL 证书指纹 | 空 | 正式上线前 |
|
||||||
|
| `RSA_PUBLIC_KEY` | 密码加密公钥 | 空 | 登录加密时 |
|
||||||
|
|
||||||
|
### 方式 A:修改 api_config.dart(推荐,永久生效)
|
||||||
|
|
||||||
|
编辑 `lib/core/config/api_config.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
return switch (Env.name) {
|
||||||
|
'dev' => 'http://192.168.1.100:8080', // ← 改为 dev 服务器
|
||||||
|
'test' => 'https://staging.your-domain.com', // ← 改为 staging 服务器
|
||||||
|
'release' => 'https://api.your-domain.com', // ← 改为生产服务器
|
||||||
|
_ => 'http://192.168.1.100:8080',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
同时将 `dart-defines/dev.json` 中的 `USE_MOCK` 改为 `false`。
|
||||||
|
|
||||||
|
### 方式 B:临时覆盖(调试私有环境)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 直接编辑,此文件可提交(含占位符,无真实密钥)
|
flutter run --flavor dev \
|
||||||
nano dart-defines/dev.json
|
--dart-define-from-file=dart-defines/dev.json \
|
||||||
|
--dart-define=API_BASE_URL=http://192.168.1.200:8080 \
|
||||||
|
--dart-define=USE_MOCK=false
|
||||||
```
|
```
|
||||||
|
|
||||||
需填入的字段:
|
### 配置 RSA 公钥(密码加密)
|
||||||
|
|
||||||
```json
|
若后端使用 RSA 加密传输密码,从后端获取公钥后填入各环境 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
|
```bash
|
||||||
cp dart-defines/prod.json.template dart-defines/prod.json
|
# 从 Java 后端 PEM 文件提取 Base64 DER(去掉 header/footer,合并为单行)
|
||||||
nano dart-defines/prod.json
|
cat server-public.key | grep -v "BEGIN\|END" | tr -d '\n'
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
将结果填入 `dart-defines/dev.json` / `staging.json` / `prod.json` 的 `RSA_PUBLIC_KEY` 字段。
|
||||||
{
|
|
||||||
"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 加密,修改 `auth_repository_impl.dart` 的 `loginWithPassword` 方法,将明文密码直接传入(或使用 HTTPS 保护)。
|
||||||
|
|
||||||
### 获取 RSA 公钥
|
### 配置 SSL 证书指纹(生产必须)
|
||||||
|
|
||||||
后端一般提供 RSA 公钥的 Base64 DER 编码。若后端使用 sa-token + RSA:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Java 后端:从 public.key 文件提取(去掉 PEM header/footer,拼接成单行)
|
# 获取服务器证书 SHA-256 指纹
|
||||||
cat public.key | grep -v "BEGIN\|END" | tr -d '\n'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 获取证书 SHA-256 指纹
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 方法 1:openssl(推荐)
|
|
||||||
echo | openssl s_client -connect api.your-domain.com:443 2>/dev/null \
|
echo | openssl s_client -connect api.your-domain.com:443 2>/dev/null \
|
||||||
| openssl x509 -fingerprint -sha256 -noout \
|
| openssl x509 -fingerprint -sha256 -noout \
|
||||||
| sed 's/SHA256 Fingerprint=//'
|
| sed 's/SHA256 Fingerprint=//'
|
||||||
|
|
||||||
# 方法 2:Chrome → 锁图标 → 证书 → 指纹
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
将结果填入 `dart-defines/prod.json` 的 `PINNED_FINGERPRINTS` 字段(多个指纹用逗号分隔)。
|
||||||
|
|
||||||
## Step 4 — 验证首次运行
|
### 配置生产环境(prod.json)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 方式 1:使用 Mock(推荐首次验证,不需要后端服务)
|
cp dart-defines/prod.json.template dart-defines/prod.json
|
||||||
# 确保 dart-defines/dev.json 中 USE_MOCK=true
|
# 编辑 prod.json,填入 Sentry DSN / 证书指纹 / RSA 公钥
|
||||||
make run-dev
|
|
||||||
|
|
||||||
# 方式 2:连接真实后端
|
|
||||||
# 确保 dart-defines/dev.json 中 USE_MOCK=false + API_BASE_URL 已填入
|
|
||||||
make run-dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**期望结果:**
|
> **prod.json 已在 .gitignore 中**,不会被提交。CI/CD 通过 secret 注入。
|
||||||
- APP 启动,显示登录页(手机号 + SMS/密码双模式)
|
|
||||||
- 右上角(或侧滑)可进入 Dev Panel(Talker 日志面板)
|
|
||||||
- `flutter analyze` 输出 `103 issues found`(均为 info 级别,0 errors)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 5 — 更新包名(业务项目必须)
|
## Step 5 — 修改包名(业务项目必须)
|
||||||
|
|
||||||
脚手架默认包名为 `com.example.sunny_mochi`,接手后必须替换:
|
|
||||||
|
|
||||||
### Android
|
### Android
|
||||||
编辑 `android/app/build.gradle.kts`:
|
|
||||||
|
`android/app/build.gradle.kts` → `defaultConfig.applicationId`:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// dev Flavor
|
|
||||||
applicationId = "com.your_company.your_app.dev"
|
|
||||||
|
|
||||||
// staging Flavor
|
|
||||||
applicationId = "com.your_company.your_app.staging"
|
|
||||||
|
|
||||||
// prod / release
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.your_company.your_app"
|
applicationId = "com.your_company.your_app" // ← 修改
|
||||||
|
}
|
||||||
|
productFlavors {
|
||||||
|
create("dev") {
|
||||||
|
applicationIdSuffix = ".dev"
|
||||||
|
resValue("string", "app_name", "YourApp Dev") // ← 修改
|
||||||
|
}
|
||||||
|
create("staging") {
|
||||||
|
applicationIdSuffix = ".staging"
|
||||||
|
resValue("string", "app_name", "YourApp Beta") // ← 修改
|
||||||
|
}
|
||||||
|
create("prod") {
|
||||||
|
resValue("string", "app_name", "YourApp") // ← 修改
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### iOS
|
### iOS
|
||||||
在 Xcode 中修改 Bundle Identifier(见 [setup-ios.md](setup-ios.md) Step 3)。
|
|
||||||
|
打开 Xcode(必须用 `.xcworkspace`):
|
||||||
|
```bash
|
||||||
|
open ios/Runner.xcworkspace
|
||||||
|
```
|
||||||
|
Runner Target → General → Bundle Identifier → 修改为实际 ID。
|
||||||
|
|
||||||
### pubspec.yaml + Dart 导入路径
|
### pubspec.yaml + Dart 导入路径
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: your_app_name # 修改后所有 import 路径也需要更新
|
# pubspec.yaml
|
||||||
|
name: your_app_name # ← 修改
|
||||||
```
|
```
|
||||||
|
|
||||||
批量替换导入:
|
批量替换代码中的 package 名:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS / Linux
|
# macOS / Linux
|
||||||
find lib -name "*.dart" -exec sed -i '' 's/package:sunny_mochi/package:your_app_name/g' {} \;
|
find lib -name "*.dart" -exec sed -i '' \
|
||||||
|
's/package:sunny_mochi/package:your_app_name/g' {} \;
|
||||||
|
|
||||||
# Windows PowerShell
|
# Windows PowerShell
|
||||||
Get-ChildItem -Path lib -Recurse -Filter "*.dart" |
|
Get-ChildItem -Path lib -Recurse -Filter "*.dart" | ForEach-Object {
|
||||||
ForEach-Object { (Get-Content $_.FullName) -replace 'sunny_mochi', 'your_app_name' |
|
(Get-Content $_.FullName) -replace 'sunny_mochi', 'your_app_name' |
|
||||||
Set-Content $_.FullName }
|
Set-Content $_.FullName
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
运行 `make gen` 重新生成后确认无编译错误。
|
替换后运行 `make gen` 重新生成,确认 `flutter analyze` 无错误。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 6 — 平台专属配置
|
## Step 6 — 平台专属配置
|
||||||
|
|
||||||
- **Android**(签名 / Flavor 验证 / 发布包构建)→ [setup-android.md](setup-android.md)
|
| 需求 | 文档 |
|
||||||
- **iOS**(Xcode Scheme / CocoaPods / 证书 / Archive)→ [setup-ios.md](setup-ios.md)
|
|------|------|
|
||||||
|
| Android 签名 / Flavor 验证 / 发布包 | [setup-android.md](setup-android.md) |
|
||||||
|
| iOS Scheme / CocoaPods / 证书 / Archive | [setup-ios.md](setup-ios.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
### `build_runner` 生成失败,报 analyzer 版本冲突
|
| 症状 | 原因 | 解决 |
|
||||||
|
|------|------|------|
|
||||||
确认 `pubspec.yaml` 中的 `dependency_overrides`:
|
| `make gen` 报错 `dart: command not found` | Dart 不在 PATH | `export PATH="$PATH:/path/to/flutter/bin"` |
|
||||||
```yaml
|
| `build_runner` 报 analyzer 版本冲突 | dependency_overrides 被修改 | 恢复 `drift: 2.31.0` 和 `json_serializable: 6.13.0` |
|
||||||
dependency_overrides:
|
| App 启动后立即跳登录页 | 正常(未配置 token)| 用 Mock 登录验证,或连后端后正式登录 |
|
||||||
drift: 2.31.0
|
| Mock 登录后 Home 显示占位文字 | 正常(脚手架默认)| 实现 `routes.dart` 中 HomeRoute 的 build |
|
||||||
json_serializable: 6.13.0
|
| `flutter analyze` 显示 103 issues | 正常(全为 info 级别)| 0 errors 即通过,info 为风格建议 |
|
||||||
```
|
| 连接后端但请求无响应 | api_config.dart 仍是占位 URL | 修改 api_config.dart 三套 baseUrl |
|
||||||
若被修改,恢复后重跑 `flutter pub get && make gen`。
|
| iOS 真机崩溃,模拟器正常 | SecureStorage Keychain 权限 | 确认 Signing & Capabilities 配置正确 |
|
||||||
|
|
||||||
### App 启动后 token 丢失 / 每次重启都跳登录
|
|
||||||
|
|
||||||
SecureStorage 在 iOS Simulator 上可能行为异常。请在真机测试,或检查 `Keychain Sharing` 是否开启。
|
|
||||||
|
|
||||||
### Mock 模式下请求崩溃 `Unable to load asset`
|
|
||||||
|
|
||||||
检查 `assets/fixtures/` 下是否有对应的 JSON 文件,且 `pubspec.yaml` 中的 `assets:` 包含了对应子目录。
|
|
||||||
|
|
||||||
### `flutter analyze` 报错(非 info 级别)
|
|
||||||
|
|
||||||
先运行 `make gen` 确保生成文件是最新的,再重新 analyze。
|
|
||||||
|
|||||||
@@ -1,43 +1,36 @@
|
|||||||
import 'package:sunny_mochi/core/config/env.dart';
|
import 'package:sunny_mochi/core/config/env.dart';
|
||||||
|
|
||||||
/// API 网络配置 — **baseUrl 决策的单一权威**
|
/// API 网络配置 — baseUrl 的单一决策权威。
|
||||||
///
|
///
|
||||||
/// **对应 iOS BasicModule/Configuration/NetworkConfig.swift(2026-05 同步)**:
|
/// **新项目必填**:在下方 switch 中填入三套环境的实际 baseUrl,
|
||||||
|
/// 或者在 dart-defines/*.json 中设置 API_BASE_URL 做临时覆盖。
|
||||||
///
|
///
|
||||||
/// | 环境 | iOS apiBaseURL | Flutter ApiConfig.baseUrl |
|
/// 优先级:
|
||||||
/// |------|---------------|--------------------------|
|
/// 1. dart-define `API_BASE_URL` 不为空时优先(CI/CD / 临时调试)
|
||||||
/// | dev | http://192.168.1.201:24801 | 同 |
|
/// 2. 否则按 `Env.name`(dev / test / release)返回对应 URL
|
||||||
/// | test | https://dev.yixiong-tech.com:8081 | 同 |
|
|
||||||
/// | release | https://bac.new.hamkke.top | 同 |
|
|
||||||
///
|
///
|
||||||
/// **优先级**:
|
/// **API 路径**:见 lib/core/config/api_paths.dart(禁止 datasource 散落字面量)
|
||||||
/// 1. CI/CD / 临时调试通过 `--dart-define=API_BASE_URL=...` 注入 → 优先
|
/// **响应码**:见 lib/core/network/response_code.dart
|
||||||
/// 2. 否则按 `Env.name`(dev/test/release)返回 iOS 同源 URL
|
|
||||||
///
|
|
||||||
/// **不再引入 h5BaseURL** — H5 评估报告已全部 Flutter 原生化(详见
|
|
||||||
/// docs/h5-to-native-decision-2026-05-10.md),不再需要 in-app WebView。
|
|
||||||
///
|
|
||||||
/// **响应码请用 [ResponseCode]**(lib/core/network/response_code.dart)。
|
|
||||||
/// **API 路径请用 [ApiPaths]**(lib/core/config/api_paths.dart)—
|
|
||||||
/// 不允许 datasource 散落字面量。
|
|
||||||
abstract class ApiConfig {
|
abstract class ApiConfig {
|
||||||
/// API 网关 baseUrl — 与 iOS NetworkConfig.swift 同源
|
/// API 网关 baseUrl
|
||||||
|
///
|
||||||
|
/// TODO: 将下方三个 URL 替换为项目实际后端地址。
|
||||||
static String get baseUrl {
|
static String get baseUrl {
|
||||||
// 1. dart-define 覆盖优先(CI/CD / 临时切换私有环境)
|
// 1. dart-define 覆盖优先(临时切换私有环境 / CI 注入)
|
||||||
if (Env.apiBaseUrlOverride.isNotEmpty) {
|
if (Env.apiBaseUrlOverride.isNotEmpty) {
|
||||||
return Env.apiBaseUrlOverride;
|
return Env.apiBaseUrlOverride;
|
||||||
}
|
}
|
||||||
// 2. 按 Env.name 返回 iOS 同源 URL
|
// 2. 按环境返回固定 URL — TODO: 填入实际地址
|
||||||
return switch (Env.name) {
|
return switch (Env.name) {
|
||||||
'dev' => 'http://192.168.1.201:24801',
|
'dev' => 'http://localhost:8080', // TODO: dev 服务器地址
|
||||||
'test' => 'https://dev.yixiong-tech.com:8081',
|
'test' => 'https://staging.your-domain.com', // TODO: staging 服务器地址
|
||||||
'release' => 'https://bac.new.hamkke.top',
|
'release' => 'https://api.your-domain.com', // TODO: 生产服务器地址
|
||||||
_ => 'http://192.168.1.201:24801', // 默认 dev(与 iOS Debug 包一致)
|
_ => 'http://localhost:8080',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网络超时 — 对应共性需求说明 §移动端 7.网络异常处理(10s 超时建议)
|
// 网络超时配置
|
||||||
static const Duration connectTimeout = Duration(seconds: 15);
|
static const Duration connectTimeout = Duration(seconds: 15);
|
||||||
static const Duration receiveTimeout = Duration(seconds: 30);
|
static const Duration receiveTimeout = Duration(seconds: 30);
|
||||||
static const Duration sendTimeout = Duration(seconds: 30);
|
static const Duration sendTimeout = Duration(seconds: 30);
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-76
@@ -1,109 +1,71 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart' show SentryFlutter;
|
|
||||||
|
|
||||||
/// 全局编译期环境配置。所有值通过 --dart-define 注入,避免明文 secrets。
|
/// 全局编译期环境配置。所有值通过 `--dart-define-from-file` 注入,明文 secrets 不入代码。
|
||||||
///
|
///
|
||||||
/// **环境名与 iOS NetworkConfig.swift 对齐**:dev / test / release
|
/// 三套环境对应 Flavor:
|
||||||
|
/// - `dev` → dev Flavor,开发调试(可开 Mock)
|
||||||
|
/// - `test` → staging Flavor,连测试服务器
|
||||||
|
/// - `release` → prod Flavor,正式发布
|
||||||
///
|
///
|
||||||
/// 用法:
|
/// 用法:
|
||||||
/// ```bash
|
/// ```bash
|
||||||
/// # 开发环境 + mock(默认)
|
/// # 开发 + Mock(无需后端)
|
||||||
/// flutter run --dart-define=ENV=dev --dart-define=USE_MOCK=true
|
/// flutter run --flavor dev --dart-define-from-file=dart-defines/dev.json
|
||||||
///
|
///
|
||||||
/// # 测试环境(连真服 dev.yixiong-tech.com:8081)
|
/// # 临时覆盖 API URL(不修改源码)
|
||||||
/// flutter run --dart-define=ENV=test
|
|
||||||
///
|
|
||||||
/// # 正式环境(连 bac.new.hamkke.top)
|
|
||||||
/// flutter build apk --release --dart-define=ENV=release \
|
|
||||||
/// --dart-define=SENTRY_DSN=https://...@sentry/1
|
|
||||||
///
|
|
||||||
/// # CI/CD 临时覆盖 API URL(不修改源码)
|
|
||||||
/// flutter run --dart-define=API_BASE_URL=https://my-private.example.com
|
/// flutter run --dart-define=API_BASE_URL=https://my-private.example.com
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// 详见:
|
/// **需要填写的字段**:见 dart-defines/dev.json(所有字段带 TODO 注释)
|
||||||
/// - `docs/flutter-architecture-design.md` §九.1 / §十一.5
|
/// **API 路径**:见 lib/core/config/api_paths.dart
|
||||||
/// - `docs/real-environment-verification.md`(环境就绪后填值)
|
/// **baseUrl 决策**:见 lib/core/config/api_config.dart
|
||||||
/// - `lib/core/config/api_config.dart`(baseUrl 决策权威)
|
|
||||||
abstract class Env {
|
abstract class Env {
|
||||||
/// 当前环境名:**dev / test / release**(与 iOS NetworkConfig 三态对齐)。默认 dev。
|
/// 当前环境名:dev / test / release
|
||||||
///
|
static const String name = String.fromEnvironment(
|
||||||
/// - dev:开发环境(局域网 192.168.1.201:24801 / mock 模式可用)
|
'ENV',
|
||||||
/// - test:测试环境(dev.yixiong-tech.com:8081 — 与 iOS test 同源)
|
defaultValue: 'dev',
|
||||||
/// - release:正式环境(bac.new.hamkke.top — Release 包应锁定此值)
|
);
|
||||||
static const String name = String.fromEnvironment('ENV', defaultValue: 'dev');
|
|
||||||
|
|
||||||
/// API baseUrl 临时覆盖(CI/CD / 私有环境调试用)。
|
/// API baseUrl 临时覆盖(CI/CD / 私有环境调试用)。
|
||||||
///
|
///
|
||||||
/// **正常情况下不应使用此变量** — 让 [ApiConfig.baseUrl] 按 [name] 自动选择
|
/// 正常情况下留空 — 由 [api_config.dart] 按 [name] 自动选择。
|
||||||
/// iOS 同源 URL。仅当需要连私有/临时环境时通过 dart-define 注入。
|
/// 仅当需要临时连私有/临时环境时通过 dart-define 注入:
|
||||||
///
|
/// `--dart-define=API_BASE_URL=https://my-private.example.com`
|
||||||
/// ```bash
|
|
||||||
/// flutter run --dart-define=API_BASE_URL=https://my-private.example.com
|
|
||||||
/// ```
|
|
||||||
static const String apiBaseUrlOverride = String.fromEnvironment(
|
static const String apiBaseUrlOverride = String.fromEnvironment(
|
||||||
'API_BASE_URL',
|
'API_BASE_URL',
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Sentry DSN。**Q9 待答前**为空,[SentryFlutter.init] 自动跳过实际上报。
|
/// Sentry DSN。空字符串时 SentryFlutter.init 自动跳过实际上报。
|
||||||
static const String sentryDsn = String.fromEnvironment(
|
/// TODO: prod 环境通过 dart-defines/prod.json 填入真实 DSN。
|
||||||
'SENTRY_DSN',
|
static const String sentryDsn = String.fromEnvironment('SENTRY_DSN');
|
||||||
);
|
|
||||||
|
|
||||||
/// 是否为内部测试包(决定 TalkerScreen 调试面板是否挂载)。
|
/// 是否为内部测试包(控制 Talker 调试面板入口是否挂载)。
|
||||||
/// 详见 docs/real-environment-verification.md §M4 / §Talker 准入。
|
static const bool isInternalBuild = bool.fromEnvironment('INTERNAL_BUILD');
|
||||||
static const bool isInternalBuild = bool.fromEnvironment(
|
|
||||||
'INTERNAL_BUILD',
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 是否启用 mock fixtures 路径(绕过真实网络请求)。
|
/// 是否启用 Mock 模式(绕过真实网络,使用 assets/fixtures/ 下的 JSON)。
|
||||||
/// 详见 plan §Mock 与真实环境验证分层策略。
|
/// TODO: dev.json 默认 true,上线前确认 staging/prod 为 false。
|
||||||
static const bool useMock = bool.fromEnvironment(
|
static const bool useMock = bool.fromEnvironment('USE_MOCK');
|
||||||
'USE_MOCK',
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 腾讯云 IM SDKAppID(数字 ID)。
|
|
||||||
/// **Q3 待答前**为 0(无效值,init 会返回失败但不崩溃)。
|
|
||||||
/// 沙箱测试用控制台测试 SDKAppID;生产用真实业务 SDKAppID。
|
|
||||||
/// 注入:--dart-define=IM_SDK_APP_ID=1400xxxxxx
|
|
||||||
static const int imSdkAppId = int.fromEnvironment(
|
|
||||||
'IM_SDK_APP_ID',
|
|
||||||
);
|
|
||||||
|
|
||||||
/// RSA 公钥(DER-SPKI Base64)— 用于登录密码加密。
|
/// RSA 公钥(DER-SPKI Base64)— 用于登录密码加密。
|
||||||
/// 默认值 = iOS dev 公钥(对应 platform-ios/.../RSAEncryption.swift 中的 publicKeyString)。
|
/// TODO: 从后端获取公钥后填入 dart-defines/*.json 的 RSA_PUBLIC_KEY 字段。
|
||||||
/// 生产 / Q6 答复后通过 --dart-define=RSA_PUBLIC_KEY=... 注入真实公钥。
|
/// 若后端不需要 RSA 加密,可忽略此字段并移除 auth_repository_impl 中的加密逻辑。
|
||||||
static const String rsaPublicKey = String.fromEnvironment(
|
static const String rsaPublicKey = String.fromEnvironment('RSA_PUBLIC_KEY');
|
||||||
'RSA_PUBLIC_KEY',
|
|
||||||
defaultValue:
|
|
||||||
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmZfR/bA9X3vp86y1aEpvwzXJYKRRF1fLau2+05/ZtaITLpV8bhkmSf3neSy/Q9gAdvG75Fr73E+GWE+K5b0BpvIS1jDGo319+PpZR39SaZTKZ27XFXrosmJTZutN79t819HS1VseleunHAFgMVufE9U5jP6LGzl/wbkSy01GhzwIDAQAB',
|
|
||||||
);
|
|
||||||
|
|
||||||
/// TLS 证书绑定指纹列表(SHA-256,多指纹支持轮换)。
|
/// TLS 证书绑定指纹列表(SHA-256,逗号分隔,支持多指纹轮换)。
|
||||||
/// **Q2 待答前**为空,CertificatePinningInterceptor 在空列表时跳过校验。
|
/// TODO: 填入 PINNED_FINGERPRINTS 字段;dev 留空时 CertPinning 拦截器自动跳过。
|
||||||
/// 真实指纹通过 --dart-define=PINNED_FINGERPRINTS=AA:BB,CC:DD 注入。
|
|
||||||
static List<String> get pinnedFingerprints {
|
static List<String> get pinnedFingerprints {
|
||||||
const raw = String.fromEnvironment(
|
const raw = String.fromEnvironment('PINNED_FINGERPRINTS');
|
||||||
'PINNED_FINGERPRINTS',
|
|
||||||
);
|
|
||||||
if (raw.isEmpty) return const [];
|
if (raw.isEmpty) return const [];
|
||||||
return raw
|
return raw.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
|
||||||
.split(',')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.where((s) => s.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 便捷判断(对齐 iOS NetworkConfig.Environment 三态)----
|
// ── 便捷判断 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static bool get isDev => name == 'dev';
|
static bool get isDev => name == 'dev';
|
||||||
static bool get isTest => name == 'test';
|
static bool get isTest => name == 'test';
|
||||||
static bool get isRelease => name == 'release';
|
static bool get isRelease => name == 'release';
|
||||||
|
|
||||||
/// 兼容旧调用 — 历史代码可能用 isProd 判断
|
/// Talker / Riverpod observer 等调试工具的门控。
|
||||||
/// @Deprecated 新代码请用 [isRelease]
|
/// Debug 包(flutter run)或 INTERNAL_BUILD=true 时开启。
|
||||||
static bool get isProd => isRelease;
|
|
||||||
|
|
||||||
/// Release 包除非 INTERNAL_BUILD=true,否则视为生产模式。
|
|
||||||
/// 用于 TalkerScreen / Riverpod observer 等开发面板的门控。
|
|
||||||
static bool get enableDevPanel => kDebugMode || isInternalBuild;
|
static bool get enableDevPanel => kDebugMode || isInternalBuild;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ ExceptionMapper exceptionMapper(Ref ref) => const ExceptionMapper();
|
|||||||
/// - 数据库异常 → CacheFailure
|
/// - 数据库异常 → CacheFailure
|
||||||
/// - 兜底 → UnknownFailure
|
/// - 兜底 → UnknownFailure
|
||||||
///
|
///
|
||||||
/// 详见 docs/flutter-architecture-design.md §四.2。
|
|
||||||
class ExceptionMapper {
|
class ExceptionMapper {
|
||||||
const ExceptionMapper();
|
const ExceptionMapper();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:sunny_mochi/core/error/exception_mapper.dart'
|
|||||||
/// **实现 [Exception]**:让 datasource/repository 可以直接 `throw failure;`
|
/// **实现 [Exception]**:让 datasource/repository 可以直接 `throw failure;`
|
||||||
/// 不触发 `only_throw_errors` lint。
|
/// 不触发 `only_throw_errors` lint。
|
||||||
///
|
///
|
||||||
/// 详见 docs/flutter-architecture-design.md §四.2。
|
|
||||||
sealed class Failure implements Exception {
|
sealed class Failure implements Exception {
|
||||||
const Failure({required this.message, this.cause, this.stackTrace});
|
const Failure({required this.message, this.cause, this.stackTrace});
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import 'package:sunny_mochi/core/config/env.dart';
|
|||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
/// 包装 [SentryFlutter.init],统一注入:
|
/// 包装 [SentryFlutter.init],统一注入:
|
||||||
/// - DSN 来自 [Env.sentryDsn](空 DSN 跳过实际上报,便于 P0 兜底)
|
/// - DSN 来自 [Env.sentryDsn](空 DSN 跳过实际上报)
|
||||||
/// - PII 脱敏(健康类 App 强约束,详见 docs/flutter-architecture-design.md §十一.5)
|
/// - PII 脱敏(屏蔽手机号 / token / 身份证等敏感数据)
|
||||||
/// - 屏蔽 screenshot / view hierarchy(含敏感页面)
|
/// - 屏蔽 screenshot / view hierarchy
|
||||||
///
|
///
|
||||||
/// 调用方在 main.dart 包一层:
|
/// 调用方在 main.dart 包一层:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@@ -27,7 +27,7 @@ class SentrySetup {
|
|||||||
options
|
options
|
||||||
..dsn = Env.sentryDsn
|
..dsn = Env.sentryDsn
|
||||||
..environment = Env.name
|
..environment = Env.name
|
||||||
..tracesSampleRate = Env.isProd ? 0.2 : 1.0
|
..tracesSampleRate = Env.isRelease ? 0.2 : 1.0
|
||||||
..debug = kDebugMode
|
..debug = kDebugMode
|
||||||
// === 健康数据 PII 脱敏(强约束)===
|
// === 健康数据 PII 脱敏(强约束)===
|
||||||
..sendDefaultPii = false
|
..sendDefaultPii = false
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import 'package:talker_flutter/talker_flutter.dart';
|
|||||||
///
|
///
|
||||||
/// **Release 包准入**(合规底线):[TalkerSettings.enabled] = `kDebugMode || INTERNAL_BUILD`,
|
/// **Release 包准入**(合规底线):[TalkerSettings.enabled] = `kDebugMode || INTERNAL_BUILD`,
|
||||||
/// 用户线 Release 包硬关闭日志记录与设备调试面板(避免敏感数据/PII 写入设备)。
|
/// 用户线 Release 包硬关闭日志记录与设备调试面板(避免敏感数据/PII 写入设备)。
|
||||||
/// 详见 docs/flutter-architecture-design.md §十一.5。
|
|
||||||
final Talker appTalker = TalkerFlutter.init(
|
final Talker appTalker = TalkerFlutter.init(
|
||||||
settings: TalkerSettings(
|
settings: TalkerSettings(
|
||||||
enabled: kDebugMode || Env.isInternalBuild,
|
enabled: kDebugMode || Env.isInternalBuild,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ void _sqlCipherIsolateSetup() {
|
|||||||
/// 3. 探测现有文件是否可用当前密钥打开;失败则删除重建
|
/// 3. 探测现有文件是否可用当前密钥打开;失败则删除重建
|
||||||
/// 4. NativeDatabase setup 时执行 `PRAGMA key = '...'` 解锁
|
/// 4. NativeDatabase setup 时执行 `PRAGMA key = '...'` 解锁
|
||||||
///
|
///
|
||||||
/// 详见 docs/flutter-architecture-design.md §九.1.1 + §十一.5。
|
|
||||||
@DriftDatabase(tables: [Users, ErrorLogs])
|
@DriftDatabase(tables: [Users, ErrorLogs])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase._(super.e);
|
AppDatabase._(super.e);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
/// - 服务端下发:可主动撤销,离线不可解锁
|
/// - 服务端下发:可主动撤销,离线不可解锁
|
||||||
/// - 两段式(A+B 组合):最复杂
|
/// - 两段式(A+B 组合):最复杂
|
||||||
///
|
///
|
||||||
/// 详见 docs/flutter-architecture-design.md §九.1.2 与 §十一.5。
|
|
||||||
abstract class KeyDerivationStrategy {
|
abstract class KeyDerivationStrategy {
|
||||||
Future<String> deriveKey();
|
Future<String> deriveKey();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:sunny_mochi/core/sync/sync_status.dart';
|
|||||||
/// - [serverUpdatedAt] 服务端最后修改时间(last-write-wins 比较基准)
|
/// - [serverUpdatedAt] 服务端最后修改时间(last-write-wins 比较基准)
|
||||||
/// - [conflictPayload] 冲突时备份的服务端版本 JSON(人工或策略恢复)
|
/// - [conflictPayload] 冲突时备份的服务端版本 JSON(人工或策略恢复)
|
||||||
///
|
///
|
||||||
/// 详见 docs/flutter-architecture-design.md §五.21。
|
|
||||||
mixin SyncColumns on Table {
|
mixin SyncColumns on Table {
|
||||||
IntColumn get syncStatus =>
|
IntColumn get syncStatus =>
|
||||||
intEnum<SyncStatus>().withDefault(const Constant(0))();
|
intEnum<SyncStatus>().withDefault(const Constant(0))();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ abstract class SyncTask {
|
|||||||
/// - 业务层通过 [enqueue] 注册 SyncTask
|
/// - 业务层通过 [enqueue] 注册 SyncTask
|
||||||
/// - 串行执行(避免并发污染服务端)
|
/// - 串行执行(避免并发污染服务端)
|
||||||
///
|
///
|
||||||
/// 详见 docs/flutter-architecture-design.md §五.21。
|
|
||||||
class SyncService {
|
class SyncService {
|
||||||
SyncService({
|
SyncService({
|
||||||
required Connectivity connectivity,
|
required Connectivity connectivity,
|
||||||
|
|||||||
Reference in New Issue
Block a user