import Decimal from 'decimal.js'; import { UserId, WalletId, AssetType, WalletStatus, Money, Balance, Hashpower, } from '@/domain/value-objects'; import { DomainEvent, DepositCompletedEvent, BalanceDeductedEvent, RewardAddedEvent, RewardMovedToSettleableEvent, RewardExpiredEvent, SettlementCompletedEvent, } from '@/domain/events'; import { DomainError, InsufficientBalanceError, WalletFrozenError, } from '@/shared/exceptions/domain.exception'; export interface WalletBalances { usdt: Balance; dst: Balance; bnb: Balance; og: Balance; rwad: Balance; } export interface WalletRewards { pendingUsdt: Money; pendingHashpower: Hashpower; pendingExpireAt: Date | null; settleableUsdt: Money; settleableHashpower: Hashpower; settledTotalUsdt: Money; settledTotalHashpower: Hashpower; expiredTotalUsdt: Money; expiredTotalHashpower: Hashpower; } export class WalletAccount { private readonly _walletId: WalletId; private readonly _accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) private readonly _userId: UserId; // 保留兼容 private _balances: WalletBalances; private _hashpower: Hashpower; private _rewards: WalletRewards; private _status: WalletStatus; private _hasPlanted: boolean; // 是否已认种过 private readonly _createdAt: Date; private _updatedAt: Date; private _domainEvents: DomainEvent[] = []; private constructor( walletId: WalletId, accountSequence: string, userId: UserId, balances: WalletBalances, hashpower: Hashpower, rewards: WalletRewards, status: WalletStatus, hasPlanted: boolean, createdAt: Date, updatedAt: Date, ) { this._walletId = walletId; this._accountSequence = accountSequence; this._userId = userId; this._balances = balances; this._hashpower = hashpower; this._rewards = rewards; this._status = status; this._hasPlanted = hasPlanted; this._createdAt = createdAt; this._updatedAt = updatedAt; } // Getters get walletId(): WalletId { return this._walletId; } get accountSequence(): string { return this._accountSequence; } get userId(): UserId { return this._userId; } get balances(): WalletBalances { return this._balances; } get hashpower(): Hashpower { return this._hashpower; } get rewards(): WalletRewards { return this._rewards; } get status(): WalletStatus { return this._status; } get createdAt(): Date { return this._createdAt; } get updatedAt(): Date { return this._updatedAt; } get hasPlanted(): boolean { return this._hasPlanted; } get isActive(): boolean { return this._status === WalletStatus.ACTIVE; } get domainEvents(): DomainEvent[] { return [...this._domainEvents]; } static createNew(accountSequence: string, userId: UserId): WalletAccount { const now = new Date(); return new WalletAccount( WalletId.create(0), // Will be set by database accountSequence, userId, { usdt: Balance.zero('USDT'), dst: Balance.zero('DST'), bnb: Balance.zero('BNB'), og: Balance.zero('OG'), rwad: Balance.zero('RWAD'), }, Hashpower.zero(), { pendingUsdt: Money.zero('USDT'), pendingHashpower: Hashpower.zero(), pendingExpireAt: null, settleableUsdt: Money.zero('USDT'), settleableHashpower: Hashpower.zero(), settledTotalUsdt: Money.zero('USDT'), settledTotalHashpower: Hashpower.zero(), expiredTotalUsdt: Money.zero('USDT'), expiredTotalHashpower: Hashpower.zero(), }, WalletStatus.ACTIVE, false, // hasPlanted now, now, ); } static reconstruct(params: { walletId: bigint; accountSequence: string; userId: bigint; usdtAvailable: Decimal; usdtFrozen: Decimal; dstAvailable: Decimal; dstFrozen: Decimal; bnbAvailable: Decimal; bnbFrozen: Decimal; ogAvailable: Decimal; ogFrozen: Decimal; rwadAvailable: Decimal; rwadFrozen: Decimal; hashpower: Decimal; pendingUsdt: Decimal; pendingHashpower: Decimal; pendingExpireAt: Date | null; settleableUsdt: Decimal; settleableHashpower: Decimal; settledTotalUsdt: Decimal; settledTotalHashpower: Decimal; expiredTotalUsdt: Decimal; expiredTotalHashpower: Decimal; status: string; hasPlanted: boolean; createdAt: Date; updatedAt: Date; }): WalletAccount { return new WalletAccount( WalletId.create(params.walletId), params.accountSequence, UserId.create(params.userId), { usdt: Balance.create(Money.USDT(params.usdtAvailable), Money.USDT(params.usdtFrozen)), dst: Balance.create(Money.DST(params.dstAvailable), Money.DST(params.dstFrozen)), bnb: Balance.create(Money.BNB(params.bnbAvailable), Money.BNB(params.bnbFrozen)), og: Balance.create(Money.OG(params.ogAvailable), Money.OG(params.ogFrozen)), rwad: Balance.create(Money.RWAD(params.rwadAvailable), Money.RWAD(params.rwadFrozen)), }, Hashpower.create(params.hashpower), { pendingUsdt: Money.USDT(params.pendingUsdt), pendingHashpower: Hashpower.create(params.pendingHashpower), pendingExpireAt: params.pendingExpireAt, settleableUsdt: Money.USDT(params.settleableUsdt), settleableHashpower: Hashpower.create(params.settleableHashpower), settledTotalUsdt: Money.USDT(params.settledTotalUsdt), settledTotalHashpower: Hashpower.create(params.settledTotalHashpower), expiredTotalUsdt: Money.USDT(params.expiredTotalUsdt), expiredTotalHashpower: Hashpower.create(params.expiredTotalHashpower), }, params.status as WalletStatus, params.hasPlanted, params.createdAt, params.updatedAt, ); } // 标记为已认种 markAsPlanted(): void { this._hasPlanted = true; this._updatedAt = new Date(); } // 充值入账 deposit(amount: Money, chainType: string, txHash: string): void { this.ensureActive(); const balance = this.getBalance(amount.currency as AssetType); const newBalance = balance.add(amount); this.setBalance(amount.currency as AssetType, newBalance); this._updatedAt = new Date(); this.addDomainEvent(new DepositCompletedEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), amount: amount.value.toString(), assetType: amount.currency, chainType, txHash, balanceAfter: newBalance.available.value.toString(), })); } // 直接增加可用余额(用于系统账户分配) addAvailableBalance(amount: Money): void { this.ensureActive(); const balance = this.getBalance(amount.currency as AssetType); const newBalance = balance.add(amount); this.setBalance(amount.currency as AssetType, newBalance); this._updatedAt = new Date(); } // 扣款 (如认种支付) deduct(amount: Money, reason: string, refOrderId?: string): void { this.ensureActive(); const balance = this.getBalance(amount.currency as AssetType); if (balance.available.lessThan(amount)) { throw new InsufficientBalanceError( amount.currency, amount.value.toString(), balance.available.value.toString(), ); } const newBalance = balance.deduct(amount); this.setBalance(amount.currency as AssetType, newBalance); this._updatedAt = new Date(); this.addDomainEvent(new BalanceDeductedEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), amount: amount.value.toString(), assetType: amount.currency, reason, refOrderId, balanceAfter: newBalance.available.value.toString(), })); } // 冻结资金 freeze(amount: Money): void { this.ensureActive(); const balance = this.getBalance(amount.currency as AssetType); const newBalance = balance.freeze(amount); this.setBalance(amount.currency as AssetType, newBalance); this._updatedAt = new Date(); } // 解冻资金 unfreeze(amount: Money): void { this.ensureActive(); const balance = this.getBalance(amount.currency as AssetType); const newBalance = balance.unfreeze(amount); this.setBalance(amount.currency as AssetType, newBalance); this._updatedAt = new Date(); } // 从冻结余额扣款 deductFrozen(amount: Money, reason: string, refOrderId?: string): void { this.ensureActive(); const balance = this.getBalance(amount.currency as AssetType); const newBalance = balance.deductFrozen(amount); this.setBalance(amount.currency as AssetType, newBalance); this._updatedAt = new Date(); this.addDomainEvent(new BalanceDeductedEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), amount: amount.value.toString(), assetType: amount.currency, reason, refOrderId, balanceAfter: newBalance.available.value.toString(), })); } // 添加待领取奖励 addPendingReward(usdtAmount: Money, hashpowerAmount: Hashpower, expireAt: Date, refOrderId?: string): void { this.ensureActive(); this._rewards = { ...this._rewards, pendingUsdt: this._rewards.pendingUsdt.add(usdtAmount), pendingHashpower: this._rewards.pendingHashpower.add(hashpowerAmount), pendingExpireAt: expireAt, }; this._updatedAt = new Date(); this.addDomainEvent(new RewardAddedEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), usdtAmount: usdtAmount.value.toString(), hashpowerAmount: hashpowerAmount.value.toString(), expireAt: expireAt.toISOString(), refOrderId, })); } // 待领取 -> 可结算 movePendingToSettleable(): void { this.ensureActive(); if (this._rewards.pendingUsdt.isZero() && this._rewards.pendingHashpower.isZero()) { throw new DomainError('No pending rewards to move'); } const movedUsdt = this._rewards.pendingUsdt; const movedHashpower = this._rewards.pendingHashpower; this._rewards = { ...this._rewards, pendingUsdt: Money.zero('USDT'), pendingHashpower: Hashpower.zero(), pendingExpireAt: null, settleableUsdt: this._rewards.settleableUsdt.add(movedUsdt), settleableHashpower: this._rewards.settleableHashpower.add(movedHashpower), }; // 增加算力 this._hashpower = this._hashpower.add(movedHashpower); this._updatedAt = new Date(); this.addDomainEvent(new RewardMovedToSettleableEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), usdtAmount: movedUsdt.value.toString(), hashpowerAmount: movedHashpower.value.toString(), })); } /** * 直接增加可结算余额(用于 pending_rewards 表方案) * 当用户认种后,从 pending_rewards 表中结算的金额直接加到可结算余额 */ addSettleableReward(usdtAmount: Money, hashpowerAmount: Hashpower): void { this.ensureActive(); this._rewards = { ...this._rewards, settleableUsdt: this._rewards.settleableUsdt.add(usdtAmount), settleableHashpower: this._rewards.settleableHashpower.add(hashpowerAmount), }; // 增加算力 this._hashpower = this._hashpower.add(hashpowerAmount); this._updatedAt = new Date(); this.addDomainEvent(new RewardMovedToSettleableEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), usdtAmount: usdtAmount.value.toString(), hashpowerAmount: hashpowerAmount.value.toString(), })); } // 奖励过期 expirePendingRewards(): void { if (this._rewards.pendingUsdt.isZero() && this._rewards.pendingHashpower.isZero()) { return; } const expiredUsdt = this._rewards.pendingUsdt; const expiredHashpower = this._rewards.pendingHashpower; this._rewards = { ...this._rewards, pendingUsdt: Money.zero('USDT'), pendingHashpower: Hashpower.zero(), pendingExpireAt: null, expiredTotalUsdt: this._rewards.expiredTotalUsdt.add(expiredUsdt), expiredTotalHashpower: this._rewards.expiredTotalHashpower.add(expiredHashpower), }; this._updatedAt = new Date(); this.addDomainEvent(new RewardExpiredEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), usdtAmount: expiredUsdt.value.toString(), hashpowerAmount: expiredHashpower.value.toString(), })); } // 结算奖励 settleRewards(usdtAmount: Money, settleCurrency: string, receivedAmount: Money, settlementOrderId: string, swapTxHash?: string): void { this.ensureActive(); if (this._rewards.settleableUsdt.lessThan(usdtAmount)) { throw new InsufficientBalanceError( 'USDT (settleable)', usdtAmount.value.toString(), this._rewards.settleableUsdt.value.toString(), ); } // 扣减可结算USDT this._rewards = { ...this._rewards, settleableUsdt: this._rewards.settleableUsdt.subtract(usdtAmount), settledTotalUsdt: this._rewards.settledTotalUsdt.add(usdtAmount), }; // 增加目标币种余额 const targetBalance = this.getBalance(settleCurrency as AssetType); this.setBalance(settleCurrency as AssetType, targetBalance.add(receivedAmount)); this._updatedAt = new Date(); this.addDomainEvent(new SettlementCompletedEvent({ userId: this._userId.toString(), walletId: this._walletId.toString(), settlementOrderId, usdtAmount: usdtAmount.value.toString(), settleCurrency, receivedAmount: receivedAmount.value.toString(), swapTxHash, })); } // 冻结钱包 freezeWallet(): void { if (this._status === WalletStatus.FROZEN) { throw new DomainError('Wallet is already frozen'); } this._status = WalletStatus.FROZEN; this._updatedAt = new Date(); } // 解冻钱包 unfreezeWallet(): void { if (this._status !== WalletStatus.FROZEN) { throw new DomainError('Wallet is not frozen'); } this._status = WalletStatus.ACTIVE; this._updatedAt = new Date(); } private getBalance(assetType: AssetType): Balance { switch (assetType) { case AssetType.USDT: return this._balances.usdt; case AssetType.DST: return this._balances.dst; case AssetType.BNB: return this._balances.bnb; case AssetType.OG: return this._balances.og; case AssetType.RWAD: return this._balances.rwad; default: throw new DomainError(`Unknown asset type: ${assetType}`); } } private setBalance(assetType: AssetType, balance: Balance): void { switch (assetType) { case AssetType.USDT: this._balances.usdt = balance; break; case AssetType.DST: this._balances.dst = balance; break; case AssetType.BNB: this._balances.bnb = balance; break; case AssetType.OG: this._balances.og = balance; break; case AssetType.RWAD: this._balances.rwad = balance; break; default: throw new DomainError(`Unknown asset type: ${assetType}`); } } private ensureActive(): void { if (this._status !== WalletStatus.ACTIVE) { throw new WalletFrozenError(this._walletId.toString()); } } private addDomainEvent(event: DomainEvent): void { this._domainEvents.push(event); } clearDomainEvents(): void { this._domainEvents = []; } }