From 8f8a9230d07a8ccbac1648b6be66525fcb5d5d0c Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 25 Feb 2026 06:13:05 -0800 Subject: [PATCH] =?UTF-8?q?fix(mobile-app):=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=88=87=E6=8D=A2=E6=95=B0=E6=8D=AE=E4=B8=B2?= =?UTF-8?q?=E5=8F=B7=E9=97=AE=E9=A2=98=EF=BC=8C=E5=AE=8C=E5=96=84=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E9=9A=94=E7=A6=BB=E4=B8=8E=E7=8A=B6=E6=80=81=E9=87=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: 多账号切换时,前一个账号的推荐码、种植省市、缓存数据等会串到下一个账号, 同时定时器(钱包轮询、通知刷新、遥测上传)未正确停止/重启, 导致旧账号的 API 请求混入新账号上下文。 修复内容: 1. StorageKeys 补充种植省市常量(storage_keys.dart) - 新增 plantingProvinceName/Code、plantingCityName/Code 4个常量 - 将硬编码的 key 统一收口,确保隔离列表引用一致 2. MultiAccountService 补全隔离列表(multi_account_service.dart) - _accountSecureKeys 新增 inviterReferralCode(修复邀请码串号) - _accountLocalKeys 新增 4个种植省市key + cachedAppAssets + cachedCustomerServiceContacts(修复种植/缓存数据串号) - switchToAccount() 新增 onBeforeRestore 回调参数,用于在 storage清空后、恢复新数据前 停止定时器 3. AccountSwitchPage 三层状态重置(account_switch_page.dart) - _switchToAccount: onBeforeRestore 内停止 walletStatus 轮询、 pendingAction 轮询、telemetry 上传;返回后 invalidate authProvider/walletStatusProvider/notificationBadgeProvider, 最后 resumeAfterLogin 恢复遥测 - _addNewAccount: 退出前停止定时器,退出后 invalidate Provider 4. ProfilePage 退出登录补全清理(profile_page.dart) - _performLogout: 退出前停止 walletStatus/pendingAction 轮询, 退出后 invalidate 三个 Provider 5. 页面 key 统一引用 StorageKeys 常量 - planting_location_page.dart 和 authorization_apply_page.dart 将硬编码 key 替换为 StorageKeys.plantingXxx 常量 关键时序(switchToAccount 内部): save → clear → onBeforeRestore(停timer) → restore → 返回 → invalidate Provider(此时storage已恢复新数据)→ resume telemetry → navigate Co-Authored-By: Claude Opus 4.6 --- .../core/services/multi_account_service.dart | 27 +++++++++++-- .../lib/core/storage/storage_keys.dart | 6 +++ .../pages/account_switch_page.dart | 40 ++++++++++++++++--- .../pages/authorization_apply_page.dart | 11 ++--- .../pages/planting_location_page.dart | 11 ++--- .../presentation/pages/profile_page.dart | 10 +++++ 6 files changed, 86 insertions(+), 19 deletions(-) diff --git a/frontend/mobile-app/lib/core/services/multi_account_service.dart b/frontend/mobile-app/lib/core/services/multi_account_service.dart index 2460306e..5da9ed01 100644 --- a/frontend/mobile-app/lib/core/services/multi_account_service.dart +++ b/frontend/mobile-app/lib/core/services/multi_account_service.dart @@ -75,6 +75,7 @@ class MultiAccountService { StorageKeys.avatarUrl, StorageKeys.referralCode, StorageKeys.inviterSequence, + StorageKeys.inviterReferralCode, StorageKeys.isAccountCreated, StorageKeys.phoneNumber, StorageKeys.isPasswordSet, @@ -94,6 +95,14 @@ class MultiAccountService { StorageKeys.lastSyncTime, StorageKeys.cachedRankingData, StorageKeys.cachedMiningStatus, + // 种植省市选择 + StorageKeys.plantingProvinceName, + StorageKeys.plantingProvinceCode, + StorageKeys.plantingCityName, + StorageKeys.plantingCityCode, + // 非敏感缓存(仍需隔离以保证干净环境) + StorageKeys.cachedAppAssets, + StorageKeys.cachedCustomerServiceContacts, ]; /// 获取账号列表 @@ -206,15 +215,20 @@ class MultiAccountService { /// 切换到指定账号 /// 返回 true 表示切换成功 /// + /// [onBeforeRestore] 在清除旧数据之后、恢复新账号数据之前调用, + /// 用于重置内存中的 Provider 状态(如 authProvider、walletStatusProvider 等), + /// 确保不会有前一个账号的内存数据残留。 + /// /// 切换流程: /// 1. 验证目标账号存在 /// 2. 验证目标账号数据完整性 /// 3. 保存当前账号数据到账号专用存储 /// 4. 清除当前存储空间(确保干净环境) + /// 4.5 调用 onBeforeRestore 重置内存状态 /// 5. 从账号专用存储恢复目标账号数据 /// 6. 设置当前账号标记 /// 7. 更新遥测和错误追踪服务的用户信息 - Future switchToAccount(String userSerialNum) async { + Future switchToAccount(String userSerialNum, {Future Function()? onBeforeRestore}) async { debugPrint('$_tag switchToAccount() - 切换到账号: $userSerialNum'); // 1. 验证账号存在于列表中 @@ -243,6 +257,12 @@ class MultiAccountService { // 4. 清除当前存储空间(确保干净环境,避免数据残留) await _clearCurrentAccountData(); + // 4.5 重置内存中的 Provider 状态(避免前账号数据残留在内存中) + if (onBeforeRestore != null) { + await onBeforeRestore(); + debugPrint('$_tag switchToAccount() - 已重置内存状态'); + } + // 5. 从账号专用存储恢复目标账号数据 await _restoreAccountData(userSerialNum); @@ -395,9 +415,8 @@ class MultiAccountService { for (final key in _accountSecureKeys) { await _secureStorage.delete(key: key); } - // 额外清除临时数据(不属于账号专用存储) - await _secureStorage.delete(key: StorageKeys.inviterReferralCode); - debugPrint('$_tag logoutCurrentAccount() - 已清除 ${_accountSecureKeys.length + 1} 个 SecureStorage 键'); + // inviterReferralCode 已包含在 _accountSecureKeys 中,无需额外清除 + debugPrint('$_tag logoutCurrentAccount() - 已清除 ${_accountSecureKeys.length} 个 SecureStorage 键'); // ===== 2. 清除 LocalStorage 中的缓存数据 ===== for (final key in _accountLocalKeys) { diff --git a/frontend/mobile-app/lib/core/storage/storage_keys.dart b/frontend/mobile-app/lib/core/storage/storage_keys.dart index a185aac8..b85a9af3 100644 --- a/frontend/mobile-app/lib/core/storage/storage_keys.dart +++ b/frontend/mobile-app/lib/core/storage/storage_keys.dart @@ -45,6 +45,12 @@ class StorageKeys { static const String deviceId = 'device_id'; static const String deviceName = 'device_name'; + // Planting Location (种植省市选择) + static const String plantingProvinceName = 'planting_province_name'; + static const String plantingProvinceCode = 'planting_province_code'; + static const String plantingCityName = 'planting_city_name'; + static const String plantingCityCode = 'planting_city_code'; + // Cache static const String lastSyncTime = 'last_sync_time'; static const String cachedRankingData = 'cached_ranking_data'; 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 075f7a85..a72d1d8e 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 @@ -5,6 +5,9 @@ import 'package:go_router/go_router.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../core/services/multi_account_service.dart'; import '../../../../core/providers/notification_badge_provider.dart'; +import '../../../../core/telemetry/telemetry_service.dart'; +import '../../../auth/presentation/providers/auth_provider.dart'; +import '../../../auth/presentation/providers/wallet_status_provider.dart'; import '../../../../routes/route_paths.dart'; /// 账号切换页面 @@ -65,11 +68,32 @@ class _AccountSwitchPageState extends ConsumerState { await multiAccountService.saveCurrentAccountData(); // 切换到新账号 - final success = await multiAccountService.switchToAccount(account.userSerialNum); + // onBeforeRestore: storage 已清空、新数据尚未恢复 → 只停定时器 + // Provider invalidate 必须在 switchToAccount 返回后做(此时 storage 已恢复新账号数据) + final success = await multiAccountService.switchToAccount( + account.userSerialNum, + onBeforeRestore: () async { + // ===== 1. 停止所有用户相关的定时任务 ===== + ref.read(walletStatusProvider.notifier).stopPolling(); + ref.read(pendingActionPollingServiceProvider).stop(); + // 遥测:清空旧账号事件队列、停止上传 + if (TelemetryService().isInitialized) { + await TelemetryService().pauseForLogout(); + } + }, + ); if (success && mounted) { - // 刷新新账号的未读通知数量 - ref.read(notificationBadgeProvider.notifier).refresh(); + // ===== 2. invalidate 所有账号相关 Provider(此时 storage 已是新账号数据)===== + ref.invalidate(authProvider); + ref.invalidate(walletStatusProvider); + ref.invalidate(notificationBadgeProvider); + + // ===== 3. 恢复遥测上传(新账号上下文)===== + if (TelemetryService().isInitialized) { + TelemetryService().resumeAfterLogin(); + } + // 切换成功,跳转到主页刷新状态 context.go(RoutePaths.ranking); } else if (mounted) { @@ -102,11 +126,17 @@ class _AccountSwitchPageState extends ConsumerState { // 保存当前账号数据 await multiAccountService.saveCurrentAccountData(); + // ===== 1. 停止所有用户相关的定时任务 ===== + ref.read(walletStatusProvider.notifier).stopPolling(); + ref.read(pendingActionPollingServiceProvider).stop(); + // 退出当前账号但保留数据 await multiAccountService.logoutCurrentAccount(); - // 清空未读通知数量 - ref.read(notificationBadgeProvider.notifier).clearCount(); + // ===== 2. invalidate 所有账号相关 Provider ===== + ref.invalidate(authProvider); + ref.invalidate(walletStatusProvider); + ref.invalidate(notificationBadgeProvider); // 跳转到向导页创建新账号 if (mounted) { diff --git a/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart b/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart index 6733a78b..d042849d 100644 --- a/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart +++ b/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:city_pickers/city_pickers.dart'; import '../../../../core/di/injection_container.dart'; +import '../../../../core/storage/storage_keys.dart'; import '../../../../core/services/authorization_service.dart'; /// 授权类型枚举 @@ -50,11 +51,11 @@ class AuthorizationApplyPage extends ConsumerStatefulWidget { class _AuthorizationApplyPageState extends ConsumerState { - /// 本地存储 key(与认种页面保持一致) - static const String _keyProvinceName = 'planting_province_name'; - static const String _keyProvinceCode = 'planting_province_code'; - static const String _keyCityName = 'planting_city_name'; - static const String _keyCityCode = 'planting_city_code'; + /// 本地存储 key(统一使用 StorageKeys 常量,确保多账号隔离一致性) + static const String _keyProvinceName = StorageKeys.plantingProvinceName; + static const String _keyProvinceCode = StorageKeys.plantingProvinceCode; + static const String _keyCityName = StorageKeys.plantingCityName; + static const String _keyCityCode = StorageKeys.plantingCityCode; /// 选中的授权类型 AuthorizationType? _selectedType; diff --git a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart index ea61dc4f..06a4ea24 100644 --- a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart +++ b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart @@ -5,6 +5,7 @@ import 'package:city_pickers/city_pickers.dart'; import '../widgets/planting_confirm_dialog.dart'; import '../widgets/kyc_required_dialog.dart'; import '../../../../core/di/injection_container.dart'; +import '../../../../core/storage/storage_keys.dart'; import '../../../../routes/route_paths.dart'; /// 认种省市选择页面参数 @@ -40,11 +41,11 @@ class PlantingLocationPage extends ConsumerStatefulWidget { } class _PlantingLocationPageState extends ConsumerState { - /// 本地存储 key 前缀 - static const String _keyProvinceName = 'planting_province_name'; - static const String _keyProvinceCode = 'planting_province_code'; - static const String _keyCityName = 'planting_city_name'; - static const String _keyCityCode = 'planting_city_code'; + /// 本地存储 key(统一使用 StorageKeys 常量,确保多账号隔离一致性) + static const String _keyProvinceName = StorageKeys.plantingProvinceName; + static const String _keyProvinceCode = StorageKeys.plantingProvinceCode; + static const String _keyCityName = StorageKeys.plantingCityName; + static const String _keyCityCode = StorageKeys.plantingCityCode; /// 选中的省份名称 String? _selectedProvinceName; diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index 166d1778..825c09c6 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -4794,9 +4794,19 @@ class _ProfilePageState extends ConsumerState { /// 执行退出登录 Future _performLogout() async { try { + // ===== 1. 停止所有用户相关的定时任务 ===== + ref.read(walletStatusProvider.notifier).stopPolling(); + ref.read(pendingActionPollingServiceProvider).stop(); + final multiAccountService = ref.read(multiAccountServiceProvider); // 退出当前账号(保留账号数据) await multiAccountService.logoutCurrentAccount(); + + // ===== 2. invalidate 所有账号相关 Provider ===== + ref.invalidate(authProvider); + ref.invalidate(walletStatusProvider); + ref.invalidate(notificationBadgeProvider); + // 导航到向导页 if (mounted) { context.go(RoutePaths.guide);