Commit ad19996609a85d5760a5195125dfec64edd9fe76

Authored by 权海
1 parent cf6c3a4f

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

(cherry picked from commit 8556a032)

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