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:
parent
05e590ef04
commit
27cd72fe01
|
|
@ -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: '已过期奖励列表' })
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分配资金 - 用于认种订单支付后的资金分配
|
* 分配资金 - 用于认种订单支付后的资金分配
|
||||||
* 支持分配给用户钱包或系统账户
|
* 支持分配给用户钱包或系统账户
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue