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 {
|
enum FeeType {
|
||||||
fixed, // 固定金额
|
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] 结算预种可结算收益
|
/// [2026-03-01] 结算预种可结算收益
|
||||||
///
|
///
|
||||||
/// 调用 POST /wallet/settle-pre-planting (wallet-service)
|
/// 调用 POST /wallet/settle-pre-planting (wallet-service)
|
||||||
|
|
|
||||||
|
|
@ -785,17 +785,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
rewardService.getSettleableRewards(),
|
rewardService.getSettleableRewards(),
|
||||||
rewardService.getExpiredRewards(),
|
rewardService.getExpiredRewards(),
|
||||||
walletService.getLedgerStatistics(),
|
walletService.getLedgerStatistics(),
|
||||||
prePlantingService.getMyRewards(), // 预种奖励
|
prePlantingService.getMyRewards(), // 预种奖励(分配记录)
|
||||||
walletService.getMyWallet(), // [2026-03-01] 新增:用于获取准确的 settleable_usdt
|
walletService.getMyWallet(), // [2026-03-01] 新增:用于获取准确的汇总金额
|
||||||
|
walletService.getWalletPendingRewards(), // [2026-03-01] 新增:wallet 待领取列表(含预种)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final summary = results[0] as RewardSummary;
|
final summary = results[0] as RewardSummary;
|
||||||
final pendingRewards = results[1] as List<PendingRewardItem>;
|
final pendingRewards = results[1] as List<PendingRewardItem>;
|
||||||
var settleableRewards = results[2] as List<SettleableRewardItem>;
|
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 ledgerStats = results[4] as LedgerStatistics;
|
||||||
final prePlantingRewards = results[5] as PrePlantingMyRewards;
|
final prePlantingRewards = results[5] as PrePlantingMyRewards;
|
||||||
final walletInfo = results[6] as WalletResponse;
|
final walletInfo = results[6] as WalletResponse;
|
||||||
|
final walletPendingRewards = results[7] as List<WalletPendingRewardItem>;
|
||||||
|
|
||||||
// 合并预种可结算奖励到列表中
|
// 合并预种可结算奖励到列表中
|
||||||
// 预种奖励转为 SettleableRewardItem 格式,与正常认种统一展示
|
// 预种奖励转为 SettleableRewardItem 格式,与正常认种统一展示
|
||||||
|
|
@ -815,6 +817,31 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
settleableRewards = [...settleableRewards, ...prePlantingSettleable];
|
settleableRewards = [...settleableRewards, ...prePlantingSettleable];
|
||||||
debugPrint('[ProfilePage] 预种可结算奖励: ${prePlantingSettleable.length} 条, 金额: ${prePlantingRewards.settleableUsdt}');
|
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 类型的总金额作为"已结算"数据
|
// 从流水统计中获取 REWARD_SETTLED 类型的总金额作为"已结算"数据
|
||||||
// 这比 summary.settledTotalUsdt 更准确,因为直接来自交易流水
|
// 这比 summary.settledTotalUsdt 更准确,因为直接来自交易流水
|
||||||
double settledFromLedger = 0.0;
|
double settledFromLedger = 0.0;
|
||||||
|
|
@ -842,18 +869,18 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
debugPrint('[ProfilePage] 更新 UI 状态...');
|
debugPrint('[ProfilePage] 更新 UI 状态...');
|
||||||
setState(() {
|
setState(() {
|
||||||
_pendingUsdt = summary.pendingUsdt;
|
// [2026-03-01] 所有汇总金额统一从 wallet-service 取值
|
||||||
_pendingPower = summary.pendingHashpower;
|
// wallet_accounts 包含正常认种 + 预种的所有奖励,是唯一的 source of truth
|
||||||
// [2026-03-01] 可结算金额统一从 wallet-service 取值
|
// reward-service 的 summary 只包含正常认种部分,不包含预种
|
||||||
// wallet_accounts.settleable_usdt 包含正常认种 + 预种的可结算部分,是唯一的 source of truth
|
_pendingUsdt = walletInfo.rewards.pendingUsdt;
|
||||||
// reward-service 的 summary.settleableUsdt 只包含正常认种部分,不包含预种
|
_pendingPower = walletInfo.rewards.pendingHashpower;
|
||||||
_settleableUsdt = walletInfo.rewards.settleableUsdt;
|
_settleableUsdt = walletInfo.rewards.settleableUsdt;
|
||||||
// 使用流水统计的数据,更准确
|
// 使用流水统计的数据,更准确
|
||||||
_settledUsdt = settledFromLedger;
|
_settledUsdt = settledFromLedger;
|
||||||
_expiredUsdt = summary.expiredTotalUsdt;
|
_expiredUsdt = walletInfo.rewards.expiredTotalUsdt;
|
||||||
_expiredPower = summary.expiredTotalHashpower;
|
_expiredPower = walletInfo.rewards.expiredTotalHashpower;
|
||||||
_remainingSeconds = summary.pendingRemainingSeconds;
|
_remainingSeconds = walletInfo.rewards.pendingRemainingSeconds;
|
||||||
_pendingRewards = pendingRewards;
|
_pendingRewards = mergedPendingRewards;
|
||||||
_settleableRewards = settleableRewards;
|
_settleableRewards = settleableRewards;
|
||||||
_expiredRewards = expiredRewards;
|
_expiredRewards = expiredRewards;
|
||||||
_isLoadingWallet = false;
|
_isLoadingWallet = false;
|
||||||
|
|
@ -2475,6 +2502,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
final seconds = (remainingSeconds % 60).toString().padLeft(2, '0');
|
final seconds = (remainingSeconds % 60).toString().padLeft(2, '0');
|
||||||
final countdown = '$hours:$minutes:$seconds';
|
final countdown = '$hours:$minutes:$seconds';
|
||||||
|
|
||||||
|
// [2026-03-01] 预种条目(sourceOrderNo 以 PPL 开头)加 [预种] 前缀
|
||||||
|
final displayTypeName = item.sourceOrderNo.startsWith('PPL')
|
||||||
|
? '[预种] ${item.rightTypeName}'
|
||||||
|
: item.rightTypeName;
|
||||||
|
|
||||||
// 构建金额显示文本
|
// 构建金额显示文本
|
||||||
final List<String> amountParts = [];
|
final List<String> amountParts = [];
|
||||||
if (item.usdtAmount > 0) {
|
if (item.usdtAmount > 0) {
|
||||||
|
|
@ -2507,7 +2539,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.rightTypeName,
|
displayTypeName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
@ -2586,7 +2618,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.rightTypeName,
|
displayTypeName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
@ -2618,6 +2650,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
final seconds = (remainingSeconds % 60).toString().padLeft(2, '0');
|
final seconds = (remainingSeconds % 60).toString().padLeft(2, '0');
|
||||||
final countdown = '$hours:$minutes:$seconds';
|
final countdown = '$hours:$minutes:$seconds';
|
||||||
|
|
||||||
|
// [2026-03-01] 预种条目加 [预种] 前缀
|
||||||
|
final displayTypeName = item.sourceOrderNo.startsWith('PPL')
|
||||||
|
? '[预种] ${item.rightTypeName}'
|
||||||
|
: item.rightTypeName;
|
||||||
|
|
||||||
// 构建金额显示文本
|
// 构建金额显示文本
|
||||||
final List<String> amountParts = [];
|
final List<String> amountParts = [];
|
||||||
if (item.usdtAmount > 0) {
|
if (item.usdtAmount > 0) {
|
||||||
|
|
@ -2649,7 +2686,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.rightTypeName,
|
displayTypeName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Inter',
|
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')}';
|
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 = [];
|
final List<String> amountParts = [];
|
||||||
if (item.usdtAmount > 0) {
|
if (item.usdtAmount > 0) {
|
||||||
|
|
@ -3257,7 +3299,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.rightTypeName,
|
displayTypeName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Inter',
|
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')}';
|
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 = [];
|
final List<String> amountParts = [];
|
||||||
if (item.usdtAmount > 0) {
|
if (item.usdtAmount > 0) {
|
||||||
|
|
@ -3343,7 +3390,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.rightTypeName,
|
displayTypeName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
@ -3397,7 +3444,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.rightTypeName,
|
displayTypeName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue