feat(pre-planting): 预种可结算收益结算 + 前端可结算金额修正

背景:
  预种奖励通过 planting-service → wallet-service allocateFunds 链路
  直接写入 wallet_accounts.settleable_usdt,不经过 reward-service。
  因此 reward-service 的一键结算(settleToBalance)无法覆盖预种部分,
  且 reward-service 的 summary.settleableUsdt 不包含预种金额。

改动:
1. wallet-service 新增 POST /wallet/settle-pre-planting 端点
   - 将 wallet 中剩余的 settleable_usdt 转入 available 余额
   - settleable_usdt=0 时幂等跳过,不创建空流水
   - 流水备注标注 [预种],payloadJson.source='pre-planting'

2. mobile-app 兑换页(trading_page):
   - 可结算金额改为从 wallet-service 的 wallet.rewards.settleableUsdt 取值
     (包含正常认种 + 预种的可结算部分,是唯一的 source of truth)
   - 一键结算流程改为两步串行:
     先调 reward-service settleToBalance(正常认种,不动现有逻辑),
     再调 wallet-service settle-pre-planting(预种部分,纯增量)

3. mobile-app 我的页(profile_page):
   - 并行加载新增 walletService.getMyWallet() 调用
   - _settleableUsdt 改为从 wallet.rewards.settleableUsdt 取值

不涉及的系统:
  - reward-service:零改动
  - planting-service:零改动
  - wallet-service 现有结算逻辑:零改动
  - admin-web:零改动

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-01 07:33:53 -08:00
parent 05e590ef04
commit 27cd72fe01
5 changed files with 196 additions and 19 deletions

View File

@ -176,6 +176,26 @@ export class WalletController {
return this.walletService.getSettleableRewards(user.accountSequence); return this.walletService.getSettleableRewards(user.accountSequence);
} }
/**
* [2026-03-01]
*
* reward-service reward-service
* mobile-app reward-service settleToBalance wallet settleable_usdt
* settleable_usdt=0 reward-service success
*/
@Post('settle-pre-planting')
@ApiOperation({ summary: '结算预种可结算收益', description: '将预种部分的可结算USDT转入钱包余额reward-service 一键结算后调用)' })
@ApiResponse({ status: 200, description: '结算结果' })
async settlePrePlanting(@CurrentUser() user: CurrentUserPayload): Promise<{
success: boolean;
settlementId: string;
settledAmount: number;
balanceAfter: number;
error?: string;
}> {
return this.walletService.settlePrePlantingToBalance(user.accountSequence);
}
@Get('expired-rewards') @Get('expired-rewards')
@ApiOperation({ summary: '查询已过期奖励列表', description: '获取用户的逐笔已过期奖励24h未领取' }) @ApiOperation({ summary: '查询已过期奖励列表', description: '获取用户的逐笔已过期奖励24h未领取' })
@ApiResponse({ status: 200, description: '已过期奖励列表' }) @ApiResponse({ status: 200, description: '已过期奖励列表' })

View File

@ -995,6 +995,88 @@ export class WalletApplicationService {
} }
} }
/**
* [2026-03-01]
*
*
* planting-service wallet-service allocateFunds wallet_accounts.settleable_usdt
* reward-service reward-service "一键结算"settleToBalance
* wallet reward-service
*
*
* mobile-app reward-service settleToBalance
*
*
*
* - reward-service
* - wallet_accounts.settleable_usdt usdt_available settleToBalance
* - settleable_usdt=0 success
*/
async settlePrePlantingToBalance(accountSequence: string): Promise<{
success: boolean;
settlementId: string;
settledAmount: number;
balanceAfter: number;
error?: string;
}> {
this.logger.log(`[settlePrePlantingToBalance] Processing for ${accountSequence}`);
try {
const wallet = await this.walletRepo.findByAccountSequence(accountSequence);
if (!wallet) {
throw new WalletNotFoundError(`accountSequence: ${accountSequence}`);
}
const remainingSettleable = wallet.rewards.settleableUsdt.value;
if (remainingSettleable <= 0) {
this.logger.log(`[settlePrePlantingToBalance] No remaining settleable for ${accountSequence}, skip`);
return { success: true, settlementId: '', settledAmount: 0, balanceAfter: wallet.balances.usdt.available.value };
}
const usdtAmount = Money.USDT(remainingSettleable);
const userId = wallet.userId.value;
const settlementId = `STL_PP_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const savedWallet = await this.unitOfWork.runInTransaction(async (tx) => {
wallet.settleToBalance(usdtAmount, settlementId);
const updated = await this.walletRepo.save(wallet, { tx });
const ledgerEntry = LedgerEntry.create({
accountSequence: wallet.accountSequence,
userId: UserId.create(userId),
entryType: LedgerEntryType.REWARD_SETTLED,
amount: usdtAmount,
balanceAfter: updated.balances.usdt.available,
refOrderId: settlementId,
memo: `[预种] 结算预种可结算收益 ${remainingSettleable.toFixed(2)} 绿积分到钱包余额`,
payloadJson: {
settlementType: 'PRE_PLANTING_SETTLE_TO_BALANCE',
source: 'pre-planting',
},
});
await this.ledgerRepo.save(ledgerEntry, { tx });
return updated;
});
await this.walletCacheService.invalidateWallet(userId);
this.logger.log(
`[settlePrePlantingToBalance] Settled ${remainingSettleable} USDT for ${accountSequence}, balanceAfter=${savedWallet.balances.usdt.available.value}`,
);
return {
success: true,
settlementId,
settledAmount: remainingSettleable,
balanceAfter: savedWallet.balances.usdt.available.value,
};
} catch (error) {
this.logger.error(`[settlePrePlantingToBalance] Failed for ${accountSequence}: ${error.message}`);
return { success: false, settlementId: '', settledAmount: 0, balanceAfter: 0, error: error.message };
}
}
/** /**
* - * -
* *

View File

@ -137,6 +137,30 @@ class ClaimRewardsResponse {
} }
} }
/// [2026-03-01]
class SettlePrePlantingResult {
final bool success;
final double settledAmount;
final double balanceAfter;
final String? error;
SettlePrePlantingResult({
required this.success,
required this.settledAmount,
required this.balanceAfter,
this.error,
});
factory SettlePrePlantingResult.fromJson(Map<String, dynamic> json) {
return SettlePrePlantingResult(
success: json['success'] ?? false,
settledAmount: (json['settledAmount'] ?? 0).toDouble(),
balanceAfter: (json['balanceAfter'] ?? 0).toDouble(),
error: json['error'],
);
}
}
/// ///
enum FeeType { enum FeeType {
fixed, // fixed, //
@ -442,6 +466,41 @@ class WalletService {
} }
} }
/// [2026-03-01]
///
/// POST /wallet/settle-pre-planting (wallet-service)
/// wallet settleable_usdt
/// reward-service settleToBalance
/// settleable_usdt success
Future<SettlePrePlantingResult> settlePrePlanting() async {
try {
debugPrint('[WalletService] ========== 结算预种可结算收益 ==========');
debugPrint('[WalletService] 请求: POST /wallet/settle-pre-planting');
final response = await _apiClient.post('/wallet/settle-pre-planting');
debugPrint('[WalletService] 响应状态码: ${response.statusCode}');
debugPrint('[WalletService] 响应数据: ${response.data}');
if (response.statusCode == 200 || response.statusCode == 201) {
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>? ?? responseData;
final result = SettlePrePlantingResult.fromJson(data);
debugPrint('[WalletService] 预种结算结果: success=${result.success}, settledAmount=${result.settledAmount}');
debugPrint('[WalletService] ================================');
return result;
}
debugPrint('[WalletService] 预种结算失败,状态码: ${response.statusCode}');
return SettlePrePlantingResult(success: false, settledAmount: 0, balanceAfter: 0, error: '请求失败: ${response.statusCode}');
} catch (e, stackTrace) {
debugPrint('[WalletService] !!!!!!!!!! 预种结算异常 !!!!!!!!!!');
debugPrint('[WalletService] 错误: $e');
debugPrint('[WalletService] 堆栈: $stackTrace');
return SettlePrePlantingResult(success: false, settledAmount: 0, balanceAfter: 0, error: e.toString());
}
}
/// ///
/// ///
/// POST /wallet/withdraw/send-sms (wallet-service) /// POST /wallet/withdraw/send-sms (wallet-service)

View File

@ -777,7 +777,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
final prePlantingService = ref.read(prePlantingServiceProvider); final prePlantingService = ref.read(prePlantingServiceProvider);
final walletService = ref.read(walletServiceProvider); final walletService = ref.read(walletServiceProvider);
// + + // + + +
debugPrint('[ProfilePage] 并行调用 reward-service + planting-service(预种) + wallet-service...'); debugPrint('[ProfilePage] 并行调用 reward-service + planting-service(预种) + wallet-service...');
final results = await Future.wait([ final results = await Future.wait([
rewardService.getMyRewardSummary(), rewardService.getMyRewardSummary(),
@ -786,6 +786,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
rewardService.getExpiredRewards(), rewardService.getExpiredRewards(),
walletService.getLedgerStatistics(), walletService.getLedgerStatistics(),
prePlantingService.getMyRewards(), // prePlantingService.getMyRewards(), //
walletService.getMyWallet(), // [2026-03-01] settleable_usdt
]); ]);
final summary = results[0] as RewardSummary; final summary = results[0] as RewardSummary;
@ -794,6 +795,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
final expiredRewards = results[3] as List<ExpiredRewardItem>; final 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;
// //
// SettleableRewardItem // SettleableRewardItem
@ -842,8 +844,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
setState(() { setState(() {
_pendingUsdt = summary.pendingUsdt; _pendingUsdt = summary.pendingUsdt;
_pendingPower = summary.pendingHashpower; _pendingPower = summary.pendingHashpower;
// wallet-service summary.settleableUsdt // [2026-03-01] wallet-service
_settleableUsdt = summary.settleableUsdt; // wallet_accounts.settleable_usdt + source of truth
// reward-service summary.settleableUsdt
_settleableUsdt = walletInfo.rewards.settleableUsdt;
// 使 // 使
_settledUsdt = settledFromLedger; _settledUsdt = settledFromLedger;
_expiredUsdt = summary.expiredTotalUsdt; _expiredUsdt = summary.expiredTotalUsdt;

View File

@ -38,21 +38,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
try { try {
debugPrint('[TradingPage] 开始加载数据...'); debugPrint('[TradingPage] 开始加载数据...');
// reward-service settleableUsdt wallet-service // [2026-03-01] wallet-service
final rewardService = ref.read(rewardServiceProvider); // wallet_accounts.settleable_usdt + source of truth
// reward-service summary.settleableUsdt
final walletService = ref.read(walletServiceProvider); final walletService = ref.read(walletServiceProvider);
final wallet = await walletService.getMyWallet();
final results = await Future.wait([
rewardService.getMyRewardSummary(),
walletService.getMyWallet(),
]);
final summary = results[0] as dynamic;
final wallet = results[1] as dynamic;
if (mounted) { if (mounted) {
setState(() { setState(() {
_settleableAmount = summary.settleableUsdt; _settleableAmount = wallet.rewards.settleableUsdt;
_dstBalance = wallet.balances.dst.available; _dstBalance = wallet.balances.dst.available;
_usdtBalance = wallet.balances.usdt.available; _usdtBalance = wallet.balances.usdt.available;
_isLoading = false; _isLoading = false;
@ -128,25 +122,43 @@ class _TradingPageState extends ConsumerState<TradingPage> {
debugPrint('[TradingPage] 开始结算...'); debugPrint('[TradingPage] 开始结算...');
debugPrint('[TradingPage] 金额: $_settleableAmount 绿积分'); debugPrint('[TradingPage] 金额: $_settleableAmount 绿积分');
// 使 reward-service settleToBalance API // reward-service
final rewardService = ref.read(rewardServiceProvider); final rewardService = ref.read(rewardServiceProvider);
final result = await rewardService.settleToBalance(); final result = await rewardService.settleToBalance();
double totalSettled = 0;
if (result.success) { if (result.success) {
debugPrint('[TradingPage] 结算成功: settlementId=${result.settlementId}, amount=${result.settledUsdtAmount}'); totalSettled += result.settledUsdtAmount;
debugPrint('[TradingPage] 正常认种结算成功: ${result.settledUsdtAmount}');
} else {
// reward-service "没有可结算的收益"
debugPrint('[TradingPage] 正常认种结算: ${result.error ?? "无可结算"}');
}
// [2026-03-01] wallet-service
// wallet settleable_usdt 0
final walletService = ref.read(walletServiceProvider);
final ppResult = await walletService.settlePrePlanting();
if (ppResult.success && ppResult.settledAmount > 0) {
totalSettled += ppResult.settledAmount;
debugPrint('[TradingPage] 预种结算成功: ${ppResult.settledAmount}');
} else {
debugPrint('[TradingPage] 预种结算: ${ppResult.error ?? "无预种可结算"}');
}
if (totalSettled > 0) {
debugPrint('[TradingPage] 总结算金额: $totalSettled');
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('结算成功,${result.settledUsdtAmount.toStringAsFixed(2)} 绿积分已转入钱包余额'), content: Text('结算成功,${totalSettled.toStringAsFixed(2)} 绿积分已转入钱包余额'),
backgroundColor: const Color(0xFFD4AF37), backgroundColor: const Color(0xFFD4AF37),
), ),
); );
//
_loadWalletData(); _loadWalletData();
} }
} else { } else {
throw Exception(result.error ?? '结算失败'); throw Exception('没有可结算的收益');
} }
} catch (e) { } catch (e) {
debugPrint('[TradingPage] 结算失败: $e'); debugPrint('[TradingPage] 结算失败: $e');