feat(p2p-transfer): 实现P2P划转手续费功能(全栈)
## 功能概述 - P2P转账(积分值)支持手续费收取,手续费存入系统成本费账户 S0000000002 - 发送方实际扣除 = 转账金额 + 手续费,接收方全额收到转账金额 - 手续费金额和最小划转金额可在管理后台动态配置(默认: 手续费5, 最小划转6) ## 后端 — mining-admin-service - GET /configs/p2p-transfer-fee: 管理端获取手续费配置(需鉴权) - POST /configs/p2p-transfer-fee: 管理端设置手续费配置,校验最小划转 > 手续费 - GET /configs/internal/p2p-transfer-fee: 内部调用端点(@Public 无鉴权) ## 后端 — trading-service - Prisma schema: P2pTransfer model 新增 fee Decimal(30,8) 字段 - docker-compose: 新增 MINING_ADMIN_SERVICE_URL 环境变量 - p2p-transfer.service: 动态获取手续费配置,余额校验含手续费, 事务内分别记录转账流水和手续费流水(P2P_TRANSFER_FEE), 手续费存入系统成本费账户 S0000000002 - p2p-transfer.controller: 新增 GET /p2p/transfer-fee-config 代理端点 - 转账结果和历史记录新增 fee 字段返回 ## 前端 — mining-admin-web - configs.api.ts: 新增 getP2pTransferFee / setP2pTransferFee API - use-configs.ts: 新增 useP2pTransferFee / useSetP2pTransferFee hooks - configs/page.tsx: 新增"P2P划转手续费设置"卡片(手续费 + 最小划转金额) ## 前端 — mining-app (Flutter) - api_endpoints.dart: 新增 p2pTransferFeeConfig 端点常量 - p2p_transfer_fee_config_model.dart: 新增手续费配置 Model - trading_remote_datasource.dart: 新增 getP2pTransferFeeConfig 方法 - transfer_providers.dart: 新增 p2pTransferFeeConfigProvider - send_shares_page.dart: 发送页面显示手续费信息、最小划转金额提示、 实际扣除金额计算、"全部"按钮扣除手续费、确认弹窗展示手续费明细 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
817b7d3a9f
commit
ca4e5393be
|
|
@ -113,6 +113,7 @@ services:
|
||||||
# 2.0 内部服务调用
|
# 2.0 内部服务调用
|
||||||
MINING_SERVICE_URL: http://mining-service:3021
|
MINING_SERVICE_URL: http://mining-service:3021
|
||||||
AUTH_SERVICE_URL: http://auth-service:3024
|
AUTH_SERVICE_URL: http://auth-service:3024
|
||||||
|
MINING_ADMIN_SERVICE_URL: http://mining-admin-service:3023
|
||||||
# JWT 配置 (与 auth-service 共享密钥以验证 token)
|
# JWT 配置 (与 auth-service 共享密钥以验证 token)
|
||||||
JWT_SECRET: ${JWT_SECRET:-your-jwt-secret-change-in-production}
|
JWT_SECRET: ${JWT_SECRET:-your-jwt-secret-change-in-production}
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Controller, Get, Post, Delete, Body, Param, Query, Req, Logger } from '@nestjs/common';
|
import { Controller, Get, Post, Delete, Body, Param, Query, Req, Logger, BadRequestException } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ConfigManagementService } from '../../application/services/config.service';
|
import { ConfigManagementService } from '../../application/services/config.service';
|
||||||
|
import { Public } from '../../shared/guards/admin-auth.guard';
|
||||||
|
|
||||||
class SetConfigDto { category: string; key: string; value: string; description?: string; }
|
class SetConfigDto { category: string; key: string; value: string; description?: string; }
|
||||||
|
|
||||||
|
|
@ -187,6 +188,67 @@ export class ConfigController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ P2P 划转手续费配置 ============
|
||||||
|
|
||||||
|
@Get('p2p-transfer-fee')
|
||||||
|
@ApiOperation({ summary: '获取P2P划转手续费配置' })
|
||||||
|
async getP2pTransferFee() {
|
||||||
|
const [feeConfig, minAmountConfig] = await Promise.all([
|
||||||
|
this.configService.getConfig('trading', 'p2p_transfer_fee'),
|
||||||
|
this.configService.getConfig('trading', 'min_p2p_transfer_amount'),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
fee: feeConfig?.configValue ?? '5',
|
||||||
|
minTransferAmount: minAmountConfig?.configValue ?? '6',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('p2p-transfer-fee')
|
||||||
|
@ApiOperation({ summary: '设置P2P划转手续费配置' })
|
||||||
|
async setP2pTransferFee(
|
||||||
|
@Body() body: { fee: string; minTransferAmount: string },
|
||||||
|
@Req() req: any,
|
||||||
|
) {
|
||||||
|
const fee = parseFloat(body.fee);
|
||||||
|
const minAmount = parseFloat(body.minTransferAmount);
|
||||||
|
|
||||||
|
if (isNaN(fee) || fee < 0) {
|
||||||
|
throw new BadRequestException('手续费必须 >= 0');
|
||||||
|
}
|
||||||
|
if (isNaN(minAmount) || minAmount <= fee) {
|
||||||
|
throw new BadRequestException('最小划转金额必须大于手续费');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.configService.setConfig(
|
||||||
|
req.admin.id, 'trading', 'p2p_transfer_fee',
|
||||||
|
body.fee, 'P2P划转手续费(积分值)',
|
||||||
|
),
|
||||||
|
this.configService.setConfig(
|
||||||
|
req.admin.id, 'trading', 'min_p2p_transfer_amount',
|
||||||
|
body.minTransferAmount, 'P2P最小划转金额(积分值)',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('internal/p2p-transfer-fee')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '获取P2P划转手续费配置(内部调用,无需鉴权)' })
|
||||||
|
async getP2pTransferFeeInternal() {
|
||||||
|
const [feeConfig, minAmountConfig] = await Promise.all([
|
||||||
|
this.configService.getConfig('trading', 'p2p_transfer_fee'),
|
||||||
|
this.configService.getConfig('trading', 'min_p2p_transfer_amount'),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
fee: feeConfig?.configValue ?? '5',
|
||||||
|
minTransferAmount: minAmountConfig?.configValue ?? '6',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 通用配置 ============
|
||||||
|
|
||||||
@Get(':category/:key')
|
@Get(':category/:key')
|
||||||
@ApiOperation({ summary: '获取单个配置' })
|
@ApiOperation({ summary: '获取单个配置' })
|
||||||
@ApiParam({ name: 'category' })
|
@ApiParam({ name: 'category' })
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,7 @@ model P2pTransfer {
|
||||||
fromPhone String? @map("from_phone") // 发送方手机号
|
fromPhone String? @map("from_phone") // 发送方手机号
|
||||||
fromNickname String? @map("from_nickname") // 发送方昵称
|
fromNickname String? @map("from_nickname") // 发送方昵称
|
||||||
amount Decimal @db.Decimal(30, 8)
|
amount Decimal @db.Decimal(30, 8)
|
||||||
|
fee Decimal @default(0) @db.Decimal(30, 8) // P2P转账手续费
|
||||||
memo String? @db.Text // 备注
|
memo String? @db.Text // 备注
|
||||||
status String @default("PENDING") // PENDING, COMPLETED, FAILED
|
status String @default("PENDING") // PENDING, COMPLETED, FAILED
|
||||||
errorMessage String? @map("error_message")
|
errorMessage String? @map("error_message")
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ class P2pTransferDto {
|
||||||
export class P2pTransferController {
|
export class P2pTransferController {
|
||||||
constructor(private readonly p2pTransferService: P2pTransferService) {}
|
constructor(private readonly p2pTransferService: P2pTransferService) {}
|
||||||
|
|
||||||
|
@Get('transfer-fee-config')
|
||||||
|
@ApiOperation({ summary: '获取P2P转账手续费配置' })
|
||||||
|
async getTransferFeeConfig() {
|
||||||
|
return this.p2pTransferService.getP2pFeeConfigPublic();
|
||||||
|
}
|
||||||
|
|
||||||
@Post('transfer')
|
@Post('transfer')
|
||||||
@ApiOperation({ summary: 'P2P转账(积分值)' })
|
@ApiOperation({ summary: 'P2P转账(积分值)' })
|
||||||
async transfer(
|
async transfer(
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,15 @@ interface RecipientInfo {
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface P2pFeeConfig {
|
||||||
|
fee: number;
|
||||||
|
minTransferAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface P2pTransferResult {
|
export interface P2pTransferResult {
|
||||||
transferNo: string;
|
transferNo: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
|
fee: string;
|
||||||
toPhone: string;
|
toPhone: string;
|
||||||
toNickname?: string;
|
toNickname?: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|
@ -26,16 +32,20 @@ export interface P2pTransferHistoryItem {
|
||||||
toAccountSequence: string;
|
toAccountSequence: string;
|
||||||
toPhone: string;
|
toPhone: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
|
fee?: string;
|
||||||
memo?: string | null;
|
memo?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 系统成本费账户序列号 */
|
||||||
|
const FEE_ACCOUNT_SEQUENCE = 'S0000000002';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class P2pTransferService {
|
export class P2pTransferService {
|
||||||
private readonly logger = new Logger(P2pTransferService.name);
|
private readonly logger = new Logger(P2pTransferService.name);
|
||||||
private readonly authServiceUrl: string;
|
private readonly authServiceUrl: string;
|
||||||
private readonly minTransferAmount: number;
|
private readonly miningAdminServiceUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly accountRepository: TradingAccountRepository,
|
private readonly accountRepository: TradingAccountRepository,
|
||||||
|
|
@ -43,7 +53,44 @@ export class P2pTransferService {
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.authServiceUrl = this.configService.get<string>('AUTH_SERVICE_URL', 'http://auth-service:3024');
|
this.authServiceUrl = this.configService.get<string>('AUTH_SERVICE_URL', 'http://auth-service:3024');
|
||||||
this.minTransferAmount = this.configService.get<number>('MIN_P2P_TRANSFER_AMOUNT', 0.01);
|
this.miningAdminServiceUrl = this.configService.get<string>(
|
||||||
|
'MINING_ADMIN_SERVICE_URL', 'http://mining-admin-service:3023',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取P2P手续费配置(从 mining-admin-service 动态读取)
|
||||||
|
*/
|
||||||
|
private async getP2pFeeConfig(): Promise<P2pFeeConfig> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.miningAdminServiceUrl}/api/v2/configs/internal/p2p-transfer-fee`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
this.logger.warn('Failed to fetch P2P fee config, using defaults');
|
||||||
|
return { fee: 5, minTransferAmount: 6 };
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
const data = result.data || result;
|
||||||
|
return {
|
||||||
|
fee: parseFloat(data.fee) || 5,
|
||||||
|
minTransferAmount: parseFloat(data.minTransferAmount) || 6,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`Failed to fetch P2P fee config: ${error.message}`);
|
||||||
|
return { fee: 5, minTransferAmount: 6 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取P2P手续费配置(供 Controller 暴露给前端)
|
||||||
|
*/
|
||||||
|
async getP2pFeeConfigPublic(): Promise<{ fee: string; minTransferAmount: string }> {
|
||||||
|
const config = await this.getP2pFeeConfig();
|
||||||
|
return {
|
||||||
|
fee: config.fee.toString(),
|
||||||
|
minTransferAmount: config.minTransferAmount.toString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -75,7 +122,7 @@ export class P2pTransferService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P2P转账(积分值)
|
* P2P转账(积分值)— 含手续费扣除
|
||||||
*/
|
*/
|
||||||
async transfer(
|
async transfer(
|
||||||
fromAccountSequence: string,
|
fromAccountSequence: string,
|
||||||
|
|
@ -86,9 +133,13 @@ export class P2pTransferService {
|
||||||
): Promise<P2pTransferResult> {
|
): Promise<P2pTransferResult> {
|
||||||
const transferAmount = new Money(amount);
|
const transferAmount = new Money(amount);
|
||||||
|
|
||||||
// 验证转账金额
|
// 获取动态手续费配置
|
||||||
if (transferAmount.value.lessThan(this.minTransferAmount)) {
|
const feeConfig = await this.getP2pFeeConfig();
|
||||||
throw new BadRequestException(`最小转账金额为 ${this.minTransferAmount}`);
|
const feeAmount = new Money(feeConfig.fee.toString());
|
||||||
|
|
||||||
|
// 验证最小转账金额
|
||||||
|
if (transferAmount.value.lessThan(feeConfig.minTransferAmount)) {
|
||||||
|
throw new BadRequestException(`最小转账金额为 ${feeConfig.minTransferAmount} 积分值`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找收款方
|
// 查找收款方
|
||||||
|
|
@ -108,9 +159,14 @@ export class P2pTransferService {
|
||||||
throw new NotFoundException('发送方账户不存在');
|
throw new NotFoundException('发送方账户不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查余额
|
// 计算总扣除金额 = 转账金额 + 手续费
|
||||||
if (fromAccount.availableCash.isLessThan(transferAmount)) {
|
const totalDeduction = transferAmount.add(feeAmount);
|
||||||
throw new BadRequestException('可用积分值不足');
|
|
||||||
|
// 检查余额(需覆盖手续费)
|
||||||
|
if (fromAccount.availableCash.isLessThan(totalDeduction)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`可用积分值不足(需要 ${totalDeduction.toFixed(2)},含手续费 ${feeAmount.toFixed(2)})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transferNo = this.generateTransferNo();
|
const transferNo = this.generateTransferNo();
|
||||||
|
|
@ -127,13 +183,14 @@ export class P2pTransferService {
|
||||||
toPhone,
|
toPhone,
|
||||||
toNickname: recipient.nickname,
|
toNickname: recipient.nickname,
|
||||||
amount: transferAmount.value,
|
amount: transferAmount.value,
|
||||||
|
fee: feeAmount.value,
|
||||||
memo,
|
memo,
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 扣减发送方余额
|
// 2. 扣减发送方余额(转账金额 + 手续费)
|
||||||
fromAccount.withdraw(transferAmount, transferNo);
|
fromAccount.withdraw(totalDeduction, transferNo);
|
||||||
|
|
||||||
// 保存发送方账户变动
|
// 保存发送方账户变动
|
||||||
await tx.tradingAccount.update({
|
await tx.tradingAccount.update({
|
||||||
|
|
@ -143,27 +200,48 @@ export class P2pTransferService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 记录发送方交易流水
|
// 手动写入发送方交易流水(拆分为转账和手续费两条记录)
|
||||||
for (const txn of fromAccount.pendingTransactions) {
|
const balanceBeforeTransfer = fromAccount.cashBalance.add(totalDeduction);
|
||||||
|
const balanceAfterTransfer = balanceBeforeTransfer.subtract(transferAmount);
|
||||||
|
|
||||||
|
// 流水1: 转账金额
|
||||||
|
await tx.tradingTransaction.create({
|
||||||
|
data: {
|
||||||
|
accountSequence: fromAccountSequence,
|
||||||
|
type: 'WITHDRAW',
|
||||||
|
assetType: 'CASH',
|
||||||
|
amount: transferAmount.value,
|
||||||
|
balanceBefore: balanceBeforeTransfer.value,
|
||||||
|
balanceAfter: balanceAfterTransfer.value,
|
||||||
|
referenceId: transferNo,
|
||||||
|
referenceType: 'P2P_TRANSFER',
|
||||||
|
counterpartyType: 'USER',
|
||||||
|
counterpartyAccountSeq: recipient.accountSequence,
|
||||||
|
memo: `P2P转出给 ${toPhone}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 流水2: 手续费(仅当手续费 > 0)
|
||||||
|
if (!feeAmount.isZero()) {
|
||||||
await tx.tradingTransaction.create({
|
await tx.tradingTransaction.create({
|
||||||
data: {
|
data: {
|
||||||
accountSequence: fromAccountSequence,
|
accountSequence: fromAccountSequence,
|
||||||
type: txn.type,
|
type: 'WITHDRAW',
|
||||||
assetType: txn.assetType,
|
assetType: 'CASH',
|
||||||
amount: txn.amount.value,
|
amount: feeAmount.value,
|
||||||
balanceBefore: txn.balanceBefore.value,
|
balanceBefore: balanceAfterTransfer.value,
|
||||||
balanceAfter: txn.balanceAfter.value,
|
balanceAfter: fromAccount.cashBalance.value,
|
||||||
referenceId: transferNo,
|
referenceId: transferNo,
|
||||||
referenceType: 'P2P_TRANSFER',
|
referenceType: 'P2P_TRANSFER_FEE',
|
||||||
counterpartyType: 'USER',
|
counterpartyType: 'SYSTEM',
|
||||||
counterpartyAccountSeq: recipient.accountSequence,
|
memo: 'P2P转账手续费',
|
||||||
memo: `P2P转出给 ${toPhone}`,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fromAccount.clearPendingTransactions();
|
fromAccount.clearPendingTransactions();
|
||||||
|
|
||||||
// 3. 增加收款方余额
|
// 3. 增加收款方余额(仅转账金额,不含手续费)
|
||||||
let toAccount = await this.accountRepository.findByAccountSequence(recipient.accountSequence!);
|
let toAccount = await this.accountRepository.findByAccountSequence(recipient.accountSequence!);
|
||||||
if (!toAccount) {
|
if (!toAccount) {
|
||||||
toAccount = TradingAccountAggregate.create(recipient.accountSequence!);
|
toAccount = TradingAccountAggregate.create(recipient.accountSequence!);
|
||||||
|
|
@ -210,7 +288,42 @@ export class P2pTransferService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 更新转账记录为完成
|
// 4. 手续费存入系统成本费账户(仅当手续费 > 0)
|
||||||
|
if (!feeAmount.isZero()) {
|
||||||
|
let feeAccount = await this.accountRepository.findByAccountSequence(FEE_ACCOUNT_SEQUENCE);
|
||||||
|
if (feeAccount) {
|
||||||
|
feeAccount.deposit(feeAmount, transferNo);
|
||||||
|
|
||||||
|
await tx.tradingAccount.update({
|
||||||
|
where: { accountSequence: FEE_ACCOUNT_SEQUENCE },
|
||||||
|
data: {
|
||||||
|
cashBalance: feeAccount.cashBalance.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const txn of feeAccount.pendingTransactions) {
|
||||||
|
await tx.tradingTransaction.create({
|
||||||
|
data: {
|
||||||
|
accountSequence: FEE_ACCOUNT_SEQUENCE,
|
||||||
|
type: txn.type,
|
||||||
|
assetType: txn.assetType,
|
||||||
|
amount: txn.amount.value,
|
||||||
|
balanceBefore: txn.balanceBefore.value,
|
||||||
|
balanceAfter: txn.balanceAfter.value,
|
||||||
|
referenceId: transferNo,
|
||||||
|
referenceType: 'P2P_TRANSFER_FEE',
|
||||||
|
counterpartyType: 'USER',
|
||||||
|
counterpartyAccountSeq: fromAccountSequence,
|
||||||
|
memo: 'P2P转账手续费收入',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Fee account ${FEE_ACCOUNT_SEQUENCE} not found, fee not deposited`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新转账记录为完成
|
||||||
await tx.p2pTransfer.update({
|
await tx.p2pTransfer.update({
|
||||||
where: { transferNo },
|
where: { transferNo },
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -220,11 +333,14 @@ export class P2pTransferService {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`P2P transfer completed: ${transferNo}, ${fromAccountSequence} -> ${toPhone}, amount=${amount}`);
|
this.logger.log(
|
||||||
|
`P2P transfer completed: ${transferNo}, ${fromAccountSequence} -> ${toPhone}, amount=${amount}, fee=${feeAmount.toFixed(8)}`,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transferNo,
|
transferNo,
|
||||||
amount,
|
amount,
|
||||||
|
fee: feeAmount.toFixed(8),
|
||||||
toPhone,
|
toPhone,
|
||||||
toNickname: recipient.nickname,
|
toNickname: recipient.nickname,
|
||||||
status: 'COMPLETED',
|
status: 'COMPLETED',
|
||||||
|
|
@ -285,6 +401,7 @@ export class P2pTransferService {
|
||||||
toAccountSequence: record.toAccountSequence,
|
toAccountSequence: record.toAccountSequence,
|
||||||
toPhone: record.toPhone,
|
toPhone: record.toPhone,
|
||||||
amount: record.amount.toString(),
|
amount: record.amount.toString(),
|
||||||
|
fee: record.fee.toString(),
|
||||||
memo: record.memo,
|
memo: record.memo,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
createdAt: record.createdAt,
|
createdAt: record.createdAt,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { PageHeader } from '@/components/layout/page-header';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { useConfigs, useUpdateConfig, useTransferEnabled, useSetTransferEnabled, useMiningStatus, useActivateMining, useDeactivateMining } from '@/features/configs/hooks/use-configs';
|
import { useConfigs, useUpdateConfig, useTransferEnabled, useSetTransferEnabled, useMiningStatus, useActivateMining, useDeactivateMining, useP2pTransferFee, useSetP2pTransferFee } from '@/features/configs/hooks/use-configs';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -31,8 +31,20 @@ export default function ConfigsPage() {
|
||||||
const activateMining = useActivateMining();
|
const activateMining = useActivateMining();
|
||||||
const deactivateMining = useDeactivateMining();
|
const deactivateMining = useDeactivateMining();
|
||||||
|
|
||||||
|
const { data: feeConfig, isLoading: feeLoading } = useP2pTransferFee();
|
||||||
|
const setP2pTransferFee = useSetP2pTransferFee();
|
||||||
|
|
||||||
const [editingConfig, setEditingConfig] = useState<SystemConfig | null>(null);
|
const [editingConfig, setEditingConfig] = useState<SystemConfig | null>(null);
|
||||||
const [editValue, setEditValue] = useState('');
|
const [editValue, setEditValue] = useState('');
|
||||||
|
const [feeValue, setFeeValue] = useState('');
|
||||||
|
const [minAmountValue, setMinAmountValue] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (feeConfig) {
|
||||||
|
setFeeValue(feeConfig.fee);
|
||||||
|
setMinAmountValue(feeConfig.minTransferAmount);
|
||||||
|
}
|
||||||
|
}, [feeConfig]);
|
||||||
|
|
||||||
const handleEdit = (config: SystemConfig) => {
|
const handleEdit = (config: SystemConfig) => {
|
||||||
setEditingConfig(config);
|
setEditingConfig(config);
|
||||||
|
|
@ -206,6 +218,61 @@ export default function ConfigsPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* P2P划转手续费设置 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">P2P划转手续费设置</CardTitle>
|
||||||
|
<CardDescription>配置用户间划转(P2P转账)的手续费和最小划转金额</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{feeLoading ? (
|
||||||
|
<Skeleton className="h-32 w-full" />
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>划转手续费 (积分值)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={feeValue}
|
||||||
|
onChange={(e) => setFeeValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>最小划转金额 (积分值)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={minAmountValue}
|
||||||
|
onChange={(e) => setMinAmountValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{parseFloat(minAmountValue) <= parseFloat(feeValue) && feeValue !== '' && minAmountValue !== '' && (
|
||||||
|
<p className="text-sm text-red-500">最小划转金额必须大于手续费</p>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => setP2pTransferFee.mutate({ fee: feeValue, minTransferAmount: minAmountValue })}
|
||||||
|
disabled={
|
||||||
|
setP2pTransferFee.isPending ||
|
||||||
|
parseFloat(minAmountValue) <= parseFloat(feeValue) ||
|
||||||
|
isNaN(parseFloat(feeValue)) ||
|
||||||
|
isNaN(parseFloat(minAmountValue))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
{setP2pTransferFee.isPending ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,13 @@ export const configsApi = {
|
||||||
const response = await apiClient.post('/configs/mining/deactivate');
|
const response = await apiClient.post('/configs/mining/deactivate');
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getP2pTransferFee: async (): Promise<{ fee: string; minTransferAmount: string }> => {
|
||||||
|
const response = await apiClient.get('/configs/p2p-transfer-fee');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
setP2pTransferFee: async (fee: string, minTransferAmount: string): Promise<void> => {
|
||||||
|
await apiClient.post('/configs/p2p-transfer-fee', { fee, minTransferAmount });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -94,3 +94,28 @@ export function useDeactivateMining() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useP2pTransferFee() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['configs', 'p2p-transfer-fee'],
|
||||||
|
queryFn: () => configsApi.getP2pTransferFee(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSetP2pTransferFee() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ fee, minTransferAmount }: { fee: string; minTransferAmount: string }) =>
|
||||||
|
configsApi.setP2pTransferFee(fee, minTransferAmount),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['configs', 'p2p-transfer-fee'] });
|
||||||
|
toast({ title: 'P2P划转手续费配置已更新', variant: 'success' as any });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
const message = error?.response?.data?.message || '更新失败';
|
||||||
|
toast({ title: message, variant: 'destructive' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ class ApiEndpoints {
|
||||||
|
|
||||||
// P2P Transfer endpoints (用户间转账)
|
// P2P Transfer endpoints (用户间转账)
|
||||||
static const String p2pTransfer = '/api/v2/trading/p2p/transfer';
|
static const String p2pTransfer = '/api/v2/trading/p2p/transfer';
|
||||||
|
static const String p2pTransferFeeConfig = '/api/v2/trading/p2p/transfer-fee-config';
|
||||||
static String p2pTransferHistory(String accountSequence) =>
|
static String p2pTransferHistory(String accountSequence) =>
|
||||||
'/api/v2/trading/p2p/transfers/$accountSequence';
|
'/api/v2/trading/p2p/transfers/$accountSequence';
|
||||||
static const String lookupAccount = '/api/v2/auth/user/lookup';
|
static const String lookupAccount = '/api/v2/auth/user/lookup';
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../../models/market_overview_model.dart';
|
||||||
import '../../models/asset_display_model.dart';
|
import '../../models/asset_display_model.dart';
|
||||||
import '../../models/kline_model.dart';
|
import '../../models/kline_model.dart';
|
||||||
import '../../models/p2p_transfer_model.dart';
|
import '../../models/p2p_transfer_model.dart';
|
||||||
|
import '../../models/p2p_transfer_fee_config_model.dart';
|
||||||
import '../../models/c2c_order_model.dart';
|
import '../../models/c2c_order_model.dart';
|
||||||
import '../../../core/network/api_client.dart';
|
import '../../../core/network/api_client.dart';
|
||||||
import '../../../core/network/api_endpoints.dart';
|
import '../../../core/network/api_endpoints.dart';
|
||||||
|
|
@ -80,6 +81,9 @@ abstract class TradingRemoteDataSource {
|
||||||
/// 获取P2P转账历史
|
/// 获取P2P转账历史
|
||||||
Future<List<P2pTransferModel>> getP2pTransferHistory(String accountSequence);
|
Future<List<P2pTransferModel>> getP2pTransferHistory(String accountSequence);
|
||||||
|
|
||||||
|
/// 获取P2P转账手续费配置
|
||||||
|
Future<P2pTransferFeeConfigModel> getP2pTransferFeeConfig();
|
||||||
|
|
||||||
// ============ C2C交易接口 ============
|
// ============ C2C交易接口 ============
|
||||||
|
|
||||||
/// 获取C2C订单列表(市场广告)
|
/// 获取C2C订单列表(市场广告)
|
||||||
|
|
@ -404,6 +408,17 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<P2pTransferFeeConfigModel> getP2pTransferFeeConfig() async {
|
||||||
|
try {
|
||||||
|
final response = await client.get(ApiEndpoints.p2pTransferFeeConfig);
|
||||||
|
return P2pTransferFeeConfigModel.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
// 失败时返回默认值
|
||||||
|
return P2pTransferFeeConfigModel(fee: '5', minTransferAmount: '6');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ C2C交易实现 ============
|
// ============ C2C交易实现 ============
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
class P2pTransferFeeConfigModel {
|
||||||
|
final String fee;
|
||||||
|
final String minTransferAmount;
|
||||||
|
|
||||||
|
P2pTransferFeeConfigModel({
|
||||||
|
required this.fee,
|
||||||
|
required this.minTransferAmount,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory P2pTransferFeeConfigModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return P2pTransferFeeConfigModel(
|
||||||
|
fee: json['fee']?.toString() ?? '5',
|
||||||
|
minTransferAmount: json['minTransferAmount']?.toString() ?? '6',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,8 +45,12 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
final accountSequence = user.accountSequence ?? '';
|
final accountSequence = user.accountSequence ?? '';
|
||||||
final assetAsync = ref.watch(accountAssetProvider(accountSequence));
|
final assetAsync = ref.watch(accountAssetProvider(accountSequence));
|
||||||
final transferState = ref.watch(transferNotifierProvider);
|
final transferState = ref.watch(transferNotifierProvider);
|
||||||
|
final feeConfigAsync = ref.watch(p2pTransferFeeConfigProvider);
|
||||||
|
|
||||||
final availableCash = assetAsync.valueOrNull?.availableCash ?? '0';
|
final availableCash = assetAsync.valueOrNull?.availableCash ?? '0';
|
||||||
|
final feeConfig = feeConfigAsync.valueOrNull;
|
||||||
|
final feeAmount = double.tryParse(feeConfig?.fee ?? '5') ?? 5;
|
||||||
|
final minTransferAmount = double.tryParse(feeConfig?.minTransferAmount ?? '6') ?? 6;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: _bgGray,
|
backgroundColor: _bgGray,
|
||||||
|
|
@ -92,7 +96,7 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 转账金额
|
// 转账金额
|
||||||
_buildAmountSection(availableCash),
|
_buildAmountSection(availableCash, feeAmount, minTransferAmount),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
|
@ -102,7 +106,7 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// 发送按钮
|
// 发送按钮
|
||||||
_buildSendButton(transferState, availableCash),
|
_buildSendButton(transferState, availableCash, feeAmount, minTransferAmount),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
|
@ -275,7 +279,9 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAmountSection(String availableCash) {
|
Widget _buildAmountSection(String availableCash, double feeAmount, double minTransferAmount) {
|
||||||
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|
@ -314,7 +320,7 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,8}')),
|
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,8}')),
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '请输入转账数量',
|
hintText: '最小 ${minTransferAmount.toStringAsFixed(0)} 积分值',
|
||||||
hintStyle: const TextStyle(color: _grayText),
|
hintStyle: const TextStyle(color: _grayText),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: _bgGray,
|
fillColor: _bgGray,
|
||||||
|
|
@ -328,7 +334,12 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
),
|
),
|
||||||
suffixIcon: TextButton(
|
suffixIcon: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_amountController.text = availableCash;
|
final available = double.tryParse(availableCash) ?? 0;
|
||||||
|
final maxAmount = available - feeAmount;
|
||||||
|
if (maxAmount > 0) {
|
||||||
|
_amountController.text = maxAmount.toStringAsFixed(8);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'全部',
|
'全部',
|
||||||
|
|
@ -343,6 +354,81 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 手续费信息
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _orange.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'划转手续费:',
|
||||||
|
style: TextStyle(fontSize: 13, color: _grayText),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${feeAmount.toStringAsFixed(feeAmount == feeAmount.roundToDouble() ? 0 : 2)} 积分值',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _darkText,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'最小划转金额:',
|
||||||
|
style: TextStyle(fontSize: 13, color: _grayText),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${minTransferAmount.toStringAsFixed(minTransferAmount == minTransferAmount.roundToDouble() ? 0 : 2)} 积分值',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _darkText,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (amount > 0) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'实际扣除:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _darkText,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${(amount + feeAmount).toStringAsFixed(2)} 积分值',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _orange,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -392,10 +478,13 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSendButton(TransferState transferState, String availableCash) {
|
Widget _buildSendButton(TransferState transferState, String availableCash, double feeAmount, double minTransferAmount) {
|
||||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||||
final available = double.tryParse(availableCash) ?? 0;
|
final available = double.tryParse(availableCash) ?? 0;
|
||||||
final isValid = _isRecipientVerified && amount > 0 && amount <= available;
|
final totalRequired = amount + feeAmount;
|
||||||
|
final isValid = _isRecipientVerified &&
|
||||||
|
amount >= minTransferAmount &&
|
||||||
|
totalRequired <= available;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
|
@ -404,7 +493,7 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
height: 50,
|
height: 50,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: isValid && !transferState.isLoading
|
onPressed: isValid && !transferState.isLoading
|
||||||
? _handleTransfer
|
? () => _handleTransfer(feeAmount)
|
||||||
: null,
|
: null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: _orange,
|
backgroundColor: _orange,
|
||||||
|
|
@ -463,7 +552,7 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'1. 转账前请确认收款方账号正确\n2. 积分值转账不可撤销,请谨慎操作\n3. 转账后将从您的可用积分值中扣除\n4. 积分值是通过卖出积分股获得的,如需转账请先卖出积分股',
|
'1. 转账前请确认收款方账号正确\n2. 积分值转账不可撤销,请谨慎操作\n3. 转账后将从您的可用积分值中扣除转账金额及手续费\n4. 积分值是通过卖出积分股获得的,如需转账请先卖出积分股',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: _grayText,
|
color: _grayText,
|
||||||
|
|
@ -566,13 +655,18 @@ class _SendSharesPageState extends ConsumerState<SendSharesPage> {
|
||||||
return '${phone.substring(0, 3)}****${phone.substring(7)}';
|
return '${phone.substring(0, 3)}****${phone.substring(7)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleTransfer() async {
|
Future<void> _handleTransfer(double feeAmount) async {
|
||||||
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('确认转账'),
|
title: const Text('确认转账'),
|
||||||
content: Text(
|
content: Text(
|
||||||
'确定要向 $_recipientNickname 发送 ${_amountController.text} 积分值吗?\n\n此操作不可撤销。',
|
'确定要向 $_recipientNickname 发送 ${_amountController.text} 积分值吗?\n'
|
||||||
|
'手续费: ${feeAmount.toStringAsFixed(feeAmount == feeAmount.roundToDouble() ? 0 : 2)} 积分值\n'
|
||||||
|
'实际扣除: ${(amount + feeAmount).toStringAsFixed(2)} 积分值\n\n'
|
||||||
|
'此操作不可撤销。',
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../data/datasources/remote/trading_remote_datasource.dart';
|
import '../../data/datasources/remote/trading_remote_datasource.dart';
|
||||||
import '../../data/models/p2p_transfer_model.dart';
|
import '../../data/models/p2p_transfer_model.dart';
|
||||||
|
import '../../data/models/p2p_transfer_fee_config_model.dart';
|
||||||
import '../../core/di/injection.dart';
|
import '../../core/di/injection.dart';
|
||||||
|
|
||||||
/// P2P转账状态
|
/// P2P转账状态
|
||||||
|
|
@ -101,6 +102,14 @@ final transferNotifierProvider =
|
||||||
(ref) => TransferNotifier(getIt<TradingRemoteDataSource>()),
|
(ref) => TransferNotifier(getIt<TradingRemoteDataSource>()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// P2P转账手续费配置
|
||||||
|
final p2pTransferFeeConfigProvider = FutureProvider<P2pTransferFeeConfigModel>(
|
||||||
|
(ref) async {
|
||||||
|
final dataSource = getIt<TradingRemoteDataSource>();
|
||||||
|
return dataSource.getP2pTransferFeeConfig();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/// P2P转账历史记录
|
/// P2P转账历史记录
|
||||||
final p2pTransferHistoryProvider =
|
final p2pTransferHistoryProvider =
|
||||||
FutureProvider.family<List<P2pTransferModel>, String>(
|
FutureProvider.family<List<P2pTransferModel>, String>(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue