fix(mobile-app): 修复多账号切换数据串号问题,完善存储隔离与状态重置

问题:
多账号切换时,前一个账号的推荐码、种植省市、缓存数据等会串到下一个账号,
同时定时器(钱包轮询、通知刷新、遥测上传)未正确停止/重启,
导致旧账号的 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-25 06:13:05 -08:00
parent a5a69645b4
commit 8f8a9230d0
6 changed files with 86 additions and 19 deletions

View File

@ -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 authProviderwalletStatusProvider
///
///
///
/// 1.
/// 2.
/// 3.
/// 4.
/// 4.5 onBeforeRestore
/// 5.
/// 6.
/// 7.
Future<bool> switchToAccount(String userSerialNum) async {
Future<bool> switchToAccount(String userSerialNum, {Future<void> 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) {

View File

@ -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';

View File

@ -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<AccountSwitchPage> {
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<AccountSwitchPage> {
//
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) {

View File

@ -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<AuthorizationApplyPage> {
/// 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;

View File

@ -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<PlantingLocationPage> {
/// 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;

View File

@ -4794,9 +4794,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
/// 退
Future<void> _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);