fix(mobile-app): 修复账号切换的严重bug和数据隔离问题

问题修复:
1. 键列表不一致 - 统一定义 _accountSecureKeys 和 _accountLocalKeys
2. 缺少 phoneNumber/isPasswordSet/biometricEnabled - 补充到键列表
3. 切换前未清除旧数据 - 新增 _clearCurrentAccountData 方法
4. 缓存数据未按账号隔离 - LocalStorage 数据也按账号保存/恢复
5. 遥测队列未隔离 - 切换时清除遥测事件队列

新增功能:
- _validateAccountData: 切换前验证目标账号数据完整性
- _clearCurrentAccountData: 切换前清除当前存储空间

优化:
- switchToAccount: 完整的切换流程(验证→保存→清除→恢复)
- saveCurrentAccountData: 同时保存 SecureStorage 和 LocalStorage
- _restoreAccountData: 同时恢复 SecureStorage 和 LocalStorage
- deleteAccount: 同时删除 SecureStorage 和 LocalStorage 专用键
- logoutCurrentAccount: 使用统一键列表,确保一致性

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-27 10:01:11 -08:00
parent 6216a1563a
commit 8eecc4c55f
1 changed files with 184 additions and 116 deletions

View File

@ -60,6 +60,42 @@ class MultiAccountService {
this._telemetryStorage, this._telemetryStorage,
); );
// ===== =====
/// SecureStorage
///
static const List<String> _accountSecureKeys = [
// Token
StorageKeys.accessToken,
StorageKeys.refreshToken,
//
StorageKeys.userSerialNum,
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.isAccountCreated,
StorageKeys.phoneNumber,
StorageKeys.isPasswordSet,
//
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
//
StorageKeys.biometricEnabled,
];
/// LocalStorage
static const List<String> _accountLocalKeys = [
StorageKeys.lastSyncTime,
StorageKeys.cachedRankingData,
StorageKeys.cachedMiningStatus,
];
/// ///
Future<List<AccountSummary>> getAccountList() async { Future<List<AccountSummary>> getAccountList() async {
debugPrint('$_tag getAccountList() - 获取账号列表'); debugPrint('$_tag getAccountList() - 获取账号列表');
@ -136,31 +172,57 @@ class MultiAccountService {
/// ///
/// true /// true
///
///
/// 1.
/// 2.
/// 3.
/// 4.
/// 5.
/// 6.
/// 7.
Future<bool> switchToAccount(String userSerialNum) async { Future<bool> switchToAccount(String userSerialNum) async {
debugPrint('$_tag switchToAccount() - 切换到账号: $userSerialNum'); debugPrint('$_tag switchToAccount() - 切换到账号: $userSerialNum');
// // 1.
final accounts = await getAccountList(); final accounts = await getAccountList();
final account = accounts.where((a) => a.userSerialNum == userSerialNum).firstOrNull; final account = accounts.where((a) => a.userSerialNum == userSerialNum).firstOrNull;
if (account == null) { if (account == null) {
debugPrint('$_tag switchToAccount() - 账号不存在'); debugPrint('$_tag switchToAccount() - 账号不列表中');
return false; return false;
} }
// // 2. accessToken mnemonic
final hasValidData = await _validateAccountData(userSerialNum);
if (!hasValidData) {
debugPrint('$_tag switchToAccount() - 账号数据不完整,无法切换');
return false;
}
// 3.
final currentId = await getCurrentAccountId();
if (currentId != null && currentId != userSerialNum) {
await saveCurrentAccountData();
debugPrint('$_tag switchToAccount() - 已保存当前账号数据: $currentId');
}
// 4.
await _clearCurrentAccountData();
// 5.
await _restoreAccountData(userSerialNum); await _restoreAccountData(userSerialNum);
// // 6.
await setCurrentAccountId(userSerialNum); await setCurrentAccountId(userSerialNum);
// ID使userSerialNumD25121400005 // 7. ID
if (TelemetryService().isInitialized) { if (TelemetryService().isInitialized) {
TelemetryService().setUserId(userSerialNum); TelemetryService().setUserId(userSerialNum);
debugPrint('$_tag switchToAccount() - 设置TelemetryService userId: $userSerialNum'); debugPrint('$_tag switchToAccount() - 设置TelemetryService userId: $userSerialNum');
} }
// Sentry // 8. Sentry
if (SentryService().isInitialized) { if (SentryService().isInitialized) {
SentryService().setUser(userId: userSerialNum); SentryService().setUser(userId: userSerialNum);
debugPrint('$_tag switchToAccount() - 设置SentryService userId: $userSerialNum'); debugPrint('$_tag switchToAccount() - 设置SentryService userId: $userSerialNum');
@ -170,7 +232,57 @@ class MultiAccountService {
return true; return true;
} }
///
///
Future<bool> _validateAccountData(String accountId) async {
// userSerialNum
final userSerialNumKey = StorageKeys.withAccountPrefix(accountId, StorageKeys.userSerialNum);
final userSerialNum = await _secureStorage.read(key: userSerialNumKey);
if (userSerialNum == null || userSerialNum.isEmpty) {
debugPrint('$_tag _validateAccountData() - 缺少 userSerialNum');
return false;
}
// token 使
final accessTokenKey = StorageKeys.withAccountPrefix(accountId, StorageKeys.accessToken);
final mnemonicKey = StorageKeys.withAccountPrefix(accountId, StorageKeys.mnemonic);
final accessToken = await _secureStorage.read(key: accessTokenKey);
final mnemonic = await _secureStorage.read(key: mnemonicKey);
if ((accessToken == null || accessToken.isEmpty) &&
(mnemonic == null || mnemonic.isEmpty)) {
debugPrint('$_tag _validateAccountData() - 缺少 accessToken 和 mnemonic');
return false;
}
return true;
}
///
///
Future<void> _clearCurrentAccountData() async {
debugPrint('$_tag _clearCurrentAccountData() - 清除当前存储空间');
// SecureStorage
for (final key in _accountSecureKeys) {
await _secureStorage.delete(key: key);
}
// LocalStorage
for (final key in _accountLocalKeys) {
await _localStorage.remove(key);
}
//
await _telemetryStorage.clearUserData();
debugPrint('$_tag _clearCurrentAccountData() - 清除完成');
}
/// ///
/// SecureStorage LocalStorage
Future<void> saveCurrentAccountData() async { Future<void> saveCurrentAccountData() async {
final currentId = await getCurrentAccountId(); final currentId = await getCurrentAccountId();
if (currentId == null) { if (currentId == null) {
@ -180,71 +292,60 @@ class MultiAccountService {
debugPrint('$_tag saveCurrentAccountData() - 保存账号数据: $currentId'); debugPrint('$_tag saveCurrentAccountData() - 保存账号数据: $currentId');
// // SecureStorage
final keysToSave = [ int secureCount = 0;
StorageKeys.userSerialNum, for (final key in _accountSecureKeys) {
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.isAccountCreated,
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
StorageKeys.accessToken,
StorageKeys.refreshToken,
];
for (final key in keysToSave) {
final value = await _secureStorage.read(key: key); final value = await _secureStorage.read(key: key);
if (value != null) { if (value != null) {
final prefixedKey = StorageKeys.withAccountPrefix(currentId, key); final prefixedKey = StorageKeys.withAccountPrefix(currentId, key);
await _secureStorage.write(key: prefixedKey, value: value); await _secureStorage.write(key: prefixedKey, value: value);
secureCount++;
} }
} }
debugPrint('$_tag saveCurrentAccountData() - 保存完成'); // LocalStorage
int localCount = 0;
for (final key in _accountLocalKeys) {
final value = _localStorage.getString(key);
if (value != null) {
final prefixedKey = StorageKeys.withAccountPrefix(currentId, key);
await _localStorage.setString(prefixedKey, value);
localCount++;
}
}
debugPrint('$_tag saveCurrentAccountData() - 保存完成 (Secure: $secureCount, Local: $localCount)');
} }
/// ///
/// SecureStorage LocalStorage
Future<void> _restoreAccountData(String accountId) async { Future<void> _restoreAccountData(String accountId) async {
debugPrint('$_tag _restoreAccountData() - 恢复账号数据: $accountId'); debugPrint('$_tag _restoreAccountData() - 恢复账号数据: $accountId');
// // SecureStorage
final keysToRestore = [ int secureCount = 0;
StorageKeys.userSerialNum, for (final key in _accountSecureKeys) {
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.isAccountCreated,
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
StorageKeys.accessToken,
StorageKeys.refreshToken,
];
for (final key in keysToRestore) {
final prefixedKey = StorageKeys.withAccountPrefix(accountId, key); final prefixedKey = StorageKeys.withAccountPrefix(accountId, key);
final value = await _secureStorage.read(key: prefixedKey); final value = await _secureStorage.read(key: prefixedKey);
if (value != null) { if (value != null) {
await _secureStorage.write(key: key, value: value); await _secureStorage.write(key: key, value: value);
} else { secureCount++;
// }
await _secureStorage.delete(key: key); // _clearCurrentAccountData
}
// LocalStorage
int localCount = 0;
for (final key in _accountLocalKeys) {
final prefixedKey = StorageKeys.withAccountPrefix(accountId, key);
final value = _localStorage.getString(prefixedKey);
if (value != null) {
await _localStorage.setString(key, value);
localCount++;
} }
} }
debugPrint('$_tag _restoreAccountData() - 恢复完成'); debugPrint('$_tag _restoreAccountData() - 恢复完成 (Secure: $secureCount, Local: $localCount)');
} }
/// 退 /// 退
@ -259,49 +360,20 @@ class MultiAccountService {
// //
await setCurrentAccountId(null); await setCurrentAccountId(null);
// ===== 1. SecureStorage ===== // ===== 1. SecureStorage =====
final secureKeysToClear = [ // 使
// Token for (final key in _accountSecureKeys) {
StorageKeys.accessToken,
StorageKeys.refreshToken,
//
StorageKeys.userSerialNum,
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.inviterReferralCode, //
StorageKeys.isAccountCreated,
StorageKeys.phoneNumber,
StorageKeys.isPasswordSet,
//
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
//
StorageKeys.biometricEnabled,
];
for (final key in secureKeysToClear) {
await _secureStorage.delete(key: key); await _secureStorage.delete(key: key);
} }
debugPrint('$_tag logoutCurrentAccount() - 已清除 ${secureKeysToClear.length} 个 SecureStorage 键'); //
await _secureStorage.delete(key: StorageKeys.inviterReferralCode);
debugPrint('$_tag logoutCurrentAccount() - 已清除 ${_accountSecureKeys.length + 1} 个 SecureStorage 键');
// ===== 2. LocalStorage ===== // ===== 2. LocalStorage =====
final localKeysToClear = [ for (final key in _accountLocalKeys) {
StorageKeys.lastSyncTime,
StorageKeys.cachedRankingData,
StorageKeys.cachedMiningStatus,
];
for (final key in localKeysToClear) {
await _localStorage.remove(key); await _localStorage.remove(key);
} }
debugPrint('$_tag logoutCurrentAccount() - 已清除 ${localKeysToClear.length} 个 LocalStorage 缓存'); debugPrint('$_tag logoutCurrentAccount() - 已清除 ${_accountLocalKeys.length} 个 LocalStorage 缓存');
// ===== 3. ===== // ===== 3. =====
await _telemetryStorage.clearUserData(); await _telemetryStorage.clearUserData();
@ -318,7 +390,7 @@ class MultiAccountService {
debugPrint('$_tag logoutCurrentAccount() - 清除 SentryService userId'); debugPrint('$_tag logoutCurrentAccount() - 清除 SentryService userId');
} }
final totalCleared = secureKeysToClear.length + localKeysToClear.length; final totalCleared = _accountSecureKeys.length + _accountLocalKeys.length + 1;
debugPrint('$_tag logoutCurrentAccount() - 退出完成,共清除 $totalCleared 个存储键 + 遥测数据'); debugPrint('$_tag logoutCurrentAccount() - 退出完成,共清除 $totalCleared 个存储键 + 遥测数据');
} }
@ -326,42 +398,38 @@ class MultiAccountService {
Future<void> deleteAccount(String userSerialNum) async { Future<void> deleteAccount(String userSerialNum) async {
debugPrint('$_tag deleteAccount() - 删除账号: $userSerialNum'); debugPrint('$_tag deleteAccount() - 删除账号: $userSerialNum');
// 退
final currentId = await getCurrentAccountId();
if (currentId == userSerialNum) {
//
await setCurrentAccountId(null);
await _clearCurrentAccountData();
// Sentry
if (TelemetryService().isInitialized) {
TelemetryService().clearUserId();
}
if (SentryService().isInitialized) {
SentryService().clearUser();
}
}
// //
await removeAccount(userSerialNum); await removeAccount(userSerialNum);
// // SecureStorage
final keysToDelete = [ for (final key in _accountSecureKeys) {
StorageKeys.userSerialNum,
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.isAccountCreated,
StorageKeys.phoneNumber,
StorageKeys.isPasswordSet,
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
StorageKeys.accessToken,
StorageKeys.refreshToken,
];
for (final key in keysToDelete) {
final prefixedKey = StorageKeys.withAccountPrefix(userSerialNum, key); final prefixedKey = StorageKeys.withAccountPrefix(userSerialNum, key);
await _secureStorage.delete(key: prefixedKey); await _secureStorage.delete(key: prefixedKey);
} }
// // LocalStorage
final currentId = await getCurrentAccountId(); for (final key in _accountLocalKeys) {
if (currentId == userSerialNum) { final prefixedKey = StorageKeys.withAccountPrefix(userSerialNum, key);
await logoutCurrentAccount(); await _localStorage.remove(prefixedKey);
} }
debugPrint('$_tag deleteAccount() - 删除完成'); debugPrint('$_tag deleteAccount() - 删除完成,已清除 ${_accountSecureKeys.length + _accountLocalKeys.length} 个账号专用键');
} }
/// ///