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 b3ed1df6..80564edd 100644 --- a/frontend/mobile-app/lib/core/services/multi_account_service.dart +++ b/frontend/mobile-app/lib/core/services/multi_account_service.dart @@ -60,6 +60,42 @@ class MultiAccountService { this._telemetryStorage, ); + // ===== 账号数据键列表(统一定义,确保一致性) ===== + + /// 需要按账号隔离保存的 SecureStorage 键 + /// 这些数据在切换账号时会保存到账号专用存储,并在切换回来时恢复 + static const List _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 _accountLocalKeys = [ + StorageKeys.lastSyncTime, + StorageKeys.cachedRankingData, + StorageKeys.cachedMiningStatus, + ]; + /// 获取账号列表 Future> getAccountList() async { debugPrint('$_tag getAccountList() - 获取账号列表'); @@ -136,31 +172,57 @@ class MultiAccountService { /// 切换到指定账号 /// 返回 true 表示切换成功 + /// + /// 切换流程: + /// 1. 验证目标账号存在 + /// 2. 验证目标账号数据完整性 + /// 3. 保存当前账号数据到账号专用存储 + /// 4. 清除当前存储空间(确保干净环境) + /// 5. 从账号专用存储恢复目标账号数据 + /// 6. 设置当前账号标记 + /// 7. 更新遥测和错误追踪服务的用户信息 Future 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 _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 _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 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 _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 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} 个账号专用键'); } /// 迁移旧数据到多账号架构