From 036696878fdb9625e018a34459a965b27f10b3a1 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 3 Jan 2026 04:29:38 -0800 Subject: [PATCH] feat(settlement): implement settle-to-balance with detailed source tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete settlement-to-balance feature that transfers settleable earnings directly to wallet USDT balance (no currency swap). Key changes: Backend (wallet-service): - Add SettleToBalanceCommand for settlement operations - Add settleToBalance method to WalletAccountAggregate - Add settleToBalance application service with ledger recording - Add internal API endpoint POST /api/v1/wallets/settle-to-balance Backend (reward-service): - Add settleToBalance client method for wallet-service communication - Add settleRewardsToBalance application service method - Add user-facing API endpoint POST /rewards/settle-to-balance - Build detailed settlement memo with source user tracking per reward Frontend (mobile-app): - Add SettleToBalanceResult model class - Add settleToBalance() method to RewardService - Update pending_actions_page to handle SETTLE_REWARDS action - Add completion detection via settleableUsdt balance check Settlement memo now includes detailed breakdown by right type with source user accountSequence for each reward entry, e.g.: 结算 1000.00 绿积分到钱包余额 涉及 5 笔奖励 - SHARE_RIGHT: 500.00 绿积分 来自 D2512120001: 288.00 绿积分 来自 D2512120002: 212.00 绿积分 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 3 +- .../src/api/controllers/reward.controller.ts | 33 ++++- .../services/reward-application.service.ts | 123 ++++++++++++++++++ .../wallet-service/wallet-service.client.ts | 55 ++++++++ .../controllers/internal-wallet.controller.ts | 28 ++++ .../src/application/commands/index.ts | 1 + .../commands/settle-to-balance.command.ts | 102 +++++++++++++++ .../services/wallet-application.service.ts | 80 ++++++++++++ .../aggregates/wallet-account.aggregate.ts | 38 ++++++ .../src/types/pending-action.types.ts | 2 + .../lib/core/services/reward_service.dart | 97 ++++++++++++++ .../pages/pending_actions_page.dart | 60 ++++++++- 12 files changed, 613 insertions(+), 9 deletions(-) create mode 100644 backend/services/wallet-service/src/application/commands/settle-to-balance.command.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 94edf3d7..7d1b1995 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -573,7 +573,8 @@ "Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\identity-service\\\\src\\\\infrastructure\\\\persistence\\\\repositories\\\\*.ts\")", "Bash(head:*)", "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(pending-actions\\): add user pending actions system\n\nAdd a fully optional pending actions system that allows admins to configure\nspecific tasks that users must complete after login.\n\nBackend \\(identity-service\\):\n- Add UserPendingAction model to Prisma schema\n- Add migration for user_pending_actions table\n- Add PendingActionService with full CRUD operations\n- Add user-facing API \\(GET list, POST complete\\)\n- Add admin API \\(CRUD, batch create\\)\n\nAdmin Web:\n- Add pending actions management page\n- Support single/batch create, edit, cancel, delete\n- View action details including completion time\n- Filter by userId, actionCode, status\n\nFlutter Mobile App:\n- Add PendingActionService and PendingActionCheckService\n- Add PendingActionsPage for forced task execution\n- Integrate into splash_page login flow\n- Users must complete all pending tasks in priority order\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", - "Bash(npm run type-check:*)" + "Bash(npm run type-check:*)", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(settlement\\): implement settle-to-balance with detailed source tracking\n\nAdd complete settlement-to-balance feature that transfers settleable\nearnings directly to wallet USDT balance \\(no currency swap\\). Key changes:\n\nBackend \\(wallet-service\\):\n- Add SettleToBalanceCommand for settlement operations\n- Add settleToBalance method to WalletAccountAggregate\n- Add settleToBalance application service with ledger recording\n- Add internal API endpoint POST /api/v1/wallets/settle-to-balance\n\nBackend \\(reward-service\\):\n- Add settleToBalance client method for wallet-service communication\n- Add settleRewardsToBalance application service method\n- Add user-facing API endpoint POST /rewards/settle-to-balance\n- Build detailed settlement memo with source user tracking per reward\n\nFrontend \\(mobile-app\\):\n- Add SettleToBalanceResult model class\n- Add settleToBalance\\(\\) method to RewardService\n- Update pending_actions_page to handle SETTLE_REWARDS action\n- Add completion detection via settleableUsdt balance check\n\nSettlement memo now includes detailed breakdown by right type with\nsource user accountSequence for each reward entry, e.g.:\n 结算 1000.00 绿积分到钱包余额\n 涉及 5 笔奖励\n - SHARE_RIGHT: 500.00 绿积分\n 来自 D2512120001: 288.00 绿积分\n 来自 D2512120002: 212.00 绿积分\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" ], "deny": [], "ask": [] diff --git a/backend/services/reward-service/src/api/controllers/reward.controller.ts b/backend/services/reward-service/src/api/controllers/reward.controller.ts index 5ebb336a..4612b5ea 100644 --- a/backend/services/reward-service/src/api/controllers/reward.controller.ts +++ b/backend/services/reward-service/src/api/controllers/reward.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query, UseGuards, Request, DefaultValuePipe, ParseIntPipe } from '@nestjs/common'; +import { Controller, Get, Post, Query, UseGuards, Request, DefaultValuePipe, ParseIntPipe } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard'; import { RewardApplicationService } from '../../application/services/reward-application.service'; @@ -66,4 +66,35 @@ export class RewardController { const accountSequence = req.user.accountSequence; return this.rewardService.getSettleableRewards(accountSequence); } + + @Post('settle-to-balance') + @ApiOperation({ + summary: '结算到余额', + description: '将可结算收益直接转入钱包USDT余额(无币种兑换)', + }) + @ApiResponse({ + status: 200, + description: '结算结果', + schema: { + type: 'object', + properties: { + success: { type: 'boolean', description: '是否成功' }, + settlementId: { type: 'string', description: '结算ID' }, + settledUsdtAmount: { type: 'number', description: '结算的USDT总额' }, + settledHashpowerAmount: { type: 'number', description: '结算的算力总额' }, + rewardCount: { type: 'number', description: '涉及的奖励数量' }, + breakdown: { + type: 'object', + description: '按权益类型分解', + additionalProperties: { type: 'number' }, + }, + balanceAfter: { type: 'number', description: '结算后USDT余额' }, + error: { type: 'string', description: '错误信息(如果失败)' }, + }, + }, + }) + async settleToBalance(@Request() req) { + const accountSequence = req.user.accountSequence; + return this.rewardService.settleRewardsToBalance(accountSequence); + } } diff --git a/backend/services/reward-service/src/application/services/reward-application.service.ts b/backend/services/reward-service/src/application/services/reward-application.service.ts index f582730c..b7f2a15e 100644 --- a/backend/services/reward-service/src/application/services/reward-application.service.ts +++ b/backend/services/reward-service/src/application/services/reward-application.service.ts @@ -390,6 +390,129 @@ export class RewardApplicationService { }; } + /** + * 结算到余额 (可结算USDT → 钱包USDT余额,无币种兑换) + * 将可结算收益直接转入钱包 USDT 余额,并记录详细的来源信息 + */ + async settleRewardsToBalance(accountSequence: string): Promise<{ + success: boolean; + settlementId?: string; + settledUsdtAmount: number; + settledHashpowerAmount: number; + rewardCount: number; + breakdown?: Record; + balanceAfter?: number; + error?: string; + }> { + this.logger.log(`Settling rewards to balance for accountSequence ${accountSequence}`); + + // 1. 获取可结算奖励 + const settleableRewards = await this.rewardLedgerEntryRepository.findSettleableByAccountSequence(accountSequence); + + if (settleableRewards.length === 0) { + return { + success: false, + settledUsdtAmount: 0, + settledHashpowerAmount: 0, + rewardCount: 0, + error: '没有可结算的收益', + }; + } + + // 2. 计算总金额和按权益类型分解 + const totalUsdt = settleableRewards.reduce((sum, r) => sum + r.usdtAmount.amount, 0); + const totalHashpower = settleableRewards.reduce((sum, r) => sum + r.hashpowerAmount.value, 0); + const rewardEntryIds = settleableRewards.map(r => r.id?.toString() || ''); + + // 按权益类型分解 + const breakdown: Record = {}; + for (const reward of settleableRewards) { + const rightType = reward.rewardSource.rightType; + breakdown[rightType] = (breakdown[rightType] || 0) + reward.usdtAmount.amount; + } + + // 3. 构建详细的结算备注(按权益类型分组,列出每笔奖励的来源用户) + const memoLines = [ + `结算 ${totalUsdt.toFixed(2)} 绿积分到钱包余额`, + `涉及 ${settleableRewards.length} 笔奖励`, + ]; + + // 按权益类型分组,收集每笔奖励的来源用户和金额 + const rewardsByType: Record> = {}; + for (const reward of settleableRewards) { + const rightType = reward.rewardSource.rightType; + if (!rewardsByType[rightType]) { + rewardsByType[rightType] = []; + } + // 从 memo 中提取来源用户,格式如 "分享权益:来自用户D2512120001的认种" + let sourceUser = reward.rewardSource.sourceUserId.toString(); + const memoMatch = reward.memo.match(/来自用户([A-Z]\d+)的认种/); + if (memoMatch) { + sourceUser = memoMatch[1]; + } + rewardsByType[rightType].push({ + sourceUser, + amount: reward.usdtAmount.amount, + }); + } + + // 输出按权益类型分组的详细信息 + for (const [rightType, rewards] of Object.entries(rewardsByType)) { + const typeTotal = rewards.reduce((sum, r) => sum + r.amount, 0); + memoLines.push(` - ${rightType}: ${typeTotal.toFixed(2)} 绿积分`); + for (const r of rewards) { + memoLines.push(` 来自 ${r.sourceUser}: ${r.amount.toFixed(2)} 绿积分`); + } + } + const memo = memoLines.join('\n'); + + // 4. 调用钱包服务执行结算 + const walletResult = await this.walletService.settleToBalance({ + accountSequence, + usdtAmount: totalUsdt, + rewardEntryIds, + breakdown, + memo, + }); + + if (!walletResult.success) { + return { + success: false, + settledUsdtAmount: totalUsdt, + settledHashpowerAmount: totalHashpower, + rewardCount: settleableRewards.length, + error: walletResult.error, + }; + } + + // 5. 更新奖励状态为已结算 + for (const reward of settleableRewards) { + reward.settle('USDT', reward.usdtAmount.amount); + await this.rewardLedgerEntryRepository.save(reward); + await this.eventPublisher.publishAll(reward.domainEvents); + reward.clearDomainEvents(); + } + + // 6. 更新汇总数据 + const summary = await this.rewardSummaryRepository.findByAccountSequence(accountSequence); + if (summary) { + summary.settle(Money.USDT(totalUsdt), Hashpower.create(totalHashpower)); + await this.rewardSummaryRepository.save(summary); + } + + this.logger.log(`Settled ${totalUsdt} USDT to balance for accountSequence ${accountSequence}, ${settleableRewards.length} rewards`); + + return { + success: true, + settlementId: walletResult.settlementId, + settledUsdtAmount: totalUsdt, + settledHashpowerAmount: totalHashpower, + rewardCount: settleableRewards.length, + breakdown, + balanceAfter: walletResult.balanceAfter, + }; + } + /** * 过期到期的待领取奖励 (定时任务调用) */ diff --git a/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts b/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts index a11de4d9..9b0fafc8 100644 --- a/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts +++ b/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts @@ -144,6 +144,61 @@ export class WalletServiceClient { } } + /** + * 结算到余额 (可结算USDT → 钱包USDT余额,无币种兑换) + * 将可结算收益直接转入钱包 USDT 余额 + */ + async settleToBalance(params: { + accountSequence: string; + usdtAmount: number; + rewardEntryIds: string[]; + breakdown?: Record; + memo?: string; + }): Promise<{ + success: boolean; + settlementId?: string; + settledAmount?: number; + balanceAfter?: number; + error?: string; + }> { + try { + this.logger.log(`Settling ${params.usdtAmount} USDT to balance for ${params.accountSequence}`); + + const response = await fetch(`${this.baseUrl}/api/v1/wallets/settle-to-balance`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + this.logger.error(`Failed to settle to balance for ${params.accountSequence}:`, errorData); + return { + success: false, + error: errorData.message || `Settlement failed with status ${response.status}`, + }; + } + + const data = await response.json(); + this.logger.log(`Successfully settled to balance for ${params.accountSequence}`); + return { + success: data.success, + settlementId: data.settlementId, + settledAmount: data.settledAmount, + balanceAfter: data.balanceAfter, + error: data.error, + }; + } catch (error) { + this.logger.error(`Error settling to balance for ${params.accountSequence}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + /** * 执行资金分配 * 将认种订单的资金分配到各个目标账户 diff --git a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts index 84879faa..e2255fce 100644 --- a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts @@ -166,4 +166,32 @@ export class InternalWalletController { return result; } + + @Post('settle-to-balance') + @Public() + @ApiOperation({ summary: '结算到余额(内部API) - 可结算USDT转入钱包余额' }) + @ApiResponse({ status: 200, description: '结算结果' }) + async settleToBalance( + @Body() dto: { + accountSequence: string; + usdtAmount: number; + rewardEntryIds: string[]; + breakdown?: Record; + memo?: string; + }, + ) { + this.logger.log(`========== settle-to-balance 请求 ==========`); + this.logger.log(`请求参数: ${JSON.stringify(dto)}`); + + const result = await this.walletService.settleToBalance({ + accountSequence: dto.accountSequence, + usdtAmount: dto.usdtAmount, + rewardEntryIds: dto.rewardEntryIds, + breakdown: dto.breakdown, + memo: dto.memo, + }); + + this.logger.log(`结算结果: ${JSON.stringify(result)}`); + return result; + } } diff --git a/backend/services/wallet-service/src/application/commands/index.ts b/backend/services/wallet-service/src/application/commands/index.ts index 28aa0aee..dc7492ce 100644 --- a/backend/services/wallet-service/src/application/commands/index.ts +++ b/backend/services/wallet-service/src/application/commands/index.ts @@ -6,5 +6,6 @@ export * from './unfreeze-for-planting.command'; export * from './add-rewards.command'; export * from './claim-rewards.command'; export * from './settle-rewards.command'; +export * from './settle-to-balance.command'; export * from './allocate-funds.command'; export * from './request-withdrawal.command'; diff --git a/backend/services/wallet-service/src/application/commands/settle-to-balance.command.ts b/backend/services/wallet-service/src/application/commands/settle-to-balance.command.ts new file mode 100644 index 00000000..8b5c73a2 --- /dev/null +++ b/backend/services/wallet-service/src/application/commands/settle-to-balance.command.ts @@ -0,0 +1,102 @@ +/** + * 结算到钱包余额命令 + * 将可结算收益直接转入钱包 USDT 余额(同币种,无兑换) + */ +export class SettleToBalanceCommand { + constructor( + /** + * 用户账户序列号(如 D25122700022) + */ + public readonly accountSequence: string, + + /** + * 用户ID(数字形式,用于兼容) + */ + public readonly userId: string, + ) {} +} + +/** + * 结算来源信息(单条奖励的来源) + */ +export interface SettlementSourceInfo { + /** + * 奖励流水ID + */ + rewardEntryId: string; + + /** + * 来源订单号(认种订单号) + */ + sourceOrderNo: string; + + /** + * 来源用户ID(认种者) + */ + sourceUserId: string; + + /** + * 来源用户账户序列号 + */ + sourceAccountSequence?: string; + + /** + * 权益类型(SHARE_RIGHT, TEAM_RIGHT 等) + */ + rightType: string; + + /** + * USDT 金额 + */ + usdtAmount: number; + + /** + * 算力金额 + */ + hashpowerAmount: number; + + /** + * 备注 + */ + memo?: string; +} + +/** + * 结算结果 + */ +export interface SettleToBalanceResult { + /** + * 是否成功 + */ + success: boolean; + + /** + * 结算记录ID + */ + settlementId?: string; + + /** + * 结算的 USDT 总额 + */ + settledUsdtAmount: number; + + /** + * 结算的算力总额 + */ + settledHashpowerAmount: number; + + /** + * 涉及的奖励条目数量 + */ + rewardCount: number; + + /** + * 按权益类型分解 + */ + breakdown?: Record; + + /** + * 错误信息(如果失败) + */ + error?: string; +} 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 71d450fd..86decef1 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 @@ -886,6 +886,86 @@ export class WalletApplicationService { return savedOrder.id.toString(); } + /** + * 结算到余额 (可结算USDT → 钱包USDT余额,无币种兑换) + * 将可结算收益直接转入钱包 USDT 余额 + * + * @param params 结算参数 + * @returns 结算结果 + */ + async settleToBalance(params: { + accountSequence: string; + usdtAmount: number; + rewardEntryIds: string[]; + breakdown?: Record; + memo?: string; + }): Promise<{ + success: boolean; + settlementId: string; + settledAmount: number; + balanceAfter: number; + error?: string; + }> { + this.logger.log(`Settling ${params.usdtAmount} USDT to balance for ${params.accountSequence}`); + + try { + // 1. 查找钱包 + const wallet = await this.walletRepo.findByAccountSequence(params.accountSequence); + if (!wallet) { + throw new WalletNotFoundError(`accountSequence: ${params.accountSequence}`); + } + + const usdtAmount = Money.USDT(params.usdtAmount); + const userId = wallet.userId.value; + + // 2. 生成结算ID + const settlementId = `STL_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // 3. 执行钱包结算 + wallet.settleToBalance(usdtAmount, settlementId); + await this.walletRepo.save(wallet); + + // 4. 记录账本流水(含详细来源信息) + const ledgerEntry = LedgerEntry.create({ + accountSequence: wallet.accountSequence, + userId: UserId.create(userId), + entryType: LedgerEntryType.REWARD_SETTLED, + amount: usdtAmount, + balanceAfter: wallet.balances.usdt.available, + refOrderId: settlementId, + memo: params.memo || `结算 ${params.usdtAmount} 绿积分到钱包余额`, + payloadJson: { + settlementType: 'SETTLE_TO_BALANCE', + rewardEntryIds: params.rewardEntryIds, + rewardCount: params.rewardEntryIds.length, + breakdown: params.breakdown, + }, + }); + await this.ledgerRepo.save(ledgerEntry); + + // 5. 使缓存失效 + await this.walletCacheService.invalidateWallet(userId); + + this.logger.log(`Successfully settled ${params.usdtAmount} USDT to balance for ${params.accountSequence}`); + + return { + success: true, + settlementId, + settledAmount: params.usdtAmount, + balanceAfter: wallet.balances.usdt.available.value, + }; + } catch (error) { + this.logger.error(`Failed to settle to balance for ${params.accountSequence}: ${error.message}`); + return { + success: false, + settlementId: '', + settledAmount: 0, + balanceAfter: 0, + error: error.message, + }; + } + } + /** * 分配资金 - 用于认种订单支付后的资金分配 * 支持分配给用户钱包或系统账户 diff --git a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts index 0e8c501d..24440210 100644 --- a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts @@ -425,6 +425,44 @@ export class WalletAccount { })); } + /** + * 结算到余额 (可结算USDT → 钱包USDT余额,无币种兑换) + * 用于将可结算收益直接转入钱包 USDT 余额 + */ + settleToBalance(usdtAmount: Money, settlementId: string): void { + this.ensureActive(); + + if (this._rewards.settleableUsdt.lessThan(usdtAmount)) { + throw new InsufficientBalanceError( + 'USDT (settleable)', + usdtAmount.value.toString(), + this._rewards.settleableUsdt.value.toString(), + ); + } + + // 扣减可结算USDT + this._rewards = { + ...this._rewards, + settleableUsdt: this._rewards.settleableUsdt.subtract(usdtAmount), + settledTotalUsdt: this._rewards.settledTotalUsdt.add(usdtAmount), + }; + + // 增加 USDT 可用余额 + this._balances.usdt = this._balances.usdt.add(usdtAmount); + this._updatedAt = new Date(); + + // 发布结算完成事件 + this.addDomainEvent(new SettlementCompletedEvent({ + userId: this._userId.toString(), + walletId: this._walletId.toString(), + settlementOrderId: settlementId, + usdtAmount: usdtAmount.value.toString(), + settleCurrency: 'USDT', + receivedAmount: usdtAmount.value.toString(), + swapTxHash: undefined, + })); + } + // 冻结钱包 freezeWallet(): void { if (this._status === WalletStatus.FROZEN) { diff --git a/frontend/admin-web/src/types/pending-action.types.ts b/frontend/admin-web/src/types/pending-action.types.ts index b4b330c4..84d9a769 100644 --- a/frontend/admin-web/src/types/pending-action.types.ts +++ b/frontend/admin-web/src/types/pending-action.types.ts @@ -13,6 +13,7 @@ export const ACTION_CODES = { SETTLE_REWARDS: 'SETTLE_REWARDS', // 结算奖励 BIND_PHONE: 'BIND_PHONE', // 绑定手机 FORCE_KYC: 'FORCE_KYC', // 强制 KYC + SIGN_CONTRACT: 'SIGN_CONTRACT', // 签订合同 CUSTOM_NOTICE: 'CUSTOM_NOTICE', // 自定义通知 } as const; @@ -105,6 +106,7 @@ export const ACTION_CODE_OPTIONS = [ { value: ACTION_CODES.SETTLE_REWARDS, label: '结算奖励' }, { value: ACTION_CODES.BIND_PHONE, label: '绑定手机' }, { value: ACTION_CODES.FORCE_KYC, label: '强制 KYC' }, + { value: ACTION_CODES.SIGN_CONTRACT, label: '签订合同' }, { value: ACTION_CODES.CUSTOM_NOTICE, label: '自定义通知' }, ] as const; diff --git a/frontend/mobile-app/lib/core/services/reward_service.dart b/frontend/mobile-app/lib/core/services/reward_service.dart index c66b30a2..9e787cc8 100644 --- a/frontend/mobile-app/lib/core/services/reward_service.dart +++ b/frontend/mobile-app/lib/core/services/reward_service.dart @@ -144,6 +144,49 @@ class ExpiredRewardItem { String get rightTypeName => allocationTypeName; } +/// 结算到余额结果 +class SettleToBalanceResult { + final bool success; + final String? settlementId; + final double settledUsdtAmount; + final double settledHashpowerAmount; + final int rewardCount; + final Map? breakdown; + final double? balanceAfter; + final String? error; + + SettleToBalanceResult({ + required this.success, + this.settlementId, + required this.settledUsdtAmount, + required this.settledHashpowerAmount, + required this.rewardCount, + this.breakdown, + this.balanceAfter, + this.error, + }); + + factory SettleToBalanceResult.fromJson(Map json) { + Map? breakdown; + if (json['breakdown'] != null) { + breakdown = (json['breakdown'] as Map).map( + (key, value) => MapEntry(key, (value ?? 0).toDouble()), + ); + } + + return SettleToBalanceResult( + success: json['success'] ?? false, + settlementId: json['settlementId'], + settledUsdtAmount: (json['settledUsdtAmount'] ?? 0).toDouble(), + settledHashpowerAmount: (json['settledHashpowerAmount'] ?? 0).toDouble(), + rewardCount: (json['rewardCount'] ?? 0).toInt(), + breakdown: breakdown, + balanceAfter: json['balanceAfter']?.toDouble(), + error: json['error'], + ); + } +} + /// 奖励汇总信息 (从 reward-service 获取) class RewardSummary { final double pendingUsdt; @@ -357,6 +400,60 @@ class RewardService { } } + /// 结算到余额 + /// + /// 调用 POST /rewards/settle-to-balance (reward-service) + /// 将可结算收益直接转入钱包 USDT 余额(无币种兑换) + Future settleToBalance() async { + try { + debugPrint('[RewardService] ========== 结算到余额 =========='); + debugPrint('[RewardService] 请求: POST /rewards/settle-to-balance'); + + final response = await _apiClient.post('/rewards/settle-to-balance'); + + debugPrint('[RewardService] 响应状态码: ${response.statusCode}'); + debugPrint('[RewardService] 响应数据: ${response.data}'); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = response.data as Map; + // 解包可能的 data 字段 + final data = responseData['data'] as Map? ?? responseData; + final result = SettleToBalanceResult.fromJson(data); + + debugPrint('[RewardService] 结算结果:'); + debugPrint('[RewardService] - success: ${result.success}'); + debugPrint('[RewardService] - settlementId: ${result.settlementId}'); + debugPrint('[RewardService] - settledUsdtAmount: ${result.settledUsdtAmount}'); + debugPrint('[RewardService] - rewardCount: ${result.rewardCount}'); + debugPrint('[RewardService] - balanceAfter: ${result.balanceAfter}'); + debugPrint('[RewardService] ================================'); + + return result; + } + + debugPrint('[RewardService] 请求失败,状态码: ${response.statusCode}'); + debugPrint('[RewardService] 响应内容: ${response.data}'); + return SettleToBalanceResult( + success: false, + settledUsdtAmount: 0, + settledHashpowerAmount: 0, + rewardCount: 0, + error: '结算失败: ${response.statusCode}', + ); + } catch (e, stackTrace) { + debugPrint('[RewardService] !!!!!!!!!! 结算异常 !!!!!!!!!!'); + debugPrint('[RewardService] 错误: $e'); + debugPrint('[RewardService] 堆栈: $stackTrace'); + return SettleToBalanceResult( + success: false, + settledUsdtAmount: 0, + settledHashpowerAmount: 0, + rewardCount: 0, + error: e.toString(), + ); + } + } + /// 获取已过期奖励列表 /// /// 调用 GET /wallet/expired-rewards (wallet-service) diff --git a/frontend/mobile-app/lib/features/pending_actions/presentation/pages/pending_actions_page.dart b/frontend/mobile-app/lib/features/pending_actions/presentation/pages/pending_actions_page.dart index b66cd59d..d51c329b 100644 --- a/frontend/mobile-app/lib/features/pending_actions/presentation/pages/pending_actions_page.dart +++ b/frontend/mobile-app/lib/features/pending_actions/presentation/pages/pending_actions_page.dart @@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../core/services/pending_action_service.dart'; +import '../../../../core/services/contract_signing_service.dart'; +import '../../../../core/services/reward_service.dart'; import '../../../../routes/route_paths.dart'; import '../../../kyc/data/kyc_service.dart'; @@ -114,10 +116,15 @@ class _PendingActionsPageState extends ConsumerState { return result == true; case 'SETTLE_REWARDS': - // 跳转到交易页面进行结算 - // 可以从 actionParams 获取具体的结算参数 - final result = await context.push(RoutePaths.trading); - return result == true; + // 直接调用结算 API 将可结算收益转入钱包余额 + final rewardService = ref.read(rewardServiceProvider); + final settleResult = await rewardService.settleToBalance(); + if (settleResult.success) { + debugPrint('[PendingActionsPage] 结算成功: ${settleResult.settledUsdtAmount} USDT'); + return true; + } + // 结算失败,检查是否已经没有可结算收益(可能已经结算过了) + return await _checkIfAlreadyCompleted(action); case 'BIND_PHONE': // 跳转到手机号绑定页面 @@ -141,14 +148,18 @@ class _PendingActionsPageState extends ConsumerState { final result = await context.push( '${RoutePaths.contractSigning}/$orderNo', ); - return result == true; + if (result == true) return true; + // 页面返回后再次检查是否已完成(用户可能通过其他方式完成了操作) + return await _checkIfAlreadyCompleted(action); } // 跳转到待签合同列表 final result = await context.push( RoutePaths.pendingContracts, extra: true, // forceSign = true ); - return result == true; + if (result == true) return true; + // 页面返回后再次检查是否已完成 + return await _checkIfAlreadyCompleted(action); case 'UPDATE_PROFILE': // 跳转到编辑资料页面 @@ -163,7 +174,7 @@ class _PendingActionsPageState extends ConsumerState { } /// 检查操作是否已经完成 - /// 例如:KYC 已经验证过了,手机号已经绑定了 + /// 例如:KYC 已经验证过了,手机号已经绑定了,合同已经签署了 Future _checkIfAlreadyCompleted(PendingAction action) async { try { switch (action.actionCode) { @@ -188,6 +199,41 @@ class _PendingActionsPageState extends ConsumerState { } return false; + case 'SIGN_CONTRACT': + // 检查是否还有待签署的合同 + final contractService = ref.read(contractSigningServiceProvider); + final orderNo = action.actionParams?['orderNo'] as String?; + if (orderNo != null) { + // 如果指定了订单号,检查该订单的合同是否已签署 + try { + final task = await contractService.getTask(orderNo); + if (task.status == ContractSigningStatus.signed) { + debugPrint('[PendingActionsPage] 合同 $orderNo 已签署,跳过 SIGN_CONTRACT 操作'); + return true; + } + } catch (e) { + // 获取任务失败,可能是订单不存在,继续检查待签列表 + debugPrint('[PendingActionsPage] 获取合同任务失败: $e'); + } + } + // 检查是否还有待签署的合同 + final pendingTasks = await contractService.getPendingTasks(); + if (pendingTasks.isEmpty) { + debugPrint('[PendingActionsPage] 没有待签署的合同,跳过 SIGN_CONTRACT 操作'); + return true; + } + return false; + + case 'SETTLE_REWARDS': + // 检查是否还有可结算的收益 + final rewardService = ref.read(rewardServiceProvider); + final summary = await rewardService.getMyRewardSummary(); + if (summary.settleableUsdt <= 0) { + debugPrint('[PendingActionsPage] 没有可结算收益,跳过 SETTLE_REWARDS 操作'); + return true; + } + return false; + // 其他操作类型目前不做预检查 default: return false;