Commit 8556a0326b7109f713bdb5c0f852062117d89024

Authored by 权海
1 parent f027d9ea

feat(ui):修复登录卡住的Bug

@@ -10,10 +10,7 @@ import 'package:doublefeel_flutter/core/constants/app_const.dart'; @@ -10,10 +10,7 @@ import 'package:doublefeel_flutter/core/constants/app_const.dart';
10 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 10 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
11 import 'package:doublefeel_flutter/core/services/app_config_service.dart'; 11 import 'package:doublefeel_flutter/core/services/app_config_service.dart';
12 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; 12 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
13 -import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';  
14 -import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart';  
15 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 13 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
16 -import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';  
17 import 'package:doublefeel_flutter/pigeon/wechat_api.g.dart'; 14 import 'package:doublefeel_flutter/pigeon/wechat_api.g.dart';
18 import 'package:flutter/services.dart'; 15 import 'package:flutter/services.dart';
19 import 'package:flutter/widgets.dart'; 16 import 'package:flutter/widgets.dart';
@@ -23,23 +20,14 @@ import 'package:doublefeel_flutter/core/network/api/user_api.dart'; @@ -23,23 +20,14 @@ import 'package:doublefeel_flutter/core/network/api/user_api.dart';
23 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 20 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
24 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 21 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
25 import 'package:doublefeel_flutter/core/result/app_result.dart'; 22 import 'package:doublefeel_flutter/core/result/app_result.dart';
26 -import 'package:doublefeel_flutter/core/services/app_config_service.dart';  
27 import 'package:doublefeel_flutter/core/services/loading_service.dart'; 23 import 'package:doublefeel_flutter/core/services/loading_service.dart';
28 -import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';  
29 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 24 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
30 -import 'package:doublefeel_flutter/core/util/app_toast.dart';  
31 import 'package:doublefeel_flutter/data/local/local_storage.dart'; 25 import 'package:doublefeel_flutter/data/local/local_storage.dart';
32 import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; 26 import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
33 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 27 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
34 -import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';  
35 import 'package:doublefeel_flutter/data/models/user/user_models.dart'; 28 import 'package:doublefeel_flutter/data/models/user/user_models.dart';
36 import 'package:doublefeel_flutter/data/models/vip/vip_info.dart'; 29 import 'package:doublefeel_flutter/data/models/vip/vip_info.dart';
37 -import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';  
38 -import 'package:doublefeel_flutter/pigeon/wechat_api.g.dart';  
39 -import 'package:flutter/services.dart';  
40 -import 'package:flutter/widgets.dart';  
41 import 'package:flutter_timezone/flutter_timezone.dart'; 30 import 'package:flutter_timezone/flutter_timezone.dart';
42 -import 'package:get/get.dart';  
43 31
44 class LoginController extends GetxController { 32 class LoginController extends GetxController {
45 final UserApi _userApi = Get.find<UserApi>(); 33 final UserApi _userApi = Get.find<UserApi>();
@@ -381,21 +369,39 @@ class LoginController extends GetxController { @@ -381,21 +369,39 @@ class LoginController extends GetxController {
381 } 369 }
382 370
383 isLoggingIn.value = true; 371 isLoggingIn.value = true;
  372 + final loginStopwatch = Stopwatch()..start();
  373 + _logLoginTiming('phone_login.start');
384 try { 374 try {
385 await LoadingService.instance.run(() async { 375 await LoadingService.instance.run(() async {
386 - final loginRes = await _userApi.login(  
387 - telephone: cleanPhone,  
388 - verifyCode: codeInput.value, 376 + final loginRes = await _measureLoginStep(
  377 + 'phone_login.api',
  378 + () => _userApi.login(
  379 + telephone: cleanPhone,
  380 + verifyCode: codeInput.value,
  381 + ),
389 ); 382 );
390 383
391 if (loginRes is AppSuccess<LoginResponse>) { 384 if (loginRes is AppSuccess<LoginResponse>) {
392 - await _handleLoginSuccess(loginRes.data);  
393 - await Get.find<LocalStorage>().setLastLoginMethod('phone');  
394 - await environmentConfig.updateHideChooseRegion(true); 385 + await _measureLoginStep(
  386 + 'phone_login.handle_success',
  387 + () => _handleLoginSuccess(loginRes.data),
  388 + );
  389 + await _measureLoginStep(
  390 + 'phone_login.persist_login_method',
  391 + () => Get.find<LocalStorage>().setLastLoginMethod('phone'),
  392 + );
  393 + await _measureLoginStep(
  394 + 'phone_login.persist_region_choice',
  395 + () => environmentConfig.updateHideChooseRegion(true),
  396 + );
395 } 397 }
396 }); 398 });
397 } finally { 399 } finally {
398 isLoggingIn.value = false; 400 isLoggingIn.value = false;
  401 + loginStopwatch.stop();
  402 + _logLoginTiming(
  403 + 'phone_login.finish total_ms=${loginStopwatch.elapsedMilliseconds}',
  404 + );
399 } 405 }
400 } 406 }
401 407
@@ -595,7 +601,10 @@ class LoginController extends GetxController { @@ -595,7 +601,10 @@ class LoginController extends GetxController {
595 } 601 }
596 } 602 }
597 603
598 - final regRes = await _userApi.register(registerData); 604 + final regRes = await _measureLoginStep(
  605 + 'login_success.register',
  606 + () => _userApi.register(registerData),
  607 + );
599 if (regRes is AppSuccess<RegisterResponse>) { 608 if (regRes is AppSuccess<RegisterResponse>) {
600 accessToken = regRes.data.accessToken; 609 accessToken = regRes.data.accessToken;
601 } else { 610 } else {
@@ -605,12 +614,21 @@ class LoginController extends GetxController { @@ -605,12 +614,21 @@ class LoginController extends GetxController {
605 } 614 }
606 615
607 // Fetch user info and VIP info 616 // Fetch user info and VIP info
608 - final userInfoResFuture = _userApi.getUserInfo(accessToken: accessToken); 617 + final userInfoResFuture = _measureLoginStep(
  618 + 'login_success.get_user_info',
  619 + () => _userApi.getUserInfo(accessToken: accessToken),
  620 + );
609 final vipInfoResFuture = isRegister 621 final vipInfoResFuture = isRegister
610 ? Future.value(null) 622 ? Future.value(null)
611 - : _vipApi.getVipInfo(accessToken: accessToken); 623 + : _measureLoginStep(
  624 + 'login_success.get_vip_info',
  625 + () => _vipApi.getVipInfo(accessToken: accessToken),
  626 + );
612 627
613 - final results = await Future.wait([userInfoResFuture, vipInfoResFuture]); 628 + final results = await _measureLoginStep(
  629 + 'login_success.wait_user_and_vip',
  630 + () => Future.wait([userInfoResFuture, vipInfoResFuture]),
  631 + );
614 final userInfoRes = results[0]; 632 final userInfoRes = results[0];
615 final vipInfoRes = results[1]; 633 final vipInfoRes = results[1];
616 634
@@ -631,51 +649,36 @@ class LoginController extends GetxController { @@ -631,51 +649,36 @@ class LoginController extends GetxController {
631 vip = vipInfoRes.data; 649 vip = vipInfoRes.data;
632 } 650 }
633 651
634 - await _userPrefs.updateFromLogin(  
635 - accessToken: accessToken,  
636 - me: me,  
637 - partner: partner,  
638 - vip: vip, 652 + await _measureLoginStep(
  653 + 'login_success.persist_user_preferences',
  654 + () => _userPrefs.updateFromLogin(
  655 + accessToken: accessToken,
  656 + me: me,
  657 + partner: partner,
  658 + vip: vip,
  659 + ),
639 ); 660 );
640 661
641 - try {  
642 - if (Get.isRegistered<AppEnvironmentConfig>()) {  
643 - var env = Get.find<AppEnvironmentConfig>();  
644 - unawaited(  
645 - AppPlatformHostApi()  
646 - .updateLoginInfo(  
647 - jsonEncode(UserPreferences(  
648 - accessToken: accessToken,  
649 - meUserInfo: me,  
650 - partnerUserInfo: partner,  
651 - vipInfo: vip != null  
652 - ? UserPreferencesVipInfo.fromVipInfo(vip)  
653 - : null,  
654 - )),  
655 - env.serverBaseUrl,  
656 - )  
657 - .catchError((Object error, StackTrace stackTrace) {  
658 - AppLogger.e('Native login info sync failed', error, stackTrace);  
659 - }),  
660 - );  
661 - }  
662 - } on Exception catch (e) {  
663 - AppLogger.e(e);  
664 - }  
665 -  
666 - await _userStateService.onLogin(); 662 + await _measureLoginStep(
  663 + 'login_success.user_state_on_login',
  664 + _userStateService.onLogin,
  665 + );
667 if (isRegister) { 666 if (isRegister) {
668 ta.track('success_regist'); 667 ta.track('success_regist');
669 } 668 }
670 669
671 // 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用 670 // 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用
672 - await Get.find<LocalStorage>().setTermsAgreed(true); 671 + await _measureLoginStep(
  672 + 'login_success.persist_terms_agreed',
  673 + () => Get.find<LocalStorage>().setTermsAgreed(true),
  674 + );
673 675
674 // 根据引导完成状态决定跳转目标 676 // 根据引导完成状态决定跳转目标
675 final userId = me.id ?? 0; 677 final userId = me.id ?? 0;
676 if (_userAccount.hasCompletedOnboarding(userId) || 678 if (_userAccount.hasCompletedOnboarding(userId) ||
677 me.isPassNoviceGuide == 1) { 679 me.isPassNoviceGuide == 1) {
678 // 该账号已完成引导,直接进主页 680 // 该账号已完成引导,直接进主页
  681 + _logLoginTiming('login_success.route_home');
679 Get.offAllNamed(AppRoutes.home); 682 Get.offAllNamed(AppRoutes.home);
680 } else { 683 } else {
681 // 新用户或未完成引导,进入引导页(支持断点续做) 684 // 新用户或未完成引导,进入引导页(支持断点续做)
@@ -689,4 +692,30 @@ class LoginController extends GetxController { @@ -689,4 +692,30 @@ class LoginController extends GetxController {
689 // userInfoRes 失败,外层 finally 会重置 isLoggingIn 692 // userInfoRes 失败,外层 finally 会重置 isLoggingIn
690 } 693 }
691 } 694 }
  695 +
  696 + Future<T> _measureLoginStep<T>(
  697 + String name,
  698 + Future<T> Function() operation,
  699 + ) async {
  700 + if (!environmentConfig.isDebug) return operation();
  701 + final stopwatch = Stopwatch()..start();
  702 + AppLogger.i('[LoginTiming] $name.start');
  703 + try {
  704 + return await operation();
  705 + } catch (error, stackTrace) {
  706 + AppLogger.e('[LoginTiming] $name.error', error, stackTrace);
  707 + rethrow;
  708 + } finally {
  709 + stopwatch.stop();
  710 + AppLogger.i(
  711 + '[LoginTiming] $name.finish elapsed_ms=${stopwatch.elapsedMilliseconds}',
  712 + );
  713 + }
  714 + }
  715 +
  716 + void _logLoginTiming(String message) {
  717 + if (environmentConfig.isDebug) {
  718 + AppLogger.i('[LoginTiming] $message');
  719 + }
  720 + }
692 } 721 }
@@ -50,18 +50,85 @@ class UserStateService { @@ -50,18 +50,85 @@ class UserStateService {
50 } 50 }
51 51
52 Future<void> onLogin() async { 52 Future<void> onLogin() async {
53 - await initializeSdks(); 53 + final totalStopwatch = Stopwatch()..start();
  54 + _logLoginTiming('on_login.start');
  55 + await _measureLoginStep('on_login.initialize_sdks', initializeSdks);
54 _trackLogin(); 56 _trackLogin();
55 _setUserProfile(); 57 _setUserProfile();
56 - // The WebSocket handler can immediately start a calculation. Ensure its  
57 - // local result store is ready before subscribing and connecting.  
58 - await _healthRawDataCoreService.openDatabase(); 58 + // Opening/migrating the local health SQLite database can contend with the
  59 + // native uploader. Do not make the login route wait for it. The dependent
  60 + // realtime session is still started only after the database is ready.
  61 + unawaited(_startRealtimeSessionAfterHealthDatabaseReady(totalStopwatch));
  62 + _logLoginTiming('on_login.database_initialization_dispatched');
  63 + }
  64 +
  65 + Future<void> _startRealtimeSessionAfterHealthDatabaseReady(
  66 + Stopwatch totalStopwatch,
  67 + ) async {
  68 + try {
  69 + await _measureLoginStep(
  70 + 'on_login.open_health_database',
  71 + _healthRawDataCoreService.openDatabase,
  72 + );
  73 + } catch (error, stackTrace) {
  74 + AppLogger.e(
  75 + '[LoginTiming] on_login.open_health_database.failed',
  76 + error,
  77 + stackTrace,
  78 + );
  79 + return;
  80 + }
  81 +
  82 + // The user may have logged out while an existing database lock cleared.
  83 + if (!_userPrefs.isLoggedIn) {
  84 + _logLoginTiming('on_login.realtime_session_skipped_logged_out');
  85 + return;
  86 + }
  87 +
59 // Push Kit may wait for its service/network. The token is still reported 88 // Push Kit may wait for its service/network. The token is still reported
60 // after login, but must not delay the rest of the authenticated session. 89 // after login, but must not delay the rest of the authenticated session.
61 unawaited(_pushService.registerPushToken()); 90 unawaited(_pushService.registerPushToken());
62 - await _imService.connect(); 91 + try {
  92 + await _measureLoginStep('on_login.im_connect', _imService.connect);
  93 + } catch (error, stackTrace) {
  94 + AppLogger.e(
  95 + '[LoginTiming] on_login.im_connect.failed', error, stackTrace);
  96 + }
  97 + if (!_userPrefs.isLoggedIn) {
  98 + _logLoginTiming('on_login.realtime_session_stopped_logged_out');
  99 + return;
  100 + }
63 _listenForHealthDataUpdates(); 101 _listenForHealthDataUpdates();
64 _webSocketService.start(); 102 _webSocketService.start();
  103 + totalStopwatch.stop();
  104 + _logLoginTiming(
  105 + 'on_login.finish total_ms=${totalStopwatch.elapsedMilliseconds}');
  106 + }
  107 +
  108 + Future<T> _measureLoginStep<T>(
  109 + String name,
  110 + Future<T> Function() operation,
  111 + ) async {
  112 + if (!_environmentConfig.isDebug) return operation();
  113 + final stopwatch = Stopwatch()..start();
  114 + AppLogger.i('[LoginTiming] $name.start');
  115 + try {
  116 + return await operation();
  117 + } catch (error, stackTrace) {
  118 + AppLogger.e('[LoginTiming] $name.error', error, stackTrace);
  119 + rethrow;
  120 + } finally {
  121 + stopwatch.stop();
  122 + AppLogger.i(
  123 + '[LoginTiming] $name.finish elapsed_ms=${stopwatch.elapsedMilliseconds}',
  124 + );
  125 + }
  126 + }
  127 +
  128 + void _logLoginTiming(String message) {
  129 + if (_environmentConfig.isDebug) {
  130 + AppLogger.i('[LoginTiming] $message');
  131 + }
65 } 132 }
66 133
67 Future<void> onLogout({bool callServerLogout = true}) async { 134 Future<void> onLogout({bool callServerLogout = true}) async {
@@ -65,26 +65,77 @@ class UserPreferencesStorage { @@ -65,26 +65,77 @@ class UserPreferencesStorage {
65 65
66 Future<void> _syncLoginInfoToNative( 66 Future<void> _syncLoginInfoToNative(
67 {required UserPreferences userInfo}) async { 67 {required UserPreferences userInfo}) async {
  68 + final totalStopwatch = Stopwatch()..start();
  69 + _logLoginTiming('native_login_sync.start');
68 if (userInfo.accessToken.isEmpty) { 70 if (userInfo.accessToken.isEmpty) {
69 try { 71 try {
70 if (Get.isRegistered<AppEnvironmentConfig>()) { 72 if (Get.isRegistered<AppEnvironmentConfig>()) {
71 - await AppPlatformHostApi().logout(); 73 + await _measureLoginStep(
  74 + 'native_login_sync.logout',
  75 + () => AppPlatformHostApi().logout(),
  76 + );
72 } 77 }
73 } on Exception { 78 } on Exception {
74 return; 79 return;
  80 + } finally {
  81 + totalStopwatch.stop();
  82 + _logLoginTiming(
  83 + 'native_login_sync.finish total_ms=${totalStopwatch.elapsedMilliseconds}',
  84 + );
75 } 85 }
76 return; 86 return;
77 } 87 }
78 try { 88 try {
79 if (Get.isRegistered<AppEnvironmentConfig>()) { 89 if (Get.isRegistered<AppEnvironmentConfig>()) {
80 var env = Get.find<AppEnvironmentConfig>(); 90 var env = Get.find<AppEnvironmentConfig>();
81 - await AppPlatformHostApi()  
82 - .updateLoginInfo(jsonEncode(userInfo), env.serverBaseUrl);  
83 - await _syncHealthRawDatabaseToNative(); 91 + await _measureLoginStep(
  92 + 'native_login_sync.update_login_info',
  93 + () => AppPlatformHostApi()
  94 + .updateLoginInfo(jsonEncode(userInfo), env.serverBaseUrl),
  95 + );
  96 + await _measureLoginStep(
  97 + 'native_login_sync.sync_health_database',
  98 + _syncHealthRawDatabaseToNative,
  99 + );
84 } 100 }
85 AppLogger.d('Native login info synced for cold start'); 101 AppLogger.d('Native login info synced for cold start');
86 } catch (error, stackTrace) { 102 } catch (error, stackTrace) {
87 AppLogger.e('Native login info sync failed', error, stackTrace); 103 AppLogger.e('Native login info sync failed', error, stackTrace);
  104 + } finally {
  105 + totalStopwatch.stop();
  106 + _logLoginTiming(
  107 + 'native_login_sync.finish total_ms=${totalStopwatch.elapsedMilliseconds}',
  108 + );
  109 + }
  110 + }
  111 +
  112 + bool get _isDebug =>
  113 + Get.isRegistered<AppEnvironmentConfig>() &&
  114 + Get.find<AppEnvironmentConfig>().isDebug;
  115 +
  116 + Future<T> _measureLoginStep<T>(
  117 + String name,
  118 + Future<T> Function() operation,
  119 + ) async {
  120 + if (!_isDebug) return operation();
  121 + final stopwatch = Stopwatch()..start();
  122 + AppLogger.i('[LoginTiming] $name.start');
  123 + try {
  124 + return await operation();
  125 + } catch (error, stackTrace) {
  126 + AppLogger.e('[LoginTiming] $name.error', error, stackTrace);
  127 + rethrow;
  128 + } finally {
  129 + stopwatch.stop();
  130 + AppLogger.i(
  131 + '[LoginTiming] $name.finish elapsed_ms=${stopwatch.elapsedMilliseconds}',
  132 + );
  133 + }
  134 + }
  135 +
  136 + void _logLoginTiming(String message) {
  137 + if (_isDebug) {
  138 + AppLogger.i('[LoginTiming] $message');
88 } 139 }
89 } 140 }
90 141