diff --git a/frontend/mobile-app/lib/core/services/account_service.dart b/frontend/mobile-app/lib/core/services/account_service.dart index bfea0bc2..2003a5a1 100644 --- a/frontend/mobile-app/lib/core/services/account_service.dart +++ b/frontend/mobile-app/lib/core/services/account_service.dart @@ -522,6 +522,33 @@ class AccountService { } } + /// 手动重试钱包生成 (POST /user/wallet/retry) + /// + /// 当钱包生成失败或超时时,用户可手动触发重试 + Future retryWalletGeneration() async { + debugPrint('$_tag retryWalletGeneration() - 开始手动重试钱包生成'); + + try { + debugPrint('$_tag retryWalletGeneration() - 调用 POST /user/wallet/retry'); + final response = await _apiClient.post('/user/wallet/retry'); + debugPrint('$_tag retryWalletGeneration() - API 响应状态码: ${response.statusCode}'); + + if (response.statusCode == 200) { + debugPrint('$_tag retryWalletGeneration() - 重试请求已提交'); + } else { + debugPrint('$_tag retryWalletGeneration() - 请求失败,状态码: ${response.statusCode}'); + throw ApiException('重试钱包生成失败: ${response.statusCode}'); + } + } on ApiException catch (e) { + debugPrint('$_tag retryWalletGeneration() - API 异常: $e'); + rethrow; + } catch (e, stackTrace) { + debugPrint('$_tag retryWalletGeneration() - 未知异常: $e'); + debugPrint('$_tag retryWalletGeneration() - 堆栈: $stackTrace'); + throw ApiException('重试钱包生成失败: $e'); + } + } + /// 获取当前用户完整信息 (GET /me) /// /// 返回用户的所有信息,包括推荐人序列号 diff --git a/frontend/mobile-app/lib/features/auth/presentation/providers/wallet_status_provider.dart b/frontend/mobile-app/lib/features/auth/presentation/providers/wallet_status_provider.dart new file mode 100644 index 00000000..98935333 --- /dev/null +++ b/frontend/mobile-app/lib/features/auth/presentation/providers/wallet_status_provider.dart @@ -0,0 +1,184 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../core/services/account_service.dart'; +import '../../../../core/storage/secure_storage.dart'; +import '../../../../core/storage/storage_keys.dart'; + +/// 钱包状态枚举 +enum WalletGenerationStatus { + unknown, // 未知状态 + generating, // 生成中 + ready, // 已就绪 + failed, // 生成失败 +} + +/// 钱包状态信息 +class WalletStatusState { + final WalletGenerationStatus status; + final String? errorMessage; + final DateTime? lastChecked; + final bool isPolling; + + const WalletStatusState({ + this.status = WalletGenerationStatus.unknown, + this.errorMessage, + this.lastChecked, + this.isPolling = false, + }); + + WalletStatusState copyWith({ + WalletGenerationStatus? status, + String? errorMessage, + DateTime? lastChecked, + bool? isPolling, + }) { + return WalletStatusState( + status: status ?? this.status, + errorMessage: errorMessage, + lastChecked: lastChecked ?? this.lastChecked, + isPolling: isPolling ?? this.isPolling, + ); + } + + bool get isGenerating => status == WalletGenerationStatus.generating; + bool get isReady => status == WalletGenerationStatus.ready; + bool get isFailed => status == WalletGenerationStatus.failed; +} + +/// 钱包状态管理器 +class WalletStatusNotifier extends StateNotifier { + final AccountService _accountService; + final SecureStorage _secureStorage; + Timer? _pollingTimer; + + WalletStatusNotifier(this._accountService, this._secureStorage) + : super(const WalletStatusState()); + + /// 开始轮询钱包状态 + /// + /// 每5秒检查一次,直到钱包就绪或失败 + Future startPolling() async { + if (state.isPolling) { + debugPrint('[WalletStatusProvider] Already polling, skipping'); + return; + } + + debugPrint('[WalletStatusProvider] Starting wallet status polling'); + state = state.copyWith(isPolling: true); + + // 立即检查一次 + await _checkWalletStatus(); + + // 设置定时器,每5秒检查一次 + _pollingTimer?.cancel(); + _pollingTimer = Timer.periodic(const Duration(seconds: 5), (_) async { + await _checkWalletStatus(); + + // 如果钱包已就绪或失败,停止轮询 + if (state.isReady || state.isFailed) { + stopPolling(); + } + }); + } + + /// 停止轮询 + void stopPolling() { + debugPrint('[WalletStatusProvider] Stopping wallet status polling'); + _pollingTimer?.cancel(); + _pollingTimer = null; + state = state.copyWith(isPolling: false); + } + + /// 检查钱包状态 + Future _checkWalletStatus() async { + try { + debugPrint('[WalletStatusProvider] Checking wallet status...'); + + // 获取用户序列号 + final userSerialNum = + await _secureStorage.read(key: StorageKeys.userSerialNum); + if (userSerialNum == null) { + debugPrint( + '[WalletStatusProvider] No userSerialNum found, stopping polling'); + stopPolling(); + return; + } + + // 调用 API 获取钱包状态 + final walletInfo = await _accountService.getWalletInfo(userSerialNum); + + debugPrint( + '[WalletStatusProvider] Wallet status: ${walletInfo.status}'); + + // 更新状态 + WalletGenerationStatus newStatus; + if (walletInfo.isReady) { + newStatus = WalletGenerationStatus.ready; + + // 钱包已就绪,保存标记 + await _secureStorage.write( + key: StorageKeys.isWalletReady, + value: 'true', + ); + debugPrint('[WalletStatusProvider] Wallet is ready, marked in storage'); + } else if (walletInfo.isGenerating) { + newStatus = WalletGenerationStatus.generating; + } else if (walletInfo.isFailed) { + newStatus = WalletGenerationStatus.failed; + } else { + newStatus = WalletGenerationStatus.unknown; + } + + state = state.copyWith( + status: newStatus, + lastChecked: DateTime.now(), + errorMessage: null, + ); + } catch (e) { + debugPrint('[WalletStatusProvider] Error checking wallet status: $e'); + state = state.copyWith( + errorMessage: e.toString(), + lastChecked: DateTime.now(), + ); + } + } + + /// 手动触发重试 + Future retryWalletGeneration() async { + try { + debugPrint('[WalletStatusProvider] Manually retrying wallet generation'); + + // 调用重试 API + await _accountService.retryWalletGeneration(); + + // 重新开始轮询 + state = state.copyWith( + status: WalletGenerationStatus.generating, + errorMessage: null, + ); + + if (!state.isPolling) { + await startPolling(); + } + } catch (e) { + debugPrint('[WalletStatusProvider] Error retrying wallet generation: $e'); + state = state.copyWith(errorMessage: e.toString()); + } + } + + @override + void dispose() { + _pollingTimer?.cancel(); + super.dispose(); + } +} + +/// 钱包状态 Provider +final walletStatusProvider = + StateNotifierProvider((ref) { + final accountService = ref.watch(accountServiceProvider); + final secureStorage = ref.watch(secureStorageProvider); + return WalletStatusNotifier(accountService, secureStorage); +}); 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 39780862..857641dd 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 @@ -17,6 +17,7 @@ import '../../../../core/services/notification_service.dart'; import '../../../../routes/route_paths.dart'; import '../../../../routes/app_router.dart'; import '../../../auth/presentation/providers/auth_provider.dart'; +import '../../../auth/presentation/providers/wallet_status_provider.dart'; import '../widgets/team_tree_widget.dart'; import '../widgets/stacked_cards_widget.dart'; import '../../../authorization/presentation/widgets/stickman_race_widget.dart'; @@ -192,6 +193,10 @@ class _ProfilePageState extends ConsumerState { _loadUnreadNotificationCount(); // 启动定时刷新(可见区域的数据) _startAutoRefreshTimer(); + // 检查钱包状态并启动轮询(如果需要) + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkAndStartWalletPolling(); + }); } /// 加载应用信息 @@ -787,6 +792,17 @@ class _ProfilePageState extends ConsumerState { } } + /// 检查并启动钱包轮询 + Future _checkAndStartWalletPolling() async { + final authState = ref.read(authProvider); + + // 如果账号已创建但钱包未就绪,启动轮询 + if (authState.isAccountCreated && !authState.isWalletReady) { + debugPrint('[ProfilePage] Account created but wallet not ready, starting polling'); + await ref.read(walletStatusProvider.notifier).startPolling(); + } + } + @override void dispose() { _timer?.cancel(); @@ -795,6 +811,8 @@ class _ProfilePageState extends ConsumerState { _referralDebounceTimer?.cancel(); _authorizationDebounceTimer?.cancel(); _walletDebounceTimer?.cancel(); + // 停止钱包轮询 + ref.read(walletStatusProvider.notifier).stopPolling(); super.dispose(); } @@ -1382,28 +1400,7 @@ class _ProfilePageState extends ConsumerState { ), ), const SizedBox(height: 3), - Row( - children: [ - Text( - '序列号: $_serialNumber', - style: const TextStyle( - fontSize: 14, - fontFamily: 'Inter', - height: 1.5, - color: Color(0xCC5D4037), - ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: _copySerialNumber, - child: const Icon( - Icons.copy, - size: 16, - color: Color(0xCC5D4037), - ), - ), - ], - ), + _buildSerialNumberOrStatus(), ], ), ), @@ -4023,4 +4020,132 @@ class _ProfilePageState extends ConsumerState { } } } + + /// 构建序列号或钱包生成状态显示 + Widget _buildSerialNumberOrStatus() { + final walletStatus = ref.watch(walletStatusProvider); + final authState = ref.watch(authProvider); + + // 如果钱包已就绪或账号已创建且钱包也就绪,显示序列号 + if (authState.isWalletReady || walletStatus.isReady) { + return Row( + children: [ + Text( + '序列号: $_serialNumber', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xCC5D4037), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: _copySerialNumber, + child: const Icon( + Icons.copy, + size: 16, + color: Color(0xCC5D4037), + ), + ), + ], + ); + } + + // 如果钱包正在生成中,显示"账号创建审核中" + if (authState.isAccountCreated && !authState.isWalletReady) { + return Row( + children: [ + const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ), + const SizedBox(width: 8), + Text( + '账号创建审核中', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFFD4AF37), + fontStyle: FontStyle.italic, + ), + ), + ], + ); + } + + // 如果钱包生成失败,显示失败状态和重试按钮 + if (walletStatus.isFailed) { + return Row( + children: [ + const Icon( + Icons.error_outline, + size: 16, + color: Color(0xFFD32F2F), + ), + const SizedBox(width: 8), + Text( + '钱包生成失败', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFFD32F2F), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () async { + await ref.read(walletStatusProvider.notifier).retryWalletGeneration(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '重试', + style: TextStyle( + fontSize: 12, + fontFamily: 'Inter', + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ); + } + + // 默认情况显示序列号 + return Row( + children: [ + Text( + '序列号: $_serialNumber', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xCC5D4037), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: _copySerialNumber, + child: const Icon( + Icons.copy, + size: 16, + color: Color(0xCC5D4037), + ), + ), + ], + ); + } }