From 79d4e32e3e5cab4d72393f32b503f977baa15c98 Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 30 Mar 2026 14:29:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E9=80=9F=E5=BA=A6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 2 +- .../kotlin/com/petsheroai/app/MainActivity.kt | 6 +- .../com/petsheroai/app/PetsHeroApplication.kt | 15 ++++ lib/app.dart | 90 ++++++++++++++++--- lib/core/api/api_config.dart | 3 + lib/core/api/proxy_client.dart | 22 +++-- lib/core/auth/auth_service.dart | 16 ++-- lib/core/referrer/referrer_service.dart | 14 ++- lib/main.dart | 4 +- 9 files changed, 139 insertions(+), 33 deletions(-) create mode 100644 android/app/src/main/kotlin/com/petsheroai/app/PetsHeroApplication.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 205d0d8..5b81ffb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); // 冷启动时进程已在 resumed,didChangeAppLifecycleState 往往收不到「变为 resumed」,需首帧后再手动打一次 WidgetsBinding.instance.addPostFrameCallback((_) { + // 去掉原生启动屏(黑底+Logo),进入 Flutter;之后由登录遮罩承接等待提示 + FlutterNativeSplash.remove(); _reportFacebookActivateApp('first_frame'); }); } @@ -96,18 +101,7 @@ class _AppState extends State with WidgetsBindingObserver { if (snapshot.connectionState == ConnectionState.done) { return const SizedBox.shrink(); } - return Positioned.fill( - child: AbsorbPointer( - child: Container( - color: Colors.black.withValues(alpha: 0.2), - child: const Center( - child: CircularProgressIndicator( - color: AppColors.primary, - ), - ), - ), - ), - ); + return const _StartupLoginOverlay(); }, ), ], @@ -144,6 +138,78 @@ class _AppState extends State with WidgetsBindingObserver { } } +/// 启动阶段等待 [AuthService.loginComplete]:久等时提示网络/服务端问题,避免用户误以为「死机」。 +class _StartupLoginOverlay extends StatefulWidget { + const _StartupLoginOverlay(); + + @override + State<_StartupLoginOverlay> createState() => _StartupLoginOverlayState(); +} + +class _StartupLoginOverlayState extends State<_StartupLoginOverlay> { + static const _longWaitAfter = Duration(seconds: 22); + Timer? _longWaitTimer; + bool _showNetworkHint = false; + + @override + void initState() { + super.initState(); + _longWaitTimer = Timer(_longWaitAfter, () { + if (mounted) setState(() => _showNetworkHint = true); + }); + } + + @override + void dispose() { + _longWaitTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: AbsorbPointer( + child: Container( + color: Colors.black.withValues(alpha: 0.22), + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(color: AppColors.primary), + if (_showNetworkHint) ...[ + const SizedBox(height: 22), + Text( + '网络较慢或暂时无法连接', + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.surface.withValues(alpha: 0.96), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 10), + Text( + '请检查 Wi‑Fi 或移动数据,稍候片刻。若网络正常,可能是服务器繁忙,请过一会再试。', + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.surface.withValues(alpha: 0.78), + fontSize: 14, + height: 1.4, + ), + ), + ], + ], + ), + ), + ), + ), + ), + ); + } +} + class _MainScaffold extends StatefulWidget { const _MainScaffold({ required this.currentTab, diff --git a/lib/core/api/api_config.dart b/lib/core/api/api_config.dart index 6783675..9a37a3b 100644 --- a/lib/core/api/api_config.dart +++ b/lib/core/api/api_config.dart @@ -37,4 +37,7 @@ abstract final class ApiConfig { /// 代理入口完整 URL static String get proxyUrl => '$baseUrl$proxyPath'; + + /// 单次 HTTP 请求超时(避免弱网/服务端无响应时永久卡住启动流程) + static const Duration httpRequestTimeout = Duration(seconds: 20); } diff --git a/lib/core/api/proxy_client.dart b/lib/core/api/proxy_client.dart index dbca292..f1a6c60 100644 --- a/lib/core/api/proxy_client.dart +++ b/lib/core/api/proxy_client.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; @@ -282,11 +283,22 @@ class ProxyClient { final url = '$_baseUrl${ApiConfig.proxyPath}'; _log('真实请求URL: $url'); - final response = await http.post( - Uri.parse(url), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode(proxyBody), - ); + http.Response response; + try { + response = await http + .post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(proxyBody), + ) + .timeout(ApiConfig.httpRequestTimeout); + } on TimeoutException { + _proxyLog.d('HTTP 超时 (${ApiConfig.httpRequestTimeout.inSeconds}s): $path'); + return ApiResponse( + code: -1, + msg: 'Request timed out. Check your network and try again.', + ); + } return _parseResponse(response); } diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart index cc73f1c..10d7786 100644 --- a/lib/core/auth/auth_service.dart +++ b/lib/core/auth/auth_service.dart @@ -290,12 +290,8 @@ class AuthService { UserState.setNavigate(countryCode); } - // 3. 归因:android_adjust、gg 各上报一次(均等待响应);再拉一次 common_info,extConfig 影响首页时重载分类/任务 - try { - await _reportBothReferrersAndRefreshCommonInfo(uid!, deviceId); - } catch (e) { - _logMsg('referrer/common_info 流程异常: $e'); - } + // 3. 归因与 common_info:后台执行,不阻塞 loginComplete(避免服务端慢/挂起导致一直卡在启动遮罩) + unawaited(_runPostLoginReferrerWork(uid!, deviceId)); } else { _logMsg('init: 登录失败'); } @@ -371,4 +367,12 @@ class AuthService { 'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}'); } } + + static Future _runPostLoginReferrerWork(String uid, String deviceId) async { + try { + await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId); + } catch (e, _) { + _logMsg('referrer/common_info 后台任务失败: $e'); + } + } } diff --git a/lib/core/referrer/referrer_service.dart b/lib/core/referrer/referrer_service.dart index a61968b..86ab86f 100644 --- a/lib/core/referrer/referrer_service.dart +++ b/lib/core/referrer/referrer_service.dart @@ -21,6 +21,12 @@ class ReferrerService { /// 过长会拖慢 [AuthService.init] 内首次 getReferrer;过短可能拿不到 Adjust,仍有 Play Install Referrer fallback static const int _adjustTimeoutMs = 4500; + /// 与 [Future.any] 外层兜底,避免回调 Future 永不完成时永远挂起 + static const Duration _adjustRaceTimeout = Duration(seconds: 6); + + /// Play Install Referrer 读取超时(部分机型/系统上可能极慢) + static const Duration _playInstallReferrerTimeout = Duration(seconds: 10); + /// 由 Adjust attributionCallback 调用,首次安装时归因往往通过此回调返回(晚于 getAttributionWithTimeout) static void receiveAttributionFromCallback(AdjustAttribution attribution) { if (!_attributionCallbackCompleter.isCompleted) { @@ -79,7 +85,7 @@ class ReferrerService { } })(), _attributionCallbackCompleter.future, - ]); + ]).timeout(_adjustRaceTimeout, onTimeout: () => null); if (attribution != null) { final raw = _attributionToDigest(attribution); if (raw.isNotEmpty) { @@ -95,7 +101,8 @@ class ReferrerService { _referrerSource = 'gg'; if (defaultTargetPlatform == TargetPlatform.android) { try { - final details = await PlayInstallReferrer.installReferrer; + final details = await PlayInstallReferrer.installReferrer + .timeout(_playInstallReferrerTimeout); digest = details.installReferrer ?? ''; } catch (_) { digest = ''; @@ -129,7 +136,8 @@ class ReferrerService { static Future getGgReferrerDigest() async { if (defaultTargetPlatform != TargetPlatform.android) return ''; try { - final details = await PlayInstallReferrer.installReferrer; + final details = await PlayInstallReferrer.installReferrer + .timeout(_playInstallReferrerTimeout); return details.installReferrer ?? ''; } catch (_) { return ''; diff --git a/lib/main.dart b/lib/main.dart index 2bedb6a..61afbc7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,9 +24,9 @@ void main() async { statusBarBrightness: Brightness.light, ), ); - // Adjust 初始化后执行登录,确保登录时归因数据已就绪 - AuthService.init(); + // 先进入 Flutter 首帧,尽快结束原生启动屏;登录与内购监听紧随其后 runApp(const App()); + AuthService.init(); // 尽早订阅 purchaseStream,否则未确认订单不会出现在 queryPastPurchases 中,补单会为空 if (defaultTargetPlatform == TargetPlatform.android) { GooglePlayPurchaseService.startPendingPurchaseListener();