From 305bdf63af6e5f23dd882684262938280fd8e69b Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 25 Dec 2025 20:44:55 -0800 Subject: [PATCH] =?UTF-8?q?fix(wallet-service):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=92=B1=E5=8C=85=E4=B9=90=E8=A7=82=E9=94=81=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WalletAccount aggregate 添加 version 字段 - WalletAccountRepositoryImpl 使用 updateMany + version 检查实现乐观锁 - requestWithdrawal 添加重试机制处理乐观锁冲突 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../services/wallet-application.service.ts | 34 +++++++++++ .../aggregates/wallet-account.aggregate.ts | 7 +++ .../wallet-account.repository.impl.ts | 58 ++++++++++++++++--- 3 files changed, 92 insertions(+), 7 deletions(-) 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 a6dbad93..a46a7546 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 @@ -1349,6 +1349,40 @@ export class WalletApplicationService { fee: number; netAmount: number; status: string; + }> { + const MAX_RETRIES = 3; + let retries = 0; + + while (retries < MAX_RETRIES) { + try { + return await this.executeRequestWithdrawal(command); + } catch (error) { + if (this.isOptimisticLockError(error)) { + retries++; + this.logger.warn(`[requestWithdrawal] Optimistic lock conflict for ${command.userId}, retry ${retries}/${MAX_RETRIES}`); + if (retries >= MAX_RETRIES) { + this.logger.error(`[requestWithdrawal] Max retries exceeded for ${command.userId}`); + throw error; + } + await this.sleep(50 * retries); + } else { + throw error; + } + } + } + + throw new Error('Unexpected: exited retry loop without result'); + } + + /** + * Execute the withdrawal request logic + */ + private async executeRequestWithdrawal(command: RequestWithdrawalCommand): Promise<{ + orderNo: string; + amount: number; + fee: number; + netAmount: number; + status: string; }> { const userId = BigInt(command.userId); const amount = Money.USDT(command.amount); diff --git a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts index 912a5a86..0e8c501d 100644 --- a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts @@ -40,6 +40,7 @@ export class WalletAccount { private _rewards: WalletRewards; private _status: WalletStatus; private _hasPlanted: boolean; // 是否已认种过 + private _version: number; // 乐观锁版本号 private readonly _createdAt: Date; private _updatedAt: Date; private _domainEvents: DomainEvent[] = []; @@ -53,6 +54,7 @@ export class WalletAccount { rewards: WalletRewards, status: WalletStatus, hasPlanted: boolean, + version: number, createdAt: Date, updatedAt: Date, ) { @@ -64,6 +66,7 @@ export class WalletAccount { this._rewards = rewards; this._status = status; this._hasPlanted = hasPlanted; + this._version = version; this._createdAt = createdAt; this._updatedAt = updatedAt; } @@ -76,6 +79,7 @@ export class WalletAccount { get hashpower(): Hashpower { return this._hashpower; } get rewards(): WalletRewards { return this._rewards; } get status(): WalletStatus { return this._status; } + get version(): number { return this._version; } get createdAt(): Date { return this._createdAt; } get updatedAt(): Date { return this._updatedAt; } get hasPlanted(): boolean { return this._hasPlanted; } @@ -109,6 +113,7 @@ export class WalletAccount { }, WalletStatus.ACTIVE, false, // hasPlanted + 0, // version (new wallet starts at 0) now, now, ); @@ -140,6 +145,7 @@ export class WalletAccount { expiredTotalHashpower: Decimal; status: string; hasPlanted: boolean; + version: number; createdAt: Date; updatedAt: Date; }): WalletAccount { @@ -168,6 +174,7 @@ export class WalletAccount { }, params.status as WalletStatus, params.hasPlanted, + params.version, params.createdAt, params.updatedAt, ); diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts index 96a47c4b..e6850c9b 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts @@ -1,12 +1,15 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; import { IWalletAccountRepository } from '@/domain/repositories'; import { WalletAccount } from '@/domain/aggregates'; import { UserId, WalletStatus } from '@/domain/value-objects'; +import { OptimisticLockError } from '@/shared/exceptions/domain.exception'; import Decimal from 'decimal.js'; @Injectable() export class WalletAccountRepositoryImpl implements IWalletAccountRepository { + private readonly logger = new Logger(WalletAccountRepositoryImpl.name); + constructor(private readonly prisma: PrismaService) {} async save(wallet: WalletAccount): Promise { @@ -38,15 +41,54 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository { }; if (wallet.walletId.value === BigInt(0)) { - // Create new - const created = await this.prisma.walletAccount.create({ data }); + // Create new wallet with version = 0 + const created = await this.prisma.walletAccount.create({ + data: { + ...data, + version: 0, + }, + }); return this.toDomain(created); } else { - // Update existing - const updated = await this.prisma.walletAccount.update({ - where: { id: wallet.walletId.value }, - data, + // Update existing with optimistic lock + const currentVersion = wallet.version; + const newVersion = currentVersion + 1; + + const result = await this.prisma.walletAccount.updateMany({ + where: { + id: wallet.walletId.value, + version: currentVersion, // Optimistic lock: only update if version matches + }, + data: { + ...data, + version: newVersion, + updatedAt: new Date(), + }, }); + + if (result.count === 0) { + // Version mismatch - concurrent modification detected + this.logger.warn( + `[OPTIMISTIC_LOCK] Conflict detected for wallet ${wallet.accountSequence} (id=${wallet.walletId.value}, version=${currentVersion})`, + ); + throw new OptimisticLockError( + `Wallet ${wallet.accountSequence} was modified by another transaction (version=${currentVersion})`, + ); + } + + this.logger.debug( + `[SAVE] Wallet ${wallet.accountSequence} updated (version: ${currentVersion} -> ${newVersion})`, + ); + + // Fetch the updated record to return + const updated = await this.prisma.walletAccount.findUnique({ + where: { id: wallet.walletId.value }, + }); + + if (!updated) { + throw new Error(`Wallet ${wallet.walletId.value} not found after update`); + } + return this.toDomain(updated); } } @@ -120,6 +162,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository { expiredTotalHashpower: Decimal; status: string; hasPlanted: boolean; + version: number; createdAt: Date; updatedAt: Date; }): WalletAccount { @@ -149,6 +192,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository { expiredTotalHashpower: new Decimal(record.expiredTotalHashpower.toString()), status: record.status, hasPlanted: record.hasPlanted, + version: record.version, createdAt: record.createdAt, updatedAt: record.updatedAt, });