Template
fix: 清除 platform-flutter 残留代码,重构文档与开发者体验
P0 代码修复:删除 imSdkAppId、清除内网 IP 与原项目域名、修正 USE_MOCK 默认值 P1 CLAUDE.md 重组:零容忍规则前置,新增 dart-defines 字段对照表 P2 文档优化:先跑再配的 setup 流程,新增 architecture.md 架构图
This commit is contained in:
@@ -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 包所需的全部步骤。
|
||||
Android 和 iOS 的平台专属配置请分别参阅 [setup-android.md](setup-android.md) 和 [setup-ios.md](setup-ios.md)。
|
||||
> **阅读顺序建议**:先跑起来(Step 1–2),再按需配置后续步骤。
|
||||
> 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 开发需要)|
|
||||
| 工具 | 最低版本 | 说明 |
|
||||
|------|---------|------|
|
||||
| Flutter SDK | 3.41.0 | [flutter.dev/get-started](https://docs.flutter.dev/get-started/install) |
|
||||
| Android Studio | Meerkat 2024.3 | 含 Android SDK API 34 |
|
||||
| Xcode | 16.0 | 仅 iOS 开发需要(macOS 专属)|
|
||||
| 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 全绿
|
||||
flutter doctor # 确认 Android toolchain + Xcode 全绿后再继续
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — 克隆与初始化
|
||||
## Step 1 — 克隆 + 安装依赖 + 代码生成
|
||||
|
||||
```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
|
||||
|
||||
flutter pub get # 拉取所有依赖
|
||||
make gen # 生成 .g.dart / .freezed.dart(约 1 分钟)
|
||||
make gen-i18n # 生成 lib/i18n/strings.g.dart
|
||||
flutter pub get # 拉取所有依赖(约 1 分钟)
|
||||
make gen # 生成 .g.dart / .freezed.dart(约 1 分钟)
|
||||
make gen-i18n # 生成 lib/i18n/strings.g.dart(秒级)
|
||||
```
|
||||
|
||||
> **注意**:如果 `make gen` 报错 `build_runner` 找不到,请先确认 `dart` 在 PATH 中:
|
||||
> ```bash
|
||||
> dart --version
|
||||
> ```
|
||||
> 如果 `make gen` 报错,确认 `dart` 在 PATH 中:`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`,将所有空字符串替换为实际后端路径:
|
||||
|
||||
```dart
|
||||
// 示例(sa-token + Spring Boot)
|
||||
// 示例(sa-token 后端)
|
||||
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';
|
||||
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
|
||||
# 直接编辑,此文件可提交(含占位符,无真实密钥)
|
||||
nano dart-defines/dev.json
|
||||
flutter run --flavor dev \
|
||||
--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
|
||||
{
|
||||
"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)
|
||||
若后端使用 RSA 加密传输密码,从后端获取公钥后填入各环境 JSON:
|
||||
|
||||
```bash
|
||||
cp dart-defines/prod.json.template dart-defines/prod.json
|
||||
nano dart-defines/prod.json
|
||||
# 从 Java 后端 PEM 文件提取 Base64 DER(去掉 header/footer,合并为单行)
|
||||
cat server-public.key | grep -v "BEGIN\|END" | tr -d '\n'
|
||||
```
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
将结果填入 `dart-defines/dev.json` / `staging.json` / `prod.json` 的 `RSA_PUBLIC_KEY` 字段。
|
||||
|
||||
> **prod.json 已在 .gitignore 中**,不会被提交。CI/CD 系统通过 secret 变量注入。
|
||||
若后端**不需要** RSA 加密,修改 `auth_repository_impl.dart` 的 `loginWithPassword` 方法,将明文密码直接传入(或使用 HTTPS 保护)。
|
||||
|
||||
### 获取 RSA 公钥
|
||||
|
||||
后端一般提供 RSA 公钥的 Base64 DER 编码。若后端使用 sa-token + RSA:
|
||||
### 配置 SSL 证书指纹(生产必须)
|
||||
|
||||
```bash
|
||||
# Java 后端:从 public.key 文件提取(去掉 PEM header/footer,拼接成单行)
|
||||
cat public.key | grep -v "BEGIN\|END" | tr -d '\n'
|
||||
```
|
||||
|
||||
### 获取证书 SHA-256 指纹
|
||||
|
||||
```bash
|
||||
# 方法 1:openssl(推荐)
|
||||
# 获取服务器证书 SHA-256 指纹
|
||||
echo | openssl s_client -connect api.your-domain.com:443 2>/dev/null \
|
||||
| openssl x509 -fingerprint -sha256 -noout \
|
||||
| sed 's/SHA256 Fingerprint=//'
|
||||
|
||||
# 方法 2:Chrome → 锁图标 → 证书 → 指纹
|
||||
```
|
||||
|
||||
---
|
||||
将结果填入 `dart-defines/prod.json` 的 `PINNED_FINGERPRINTS` 字段(多个指纹用逗号分隔)。
|
||||
|
||||
## Step 4 — 验证首次运行
|
||||
### 配置生产环境(prod.json)
|
||||
|
||||
```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
|
||||
cp dart-defines/prod.json.template dart-defines/prod.json
|
||||
# 编辑 prod.json,填入 Sentry DSN / 证书指纹 / RSA 公钥
|
||||
```
|
||||
|
||||
**期望结果:**
|
||||
- APP 启动,显示登录页(手机号 + SMS/密码双模式)
|
||||
- 右上角(或侧滑)可进入 Dev Panel(Talker 日志面板)
|
||||
- `flutter analyze` 输出 `103 issues found`(均为 info 级别,0 errors)
|
||||
> **prod.json 已在 .gitignore 中**,不会被提交。CI/CD 通过 secret 注入。
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — 更新包名(业务项目必须)
|
||||
|
||||
脚手架默认包名为 `com.example.sunny_mochi`,接手后必须替换:
|
||||
## Step 5 — 修改包名(业务项目必须)
|
||||
|
||||
### Android
|
||||
编辑 `android/app/build.gradle.kts`:
|
||||
|
||||
`android/app/build.gradle.kts` → `defaultConfig.applicationId`:
|
||||
|
||||
```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"
|
||||
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
|
||||
在 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 导入路径
|
||||
|
||||
```yaml
|
||||
name: your_app_name # 修改后所有 import 路径也需要更新
|
||||
# pubspec.yaml
|
||||
name: your_app_name # ← 修改
|
||||
```
|
||||
|
||||
批量替换导入:
|
||||
批量替换代码中的 package 名:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
Get-ChildItem -Path lib -Recurse -Filter "*.dart" |
|
||||
ForEach-Object { (Get-Content $_.FullName) -replace 'sunny_mochi', 'your_app_name' |
|
||||
Set-Content $_.FullName }
|
||||
Get-ChildItem -Path lib -Recurse -Filter "*.dart" | ForEach-Object {
|
||||
(Get-Content $_.FullName) -replace 'sunny_mochi', 'your_app_name' |
|
||||
Set-Content $_.FullName
|
||||
}
|
||||
```
|
||||
|
||||
运行 `make gen` 重新生成后确认无编译错误。
|
||||
替换后运行 `make gen` 重新生成,确认 `flutter analyze` 无错误。
|
||||
|
||||
---
|
||||
|
||||
## 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`:
|
||||
```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。
|
||||
| 症状 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| `make gen` 报错 `dart: command not found` | Dart 不在 PATH | `export PATH="$PATH:/path/to/flutter/bin"` |
|
||||
| `build_runner` 报 analyzer 版本冲突 | dependency_overrides 被修改 | 恢复 `drift: 2.31.0` 和 `json_serializable: 6.13.0` |
|
||||
| App 启动后立即跳登录页 | 正常(未配置 token)| 用 Mock 登录验证,或连后端后正式登录 |
|
||||
| Mock 登录后 Home 显示占位文字 | 正常(脚手架默认)| 实现 `routes.dart` 中 HomeRoute 的 build |
|
||||
| `flutter analyze` 显示 103 issues | 正常(全为 info 级别)| 0 errors 即通过,info 为风格建议 |
|
||||
| 连接后端但请求无响应 | api_config.dart 仍是占位 URL | 修改 api_config.dart 三套 baseUrl |
|
||||
| iOS 真机崩溃,模拟器正常 | SecureStorage Keychain 权限 | 确认 Signing & Capabilities 配置正确 |
|
||||
|
||||
Reference in New Issue
Block a user