优化:增加gg归因上报

This commit is contained in:
ivan 2026-03-24 18:45:27 +08:00
parent d766f9c45e
commit 4e90e6f030
8 changed files with 202 additions and 42 deletions

69
docs/app_startup.md Normal file
View File

@ -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 Referrergg**`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)。

View File

@ -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

View File

@ -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';
///

View File

@ -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<dynamic>? items;
factory _HomeExtSnapshot.capture() {
final raw = UserState.extConfigItems.value;
return _HomeExtSnapshot(
UserState.needShowVideoMenu.value,
raw == null ? null : List<dynamic>.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_adjustgg common_infoextConfig /
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<String, dynamic>?;
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<String, dynamic>?;
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_adjustgg common_info extConfig
static Future<void> _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}');
}
}
}

View File

@ -111,4 +111,27 @@ class ReferrerService {
static Future<void> init() async {
await getReferrer();
}
/// Adjust digestBase64 JSON /v1/user/referrer accolade=android_adjust
static Future<String> 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<String> getGgReferrerDigest() async {
if (defaultTargetPlatform != TargetPlatform.android) return '';
try {
final details = await PlayInstallReferrer.installReferrer;
return details.installReferrer ?? '';
} catch (_) {
return '';
}
}
}

View File

@ -21,6 +21,13 @@ class UserState {
/// extConfig.items common_info surge.items
static final ValueNotifier<List<dynamic>?> extConfigItems = ValueNotifier<List<dynamic>?>(null);
/// Home need_wait
static final ValueNotifier<int> homeReloadNonce = ValueNotifier<int>(0);
static void requestHomeFullReload() {
homeReloadNonce.value = homeReloadNonce.value + 1;
}
static void setCredits(int? value) {
credits.value = value;
}

View File

@ -39,6 +39,7 @@ class _HomeScreenState extends State<HomeScreen> {
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<HomeScreen> {
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<HomeScreen> {
if (mounted) setState(() {});
}
void _onHomeReloadNonce() {
if (!mounted) return;
_loadCategories();
}
@override
void didUpdateWidget(covariant HomeScreen oldWidget) {
super.didUpdateWidget(oldWidget);

View File

@ -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