feat(wallet/profile): 添加可结算和已过期奖励逐笔显示功能

后端:
- wallet-service 新增 getSettleableRewards() 和 getExpiredRewards() 方法
- 新增 GET /wallet/settleable-rewards 和 GET /wallet/expired-rewards API

前端:
- reward_service.dart 新增 SettleableRewardItem、ExpiredRewardItem 数据模型
- profile_page.dart 可结算区域和已过期区域支持逐笔明细显示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-16 07:20:44 -08:00
parent bad14bcb32
commit 257236480f
4 changed files with 550 additions and 76 deletions

View File

@ -117,4 +117,38 @@ export class WalletController {
}>> {
return this.walletService.getPendingRewards(user.accountSequence);
}
@Get('settleable-rewards')
@ApiOperation({ summary: '查询可结算奖励列表', description: '获取用户的逐笔可结算奖励(已领取待结算)' })
@ApiResponse({ status: 200, description: '可结算奖励列表' })
async getSettleableRewards(
@CurrentUser() user: CurrentUserPayload,
): Promise<Array<{
id: string;
usdtAmount: number;
hashpowerAmount: number;
sourceOrderId: string;
allocationType: string;
settledAt: string;
createdAt: string;
}>> {
return this.walletService.getSettleableRewards(user.accountSequence);
}
@Get('expired-rewards')
@ApiOperation({ summary: '查询已过期奖励列表', description: '获取用户的逐笔已过期奖励24h未领取' })
@ApiResponse({ status: 200, description: '已过期奖励列表' })
async getExpiredRewards(
@CurrentUser() user: CurrentUserPayload,
): Promise<Array<{
id: string;
usdtAmount: number;
hashpowerAmount: number;
sourceOrderId: string;
allocationType: string;
expiredAt: string;
createdAt: string;
}>> {
return this.walletService.getExpiredRewards(user.accountSequence);
}
}

View File

@ -1622,6 +1622,62 @@ export class WalletApplicationService {
}));
}
/**
*
* pending_rewards status=SETTLED
*/
async getSettleableRewards(accountSequence: string): Promise<Array<{
id: string;
usdtAmount: number;
hashpowerAmount: number;
sourceOrderId: string;
allocationType: string;
settledAt: string;
createdAt: string;
}>> {
const rewards = await this.pendingRewardRepo.findByAccountSequence(
accountSequence,
PendingRewardStatus.SETTLED,
);
return rewards.map(r => ({
id: r.id.toString(),
usdtAmount: r.usdtAmount.value,
hashpowerAmount: r.hashpowerAmount.value,
sourceOrderId: r.sourceOrderId,
allocationType: r.allocationType,
settledAt: r.settledAt?.toISOString() ?? '',
createdAt: r.createdAt.toISOString(),
}));
}
/**
*
* pending_rewards status=EXPIRED
*/
async getExpiredRewards(accountSequence: string): Promise<Array<{
id: string;
usdtAmount: number;
hashpowerAmount: number;
sourceOrderId: string;
allocationType: string;
expiredAt: string;
createdAt: string;
}>> {
const rewards = await this.pendingRewardRepo.findByAccountSequence(
accountSequence,
PendingRewardStatus.EXPIRED,
);
return rewards.map(r => ({
id: r.id.toString(),
usdtAmount: r.usdtAmount.value,
hashpowerAmount: r.hashpowerAmount.value,
sourceOrderId: r.sourceOrderId,
allocationType: r.allocationType,
expiredAt: r.expiredAt?.toISOString() ?? '',
createdAt: r.createdAt.toISOString(),
}));
}
/**
*
* PENDING SETTLED

View File

@ -1,6 +1,26 @@
import 'package:flutter/foundation.dart';
import '../network/api_client.dart';
///
String getAllocationTypeName(String allocationType) {
switch (allocationType) {
case 'SHARE_RIGHT':
return '分享权益';
case 'PROVINCE_AREA_RIGHT':
return '省区域权益';
case 'PROVINCE_TEAM_RIGHT':
return '省团队权益';
case 'CITY_AREA_RIGHT':
return '市区域权益';
case 'CITY_TEAM_RIGHT':
return '市团队权益';
case 'COMMUNITY_RIGHT':
return '社区权益';
default:
return allocationType;
}
}
/// ( GET /rewards/pending )
class PendingRewardItem {
final String id;
@ -40,24 +60,79 @@ class PendingRewardItem {
int get remainingSeconds => (remainingTimeMs / 1000).round();
///
String get rightTypeName {
switch (rightType) {
case 'SHARE_RIGHT':
return '分享权益';
case 'PROVINCE_AREA_RIGHT':
return '省区域权益';
case 'PROVINCE_TEAM_RIGHT':
return '省团队权益';
case 'CITY_AREA_RIGHT':
return '市区域权益';
case 'CITY_TEAM_RIGHT':
return '市团队权益';
case 'COMMUNITY_RIGHT':
return '社区权益';
default:
return rightType;
}
String get rightTypeName => getAllocationTypeName(rightType);
}
/// ( GET /wallet/settleable-rewards )
class SettleableRewardItem {
final String id;
final String allocationType;
final double usdtAmount;
final double hashpowerAmount;
final DateTime createdAt;
final DateTime settledAt;
final String sourceOrderId;
SettleableRewardItem({
required this.id,
required this.allocationType,
required this.usdtAmount,
required this.hashpowerAmount,
required this.createdAt,
required this.settledAt,
required this.sourceOrderId,
});
factory SettleableRewardItem.fromJson(Map<String, dynamic> json) {
return SettleableRewardItem(
id: json['id']?.toString() ?? '',
allocationType: json['allocationType'] ?? '',
usdtAmount: (json['usdtAmount'] ?? 0).toDouble(),
hashpowerAmount: (json['hashpowerAmount'] ?? 0).toDouble(),
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
settledAt: DateTime.tryParse(json['settledAt'] ?? '') ?? DateTime.now(),
sourceOrderId: json['sourceOrderId'] ?? '',
);
}
///
String get allocationTypeName => getAllocationTypeName(allocationType);
}
/// ( GET /wallet/expired-rewards )
class ExpiredRewardItem {
final String id;
final String allocationType;
final double usdtAmount;
final double hashpowerAmount;
final DateTime createdAt;
final DateTime expiredAt;
final String sourceOrderId;
ExpiredRewardItem({
required this.id,
required this.allocationType,
required this.usdtAmount,
required this.hashpowerAmount,
required this.createdAt,
required this.expiredAt,
required this.sourceOrderId,
});
factory ExpiredRewardItem.fromJson(Map<String, dynamic> json) {
return ExpiredRewardItem(
id: json['id']?.toString() ?? '',
allocationType: json['allocationType'] ?? '',
usdtAmount: (json['usdtAmount'] ?? 0).toDouble(),
hashpowerAmount: (json['hashpowerAmount'] ?? 0).toDouble(),
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
expiredAt: DateTime.tryParse(json['expiredAt'] ?? '') ?? DateTime.now(),
sourceOrderId: json['sourceOrderId'] ?? '',
);
}
///
String get allocationTypeName => getAllocationTypeName(allocationType);
}
/// ( reward-service )
@ -219,4 +294,110 @@ class RewardService {
rethrow;
}
}
///
///
/// GET /wallet/settleable-rewards (wallet-service)
///
Future<List<SettleableRewardItem>> getSettleableRewards() async {
try {
debugPrint('[RewardService] ========== 获取可结算奖励列表 ==========');
debugPrint('[RewardService] 请求: GET /wallet/settleable-rewards');
final response = await _apiClient.get('/wallet/settleable-rewards');
debugPrint('[RewardService] 响应状态码: ${response.statusCode}');
debugPrint('[RewardService] 响应数据类型: ${response.data.runtimeType}');
if (response.statusCode == 200) {
final responseData = response.data;
debugPrint('[RewardService] 原始响应数据: $responseData');
// API { 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('[RewardService] 解析到 ${dataList.length} 条可结算奖励');
final items = dataList
.map((item) => SettleableRewardItem.fromJson(item as Map<String, dynamic>))
.toList();
for (var item in items) {
debugPrint('[RewardService] - ${item.allocationTypeName}: ${item.usdtAmount} USDT, ${item.hashpowerAmount} 算力');
}
debugPrint('[RewardService] ================================');
return items;
}
debugPrint('[RewardService] 请求失败,状态码: ${response.statusCode}');
debugPrint('[RewardService] 响应内容: ${response.data}');
throw Exception('获取可结算奖励列表失败: ${response.statusCode}');
} catch (e, stackTrace) {
debugPrint('[RewardService] !!!!!!!!!! 获取可结算奖励列表异常 !!!!!!!!!!');
debugPrint('[RewardService] 错误: $e');
debugPrint('[RewardService] 堆栈: $stackTrace');
rethrow;
}
}
///
///
/// GET /wallet/expired-rewards (wallet-service)
/// 24h未领取
Future<List<ExpiredRewardItem>> getExpiredRewards() async {
try {
debugPrint('[RewardService] ========== 获取已过期奖励列表 ==========');
debugPrint('[RewardService] 请求: GET /wallet/expired-rewards');
final response = await _apiClient.get('/wallet/expired-rewards');
debugPrint('[RewardService] 响应状态码: ${response.statusCode}');
debugPrint('[RewardService] 响应数据类型: ${response.data.runtimeType}');
if (response.statusCode == 200) {
final responseData = response.data;
debugPrint('[RewardService] 原始响应数据: $responseData');
// API { 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('[RewardService] 解析到 ${dataList.length} 条已过期奖励');
final items = dataList
.map((item) => ExpiredRewardItem.fromJson(item as Map<String, dynamic>))
.toList();
for (var item in items) {
debugPrint('[RewardService] - ${item.allocationTypeName}: ${item.usdtAmount} USDT, ${item.hashpowerAmount} 算力');
}
debugPrint('[RewardService] ================================');
return items;
}
debugPrint('[RewardService] 请求失败,状态码: ${response.statusCode}');
debugPrint('[RewardService] 响应内容: ${response.data}');
throw Exception('获取已过期奖励列表失败: ${response.statusCode}');
} catch (e, stackTrace) {
debugPrint('[RewardService] !!!!!!!!!! 获取已过期奖励列表异常 !!!!!!!!!!');
debugPrint('[RewardService] 错误: $e');
debugPrint('[RewardService] 堆栈: $stackTrace');
rethrow;
}
}
}

View File

@ -59,6 +59,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
// referral-service
List<Map<String, dynamic>> _referrals = [];
//
TeamTreeNode? _teamTreeRootNode;
// authorization-service
bool _hasCommunityAuth = false; //
bool _communityBenefitActive = false; //
@ -112,6 +115,12 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
//
List<PendingRewardItem> _pendingRewards = [];
//
List<SettleableRewardItem> _settleableRewards = [];
//
List<ExpiredRewardItem> _expiredRewards = [];
//
Timer? _timer;
int _remainingSeconds = 0;
@ -569,15 +578,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
debugPrint('[ProfilePage] 获取 rewardServiceProvider...');
final rewardService = ref.read(rewardServiceProvider);
//
debugPrint('[ProfilePage] 调用 getMyRewardSummary() 和 getPendingRewards()...');
//
debugPrint('[ProfilePage] 调用 getMyRewardSummary()、getPendingRewards()、getSettleableRewards()、getExpiredRewards()...');
final results = await Future.wait([
rewardService.getMyRewardSummary(),
rewardService.getPendingRewards(),
rewardService.getSettleableRewards(),
rewardService.getExpiredRewards(),
]);
final summary = results[0] as RewardSummary;
final pendingRewards = results[1] as List<PendingRewardItem>;
final settleableRewards = results[2] as List<SettleableRewardItem>;
final expiredRewards = results[3] as List<ExpiredRewardItem>;
debugPrint('[ProfilePage] -------- 收益数据加载成功 --------');
debugPrint('[ProfilePage] 待领取 USDT: ${summary.pendingUsdt}');
@ -589,6 +602,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
debugPrint('[ProfilePage] 过期时间: ${summary.pendingExpireAt}');
debugPrint('[ProfilePage] 剩余秒数: ${summary.pendingRemainingSeconds}');
debugPrint('[ProfilePage] 待领取奖励条目数: ${pendingRewards.length}');
debugPrint('[ProfilePage] 可结算奖励条目数: ${settleableRewards.length}');
debugPrint('[ProfilePage] 已过期奖励条目数: ${expiredRewards.length}');
if (mounted) {
debugPrint('[ProfilePage] 更新 UI 状态...');
@ -601,6 +616,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_expiredPower = summary.expiredTotalHashpower;
_remainingSeconds = summary.pendingRemainingSeconds;
_pendingRewards = pendingRewards;
_settleableRewards = settleableRewards;
_expiredRewards = expiredRewards;
_isLoadingWallet = false;
_walletError = null;
_walletRetryCount = 0;
@ -610,6 +627,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
debugPrint('[ProfilePage] _pendingUsdt: $_pendingUsdt');
debugPrint('[ProfilePage] _remainingSeconds: $_remainingSeconds');
debugPrint('[ProfilePage] _pendingRewards: ${_pendingRewards.length}');
debugPrint('[ProfilePage] _settleableRewards: ${_settleableRewards.length}');
debugPrint('[ProfilePage] _expiredRewards: ${_expiredRewards.length}');
//
if (_remainingSeconds > 0) {
@ -1415,55 +1434,38 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
width: 1,
),
),
child: Column(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// |
Row(
children: [
Expanded(
child: _buildInfoItem('推荐人的序列号', _referrerSerial),
),
Expanded(
child: _buildInfoItem('下级社区', _childCommunity),
),
],
// 线
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoItem('推荐人的序列号', _referrerSerial),
const SizedBox(height: 12),
_buildInfoItem('所属社区', _community),
const SizedBox(height: 12),
_buildInfoItem('上线社区', _parentCommunity),
const SizedBox(height: 12),
_buildInfoItem('下级社区', _childCommunity),
],
),
),
const SizedBox(height: 12),
// |
Row(
children: [
Expanded(
child: _buildInfoItem('所属社区', _community),
),
Expanded(
child: _buildInfoItem('市团队', _authCityCompany),
),
],
),
const SizedBox(height: 12),
// |
Row(
children: [
Expanded(
child: _buildInfoItem('上级社区', _parentCommunity),
),
Expanded(
child: _buildInfoItem('市区域', _cityCompany),
),
],
),
const SizedBox(height: 12),
// |
Row(
children: [
Expanded(
child: _buildInfoItem('省团队', _authProvinceCompany),
),
Expanded(
child: _buildInfoItem('省区域', _provinceCompany),
),
],
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoItem('市团队', _authCityCompany),
const SizedBox(height: 12),
_buildInfoItem('省团队', _authProvinceCompany),
const SizedBox(height: 12),
_buildInfoItem('市区域', _cityCompany),
const SizedBox(height: 12),
_buildInfoItem('省区域', _provinceCompany),
],
),
),
],
),
@ -1574,7 +1576,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
//
_buildReferralList(),
const SizedBox(height: 16),
//
//
_buildMyTeamTree(),
const SizedBox(height: 16),
//
@ -1972,6 +1974,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
],
),
//
if (_settleableRewards.isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(color: Color(0x33D4AF37), height: 1),
const SizedBox(height: 12),
const Text(
'可结算明细',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 8),
//
...(_settleableRewards.map((item) => _buildSettleableRewardItem(item))),
],
const SizedBox(height: 11),
// USDT
Row(
@ -2026,6 +2046,85 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
///
Widget _buildSettleableRewardItem(SettleableRewardItem item) {
//
final settledDate = '${item.settledAt.month}/${item.settledAt.day} ${item.settledAt.hour.toString().padLeft(2, '0')}:${item.settledAt.minute.toString().padLeft(2, '0')}';
//
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 积分');
}
if (item.hashpowerAmount > 0) {
amountParts.add('${_formatNumber(item.hashpowerAmount)} 算力');
}
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 积分';
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x22D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// +
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.allocationTypeName,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
Row(
children: [
const Icon(
Icons.check_circle_outline,
size: 14,
color: Color(0xFF4CAF50),
),
const SizedBox(width: 4),
Text(
settledDate,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF4CAF50),
),
),
],
),
],
),
const SizedBox(height: 8),
//
Text(
amountText,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
],
),
);
}
///
Widget _buildExpiredSection() {
return SizedBox(
@ -2107,12 +2206,109 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
],
),
//
if (_expiredRewards.isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(color: Color(0x33D4AF37), height: 1),
const SizedBox(height: 12),
const Text(
'已过期明细',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 8),
//
...(_expiredRewards.map((item) => _buildExpiredRewardItem(item))),
],
],
),
),
);
}
///
Widget _buildExpiredRewardItem(ExpiredRewardItem item) {
//
final expiredDate = '${item.expiredAt.month}/${item.expiredAt.day} ${item.expiredAt.hour.toString().padLeft(2, '0')}:${item.expiredAt.minute.toString().padLeft(2, '0')}';
//
final List<String> amountParts = [];
if (item.usdtAmount > 0) {
amountParts.add('${_formatNumber(item.usdtAmount)} 积分');
}
if (item.hashpowerAmount > 0) {
amountParts.add('${_formatNumber(item.hashpowerAmount)} 算力');
}
final amountText = amountParts.isNotEmpty ? amountParts.join(' ') : '0 积分';
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), //
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x22999999),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// +
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.allocationTypeName,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF999999), //
),
),
Row(
children: [
const Icon(
Icons.cancel_outlined,
size: 14,
color: Color(0xFFE57373), //
),
const SizedBox(width: 4),
Text(
expiredDate,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFFE57373), //
),
),
],
),
],
),
const SizedBox(height: 8),
//
Text(
amountText,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF999999), //
),
),
],
),
);
}
///
Widget _buildActionButtons() {
return Column(
@ -2358,15 +2554,22 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
///
///
Widget _buildMyTeamTree() {
//
final rootNode = TeamTreeNode.createRoot(
accountSequence: _serialNumber,
personalPlantingCount: _personalPlantingCount,
teamPlantingCount: _teamPlantingCount,
directReferralCount: _directReferralCount,
);
//
//
if (_teamTreeRootNode == null ||
_teamTreeRootNode!.accountSequence != _serialNumber ||
_teamTreeRootNode!.personalPlantingCount != _personalPlantingCount ||
_teamTreeRootNode!.teamPlantingCount != _teamPlantingCount ||
_teamTreeRootNode!.directReferralCount != _directReferralCount) {
_teamTreeRootNode = TeamTreeNode.createRoot(
accountSequence: _serialNumber,
personalPlantingCount: _personalPlantingCount,
teamPlantingCount: _teamPlantingCount,
directReferralCount: _directReferralCount,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -2374,7 +2577,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'我的伞下',
'我的团队',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
@ -2411,7 +2614,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
)
: TeamTreeWidget(
rootNode: rootNode,
rootNode: _teamTreeRootNode!,
referralService: ref.read(referralServiceProvider),
),
),