feat(settlement): implement settle-to-balance with detailed source tracking
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 <noreply@anthropic.com>
This commit is contained in:
parent
cbbef170e8
commit
036696878f
|
|
@ -573,7 +573,8 @@
|
||||||
"Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\identity-service\\\\src\\\\infrastructure\\\\persistence\\\\repositories\\\\*.ts\")",
|
"Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\identity-service\\\\src\\\\infrastructure\\\\persistence\\\\repositories\\\\*.ts\")",
|
||||||
"Bash(head:*)",
|
"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 <noreply@anthropic.com>\nEOF\n\\)\")",
|
"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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -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 { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard';
|
||||||
import { RewardApplicationService } from '../../application/services/reward-application.service';
|
import { RewardApplicationService } from '../../application/services/reward-application.service';
|
||||||
|
|
@ -66,4 +66,35 @@ export class RewardController {
|
||||||
const accountSequence = req.user.accountSequence;
|
const accountSequence = req.user.accountSequence;
|
||||||
return this.rewardService.getSettleableRewards(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<string, number>;
|
||||||
|
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<string, number> = {};
|
||||||
|
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<string, Array<{ sourceUser: string; amount: number }>> = {};
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 过期到期的待领取奖励 (定时任务调用)
|
* 过期到期的待领取奖励 (定时任务调用)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,61 @@ export class WalletServiceClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结算到余额 (可结算USDT → 钱包USDT余额,无币种兑换)
|
||||||
|
* 将可结算收益直接转入钱包 USDT 余额
|
||||||
|
*/
|
||||||
|
async settleToBalance(params: {
|
||||||
|
accountSequence: string;
|
||||||
|
usdtAmount: number;
|
||||||
|
rewardEntryIds: string[];
|
||||||
|
breakdown?: Record<string, number>;
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行资金分配
|
* 执行资金分配
|
||||||
* 将认种订单的资金分配到各个目标账户
|
* 将认种订单的资金分配到各个目标账户
|
||||||
|
|
|
||||||
|
|
@ -166,4 +166,32 @@ export class InternalWalletController {
|
||||||
|
|
||||||
return result;
|
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<string, number>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@ export * from './unfreeze-for-planting.command';
|
||||||
export * from './add-rewards.command';
|
export * from './add-rewards.command';
|
||||||
export * from './claim-rewards.command';
|
export * from './claim-rewards.command';
|
||||||
export * from './settle-rewards.command';
|
export * from './settle-rewards.command';
|
||||||
|
export * from './settle-to-balance.command';
|
||||||
export * from './allocate-funds.command';
|
export * from './allocate-funds.command';
|
||||||
export * from './request-withdrawal.command';
|
export * from './request-withdrawal.command';
|
||||||
|
|
|
||||||
|
|
@ -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<string, number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息(如果失败)
|
||||||
|
*/
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
@ -886,6 +886,86 @@ export class WalletApplicationService {
|
||||||
return savedOrder.id.toString();
|
return savedOrder.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结算到余额 (可结算USDT → 钱包USDT余额,无币种兑换)
|
||||||
|
* 将可结算收益直接转入钱包 USDT 余额
|
||||||
|
*
|
||||||
|
* @param params 结算参数
|
||||||
|
* @returns 结算结果
|
||||||
|
*/
|
||||||
|
async settleToBalance(params: {
|
||||||
|
accountSequence: string;
|
||||||
|
usdtAmount: number;
|
||||||
|
rewardEntryIds: string[];
|
||||||
|
breakdown?: Record<string, number>;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分配资金 - 用于认种订单支付后的资金分配
|
* 分配资金 - 用于认种订单支付后的资金分配
|
||||||
* 支持分配给用户钱包或系统账户
|
* 支持分配给用户钱包或系统账户
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
freezeWallet(): void {
|
||||||
if (this._status === WalletStatus.FROZEN) {
|
if (this._status === WalletStatus.FROZEN) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export const ACTION_CODES = {
|
||||||
SETTLE_REWARDS: 'SETTLE_REWARDS', // 结算奖励
|
SETTLE_REWARDS: 'SETTLE_REWARDS', // 结算奖励
|
||||||
BIND_PHONE: 'BIND_PHONE', // 绑定手机
|
BIND_PHONE: 'BIND_PHONE', // 绑定手机
|
||||||
FORCE_KYC: 'FORCE_KYC', // 强制 KYC
|
FORCE_KYC: 'FORCE_KYC', // 强制 KYC
|
||||||
|
SIGN_CONTRACT: 'SIGN_CONTRACT', // 签订合同
|
||||||
CUSTOM_NOTICE: 'CUSTOM_NOTICE', // 自定义通知
|
CUSTOM_NOTICE: 'CUSTOM_NOTICE', // 自定义通知
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -105,6 +106,7 @@ export const ACTION_CODE_OPTIONS = [
|
||||||
{ value: ACTION_CODES.SETTLE_REWARDS, label: '结算奖励' },
|
{ value: ACTION_CODES.SETTLE_REWARDS, label: '结算奖励' },
|
||||||
{ value: ACTION_CODES.BIND_PHONE, label: '绑定手机' },
|
{ value: ACTION_CODES.BIND_PHONE, label: '绑定手机' },
|
||||||
{ value: ACTION_CODES.FORCE_KYC, label: '强制 KYC' },
|
{ value: ACTION_CODES.FORCE_KYC, label: '强制 KYC' },
|
||||||
|
{ value: ACTION_CODES.SIGN_CONTRACT, label: '签订合同' },
|
||||||
{ value: ACTION_CODES.CUSTOM_NOTICE, label: '自定义通知' },
|
{ value: ACTION_CODES.CUSTOM_NOTICE, label: '自定义通知' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,49 @@ class ExpiredRewardItem {
|
||||||
String get rightTypeName => allocationTypeName;
|
String get rightTypeName => allocationTypeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 结算到余额结果
|
||||||
|
class SettleToBalanceResult {
|
||||||
|
final bool success;
|
||||||
|
final String? settlementId;
|
||||||
|
final double settledUsdtAmount;
|
||||||
|
final double settledHashpowerAmount;
|
||||||
|
final int rewardCount;
|
||||||
|
final Map<String, double>? 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<String, dynamic> json) {
|
||||||
|
Map<String, double>? breakdown;
|
||||||
|
if (json['breakdown'] != null) {
|
||||||
|
breakdown = (json['breakdown'] as Map<String, dynamic>).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 获取)
|
/// 奖励汇总信息 (从 reward-service 获取)
|
||||||
class RewardSummary {
|
class RewardSummary {
|
||||||
final double pendingUsdt;
|
final double pendingUsdt;
|
||||||
|
|
@ -357,6 +400,60 @@ class RewardService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 结算到余额
|
||||||
|
///
|
||||||
|
/// 调用 POST /rewards/settle-to-balance (reward-service)
|
||||||
|
/// 将可结算收益直接转入钱包 USDT 余额(无币种兑换)
|
||||||
|
Future<SettleToBalanceResult> 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<String, dynamic>;
|
||||||
|
// 解包可能的 data 字段
|
||||||
|
final data = responseData['data'] as Map<String, dynamic>? ?? 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)
|
/// 调用 GET /wallet/expired-rewards (wallet-service)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
import '../../../../core/services/pending_action_service.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 '../../../../routes/route_paths.dart';
|
||||||
import '../../../kyc/data/kyc_service.dart';
|
import '../../../kyc/data/kyc_service.dart';
|
||||||
|
|
||||||
|
|
@ -114,10 +116,15 @@ class _PendingActionsPageState extends ConsumerState<PendingActionsPage> {
|
||||||
return result == true;
|
return result == true;
|
||||||
|
|
||||||
case 'SETTLE_REWARDS':
|
case 'SETTLE_REWARDS':
|
||||||
// 跳转到交易页面进行结算
|
// 直接调用结算 API 将可结算收益转入钱包余额
|
||||||
// 可以从 actionParams 获取具体的结算参数
|
final rewardService = ref.read(rewardServiceProvider);
|
||||||
final result = await context.push<bool>(RoutePaths.trading);
|
final settleResult = await rewardService.settleToBalance();
|
||||||
return result == true;
|
if (settleResult.success) {
|
||||||
|
debugPrint('[PendingActionsPage] 结算成功: ${settleResult.settledUsdtAmount} USDT');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 结算失败,检查是否已经没有可结算收益(可能已经结算过了)
|
||||||
|
return await _checkIfAlreadyCompleted(action);
|
||||||
|
|
||||||
case 'BIND_PHONE':
|
case 'BIND_PHONE':
|
||||||
// 跳转到手机号绑定页面
|
// 跳转到手机号绑定页面
|
||||||
|
|
@ -141,14 +148,18 @@ class _PendingActionsPageState extends ConsumerState<PendingActionsPage> {
|
||||||
final result = await context.push<bool>(
|
final result = await context.push<bool>(
|
||||||
'${RoutePaths.contractSigning}/$orderNo',
|
'${RoutePaths.contractSigning}/$orderNo',
|
||||||
);
|
);
|
||||||
return result == true;
|
if (result == true) return true;
|
||||||
|
// 页面返回后再次检查是否已完成(用户可能通过其他方式完成了操作)
|
||||||
|
return await _checkIfAlreadyCompleted(action);
|
||||||
}
|
}
|
||||||
// 跳转到待签合同列表
|
// 跳转到待签合同列表
|
||||||
final result = await context.push<bool>(
|
final result = await context.push<bool>(
|
||||||
RoutePaths.pendingContracts,
|
RoutePaths.pendingContracts,
|
||||||
extra: true, // forceSign = true
|
extra: true, // forceSign = true
|
||||||
);
|
);
|
||||||
return result == true;
|
if (result == true) return true;
|
||||||
|
// 页面返回后再次检查是否已完成
|
||||||
|
return await _checkIfAlreadyCompleted(action);
|
||||||
|
|
||||||
case 'UPDATE_PROFILE':
|
case 'UPDATE_PROFILE':
|
||||||
// 跳转到编辑资料页面
|
// 跳转到编辑资料页面
|
||||||
|
|
@ -163,7 +174,7 @@ class _PendingActionsPageState extends ConsumerState<PendingActionsPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查操作是否已经完成
|
/// 检查操作是否已经完成
|
||||||
/// 例如:KYC 已经验证过了,手机号已经绑定了
|
/// 例如:KYC 已经验证过了,手机号已经绑定了,合同已经签署了
|
||||||
Future<bool> _checkIfAlreadyCompleted(PendingAction action) async {
|
Future<bool> _checkIfAlreadyCompleted(PendingAction action) async {
|
||||||
try {
|
try {
|
||||||
switch (action.actionCode) {
|
switch (action.actionCode) {
|
||||||
|
|
@ -188,6 +199,41 @@ class _PendingActionsPageState extends ConsumerState<PendingActionsPage> {
|
||||||
}
|
}
|
||||||
return false;
|
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:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue