From b9911ab460371617972b454e064fd20ea8e8b099 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 6 Jan 2026 09:10:41 -0800 Subject: [PATCH] =?UTF-8?q?feat(wallet-service):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=89=8B=E7=BB=AD=E8=B4=B9=E5=BD=92=E9=9B=86=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增系统账户 S0000000006 (user_id=-6) 用于归集提现手续费 - 新增 FEE_COLLECTION 流水类型记录手续费归集 - 区块链提现完成时使用 UnitOfWork 事务归集手续费 - 法币提现完成时在事务中归集手续费 - WithdrawalOrderRepository 添加事务支持 - 所有手续费归集操作使用乐观锁保护 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../migration.sql | 33 ++++++ .../fiat-withdrawal-application.service.ts | 71 +++++++++++- .../services/wallet-application.service.ts | 109 ++++++++++++++---- .../withdrawal-order.repository.interface.ts | 3 +- .../value-objects/ledger-entry-type.enum.ts | 1 + .../src/domain/value-objects/user-id.vo.ts | 2 + .../withdrawal-order.repository.impl.ts | 12 +- 7 files changed, 205 insertions(+), 26 deletions(-) create mode 100644 backend/services/wallet-service/prisma/migrations/20260106000000_add_fee_collection_account/migration.sql diff --git a/backend/services/wallet-service/prisma/migrations/20260106000000_add_fee_collection_account/migration.sql b/backend/services/wallet-service/prisma/migrations/20260106000000_add_fee_collection_account/migration.sql new file mode 100644 index 00000000..0ed3a256 --- /dev/null +++ b/backend/services/wallet-service/prisma/migrations/20260106000000_add_fee_collection_account/migration.sql @@ -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; diff --git a/backend/services/wallet-service/src/application/services/fiat-withdrawal-application.service.ts b/backend/services/wallet-service/src/application/services/fiat-withdrawal-application.service.ts index f599b1cf..85ba2f45 100644 --- a/backend/services/wallet-service/src/application/services/fiat-withdrawal-application.service.ts +++ b/backend/services/wallet-service/src/application/services/fiat-withdrawal-application.service.ts @@ -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 { 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}`); + } }); // 清除缓存 diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 5a6f074e..6a7187c2 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -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 { + 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 { @@ -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 = { 'S0000000001': 'COST_ACCOUNT', 'S0000000002': 'OPERATION_ACCOUNT', 'S0000000003': 'HQ_COMMUNITY', 'S0000000004': 'RWAD_POOL_PENDING', 'S0000000005': 'PLATFORM_FEE' }; + const fixedAccountTypes: Record = { '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 }> = []; diff --git a/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts index ed116d9e..fb2fe053 100644 --- a/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/withdrawal-order.repository.interface.ts @@ -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; + save(order: WithdrawalOrder, options?: RepositorySaveOptions): Promise; findById(orderId: bigint): Promise; findByOrderNo(orderNo: string): Promise; findByUserId(userId: bigint, status?: WithdrawalStatus): Promise; diff --git a/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts b/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts index fdaf9093..72a755c2 100644 --- a/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts +++ b/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts @@ -18,4 +18,5 @@ export enum LedgerEntryType { FREEZE = 'FREEZE', UNFREEZE = 'UNFREEZE', SYSTEM_ALLOCATION = 'SYSTEM_ALLOCATION', // 系统账户分配 + FEE_COLLECTION = 'FEE_COLLECTION', // 手续费归集 } diff --git a/backend/services/wallet-service/src/domain/value-objects/user-id.vo.ts b/backend/services/wallet-service/src/domain/value-objects/user-id.vo.ts index 943287f2..58898ce2 100644 --- a/backend/services/wallet-service/src/domain/value-objects/user-id.vo.ts +++ b/backend/services/wallet-service/src/domain/value-objects/user-id.vo.ts @@ -14,6 +14,8 @@ export class UserId { // -2: 成本费账户 (S0000000002) // -3: 运营费账户 (S0000000003) // -4: RWA底池 (S0000000004) + // -5: 分享权益池 (S0000000005) + // -6: 手续费归集账户 (S0000000006) return new UserId(bigintValue); } diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts index c6bb4c58..b29e5245 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts @@ -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 { + private getClient(options?: RepositorySaveOptions): PrismaService | PrismaTransactionClient { + return options?.tx || this.prisma; + } + + async save(order: WithdrawalOrder, options?: RepositorySaveOptions): Promise { + 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, });