feat(mobile-app): 添加钱包生成状态轮询和UI显示
功能: - 创建 WalletStatusProvider 管理钱包生成状态 - 每5秒轮询钱包状态直到就绪或失败 - 在个人资料页面显示钱包生成状态 - 钱包生成中:显示"账号创建审核中"和进度指示器 - 钱包生成失败:显示失败状态和重试按钮 - 钱包已就绪:显示序列号 - 在 AccountService 添加 retryWalletGeneration API 调用 - 页面初始化时自动检查并启动轮询 - 页面销毁时自动停止轮询 实现细节: - 使用 Riverpod 状态管理 - 轮询间隔:5秒 - 自动停止轮询条件:钱包就绪或失败 - 支持手动重试钱包生成 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ceee3167cb
commit
ed6c08914c
|
|
@ -522,6 +522,33 @@ class AccountService {
|
|||
}
|
||||
}
|
||||
|
||||
/// 手动重试钱包生成 (POST /user/wallet/retry)
|
||||
///
|
||||
/// 当钱包生成失败或超时时,用户可手动触发重试
|
||||
Future<void> 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)
|
||||
///
|
||||
/// 返回用户的所有信息,包括推荐人序列号
|
||||
|
|
|
|||
|
|
@ -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<WalletStatusState> {
|
||||
final AccountService _accountService;
|
||||
final SecureStorage _secureStorage;
|
||||
Timer? _pollingTimer;
|
||||
|
||||
WalletStatusNotifier(this._accountService, this._secureStorage)
|
||||
: super(const WalletStatusState());
|
||||
|
||||
/// 开始轮询钱包状态
|
||||
///
|
||||
/// 每5秒检查一次,直到钱包就绪或失败
|
||||
Future<void> 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<void> _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<void> 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<WalletStatusNotifier, WalletStatusState>((ref) {
|
||||
final accountService = ref.watch(accountServiceProvider);
|
||||
final secureStorage = ref.watch(secureStorageProvider);
|
||||
return WalletStatusNotifier(accountService, secureStorage);
|
||||
});
|
||||
|
|
@ -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<ProfilePage> {
|
|||
_loadUnreadNotificationCount();
|
||||
// 启动定时刷新(可见区域的数据)
|
||||
_startAutoRefreshTimer();
|
||||
// 检查钱包状态并启动轮询(如果需要)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkAndStartWalletPolling();
|
||||
});
|
||||
}
|
||||
|
||||
/// 加载应用信息
|
||||
|
|
@ -787,6 +792,17 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 检查并启动钱包轮询
|
||||
Future<void> _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<ProfilePage> {
|
|||
_referralDebounceTimer?.cancel();
|
||||
_authorizationDebounceTimer?.cancel();
|
||||
_walletDebounceTimer?.cancel();
|
||||
// 停止钱包轮询
|
||||
ref.read(walletStatusProvider.notifier).stopPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -1382,28 +1400,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
),
|
||||
),
|
||||
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<ProfilePage> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建序列号或钱包生成状态显示
|
||||
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>(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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue