feat(mobile): profile页待领取/可结算/已过期列表统一显示预种数据
变更概要:
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
27cd72fe01
commit
4996c1d110
|
|
@ -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<String, dynamic> 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<List<WalletPendingRewardItem>> 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<dynamic> dataList;
|
||||
if (responseData is List) {
|
||||
dataList = responseData;
|
||||
} else if (responseData is Map<String, dynamic>) {
|
||||
dataList = responseData['data'] as List<dynamic>? ?? [];
|
||||
} else {
|
||||
dataList = [];
|
||||
}
|
||||
|
||||
debugPrint('[WalletService] 解析到 ${dataList.length} 条待领取奖励');
|
||||
|
||||
final items = dataList
|
||||
.map((item) => WalletPendingRewardItem.fromJson(item as Map<String, dynamic>))
|
||||
.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)
|
||||
|
|
|
|||
|
|
@ -785,17 +785,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
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<PendingRewardItem>;
|
||||
var settleableRewards = results[2] as List<SettleableRewardItem>;
|
||||
final expiredRewards = results[3] as List<ExpiredRewardItem>;
|
||||
var expiredRewards = results[3] as List<ExpiredRewardItem>;
|
||||
final ledgerStats = results[4] as LedgerStatistics;
|
||||
final prePlantingRewards = results[5] as PrePlantingMyRewards;
|
||||
final walletInfo = results[6] as WalletResponse;
|
||||
final walletPendingRewards = results[7] as List<WalletPendingRewardItem>;
|
||||
|
||||
// 合并预种可结算奖励到列表中
|
||||
// 预种奖励转为 SettleableRewardItem 格式,与正常认种统一展示
|
||||
|
|
@ -815,6 +817,31 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
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<ProfilePage> {
|
|||
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<ProfilePage> {
|
|||
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<String> amountParts = [];
|
||||
if (item.usdtAmount > 0) {
|
||||
|
|
@ -2507,7 +2539,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
item.rightTypeName,
|
||||
displayTypeName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
|
|
@ -2586,7 +2618,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
item.rightTypeName,
|
||||
displayTypeName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'Inter',
|
||||
|
|
@ -2618,6 +2650,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
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<String> amountParts = [];
|
||||
if (item.usdtAmount > 0) {
|
||||
|
|
@ -2649,7 +2686,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
item.rightTypeName,
|
||||
displayTypeName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
|
|
@ -3227,6 +3264,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
// 格式化过期时间
|
||||
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<String> amountParts = [];
|
||||
if (item.usdtAmount > 0) {
|
||||
|
|
@ -3257,7 +3299,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
item.rightTypeName,
|
||||
displayTypeName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
|
|
@ -3311,6 +3353,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
// 格式化过期时间
|
||||
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<String> amountParts = [];
|
||||
if (item.usdtAmount > 0) {
|
||||
|
|
@ -3343,7 +3390,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
item.rightTypeName,
|
||||
displayTypeName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
|
|
@ -3397,7 +3444,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
item.rightTypeName,
|
||||
displayTypeName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'Inter',
|
||||
|
|
|
|||
Loading…
Reference in New Issue