diff --git a/backend/services/wallet-service/src/api/controllers/wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/wallet.controller.ts index aacb52bd..e5084406 100644 --- a/backend/services/wallet-service/src/api/controllers/wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/wallet.controller.ts @@ -176,6 +176,26 @@ export class WalletController { 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') @ApiOperation({ summary: '查询已过期奖励列表', description: '获取用户的逐笔已过期奖励(24h未领取)' }) @ApiResponse({ status: 200, description: '已过期奖励列表' }) diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 35f3878a..2abda856 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -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 }; + } + } + /** * 分配资金 - 用于认种订单支付后的资金分配 * 支持分配给用户钱包或系统账户 diff --git a/frontend/mobile-app/lib/core/services/wallet_service.dart b/frontend/mobile-app/lib/core/services/wallet_service.dart index 1b0a27bc..0601a8fb 100644 --- a/frontend/mobile-app/lib/core/services/wallet_service.dart +++ b/frontend/mobile-app/lib/core/services/wallet_service.dart @@ -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 json) { + return SettlePrePlantingResult( + success: json['success'] ?? false, + settledAmount: (json['settledAmount'] ?? 0).toDouble(), + balanceAfter: (json['balanceAfter'] ?? 0).toDouble(), + error: json['error'], + ); + } +} + /// 手续费类型 enum FeeType { 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 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; + final data = responseData['data'] as Map? ?? 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) 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 787b3053..a82ede06 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 @@ -777,7 +777,7 @@ class _ProfilePageState extends ConsumerState { final prePlantingService = ref.read(prePlantingServiceProvider); final walletService = ref.read(walletServiceProvider); - // 并行加载:正常认种奖励 + 预种奖励 + 流水统计 + // 并行加载:正常认种奖励 + 预种奖励 + 流水统计 + 钱包信息 debugPrint('[ProfilePage] 并行调用 reward-service + planting-service(预种) + wallet-service...'); final results = await Future.wait([ rewardService.getMyRewardSummary(), @@ -786,6 +786,7 @@ class _ProfilePageState extends ConsumerState { rewardService.getExpiredRewards(), walletService.getLedgerStatistics(), prePlantingService.getMyRewards(), // 预种奖励 + walletService.getMyWallet(), // [2026-03-01] 新增:用于获取准确的 settleable_usdt ]); final summary = results[0] as RewardSummary; @@ -794,6 +795,7 @@ class _ProfilePageState extends ConsumerState { final expiredRewards = results[3] as List; final ledgerStats = results[4] as LedgerStatistics; final prePlantingRewards = results[5] as PrePlantingMyRewards; + final walletInfo = results[6] as WalletResponse; // 合并预种可结算奖励到列表中 // 预种奖励转为 SettleableRewardItem 格式,与正常认种统一展示 @@ -842,8 +844,10 @@ class _ProfilePageState extends ConsumerState { setState(() { _pendingUsdt = summary.pendingUsdt; _pendingPower = summary.pendingHashpower; - // 预种可结算已包含在 wallet-service 的 summary.settleableUsdt 中,不再重复加 - _settleableUsdt = summary.settleableUsdt; + // [2026-03-01] 可结算金额统一从 wallet-service 取值 + // wallet_accounts.settleable_usdt 包含正常认种 + 预种的可结算部分,是唯一的 source of truth + // reward-service 的 summary.settleableUsdt 只包含正常认种部分,不包含预种 + _settleableUsdt = walletInfo.rewards.settleableUsdt; // 使用流水统计的数据,更准确 _settledUsdt = settledFromLedger; _expiredUsdt = summary.expiredTotalUsdt; diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart index d8a666c2..e4e9c99d 100644 --- a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart @@ -38,21 +38,15 @@ class _TradingPageState extends ConsumerState { try { debugPrint('[TradingPage] 开始加载数据...'); - // reward-service 的 settleableUsdt 已包含预种可结算部分(均由 wallet-service 管理) - final rewardService = ref.read(rewardServiceProvider); + // [2026-03-01] 可结算金额统一从 wallet-service 取值 + // wallet_accounts.settleable_usdt 包含正常认种 + 预种的可结算部分,是唯一的 source of truth + // reward-service 的 summary.settleableUsdt 只包含正常认种部分,不包含预种 final walletService = ref.read(walletServiceProvider); - - final results = await Future.wait([ - rewardService.getMyRewardSummary(), - walletService.getMyWallet(), - ]); - - final summary = results[0] as dynamic; - final wallet = results[1] as dynamic; + final wallet = await walletService.getMyWallet(); if (mounted) { setState(() { - _settleableAmount = summary.settleableUsdt; + _settleableAmount = wallet.rewards.settleableUsdt; _dstBalance = wallet.balances.dst.available; _usdtBalance = wallet.balances.usdt.available; _isLoading = false; @@ -128,25 +122,43 @@ class _TradingPageState extends ConsumerState { debugPrint('[TradingPage] 开始结算...'); debugPrint('[TradingPage] 金额: $_settleableAmount 绿积分'); - // 使用 reward-service 的 settleToBalance API + // 第一步:调用 reward-service 结算正常认种部分(不动现有逻辑) final rewardService = ref.read(rewardServiceProvider); final result = await rewardService.settleToBalance(); + double totalSettled = 0; 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) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('结算成功,${result.settledUsdtAmount.toStringAsFixed(2)} 绿积分已转入钱包余额'), + content: Text('结算成功,${totalSettled.toStringAsFixed(2)} 绿积分已转入钱包余额'), backgroundColor: const Color(0xFFD4AF37), ), ); - // 刷新钱包数据 _loadWalletData(); } } else { - throw Exception(result.error ?? '结算失败'); + throw Exception('没有可结算的收益'); } } catch (e) { debugPrint('[TradingPage] 结算失败: $e');