优化:增加gg归因上报
This commit is contained in:
parent
d766f9c45e
commit
4e90e6f030
69
docs/app_startup.md
Normal file
69
docs/app_startup.md
Normal 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 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)。
|
||||
@ -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)。
|
||||
|
||||
@ -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';
|
||||
|
||||
/// 生产环境域名
|
||||
|
||||
@ -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_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<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_adjust、gg(均等待服务端返回,成败都继续);结束后统一拉 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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,4 +111,27 @@ class ReferrerService {
|
||||
static Future<void> init() async {
|
||||
await getReferrer();
|
||||
}
|
||||
|
||||
/// 仅 Adjust 归因 digest(Base64 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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user