From 4996c1d110cf55fe9e655264df6ac310e98cf671 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 1 Mar 2026 07:52:02 -0800 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20profile=E9=A1=B5=E5=BE=85?= =?UTF-8?q?=E9=A2=86=E5=8F=96/=E5=8F=AF=E7=BB=93=E7=AE=97/=E5=B7=B2?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E5=88=97=E8=A1=A8=E7=BB=9F=E4=B8=80=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=A2=84=E7=A7=8D=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更概要: - wallet_service.dart: 新增 WalletPendingRewardItem 模型和 getWalletPendingRewards() 方法 调用 GET /wallet/pending-rewards 获取 wallet-service 的待领取奖励列表 - profile_page.dart: 合并预种待领取奖励到列表中 从 wallet-service 待领取列表中筛选 PPL 前缀的预种条目,转换为 PendingRewardItem 与 reward-service 的正常认种待领取统一展示 - profile_page.dart: 已过期列表标记预种条目 wallet-service GET /wallet/expired-rewards 已包含预种过期记录, 渲染时通过 sourceOrderId.startsWith('PPL') 动态添加 [预种] 前缀 - profile_page.dart: 所有汇总金额统一从 wallet-service 取值 _pendingUsdt / _expiredUsdt / _remainingSeconds 改为从 walletInfo.rewards 读取, wallet_accounts 包含正常认种 + 预种,是唯一的 source of truth 技术说明: - 后端零改动,仅前端变更(零风险) - 预种条目通过订单号 PPL 前缀与正常认种区分,避免重复显示 - 所有预种条目在卡片上显示 [预种] 前缀,方便用户区分来源 Co-Authored-By: Claude Opus 4.6 --- .../lib/core/services/wallet_service.dart | 86 +++++++++++++++++++ .../presentation/pages/profile_page.dart | 83 ++++++++++++++---- 2 files changed, 151 insertions(+), 18 deletions(-) diff --git a/frontend/mobile-app/lib/core/services/wallet_service.dart b/frontend/mobile-app/lib/core/services/wallet_service.dart index 0601a8fb..115fb992 100644 --- a/frontend/mobile-app/lib/core/services/wallet_service.dart +++ b/frontend/mobile-app/lib/core/services/wallet_service.dart @@ -161,6 +161,46 @@ class SettlePrePlantingResult { } } +/// [2026-03-01] wallet-service 的待领取奖励条目 +/// 对应 GET /wallet/pending-rewards 返回的 pending_rewards 表记录 +class WalletPendingRewardItem { + final String id; + final double usdtAmount; + final double hashpowerAmount; + final String sourceOrderId; + final String allocationType; + final DateTime expireAt; + final String status; + final DateTime createdAt; + + WalletPendingRewardItem({ + required this.id, + required this.usdtAmount, + required this.hashpowerAmount, + required this.sourceOrderId, + required this.allocationType, + required this.expireAt, + required this.status, + required this.createdAt, + }); + + factory WalletPendingRewardItem.fromJson(Map json) { + return WalletPendingRewardItem( + id: json['id']?.toString() ?? '', + usdtAmount: (json['usdtAmount'] ?? 0).toDouble(), + hashpowerAmount: (json['hashpowerAmount'] ?? 0).toDouble(), + sourceOrderId: json['sourceOrderId'] ?? '', + allocationType: json['allocationType'] ?? '', + expireAt: DateTime.tryParse(json['expireAt'] ?? '') ?? DateTime.now(), + status: json['status'] ?? '', + createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(), + ); + } + + /// 是否为预种奖励(通过订单号前缀 PPL 判断) + bool get isPrePlanting => sourceOrderId.startsWith('PPL'); +} + /// 手续费类型 enum FeeType { fixed, // 固定金额 @@ -466,6 +506,52 @@ class WalletService { } } + /// [2026-03-01] 获取 wallet-service 中的待领取奖励列表 + /// + /// 调用 GET /wallet/pending-rewards (wallet-service) + /// 返回 pending_rewards 表中 status=PENDING 的记录,包含正常认种和预种两种来源。 + /// 用于在 profile 页面识别预种的待领取条目(sourceOrderId 以 PPL 开头), + /// 合并到 reward-service 的待领取列表中统一展示。 + Future> getWalletPendingRewards() async { + try { + debugPrint('[WalletService] ========== 获取待领取奖励列表 =========='); + debugPrint('[WalletService] 请求: GET /wallet/pending-rewards'); + + final response = await _apiClient.get('/wallet/pending-rewards'); + + debugPrint('[WalletService] 响应状态码: ${response.statusCode}'); + + if (response.statusCode == 200) { + final responseData = response.data; + + List dataList; + if (responseData is List) { + dataList = responseData; + } else if (responseData is Map) { + dataList = responseData['data'] as List? ?? []; + } else { + dataList = []; + } + + debugPrint('[WalletService] 解析到 ${dataList.length} 条待领取奖励'); + + final items = dataList + .map((item) => WalletPendingRewardItem.fromJson(item as Map)) + .toList(); + + debugPrint('[WalletService] ================================'); + return items; + } + + throw Exception('获取待领取奖励列表失败: ${response.statusCode}'); + } catch (e, stackTrace) { + debugPrint('[WalletService] !!!!!!!!!! 获取待领取奖励列表异常 !!!!!!!!!!'); + debugPrint('[WalletService] 错误: $e'); + debugPrint('[WalletService] 堆栈: $stackTrace'); + rethrow; + } + } + /// [2026-03-01] 结算预种可结算收益 /// /// 调用 POST /wallet/settle-pre-planting (wallet-service) 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 a82ede06..7137bdd8 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 @@ -785,17 +785,19 @@ class _ProfilePageState extends ConsumerState { rewardService.getSettleableRewards(), rewardService.getExpiredRewards(), walletService.getLedgerStatistics(), - prePlantingService.getMyRewards(), // 预种奖励 - walletService.getMyWallet(), // [2026-03-01] 新增:用于获取准确的 settleable_usdt + prePlantingService.getMyRewards(), // 预种奖励(分配记录) + walletService.getMyWallet(), // [2026-03-01] 新增:用于获取准确的汇总金额 + walletService.getWalletPendingRewards(), // [2026-03-01] 新增:wallet 待领取列表(含预种) ]); final summary = results[0] as RewardSummary; final pendingRewards = results[1] as List; var settleableRewards = results[2] as List; - final expiredRewards = results[3] as List; + var expiredRewards = results[3] as List; final ledgerStats = results[4] as LedgerStatistics; final prePlantingRewards = results[5] as PrePlantingMyRewards; final walletInfo = results[6] as WalletResponse; + final walletPendingRewards = results[7] as List; // 合并预种可结算奖励到列表中 // 预种奖励转为 SettleableRewardItem 格式,与正常认种统一展示 @@ -815,6 +817,31 @@ class _ProfilePageState extends ConsumerState { settleableRewards = [...settleableRewards, ...prePlantingSettleable]; debugPrint('[ProfilePage] 预种可结算奖励: ${prePlantingSettleable.length} 条, 金额: ${prePlantingRewards.settleableUsdt}'); + // [2026-03-01] 合并预种待领取奖励到列表中 + // wallet-service 的 pending_rewards 表包含正常认种 + 预种的待领取记录, + // 但 reward-service 的 /rewards/pending 已经返回了正常认种的待领取记录, + // 所以这里只筛选预种的条目(sourceOrderId 以 PPL 开头),避免重复显示。 + final prePlantingPending = walletPendingRewards + .where((r) => r.isPrePlanting) + .map((r) => PendingRewardItem( + id: 'wp-${r.id}', + rightType: r.allocationType, + usdtAmount: r.usdtAmount, + hashpowerAmount: r.hashpowerAmount, + createdAt: r.createdAt, + expireAt: r.expireAt, + remainingTimeMs: r.expireAt.difference(DateTime.now()).inMilliseconds.clamp(0, 86400000), + sourceOrderNo: r.sourceOrderId, + memo: '[预种] ${getAllocationTypeName(r.allocationType)}', + )) + .toList(); + final mergedPendingRewards = [...pendingRewards, ...prePlantingPending]; + debugPrint('[ProfilePage] 预种待领取奖励: ${prePlantingPending.length} 条'); + + // [2026-03-01] 已过期列表已从 wallet-service GET /wallet/expired-rewards 获取, + // 天然包含正常认种和预种的过期记录。预种条目的 [预种] 标记在渲染卡片时 + // 通过 sourceOrderId.startsWith('PPL') 动态判断,无需在此处修改数据。 + // 从流水统计中获取 REWARD_SETTLED 类型的总金额作为"已结算"数据 // 这比 summary.settledTotalUsdt 更准确,因为直接来自交易流水 double settledFromLedger = 0.0; @@ -842,18 +869,18 @@ class _ProfilePageState extends ConsumerState { if (mounted) { debugPrint('[ProfilePage] 更新 UI 状态...'); setState(() { - _pendingUsdt = summary.pendingUsdt; - _pendingPower = summary.pendingHashpower; - // [2026-03-01] 可结算金额统一从 wallet-service 取值 - // wallet_accounts.settleable_usdt 包含正常认种 + 预种的可结算部分,是唯一的 source of truth - // reward-service 的 summary.settleableUsdt 只包含正常认种部分,不包含预种 + // [2026-03-01] 所有汇总金额统一从 wallet-service 取值 + // wallet_accounts 包含正常认种 + 预种的所有奖励,是唯一的 source of truth + // reward-service 的 summary 只包含正常认种部分,不包含预种 + _pendingUsdt = walletInfo.rewards.pendingUsdt; + _pendingPower = walletInfo.rewards.pendingHashpower; _settleableUsdt = walletInfo.rewards.settleableUsdt; // 使用流水统计的数据,更准确 _settledUsdt = settledFromLedger; - _expiredUsdt = summary.expiredTotalUsdt; - _expiredPower = summary.expiredTotalHashpower; - _remainingSeconds = summary.pendingRemainingSeconds; - _pendingRewards = pendingRewards; + _expiredUsdt = walletInfo.rewards.expiredTotalUsdt; + _expiredPower = walletInfo.rewards.expiredTotalHashpower; + _remainingSeconds = walletInfo.rewards.pendingRemainingSeconds; + _pendingRewards = mergedPendingRewards; _settleableRewards = settleableRewards; _expiredRewards = expiredRewards; _isLoadingWallet = false; @@ -2475,6 +2502,11 @@ class _ProfilePageState extends ConsumerState { final seconds = (remainingSeconds % 60).toString().padLeft(2, '0'); final countdown = '$hours:$minutes:$seconds'; + // [2026-03-01] 预种条目(sourceOrderNo 以 PPL 开头)加 [预种] 前缀 + final displayTypeName = item.sourceOrderNo.startsWith('PPL') + ? '[预种] ${item.rightTypeName}' + : item.rightTypeName; + // 构建金额显示文本 final List amountParts = []; if (item.usdtAmount > 0) { @@ -2507,7 +2539,7 @@ class _ProfilePageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - item.rightTypeName, + displayTypeName, style: const TextStyle( fontSize: 14, fontFamily: 'Inter', @@ -2586,7 +2618,7 @@ class _ProfilePageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - item.rightTypeName, + displayTypeName, style: const TextStyle( fontSize: 13, fontFamily: 'Inter', @@ -2618,6 +2650,11 @@ class _ProfilePageState extends ConsumerState { final seconds = (remainingSeconds % 60).toString().padLeft(2, '0'); final countdown = '$hours:$minutes:$seconds'; + // [2026-03-01] 预种条目加 [预种] 前缀 + final displayTypeName = item.sourceOrderNo.startsWith('PPL') + ? '[预种] ${item.rightTypeName}' + : item.rightTypeName; + // 构建金额显示文本 final List amountParts = []; if (item.usdtAmount > 0) { @@ -2649,7 +2686,7 @@ class _ProfilePageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - item.rightTypeName, + displayTypeName, style: const TextStyle( fontSize: 14, fontFamily: 'Inter', @@ -3227,6 +3264,11 @@ class _ProfilePageState extends ConsumerState { // 格式化过期时间 final expiredDate = '${item.expiredAt.month}/${item.expiredAt.day} ${item.expiredAt.hour.toString().padLeft(2, '0')}:${item.expiredAt.minute.toString().padLeft(2, '0')}'; + // [2026-03-01] 预种条目加 [预种] 前缀 + final displayTypeName = item.sourceOrderId.startsWith('PPL') + ? '[预种] ${item.rightTypeName}' + : item.rightTypeName; + // 构建金额显示文本 final List amountParts = []; if (item.usdtAmount > 0) { @@ -3257,7 +3299,7 @@ class _ProfilePageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - item.rightTypeName, + displayTypeName, style: const TextStyle( fontSize: 14, fontFamily: 'Inter', @@ -3311,6 +3353,11 @@ class _ProfilePageState extends ConsumerState { // 格式化过期时间 final expiredDate = '${item.expiredAt.month}/${item.expiredAt.day} ${item.expiredAt.hour.toString().padLeft(2, '0')}:${item.expiredAt.minute.toString().padLeft(2, '0')}'; + // [2026-03-01] 预种条目(sourceOrderId 以 PPL 开头)加 [预种] 前缀 + final displayTypeName = item.sourceOrderId.startsWith('PPL') + ? '[预种] ${item.rightTypeName}' + : item.rightTypeName; + // 构建金额显示文本 final List amountParts = []; if (item.usdtAmount > 0) { @@ -3343,7 +3390,7 @@ class _ProfilePageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - item.rightTypeName, + displayTypeName, style: const TextStyle( fontSize: 14, fontFamily: 'Inter', @@ -3397,7 +3444,7 @@ class _ProfilePageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - item.rightTypeName, + displayTypeName, style: const TextStyle( fontSize: 13, fontFamily: 'Inter',