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:
hailin 2026-01-06 09:10:41 -08:00
parent 99b725db0a
commit b9911ab460
7 changed files with 205 additions and 26 deletions

View File

@ -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;

View File

@ -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}`);
}
});
// 清除缓存

View File

@ -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 }> = [];

View File

@ -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[]>;

View File

@ -18,4 +18,5 @@ export enum LedgerEntryType {
FREEZE = 'FREEZE',
UNFREEZE = 'UNFREEZE',
SYSTEM_ALLOCATION = 'SYSTEM_ALLOCATION', // 系统账户分配
FEE_COLLECTION = 'FEE_COLLECTION', // 手续费归集
}

View File

@ -14,6 +14,8 @@ export class UserId {
// -2: 成本费账户 (S0000000002)
// -3: 运营费账户 (S0000000003)
// -4: RWA底池 (S0000000004)
// -5: 分享权益池 (S0000000005)
// -6: 手续费归集账户 (S0000000006)
return new UserId(bigintValue);
}

View File

@ -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,
});