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:
parent
6216a1563a
commit
8eecc4c55f
|
|
@ -60,6 +60,42 @@ class MultiAccountService {
|
|||
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 {
|
||||
debugPrint('$_tag getAccountList() - 获取账号列表');
|
||||
|
|
@ -136,31 +172,57 @@ class MultiAccountService {
|
|||
|
||||
/// 切换到指定账号
|
||||
/// 返回 true 表示切换成功
|
||||
///
|
||||
/// 切换流程:
|
||||
/// 1. 验证目标账号存在
|
||||
/// 2. 验证目标账号数据完整性
|
||||
/// 3. 保存当前账号数据到账号专用存储
|
||||
/// 4. 清除当前存储空间(确保干净环境)
|
||||
/// 5. 从账号专用存储恢复目标账号数据
|
||||
/// 6. 设置当前账号标记
|
||||
/// 7. 更新遥测和错误追踪服务的用户信息
|
||||
Future<bool> switchToAccount(String userSerialNum) async {
|
||||
debugPrint('$_tag switchToAccount() - 切换到账号: $userSerialNum');
|
||||
|
||||
// 验证账号存在
|
||||
// 1. 验证账号存在于列表中
|
||||
final accounts = await getAccountList();
|
||||
final account = accounts.where((a) => a.userSerialNum == userSerialNum).firstOrNull;
|
||||
|
||||
if (account == null) {
|
||||
debugPrint('$_tag switchToAccount() - 账号不存在');
|
||||
debugPrint('$_tag switchToAccount() - 账号不在列表中');
|
||||
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);
|
||||
|
||||
// 设置当前账号
|
||||
// 6. 设置当前账号标记
|
||||
await setCurrentAccountId(userSerialNum);
|
||||
|
||||
// 设置遥测服务的用户ID(使用userSerialNum,如D25121400005)
|
||||
// 7. 更新遥测服务的用户ID
|
||||
if (TelemetryService().isInitialized) {
|
||||
TelemetryService().setUserId(userSerialNum);
|
||||
debugPrint('$_tag switchToAccount() - 设置TelemetryService userId: $userSerialNum');
|
||||
}
|
||||
|
||||
// 设置 Sentry 用户信息
|
||||
// 8. 更新 Sentry 用户信息
|
||||
if (SentryService().isInitialized) {
|
||||
SentryService().setUser(userId: userSerialNum);
|
||||
debugPrint('$_tag switchToAccount() - 设置SentryService userId: $userSerialNum');
|
||||
|
|
@ -170,7 +232,57 @@ class MultiAccountService {
|
|||
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 {
|
||||
final currentId = await getCurrentAccountId();
|
||||
if (currentId == null) {
|
||||
|
|
@ -180,71 +292,60 @@ class MultiAccountService {
|
|||
|
||||
debugPrint('$_tag saveCurrentAccountData() - 保存账号数据: $currentId');
|
||||
|
||||
// 需要保存的账号相关键
|
||||
final keysToSave = [
|
||||
StorageKeys.userSerialNum,
|
||||
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) {
|
||||
// 保存 SecureStorage 中的数据
|
||||
int secureCount = 0;
|
||||
for (final key in _accountSecureKeys) {
|
||||
final value = await _secureStorage.read(key: key);
|
||||
if (value != null) {
|
||||
final prefixedKey = StorageKeys.withAccountPrefix(currentId, key);
|
||||
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 {
|
||||
debugPrint('$_tag _restoreAccountData() - 恢复账号数据: $accountId');
|
||||
|
||||
// 需要恢复的账号相关键
|
||||
final keysToRestore = [
|
||||
StorageKeys.userSerialNum,
|
||||
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) {
|
||||
// 恢复 SecureStorage 中的数据
|
||||
int secureCount = 0;
|
||||
for (final key in _accountSecureKeys) {
|
||||
final prefixedKey = StorageKeys.withAccountPrefix(accountId, key);
|
||||
final value = await _secureStorage.read(key: prefixedKey);
|
||||
if (value != null) {
|
||||
await _secureStorage.write(key: key, value: value);
|
||||
} else {
|
||||
// 如果账号存储中没有该键,删除当前存储中的值
|
||||
await _secureStorage.delete(key: key);
|
||||
secureCount++;
|
||||
}
|
||||
// 注意:不需要删除,因为 _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);
|
||||
|
||||
// ===== 1. 清除 SecureStorage 中的敏感数据 =====
|
||||
final secureKeysToClear = [
|
||||
// Token(必须清除)
|
||||
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) {
|
||||
// ===== 1. 清除 SecureStorage 中的账号数据 =====
|
||||
// 使用统一的键列表,确保一致性
|
||||
for (final key in _accountSecureKeys) {
|
||||
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 中的缓存数据 =====
|
||||
final localKeysToClear = [
|
||||
StorageKeys.lastSyncTime,
|
||||
StorageKeys.cachedRankingData,
|
||||
StorageKeys.cachedMiningStatus,
|
||||
];
|
||||
|
||||
for (final key in localKeysToClear) {
|
||||
for (final key in _accountLocalKeys) {
|
||||
await _localStorage.remove(key);
|
||||
}
|
||||
debugPrint('$_tag logoutCurrentAccount() - 已清除 ${localKeysToClear.length} 个 LocalStorage 缓存');
|
||||
debugPrint('$_tag logoutCurrentAccount() - 已清除 ${_accountLocalKeys.length} 个 LocalStorage 缓存');
|
||||
|
||||
// ===== 3. 清除遥测事件队列(用户相关数据) =====
|
||||
await _telemetryStorage.clearUserData();
|
||||
|
|
@ -318,7 +390,7 @@ class MultiAccountService {
|
|||
debugPrint('$_tag logoutCurrentAccount() - 清除 SentryService userId');
|
||||
}
|
||||
|
||||
final totalCleared = secureKeysToClear.length + localKeysToClear.length;
|
||||
final totalCleared = _accountSecureKeys.length + _accountLocalKeys.length + 1;
|
||||
debugPrint('$_tag logoutCurrentAccount() - 退出完成,共清除 $totalCleared 个存储键 + 遥测数据');
|
||||
}
|
||||
|
||||
|
|
@ -326,42 +398,38 @@ class MultiAccountService {
|
|||
Future<void> deleteAccount(String userSerialNum) async {
|
||||
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);
|
||||
|
||||
// 删除账号专用存储的所有数据
|
||||
final keysToDelete = [
|
||||
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) {
|
||||
// 删除账号专用存储的 SecureStorage 数据
|
||||
for (final key in _accountSecureKeys) {
|
||||
final prefixedKey = StorageKeys.withAccountPrefix(userSerialNum, key);
|
||||
await _secureStorage.delete(key: prefixedKey);
|
||||
}
|
||||
|
||||
// 如果删除的是当前账号,清除当前账号标记
|
||||
final currentId = await getCurrentAccountId();
|
||||
if (currentId == userSerialNum) {
|
||||
await logoutCurrentAccount();
|
||||
// 删除账号专用存储的 LocalStorage 数据
|
||||
for (final key in _accountLocalKeys) {
|
||||
final prefixedKey = StorageKeys.withAccountPrefix(userSerialNum, key);
|
||||
await _localStorage.remove(prefixedKey);
|
||||
}
|
||||
|
||||
debugPrint('$_tag deleteAccount() - 删除完成');
|
||||
debugPrint('$_tag deleteAccount() - 删除完成,已清除 ${_accountSecureKeys.length + _accountLocalKeys.length} 个账号专用键');
|
||||
}
|
||||
|
||||
/// 迁移旧数据到多账号架构
|
||||
|
|
|
|||
Loading…
Reference in New Issue