From eda39b982dd0c984cfc5de3a4e33043f898c108f Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 25 Feb 2026 09:09:37 -0800 Subject: [PATCH] =?UTF-8?q?fix(mobile-app):=20=E4=BF=AE=E5=A4=8D=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=AA=97=E5=8F=A3=E6=9C=9F=20NotificationBadge=20?= =?UTF-8?q?=E6=B7=B7=E8=B4=A6=E5=8F=B7=E8=AF=B7=E6=B1=82=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题(通过日志发现): 账号切换时存在一个 storage 空窗期(旧数据已清除、新数据尚未恢复完成)。 在此期间,NotificationBadgeNotifier 的 30s 定时器恰好触发,导致: - _loadUnreadCount() 从 authProvider 内存读到旧账号 userSerialNum - HTTP interceptor 从 storage 读到已恢复的新账号 accessToken - 发出混账号请求:?userSerialNum=旧账号 + Authorization: Bearer 新账号token 日志证据: _restoreAccountData() 执行期间出现 GET /notifications/unread-count?userSerialNum=D26022600000 Authorization: Bearer [D26022600001的token] 修复: 1. notification_badge_provider.dart 新增 stopAutoRefresh() 公开方法,取消 30s 定时器而不 dispose, Provider invalidate 重建后会自动重启定时器。 2. account_switch_page.dart - _switchToAccount 在 onBeforeRestore 中补加: ref.read(notificationBadgeProvider.notifier).stopAutoRefresh() 确保切换空窗期内 notificationBadge 定时器不触发。 同时移除 UI 层冗余的 saveCurrentAccountData() 调用—— switchToAccount() 内部已有此步骤,无需重复。 日志步骤从 [1/6]...[6/6] 更新为 [1/5]...[5/5], 并在 onBeforeRestore 注释中说明停止各定时器的原因。 切换空窗期现在所有定时器均已停止: ✓ walletStatusProvider (60s) ✓ pendingActionPollingService (4s) ✓ notificationBadgeProvider (30s) ← 本次新增 ✓ TelemetryUploader (30s) Co-Authored-By: Claude Opus 4.6 --- .../notification_badge_provider.dart | 9 +++++ .../pages/account_switch_page.dart | 35 +++++++++++-------- 2 files changed, 29 insertions(+), 15 deletions(-) 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) {