feat(wallet-service): 实现手续费归集账户功能
- 新增系统账户 S0000000006 (user_id=-6) 用于归集提现手续费 - 新增 FEE_COLLECTION 流水类型记录手续费归集 - 区块链提现完成时使用 UnitOfWork 事务归集手续费 - 法币提现完成时在事务中归集手续费 - WithdrawalOrderRepository 添加事务支持 - 所有手续费归集操作使用乐观锁保护 🤖 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
99b725db0a
commit
b9911ab460
|
|
@ -0,0 +1,33 @@
|
|||
-- 手续费归集账户 (Fee Collection Account)
|
||||
-- S0000000006: 手续费归集账户 (user_id = -6)
|
||||
-- 用于归集所有提现(区块链提现、法币提现)产生的手续费
|
||||
|
||||
INSERT INTO "wallet_accounts" (
|
||||
"account_sequence", "user_id", "status",
|
||||
"usdt_available", "usdt_frozen",
|
||||
"dst_available", "dst_frozen",
|
||||
"bnb_available", "bnb_frozen",
|
||||
"og_available", "og_frozen",
|
||||
"rwad_available", "rwad_frozen",
|
||||
"hashpower",
|
||||
"pending_usdt", "pending_hashpower", "pending_expire_at",
|
||||
"settleable_usdt", "settleable_hashpower",
|
||||
"settled_total_usdt", "settled_total_hashpower",
|
||||
"expired_total_usdt", "expired_total_hashpower",
|
||||
"has_planted", "version",
|
||||
"created_at", "updated_at"
|
||||
) VALUES (
|
||||
'S0000000006', -6, 'ACTIVE',
|
||||
0, 0,
|
||||
0, 0,
|
||||
0, 0,
|
||||
0, 0,
|
||||
0, 0,
|
||||
0,
|
||||
0, 0, NULL,
|
||||
0, 0,
|
||||
0, 0,
|
||||
0, 0,
|
||||
false, 0,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
) ON CONFLICT ("account_sequence") DO NOTHING;
|
||||
|
|
@ -384,8 +384,16 @@ export class FiatWithdrawalApplicationService {
|
|||
await this.walletCacheService.invalidateWallet(order.userId.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手续费归集账户序列号
|
||||
* S0000000006: 手续费归集账户 (user_id = -6)
|
||||
*/
|
||||
private static readonly FEE_COLLECTION_ACCOUNT = 'S0000000006';
|
||||
private static readonly FEE_COLLECTION_USER_ID = BigInt(-6);
|
||||
|
||||
/**
|
||||
* 扣除冻结余额(打款完成后)
|
||||
* 同时归集手续费到系统账户
|
||||
*/
|
||||
private async deductFrozenForOrder(order: FiatWithdrawalOrder): Promise<void> {
|
||||
const wallet = await this.walletRepo.findByAccountSequence(order.accountSequence);
|
||||
|
|
@ -394,8 +402,9 @@ export class FiatWithdrawalApplicationService {
|
|||
return;
|
||||
}
|
||||
|
||||
// 使用事务处理
|
||||
// 使用事务处理:用户钱包扣款 + 手续费归集
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// 1. 扣除用户冻结余额
|
||||
const walletRecord = await tx.walletAccount.findUnique({
|
||||
where: { accountSequence: order.accountSequence },
|
||||
});
|
||||
|
|
@ -431,7 +440,7 @@ export class FiatWithdrawalApplicationService {
|
|||
throw new OptimisticLockError(`Optimistic lock conflict for wallet ${walletRecord.id}`);
|
||||
}
|
||||
|
||||
// 记录提现流水
|
||||
// 2. 记录用户提现流水
|
||||
await tx.ledgerEntry.create({
|
||||
data: {
|
||||
accountSequence: order.accountSequence,
|
||||
|
|
@ -451,6 +460,64 @@ export class FiatWithdrawalApplicationService {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 3. 归集手续费到系统账户 S0000000006
|
||||
if (order.fee.value > 0) {
|
||||
const feeAccountSequence = FiatWithdrawalApplicationService.FEE_COLLECTION_ACCOUNT;
|
||||
const feeUserId = FiatWithdrawalApplicationService.FEE_COLLECTION_USER_ID;
|
||||
|
||||
// 获取手续费账户
|
||||
const feeAccountRecord = await tx.walletAccount.findUnique({
|
||||
where: { accountSequence: feeAccountSequence },
|
||||
});
|
||||
|
||||
if (!feeAccountRecord) {
|
||||
this.logger.error(`[FEE_COLLECTION] Fee collection account ${feeAccountSequence} not found!`);
|
||||
throw new Error(`Fee collection account ${feeAccountSequence} not found`);
|
||||
}
|
||||
|
||||
const feeAmount = new Decimal(order.fee.value);
|
||||
const newFeeBalance = new Decimal(feeAccountRecord.usdtAvailable.toString()).plus(feeAmount);
|
||||
const feeAccountVersion = feeAccountRecord.version;
|
||||
|
||||
// 更新手续费账户余额(乐观锁)
|
||||
const feeUpdateResult = await tx.walletAccount.updateMany({
|
||||
where: {
|
||||
id: feeAccountRecord.id,
|
||||
version: feeAccountVersion,
|
||||
},
|
||||
data: {
|
||||
usdtAvailable: newFeeBalance,
|
||||
version: feeAccountVersion + 1,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (feeUpdateResult.count === 0) {
|
||||
throw new OptimisticLockError(`Optimistic lock conflict for fee account ${feeAccountRecord.id}`);
|
||||
}
|
||||
|
||||
// 记录手续费归集流水
|
||||
await tx.ledgerEntry.create({
|
||||
data: {
|
||||
accountSequence: feeAccountSequence,
|
||||
userId: feeUserId,
|
||||
entryType: LedgerEntryType.FEE_COLLECTION,
|
||||
amount: feeAmount,
|
||||
assetType: 'USDT',
|
||||
balanceAfter: newFeeBalance,
|
||||
refOrderId: order.orderNo,
|
||||
memo: `法币提现手续费归集: ${order.fee.value} 绿积分`,
|
||||
payloadJson: {
|
||||
feeType: 'FIAT_WITHDRAWAL_FEE',
|
||||
sourceOrderNo: order.orderNo,
|
||||
collectedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`[FEE_COLLECTION] Fiat withdrawal fee collected: ${order.fee.value} USDT from order ${order.orderNo}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 清除缓存
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
IWithdrawalOrderRepository, WITHDRAWAL_ORDER_REPOSITORY,
|
||||
IPendingRewardRepository, PENDING_REWARD_REPOSITORY,
|
||||
} from '@/domain/repositories';
|
||||
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||||
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||||
import { IUnitOfWork, UNIT_OF_WORK } from '@/infrastructure/persistence/unit-of-work';
|
||||
import { LedgerEntry, DepositOrder, SettlementOrder, WithdrawalOrder, PendingReward, PendingRewardStatus, WalletAccount } from '@/domain/aggregates';
|
||||
import {
|
||||
|
|
@ -1673,28 +1673,36 @@ export class WalletApplicationService {
|
|||
break;
|
||||
|
||||
case 'CONFIRMED':
|
||||
order.markAsConfirmed();
|
||||
await this.withdrawalRepo.save(order);
|
||||
// 使用 UnitOfWork 事务保证原子性:订单更新、用户钱包扣款、流水记录、手续费归集
|
||||
await this.unitOfWork.runInTransaction(async (tx) => {
|
||||
order.markAsConfirmed();
|
||||
await this.withdrawalRepo.save(order, { tx });
|
||||
|
||||
// 解冻并扣除
|
||||
wallet.unfreeze(totalFrozen);
|
||||
wallet.deduct(totalFrozen, 'Withdrawal completed', order.orderNo);
|
||||
await this.walletRepo.save(wallet);
|
||||
// 解冻并扣除
|
||||
wallet.unfreeze(totalFrozen);
|
||||
wallet.deduct(totalFrozen, 'Withdrawal completed', order.orderNo);
|
||||
await this.walletRepo.save(wallet, { tx });
|
||||
|
||||
// 记录提现完成流水
|
||||
const withdrawEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: order.userId,
|
||||
entryType: LedgerEntryType.WITHDRAWAL,
|
||||
amount: Money.signed(-order.amount.value, 'USDT'),
|
||||
balanceAfter: wallet.balances.usdt.available,
|
||||
refOrderId: order.orderNo,
|
||||
refTxHash: order.txHash ?? undefined,
|
||||
memo: `Withdrawal to ${order.toAddress}`,
|
||||
// 记录提现完成流水
|
||||
const withdrawEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: order.userId,
|
||||
entryType: LedgerEntryType.WITHDRAWAL,
|
||||
amount: Money.signed(-order.amount.value, 'USDT'),
|
||||
balanceAfter: wallet.balances.usdt.available,
|
||||
refOrderId: order.orderNo,
|
||||
refTxHash: order.txHash ?? undefined,
|
||||
memo: `Withdrawal to ${order.toAddress}`,
|
||||
});
|
||||
await this.ledgerRepo.save(withdrawEntry, { tx });
|
||||
|
||||
// 归集手续费到系统账户 S0000000006
|
||||
if (order.fee.value > 0) {
|
||||
await this.collectFeeToSystemAccount(order.orderNo, order.fee, 'WITHDRAWAL_FEE', { tx });
|
||||
}
|
||||
});
|
||||
await this.ledgerRepo.save(withdrawEntry);
|
||||
|
||||
this.logger.log(`Withdrawal ${order.orderNo} confirmed, txHash: ${order.txHash}`);
|
||||
this.logger.log(`Withdrawal ${order.orderNo} confirmed, txHash: ${order.txHash}, fee collected: ${order.fee.value}`);
|
||||
break;
|
||||
|
||||
case 'FAILED':
|
||||
|
|
@ -1754,6 +1762,66 @@ export class WalletApplicationService {
|
|||
}));
|
||||
}
|
||||
|
||||
// =============== 手续费归集 ===============
|
||||
|
||||
/**
|
||||
* 手续费归集账户序列号
|
||||
* S0000000006: 手续费归集账户 (user_id = -6)
|
||||
*/
|
||||
private static readonly FEE_COLLECTION_ACCOUNT = 'S0000000006';
|
||||
private static readonly FEE_COLLECTION_USER_ID = BigInt(-6);
|
||||
|
||||
/**
|
||||
* 归集手续费到系统账户
|
||||
* 在事务中调用,确保与提现操作的原子性
|
||||
*
|
||||
* @param refOrderId 关联的订单号
|
||||
* @param fee 手续费金额
|
||||
* @param feeType 手续费类型 (WITHDRAWAL_FEE | FIAT_WITHDRAWAL_FEE)
|
||||
* @param options 仓库保存选项(包含事务客户端)
|
||||
*/
|
||||
private async collectFeeToSystemAccount(
|
||||
refOrderId: string,
|
||||
fee: Money,
|
||||
feeType: 'WITHDRAWAL_FEE' | 'FIAT_WITHDRAWAL_FEE',
|
||||
options: { tx: import('@/infrastructure/persistence/unit-of-work').PrismaTransactionClient },
|
||||
): Promise<void> {
|
||||
const feeAccountSequence = WalletApplicationService.FEE_COLLECTION_ACCOUNT;
|
||||
const feeUserId = WalletApplicationService.FEE_COLLECTION_USER_ID;
|
||||
|
||||
// 获取手续费归集账户
|
||||
const feeAccount = await this.walletRepo.findByAccountSequence(feeAccountSequence, options);
|
||||
if (!feeAccount) {
|
||||
this.logger.error(`[FEE_COLLECTION] Fee collection account ${feeAccountSequence} not found!`);
|
||||
throw new Error(`Fee collection account ${feeAccountSequence} not found`);
|
||||
}
|
||||
|
||||
// 增加手续费账户余额
|
||||
feeAccount.addAvailableBalance(fee);
|
||||
await this.walletRepo.save(feeAccount, options);
|
||||
|
||||
// 记录手续费归集流水
|
||||
const feeEntry = LedgerEntry.create({
|
||||
accountSequence: feeAccountSequence,
|
||||
userId: UserId.create(feeUserId),
|
||||
entryType: LedgerEntryType.FEE_COLLECTION,
|
||||
amount: fee,
|
||||
balanceAfter: feeAccount.balances.usdt.available,
|
||||
refOrderId,
|
||||
memo: feeType === 'WITHDRAWAL_FEE'
|
||||
? `区块链提现手续费归集: ${fee.value} 绿积分`
|
||||
: `法币提现手续费归集: ${fee.value} 绿积分`,
|
||||
payloadJson: {
|
||||
feeType,
|
||||
sourceOrderNo: refOrderId,
|
||||
collectedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
await this.ledgerRepo.save(feeEntry, options);
|
||||
|
||||
this.logger.log(`[FEE_COLLECTION] Fee collected: ${fee.value} USDT from order ${refOrderId}, type: ${feeType}`);
|
||||
}
|
||||
|
||||
// =============== Queries ===============
|
||||
|
||||
async getMyWallet(query: GetMyWalletQuery): Promise<WalletDTO> {
|
||||
|
|
@ -3086,6 +3154,7 @@ export class WalletApplicationService {
|
|||
'S0000000003': '运营费账户',
|
||||
'S0000000004': 'RWAD底池账户',
|
||||
'S0000000005': '分享权益池账户',
|
||||
'S0000000006': '手续费归集账户',
|
||||
};
|
||||
|
||||
// 查询钱包账户
|
||||
|
|
@ -3152,7 +3221,7 @@ export class WalletApplicationService {
|
|||
const transferredStats = await this.prisma.ledgerEntry.groupBy({ by: ['accountSequence'], where: { accountSequence: { in: accountSeqs }, amount: { lt: 0 } }, _sum: { amount: true } });
|
||||
const receivedMap2 = new Map(receivedStats2.map(s => [s.accountSequence, s._sum.amount]));
|
||||
const transferredMap = new Map(transferredStats.map(s => [s.accountSequence, s._sum.amount]));
|
||||
const fixedAccountTypes: Record<string, string> = { 'S0000000001': 'COST_ACCOUNT', 'S0000000002': 'OPERATION_ACCOUNT', 'S0000000003': 'HQ_COMMUNITY', 'S0000000004': 'RWAD_POOL_PENDING', 'S0000000005': 'PLATFORM_FEE' };
|
||||
const fixedAccountTypes: Record<string, string> = { 'S0000000001': 'HQ_COMMUNITY', 'S0000000002': 'COST_ACCOUNT', 'S0000000003': 'OPERATION_ACCOUNT', 'S0000000004': 'RWAD_POOL_PENDING', 'S0000000005': 'SHARE_RIGHT_POOL', 'S0000000006': 'FEE_COLLECTION' };
|
||||
const fixedAccounts: Array<{ accountSequence: string; accountType: string; usdtBalance: string; totalReceived: string; totalTransferred: string; status: string; createdAt: string }> = [];
|
||||
const provinceAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }> = [];
|
||||
const cityAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }> = [];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { WithdrawalOrder } from '@/domain/aggregates';
|
||||
import { WithdrawalStatus } from '@/domain/value-objects';
|
||||
import { RepositorySaveOptions } from '@/infrastructure/persistence/unit-of-work';
|
||||
|
||||
export interface IWithdrawalOrderRepository {
|
||||
save(order: WithdrawalOrder): Promise<WithdrawalOrder>;
|
||||
save(order: WithdrawalOrder, options?: RepositorySaveOptions): Promise<WithdrawalOrder>;
|
||||
findById(orderId: bigint): Promise<WithdrawalOrder | null>;
|
||||
findByOrderNo(orderNo: string): Promise<WithdrawalOrder | null>;
|
||||
findByUserId(userId: bigint, status?: WithdrawalStatus): Promise<WithdrawalOrder[]>;
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ export enum LedgerEntryType {
|
|||
FREEZE = 'FREEZE',
|
||||
UNFREEZE = 'UNFREEZE',
|
||||
SYSTEM_ALLOCATION = 'SYSTEM_ALLOCATION', // 系统账户分配
|
||||
FEE_COLLECTION = 'FEE_COLLECTION', // 手续费归集
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export class UserId {
|
|||
// -2: 成本费账户 (S0000000002)
|
||||
// -3: 运营费账户 (S0000000003)
|
||||
// -4: RWA底池 (S0000000004)
|
||||
// -5: 分享权益池 (S0000000005)
|
||||
// -6: 手续费归集账户 (S0000000006)
|
||||
return new UserId(bigintValue);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@ import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.servic
|
|||
import { IWithdrawalOrderRepository } from '@/domain/repositories';
|
||||
import { WithdrawalOrder } from '@/domain/aggregates';
|
||||
import { WithdrawalStatus } from '@/domain/value-objects';
|
||||
import { RepositorySaveOptions, PrismaTransactionClient } from '@/infrastructure/persistence/unit-of-work';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
@Injectable()
|
||||
export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async save(order: WithdrawalOrder): Promise<WithdrawalOrder> {
|
||||
private getClient(options?: RepositorySaveOptions): PrismaService | PrismaTransactionClient {
|
||||
return options?.tx || this.prisma;
|
||||
}
|
||||
|
||||
async save(order: WithdrawalOrder, options?: RepositorySaveOptions): Promise<WithdrawalOrder> {
|
||||
const client = this.getClient(options);
|
||||
const data = {
|
||||
orderNo: order.orderNo,
|
||||
accountSequence: order.accountSequence,
|
||||
|
|
@ -30,10 +36,10 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository
|
|||
};
|
||||
|
||||
if (order.id === BigInt(0)) {
|
||||
const created = await this.prisma.withdrawalOrder.create({ data });
|
||||
const created = await client.withdrawalOrder.create({ data });
|
||||
return this.toDomain(created);
|
||||
} else {
|
||||
const updated = await this.prisma.withdrawalOrder.update({
|
||||
const updated = await client.withdrawalOrder.update({
|
||||
where: { id: order.id },
|
||||
data,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue