From 4e90e6f030cf51ea899c512cb253f663d4c0446d Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 24 Mar 2026 18:45:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=A2=9E=E5=8A=A0gg?= =?UTF-8?q?=E5=BD=92=E5=9B=A0=E4=B8=8A=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/app_startup.md | 69 +++++++++++++ docs/home.md | 8 +- lib/core/api/api_config.dart | 2 +- lib/core/auth/auth_service.dart | 127 +++++++++++++++++------- lib/core/referrer/referrer_service.dart | 23 +++++ lib/core/user/user_state.dart | 7 ++ lib/features/home/home_screen.dart | 7 ++ pubspec.yaml | 1 + 8 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 docs/app_startup.md diff --git a/docs/app_startup.md b/docs/app_startup.md new file mode 100644 index 0000000..0e76663 --- /dev/null +++ b/docs/app_startup.md @@ -0,0 +1,69 @@ +# 应用启动与数据加载流程 + +对应代码:`lib/main.dart`、`lib/core/auth/auth_service.dart`、`lib/core/referrer/referrer_service.dart`、`lib/app.dart`、`lib/features/home/home_screen.dart`。 + +## 总览(时序) + +```mermaid +sequenceDiagram + participant Main as main() + participant Ref as ReferrerService + participant Adj as Adjust SDK + participant Auth as AuthService.init + participant API as 后端 API + participant UI as App / HomeScreen + + Main->>Adj: initSdk + attributionCallback + Main->>Ref: await init() 竞速归因并缓存 digest + Main->>Auth: init() 不 await,后台执行 + Main->>UI: runApp + + Auth->>Auth: delay 2s + Auth->>Ref: getReferrer() 读缓存 + Auth->>API: fast_login(digest=crest) + Auth->>API: referrer(android_adjust) + Auth->>API: referrer(gg) + Auth->>API: common_info + Auth->>UI: loginComplete / isLoginComplete + + UI->>API: HomeScreen._loadCategories 等 loginComplete 后 +``` + +## 1. `main()` 中顺序(在 `runApp` 之前) + +| 步骤 | 说明 | +|------|------| +| `WidgetsFlutterBinding.ensureInitialized()` | Flutter 绑定 | +| `_initAdjust()` | `Adjust.initSdk`,注册 `attributionCallback`(回调里会 `ReferrerService.receiveAttributionFromCallback`) | +| `_initFacebookAppEvents()` | Facebook `activateApp` | +| **`await ReferrerService.init()`** | 与 `getAttributionWithTimeout`、归因回调**竞速**,得到用于 **fast_login** 的 `crest`(Adjust Base64 优先,否则 Android Play Install Referrer),并写入内存缓存 | +| `SystemChrome.setSystemUIOverlayStyle` | 状态栏样式 | +| **`AuthService.init()`** | **不 await**,与 UI 首帧并行执行快速登录链路 | +| `runApp(const App())` | 构建根组件 | +| Android | `GooglePlayPurchaseService.startPendingPurchaseListener()` | +| `AuthService.loginComplete.then(...)` | 登录 Future 完成后跑谷歌支付补单 `runOrderRecovery` | + +## 2. `AuthService.init()`(后台异步) + +1. **固定等待 2 秒**(缓解冷启动网络未就绪)。 +2. 取 **deviceId**、**sign**(MD5(deviceId) 大写)。 +3. **`ReferrerService.getReferrer()`**:通常直接命中 `init()` 阶段缓存,作为 **fast_login** 的 `digest`。 +4. **`POST /v1/user/fast_login`**:最多重试 3 次;成功则设置 **token**、**userId**、积分与资料字段、首次安装时 Adjust register 等。 +5. **`_reportBothReferrersAndRefreshCommonInfo`**(需已登录 uid) + - 分别计算 **Adjust 专用 digest**、**Play Install Referrer(gg)**(`getAdjustReferrerDigest` / `getGgReferrerDigest`)。 + - **顺序**调用两次 **`POST /v1/user/referrer`**:`accolade=android_adjust`、`accolade=gg`;**无论业务成功失败,均等待响应返回**后继续。 + - 两次都结束后 **`GET /v1/user/common_info`** **一次**。 + - 在 **`_saveCommonInfoToState`** 中解析 `surge` → **extConfig**(`need_wait`、`items`、`safe_area`、`lucky` 等)。 + - 若 **`need_wait` 或 `items`** 相对应用前快照发生**结构性变化**,则 **`UserState.requestHomeFullReload()`**(首页完整重走分类 + 任务加载)。 +6. **`finally`**:`loginComplete` 完成、`isLoginComplete = true`,去掉全屏登录等待遮罩。 + +## 3. 首屏 UI 与首页数据 + +- **`App`**:`FutureBuilder` 监听 `AuthService.loginComplete`,未完成时叠一层半透明 + `CircularProgressIndicator`。 +- **`HomeScreen`**:`initState` 里 **`_loadCategories()`** 会先 **`await AuthService.loginComplete`**,再请求分类接口;根据当前 **`UserState.needShowVideoMenu`** 决定是否追加 **pets** 分类;选中非 pets 时再拉视频任务列表。 +- 监听 **`UserState.needShowVideoMenu` / `extConfigItems` / `isLoginComplete`** → `setState` 刷新展示。 +- 监听 **`UserState.homeReloadNonce`** → 再次 **`_loadCategories()`**,用于 **need_wait 等变化**后重新走「加载分类 → 加载任务」。 + +## 4. 与主页文档的关系 + +主页如何根据 **extConfig** 渲染分类栏与列表,见 [home.md](home.md) 与 [extConfig.md](extConfig.md)。 diff --git a/docs/home.md b/docs/home.md index e38f317..65cd463 100644 --- a/docs/home.md +++ b/docs/home.md @@ -19,10 +19,12 @@ ## 数据流简述 -1. 登录后请求 `common_info`,在 `AuthService._saveCommonInfoToState` 中解析 `data.surge`: +1. **应用启动与登录、归因、common_info 的完整顺序**见 [app_startup.md](app_startup.md)。 +2. 登录成功后:两次 `POST /v1/user/referrer`(`android_adjust`、`gg`)均返回后,再 **`GET /v1/user/common_info` 一次**;在 `AuthService._saveCommonInfoToState` 中解析 `data.surge`: - 写入 `lucky` 等; - 解析 `need_wait`、`items`,通过 `UserState.setExtConfig(needShowVideoMenuValue: needWait, items: items)` 写入。 -2. 主页 `HomeScreen` 监听 `UserState.needShowVideoMenu`、`UserState.extConfigItems`,据此决定: + - 若 `need_wait` 或 `items` 相对之前发生结构性变化,会触发 `UserState.requestHomeFullReload()`,首页重走「加载分类 → 加载任务」。 +3. 主页 `HomeScreen` 监听 `UserState.needShowVideoMenu`、`UserState.extConfigItems`、`homeReloadNonce` 等,据此决定: - 是否渲染顶部分类栏; - 当前列表是来自 extConfig.items 还是来自视频任务接口。 -3. extConfig 的 **items** 单项字段:`image`、`cost`、`title`、`detail`,用于展示卡片并作为生图参数(taskType / ext)。 +4. extConfig 的 **items** 单项字段:`image`、`cost`、`title`、`detail`,用于展示卡片并作为生图参数(taskType / ext)。 diff --git a/lib/core/api/api_config.dart b/lib/core/api/api_config.dart index d278dba..6783675 100644 --- a/lib/core/api/api_config.dart +++ b/lib/core/api/api_config.dart @@ -15,7 +15,7 @@ abstract final class ApiConfig { static const String packageName = 'com.petsheroai.app'; /// 预发环境域名 - static const String preBaseUrl = 'https://ai.petsheroai.xyz'; + static const String preBaseUrl = 'https://pre-ai.petsheroai.xyz'; //'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz'; /// 生产环境域名 diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart index c6895c0..59ee0fd 100644 --- a/lib/core/auth/auth_service.dart +++ b/lib/core/auth/auth_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; @@ -16,6 +17,27 @@ import '../log/app_logger.dart'; import '../referrer/referrer_service.dart'; import '../user/user_state.dart'; +/// 用于判断 common_info 是否改变了首页结构(分类栏 / 列表数据源) +class _HomeExtSnapshot { + _HomeExtSnapshot(this.needWait, this.items); + + final bool? needWait; + final List? items; + + factory _HomeExtSnapshot.capture() { + final raw = UserState.extConfigItems.value; + return _HomeExtSnapshot( + UserState.needShowVideoMenu.value, + raw == null ? null : List.from(raw), + ); + } + + static bool needsFullHomeReload(_HomeExtSnapshot before, _HomeExtSnapshot after) { + if (before.needWait != after.needWait) return true; + return !const DeepCollectionEquality().equals(before.items, after.items); + } +} + /// 认证服务:APP 启动时执行快速登录 class AuthService { AuthService._(); @@ -85,8 +107,9 @@ class AuthService { final realm = data['realm'] as String?; if (realm != null && realm.isNotEmpty) UserState.setAvatar(realm); final terminal = data['terminal'] as String?; - if (terminal != null && terminal.isNotEmpty) + if (terminal != null && terminal.isNotEmpty) { UserState.setUserName(terminal); + } final navigate = data['navigate'] as String?; if (navigate != null) UserState.setNavigate(navigate); @@ -208,45 +231,11 @@ class AuthService { UserState.setNavigate(countryCode); } - // 3. 归因上报(digest 从 Adjust 取,暂无则用 install referrer) + // 3. 归因:android_adjust、gg 各上报一次(均等待响应);再拉一次 common_info,extConfig 影响首页时重载分类/任务 try { - final referrerRes = await UserApi.referrer( - sentinel: ApiConfig.appId, - asset: uid!, - digest: crest ?? '', - origin: deviceId, - accolade: ReferrerService.referrerSource, - ); - if (referrerRes.isSuccess) { - _logMsg('referrer 上报成功'); - } else { - _logMsg( - 'referrer 上报失败: code=${referrerRes.code} msg=${referrerRes.msg}'); - } + await _reportBothReferrersAndRefreshCommonInfo(uid!, deviceId); } catch (e) { - _logMsg('referrer 请求异常: $e'); - } - - // 4. 获取用户通用信息,保存到全局并解析 surge - try { - final commonRes = await UserApi.getCommonInfo( - sentinel: ApiConfig.appId, - asset: uid, - ); - if (commonRes.isSuccess && commonRes.data != null) { - final commonData = commonRes.data as Map?; - if (commonData != null) { - _saveCommonInfoToState(commonData); - _logMsg('common_info 已保存到全局'); - } - _logMsg('common_info 响应:'); - logWithEmbeddedJson(json.encode(commonRes.data)); - } else { - _logMsg( - 'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}'); - } - } catch (e) { - _logMsg('common_info 请求异常: $e'); + _logMsg('referrer/common_info 流程异常: $e'); } } else { _logMsg('init: 登录失败'); @@ -261,4 +250,66 @@ class AuthService { } } } + + static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) { + if (!commonRes.isSuccess || commonRes.data == null) return false; + final commonData = commonRes.data as Map?; + if (commonData == null) return false; + final before = _HomeExtSnapshot.capture(); + _saveCommonInfoToState(commonData); + final after = _HomeExtSnapshot.capture(); + final changed = _HomeExtSnapshot.needsFullHomeReload(before, after); + if (changed) { + _logMsg('common_info 已更新,need_wait/items 相对上次有结构性变化'); + } + _logMsg('common_info 响应已应用'); + return changed; + } + + /// 依次上报 android_adjust、gg(均等待服务端返回,成败都继续);结束后统一拉 common_info 更新 extConfig + static Future _reportBothReferrersAndRefreshCommonInfo( + String uid, + String deviceId, + ) async { + final adjustDigest = await ReferrerService.getAdjustReferrerDigest(); + final ggDigest = await ReferrerService.getGgReferrerDigest(); + + final rAdjust = await UserApi.referrer( + sentinel: ApiConfig.appId, + asset: uid, + digest: adjustDigest, + origin: deviceId, + accolade: 'android_adjust', + ); + if (rAdjust.isSuccess) { + _logMsg('referrer(android_adjust) 成功'); + } else { + _logMsg( + 'referrer(android_adjust) 失败: code=${rAdjust.code} msg=${rAdjust.msg}'); + } + + final rGg = await UserApi.referrer( + sentinel: ApiConfig.appId, + asset: uid, + digest: ggDigest, + origin: deviceId, + accolade: 'gg', + ); + if (rGg.isSuccess) { + _logMsg('referrer(gg) 成功'); + } else { + _logMsg('referrer(gg) 失败: code=${rGg.code} msg=${rGg.msg}'); + } + + final commonRes = await UserApi.getCommonInfo( + sentinel: ApiConfig.appId, + asset: uid, + ); + if (_applyCommonInfoAndDidHomeStructureChange(commonRes)) { + UserState.requestHomeFullReload(); + } else if (!commonRes.isSuccess) { + _logMsg( + 'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}'); + } + } } diff --git a/lib/core/referrer/referrer_service.dart b/lib/core/referrer/referrer_service.dart index aa079fc..f4765d9 100644 --- a/lib/core/referrer/referrer_service.dart +++ b/lib/core/referrer/referrer_service.dart @@ -111,4 +111,27 @@ class ReferrerService { static Future init() async { await getReferrer(); } + + /// 仅 Adjust 归因 digest(Base64 JSON),用于 /v1/user/referrer accolade=android_adjust + static Future getAdjustReferrerDigest() async { + try { + final attr = await Adjust.getAttribution(); + final raw = _attributionToDigest(attr); + if (raw.isEmpty) return ''; + return base64Encode(utf8.encode(raw)); + } catch (_) { + return ''; + } + } + + /// 仅 Google Play Install Referrer 字符串,用于 /v1/user/referrer accolade=gg + static Future getGgReferrerDigest() async { + if (defaultTargetPlatform != TargetPlatform.android) return ''; + try { + final details = await PlayInstallReferrer.installReferrer; + return details.installReferrer ?? ''; + } catch (_) { + return ''; + } + } } diff --git a/lib/core/user/user_state.dart b/lib/core/user/user_state.dart index def7187..be8d3e6 100644 --- a/lib/core/user/user_state.dart +++ b/lib/core/user/user_state.dart @@ -21,6 +21,13 @@ class UserState { /// extConfig.items 图片列表(来自 common_info surge.items) static final ValueNotifier?> extConfigItems = ValueNotifier?>(null); + /// 递增后 Home 页应完整重走「加载分类 → 加载任务」流程(如 need_wait 变化) + static final ValueNotifier homeReloadNonce = ValueNotifier(0); + + static void requestHomeFullReload() { + homeReloadNonce.value = homeReloadNonce.value + 1; + } + static void setCredits(int? value) { credits.value = value; } diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index 60fcece..57bfd9b 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -39,6 +39,7 @@ class _HomeScreenState extends State { UserState.needShowVideoMenu.addListener(_onExtConfigChanged); UserState.extConfigItems.addListener(_onExtConfigChanged); AuthService.isLoginComplete.addListener(_onExtConfigChanged); + UserState.homeReloadNonce.addListener(_onHomeReloadNonce); _loadCategories(); if (widget.isActive) refreshAccount(); } @@ -48,6 +49,7 @@ class _HomeScreenState extends State { UserState.needShowVideoMenu.removeListener(_onExtConfigChanged); UserState.extConfigItems.removeListener(_onExtConfigChanged); AuthService.isLoginComplete.removeListener(_onExtConfigChanged); + UserState.homeReloadNonce.removeListener(_onHomeReloadNonce); super.dispose(); } @@ -55,6 +57,11 @@ class _HomeScreenState extends State { if (mounted) setState(() {}); } + void _onHomeReloadNonce() { + if (!mounted) return; + _loadCategories(); + } + @override void didUpdateWidget(covariant HomeScreen oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/pubspec.yaml b/pubspec.yaml index b962d6c..7bcc418 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: flutter: sdk: flutter + collection: ^1.19.0 adjust_sdk: ^5.5.1 facebook_app_events: ^0.26.0 cupertino_icons: ^1.0.6