diff --git a/frontend/mobile-app/lib/core/providers/notification_badge_provider.dart b/frontend/mobile-app/lib/core/providers/notification_badge_provider.dart index e8eab35e..120e23d7 100644 --- a/frontend/mobile-app/lib/core/providers/notification_badge_provider.dart +++ b/frontend/mobile-app/lib/core/providers/notification_badge_provider.dart @@ -70,6 +70,15 @@ class NotificationBadgeNotifier extends StateNotifier const Duration(seconds: _refreshIntervalSeconds), (_) => _loadUnreadCount(), ); + debugPrint('[NotificationBadge] 自动刷新已启动 (间隔: ${_refreshIntervalSeconds}s)'); + } + + /// 停止自动刷新定时器(账号切换时调用,防止定时器在切换窗口期触发混账号请求) + /// 调用后不会立即 dispose,invalidate Provider 时会重建并自动重启 + void stopAutoRefresh() { + _refreshTimer?.cancel(); + _refreshTimer = null; + debugPrint('[NotificationBadge] 自动刷新已停止(切换账号)'); } /// 加载未读通知数量 diff --git a/frontend/mobile-app/lib/features/account/presentation/pages/account_switch_page.dart b/frontend/mobile-app/lib/features/account/presentation/pages/account_switch_page.dart index ef761802..a54367c8 100644 --- a/frontend/mobile-app/lib/features/account/presentation/pages/account_switch_page.dart +++ b/frontend/mobile-app/lib/features/account/presentation/pages/account_switch_page.dart @@ -72,51 +72,56 @@ class _AccountSwitchPageState extends ConsumerState { try { final multiAccountService = ref.read(multiAccountServiceProvider); - // 保存当前账号数据 - debugPrint('$_tag [1/6] 保存当前账号数据...'); - await multiAccountService.saveCurrentAccountData(); - // 切换到新账号 - // onBeforeRestore: storage 已清空、新数据尚未恢复 → 只停定时器 + // switchToAccount() 内部会先调用 saveCurrentAccountData(),无需在此重复调用 + // onBeforeRestore: storage 已清空、新数据尚未恢复 → 只停定时器,禁止发起任何 API 请求 // Provider invalidate 必须在 switchToAccount 返回后做(此时 storage 已恢复新账号数据) - debugPrint('$_tag [2/6] 调用 switchToAccount()...'); + debugPrint('$_tag [1/5] 调用 switchToAccount()...'); final success = await multiAccountService.switchToAccount( account.userSerialNum, onBeforeRestore: () async { - // ===== 1. 停止所有用户相关的定时任务 ===== - debugPrint('$_tag [3/6] onBeforeRestore - 停止定时器...'); + // ===== 停止所有用户相关的定时任务 ===== + // 必须在此阶段停止,防止定时器在 storage 空窗期(clear 完成、restore 未完成) + // 触发混账号请求(旧账号 userSerialNum + 新账号 token) + debugPrint('$_tag [2/5] onBeforeRestore - 停止全部定时器...'); ref.read(walletStatusProvider.notifier).stopPolling(); debugPrint('$_tag ✓ walletStatusProvider 轮询已停止'); ref.read(pendingActionPollingServiceProvider).stop(); debugPrint('$_tag ✓ pendingActionPollingService 已停止'); + // NotificationBadge 30s 定时器:必须停止,否则在 restore 过程中触发, + // 读取 authProvider 内存中的旧 userSerialNum + 已恢复的新 token,产生混账号请求 + ref.read(notificationBadgeProvider.notifier).stopAutoRefresh(); + debugPrint('$_tag ✓ notificationBadgeProvider 自动刷新已停止'); // 遥测:清空旧账号事件队列、停止上传 if (TelemetryService().isInitialized) { await TelemetryService().pauseForLogout(); debugPrint('$_tag ✓ TelemetryService 已暂停'); } - debugPrint('$_tag [3/6] onBeforeRestore - 定时器全部停止'); + debugPrint('$_tag [2/5] onBeforeRestore - 定时器全部停止'); }, ); if (success && mounted) { - // ===== 2. invalidate 所有账号相关 Provider(此时 storage 已是新账号数据)===== - debugPrint('$_tag [4/6] switchToAccount 成功,invalidate Provider...'); + // ===== invalidate 所有账号相关 Provider(此时 storage 已是新账号数据)===== + // 必须在 switchToAccount 返回后执行,确保 storage 已完整恢复, + // Provider 重建时读到的是新账号数据 + debugPrint('$_tag [3/5] switchToAccount 成功,invalidate Provider...'); ref.invalidate(authProvider); debugPrint('$_tag ✓ authProvider invalidated'); ref.invalidate(walletStatusProvider); debugPrint('$_tag ✓ walletStatusProvider invalidated'); ref.invalidate(notificationBadgeProvider); - debugPrint('$_tag ✓ notificationBadgeProvider invalidated'); + debugPrint('$_tag ✓ notificationBadgeProvider invalidated(将重建并自动重启定时器)'); - // ===== 3. 恢复遥测上传(新账号上下文)===== - debugPrint('$_tag [5/6] 恢复遥测上传...'); + // ===== 恢复遥测上传(新账号上下文)===== + debugPrint('$_tag [4/5] 恢复遥测上传...'); if (TelemetryService().isInitialized) { TelemetryService().resumeAfterLogin(); debugPrint('$_tag ✓ TelemetryService 已恢复'); } // 切换成功,跳转到主页刷新状态 - debugPrint('$_tag [6/6] 导航到 ranking 页面'); + debugPrint('$_tag [5/5] 导航到 ranking 页面'); debugPrint('$_tag ========== 切换账号完成 =========='); context.go(RoutePaths.ranking); } else if (mounted) {