fix: 清除 platform-flutter 残留代码,重构文档与开发者体验

P0 代码修复:删除 imSdkAppId、清除内网 IP 与原项目域名、修正 USE_MOCK 默认值
P1 CLAUDE.md 重组:零容忍规则前置,新增 dart-defines 字段对照表
P2 文档优化:先跑再配的 setup 流程,新增 architecture.md 架构图
This commit is contained in:
SkyJourney
2026-05-14 13:21:06 +08:00
parent a266323905
commit b8b63282ee
17 changed files with 605 additions and 417 deletions
+19 -26
View File
@@ -1,43 +1,36 @@
import 'package:sunny_mochi/core/config/env.dart';
/// API 网络配置 — **baseUrl 决策的单一权威**
/// API 网络配置 — baseUrl 的单一决策权威。
///
/// **对应 iOS BasicModule/Configuration/NetworkConfig.swift2026-05 同步)**
/// **新项目必填**:在下方 switch 中填入三套环境的实际 baseUrl,
/// 或者在 dart-defines/*.json 中设置 API_BASE_URL 做临时覆盖。
///
/// | 环境 | iOS apiBaseURL | Flutter ApiConfig.baseUrl |
/// |------|---------------|--------------------------|
/// | dev | http://192.168.1.201:24801 | 同 |
/// | test | https://dev.yixiong-tech.com:8081 | 同 |
/// | release | https://bac.new.hamkke.top | 同 |
/// 优先级:
/// 1. dart-define `API_BASE_URL` 不为空时优先(CI/CD / 临时调试)
/// 2. 否则按 `Env.name`dev / test / release)返回对应 URL
///
/// **优先级**
/// 1. CI/CD / 临时调试通过 `--dart-define=API_BASE_URL=...` 注入 → 优先
/// 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 散落字面量。
/// **API 路径**:见 lib/core/config/api_paths.dart(禁止 datasource 散落字面量)
/// **响应码**:见 lib/core/network/response_code.dart
abstract class ApiConfig {
/// API 网关 baseUrl — 与 iOS NetworkConfig.swift 同源
/// API 网关 baseUrl
///
/// TODO: 将下方三个 URL 替换为项目实际后端地址。
static String get baseUrl {
// 1. dart-define 覆盖优先(CI/CD / 临时切换私有环境)
// 1. dart-define 覆盖优先(临时切换私有环境 / CI 注入
if (Env.apiBaseUrlOverride.isNotEmpty) {
return Env.apiBaseUrlOverride;
}
// 2. 按 Env.name 返回 iOS 同源 URL
// 2. 按环境返回固定 URL — TODO: 填入实际地址
return switch (Env.name) {
'dev' => 'http://192.168.1.201:24801',
'test' => 'https://dev.yixiong-tech.com:8081',
'release' => 'https://bac.new.hamkke.top',
_ => 'http://192.168.1.201:24801', // 默认 dev(与 iOS Debug 包一致)
'dev' => 'http://localhost:8080', // TODO: dev 服务器地址
'test' => 'https://staging.your-domain.com', // TODO: staging 服务器地址
'release' => 'https://api.your-domain.com', // TODO: 生产服务器地址
_ => 'http://localhost:8080',
};
}
// 网络超时 — 对应共性需求说明 §移动端 7.网络异常处理(10s 超时建议)
// 网络超时配置
static const Duration connectTimeout = Duration(seconds: 15);
static const Duration receiveTimeout = Duration(seconds: 30);
static const Duration sendTimeout = Duration(seconds: 30);
static const Duration sendTimeout = Duration(seconds: 30);
}
+38 -76
View File
@@ -1,109 +1,71 @@
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
/// # 开发环境 + mock默认
/// flutter run --dart-define=ENV=dev --dart-define=USE_MOCK=true
/// # 开发 + Mock无需后端
/// flutter run --flavor dev --dart-define-from-file=dart-defines/dev.json
///
/// # 测试环境(连真服 dev.yixiong-tech.com:8081
/// 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(不修改源码)
/// # 临时覆盖 API URL(不修改源码
/// flutter run --dart-define=API_BASE_URL=https://my-private.example.com
/// ```
///
/// 详见:
/// - `docs/flutter-architecture-design.md` §九.1 / §十一.5
/// - `docs/real-environment-verification.md`(环境就绪后填值)
/// - `lib/core/config/api_config.dart`baseUrl 决策权威)
/// **需要填写的字段**:见 dart-defines/dev.json(所有字段带 TODO 注释)
/// **API 路径**:见 lib/core/config/api_paths.dart
/// **baseUrl 决策**:见 lib/core/config/api_config.dart
abstract class Env {
/// 当前环境名:**dev / test / release**(与 iOS NetworkConfig 三态对齐)。默认 dev。
///
/// - dev:开发环境(局域网 192.168.1.201:24801 / mock 模式可用)
/// - test:测试环境(dev.yixiong-tech.com:8081 — 与 iOS test 同源)
/// - release:正式环境(bac.new.hamkke.top — Release 包应锁定此值)
static const String name = String.fromEnvironment('ENV', defaultValue: 'dev');
/// 当前环境名:dev / test / release
static const String name = String.fromEnvironment(
'ENV',
defaultValue: 'dev',
);
/// API baseUrl 临时覆盖(CI/CD / 私有环境调试用)。
///
/// **正常情况下不应使用此变量** [ApiConfig.baseUrl] 按 [name] 自动选择
/// iOS 同源 URL。仅当需要连私有/临时环境时通过 dart-define 注入
///
/// ```bash
/// flutter run --dart-define=API_BASE_URL=https://my-private.example.com
/// ```
/// 正常情况下留空 [api_config.dart] 按 [name] 自动选择
/// 仅当需要临时连私有/临时环境时通过 dart-define 注入
/// `--dart-define=API_BASE_URL=https://my-private.example.com`
static const String apiBaseUrlOverride = String.fromEnvironment(
'API_BASE_URL',
);
/// Sentry DSN。**Q9 待答前**为空,[SentryFlutter.init] 自动跳过实际上报。
static const String sentryDsn = String.fromEnvironment(
'SENTRY_DSN',
);
/// Sentry DSN。空字符串时 SentryFlutter.init 自动跳过实际上报。
/// TODO: prod 环境通过 dart-defines/prod.json 填入真实 DSN。
static const String sentryDsn = String.fromEnvironment('SENTRY_DSN');
/// 是否为内部测试包(决定 TalkerScreen 调试面板是否挂载)。
/// 详见 docs/real-environment-verification.md §M4 / §Talker 准入。
static const bool isInternalBuild = bool.fromEnvironment(
'INTERNAL_BUILD',
);
/// 是否为内部测试包(控制 Talker 调试面板入口是否挂载)。
static const bool isInternalBuild = bool.fromEnvironment('INTERNAL_BUILD');
/// 是否启用 mock fixtures 路径(绕过真实网络请求)。
/// 详见 plan §Mock 与真实环境验证分层策略
static const bool useMock = bool.fromEnvironment(
'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',
);
/// 是否启用 Mock 模式(绕过真实网络,使用 assets/fixtures/ 下的 JSON)。
/// TODO: dev.json 默认 true,上线前确认 staging/prod 为 false
static const bool useMock = bool.fromEnvironment('USE_MOCK');
/// RSA 公钥(DER-SPKI Base64)— 用于登录密码加密。
/// 默认值 = iOS dev 公钥(对应 platform-ios/.../RSAEncryption.swift 中的 publicKeyString
/// 生产 / Q6 答复后通过 --dart-define=RSA_PUBLIC_KEY=... 注入真实公钥
static const String rsaPublicKey = String.fromEnvironment(
'RSA_PUBLIC_KEY',
defaultValue:
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmZfR/bA9X3vp86y1aEpvwzXJYKRRF1fLau2+05/ZtaITLpV8bhkmSf3neSy/Q9gAdvG75Fr73E+GWE+K5b0BpvIS1jDGo319+PpZR39SaZTKZ27XFXrosmJTZutN79t819HS1VseleunHAFgMVufE9U5jP6LGzl/wbkSy01GhzwIDAQAB',
);
/// TODO: 从后端获取公钥后填入 dart-defines/*.json 的 RSA_PUBLIC_KEY 字段
/// 若后端不需要 RSA 加密,可忽略此字段并移除 auth_repository_impl 中的加密逻辑
static const String rsaPublicKey = String.fromEnvironment('RSA_PUBLIC_KEY');
/// TLS 证书绑定指纹列表(SHA-256,多指纹支持轮换)。
/// **Q2 待答前**为空,CertificatePinningInterceptor 在空列表时跳过校验
/// 真实指纹通过 --dart-define=PINNED_FINGERPRINTS=AA:BB,CC:DD 注入。
/// TLS 证书绑定指纹列表(SHA-256,逗号分隔,支持多指纹轮换)。
/// TODO: 填入 PINNED_FINGERPRINTS 字段;dev 留空时 CertPinning 拦截器自动跳过
static List<String> get pinnedFingerprints {
const raw = String.fromEnvironment(
'PINNED_FINGERPRINTS',
);
const raw = String.fromEnvironment('PINNED_FINGERPRINTS');
if (raw.isEmpty) return const [];
return raw
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
return raw.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
}
// ---- 便捷判断(对齐 iOS NetworkConfig.Environment 三态)----
// ── 便捷判断 ──────────────────────────────────────────────────────────────
static bool get isDev => name == 'dev';
static bool get isTest => name == 'test';
static bool get isRelease => name == 'release';
/// 兼容旧调用 — 历史代码可能用 isProd 判断
/// @Deprecated 新代码请用 [isRelease]
static bool get isProd => isRelease;
/// Release 包除非 INTERNAL_BUILD=true,否则视为生产模式。
/// 用于 TalkerScreen / Riverpod observer 等开发面板的门控。
/// Talker / Riverpod observer 等调试工具的门控。
/// Debug 包(flutter run)或 INTERNAL_BUILD=true 时开启。
static bool get enableDevPanel => kDebugMode || isInternalBuild;
}
+1 -1
View File
@@ -17,7 +17,7 @@ ExceptionMapper exceptionMapper(Ref ref) => const ExceptionMapper();
/// - 数据库异常 → CacheFailure
/// - 兜底 → UnknownFailure
///
/// 详见 docs/flutter-architecture-design.md §四.2。
class ExceptionMapper {
const ExceptionMapper();
+1 -1
View File
@@ -9,7 +9,7 @@ import 'package:sunny_mochi/core/error/exception_mapper.dart'
/// **实现 [Exception]**:让 datasource/repository 可以直接 `throw failure;`
/// 不触发 `only_throw_errors` lint。
///
/// 详见 docs/flutter-architecture-design.md §四.2。
sealed class Failure implements Exception {
const Failure({required this.message, this.cause, this.stackTrace});
+4 -4
View File
@@ -3,9 +3,9 @@ import 'package:sunny_mochi/core/config/env.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// 包装 [SentryFlutter.init],统一注入:
/// - DSN 来自 [Env.sentryDsn](空 DSN 跳过实际上报,便于 P0 兜底
/// - PII 脱敏(健康类 App 强约束,详见 docs/flutter-architecture-design.md §十一.5
/// - 屏蔽 screenshot / view hierarchy(含敏感页面)
/// - DSN 来自 [Env.sentryDsn](空 DSN 跳过实际上报)
/// - PII 脱敏(屏蔽手机号 / token / 身份证等敏感数据
/// - 屏蔽 screenshot / view hierarchy
///
/// 调用方在 main.dart 包一层:
/// ```dart
@@ -27,7 +27,7 @@ class SentrySetup {
options
..dsn = Env.sentryDsn
..environment = Env.name
..tracesSampleRate = Env.isProd ? 0.2 : 1.0
..tracesSampleRate = Env.isRelease ? 0.2 : 1.0
..debug = kDebugMode
// === 健康数据 PII 脱敏(强约束)===
..sendDefaultPii = false
+1 -1
View File
@@ -14,7 +14,7 @@ import 'package:talker_flutter/talker_flutter.dart';
///
/// **Release 包准入**(合规底线):[TalkerSettings.enabled] = `kDebugMode || INTERNAL_BUILD`
/// 用户线 Release 包硬关闭日志记录与设备调试面板(避免敏感数据/PII 写入设备)。
/// 详见 docs/flutter-architecture-design.md §十一.5。
final Talker appTalker = TalkerFlutter.init(
settings: TalkerSettings(
enabled: kDebugMode || Env.isInternalBuild,
+1 -1
View File
@@ -57,7 +57,7 @@ void _sqlCipherIsolateSetup() {
/// 3. 探测现有文件是否可用当前密钥打开;失败则删除重建
/// 4. NativeDatabase setup 时执行 `PRAGMA key = '...'` 解锁
///
/// 详见 docs/flutter-architecture-design.md §九.1.1 + §十一.5。
@DriftDatabase(tables: [Users, ErrorLogs])
class AppDatabase extends _$AppDatabase {
AppDatabase._(super.e);
+1 -1
View File
@@ -16,7 +16,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// - 服务端下发:可主动撤销,离线不可解锁
/// - 两段式(A+B 组合):最复杂
///
/// 详见 docs/flutter-architecture-design.md §九.1.2 与 §十一.5。
abstract class KeyDerivationStrategy {
Future<String> deriveKey();
}
+1 -1
View File
@@ -9,7 +9,7 @@ import 'package:sunny_mochi/core/sync/sync_status.dart';
/// - [serverUpdatedAt] 服务端最后修改时间(last-write-wins 比较基准)
/// - [conflictPayload] 冲突时备份的服务端版本 JSON(人工或策略恢复)
///
/// 详见 docs/flutter-architecture-design.md §五.21。
mixin SyncColumns on Table {
IntColumn get syncStatus =>
intEnum<SyncStatus>().withDefault(const Constant(0))();
+1 -1
View File
@@ -23,7 +23,7 @@ abstract class SyncTask {
/// - 业务层通过 [enqueue] 注册 SyncTask
/// - 串行执行(避免并发污染服务端)
///
/// 详见 docs/flutter-architecture-design.md §五.21。
class SyncService {
SyncService({
required Connectivity connectivity,