import Decimal from 'decimal.js'; import { UserId, ChainType, AssetType, Money } from '@/domain/value-objects'; import { WithdrawalStatus } from '@/domain/value-objects/withdrawal-status.enum'; import { DomainError } from '@/shared/exceptions/domain.exception'; /** * 提现订单聚合根 * * 提现流程: * 1. 用户发起提现请求 -> PENDING * 2. 冻结用户余额 -> FROZEN * 3. blockchain-service 签名并广播 -> BROADCASTED * 4. 链上确认 -> CONFIRMED * * 失败/取消时解冻资金 */ export class WithdrawalOrder { private readonly _id: bigint; private readonly _orderNo: string; private readonly _accountSequence: string; private readonly _userId: UserId; private readonly _amount: Money; private readonly _fee: Money; // 手续费 private readonly _chainType: ChainType; private readonly _toAddress: string; // 提现目标地址 private _txHash: string | null; private _status: WithdrawalStatus; private _errorMessage: string | null; private _frozenAt: Date | null; private _broadcastedAt: Date | null; private _confirmedAt: Date | null; private readonly _createdAt: Date; private constructor( id: bigint, orderNo: string, accountSequence: string, userId: UserId, amount: Money, fee: Money, chainType: ChainType, toAddress: string, txHash: string | null, status: WithdrawalStatus, errorMessage: string | null, frozenAt: Date | null, broadcastedAt: Date | null, confirmedAt: Date | null, createdAt: Date, ) { this._id = id; this._orderNo = orderNo; this._accountSequence = accountSequence; this._userId = userId; this._amount = amount; this._fee = fee; this._chainType = chainType; this._toAddress = toAddress; this._txHash = txHash; this._status = status; this._errorMessage = errorMessage; this._frozenAt = frozenAt; this._broadcastedAt = broadcastedAt; this._confirmedAt = confirmedAt; this._createdAt = createdAt; } // Getters get id(): bigint { return this._id; } get orderNo(): string { return this._orderNo; } get accountSequence(): string { return this._accountSequence; } get userId(): UserId { return this._userId; } get amount(): Money { return this._amount; } get fee(): Money { return this._fee; } get netAmount(): Money { return this._amount; } // 接收方收到完整金额,手续费由发送方额外承担 get chainType(): ChainType { return this._chainType; } get toAddress(): string { return this._toAddress; } get txHash(): string | null { return this._txHash; } get status(): WithdrawalStatus { return this._status; } get errorMessage(): string | null { return this._errorMessage; } get frozenAt(): Date | null { return this._frozenAt; } get broadcastedAt(): Date | null { return this._broadcastedAt; } get confirmedAt(): Date | null { return this._confirmedAt; } get createdAt(): Date { return this._createdAt; } get isPending(): boolean { return this._status === WithdrawalStatus.PENDING; } get isFrozen(): boolean { return this._status === WithdrawalStatus.FROZEN; } get isBroadcasted(): boolean { return this._status === WithdrawalStatus.BROADCASTED; } get isConfirmed(): boolean { return this._status === WithdrawalStatus.CONFIRMED; } get isFailed(): boolean { return this._status === WithdrawalStatus.FAILED; } get isCancelled(): boolean { return this._status === WithdrawalStatus.CANCELLED; } get isFinished(): boolean { return this._status === WithdrawalStatus.CONFIRMED || this._status === WithdrawalStatus.FAILED || this._status === WithdrawalStatus.CANCELLED; } /** * 生成提现订单号 */ private static generateOrderNo(): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8).toUpperCase(); return `WD${timestamp}${random}`; } /** * 创建提现订单 */ static create(params: { accountSequence: string; userId: UserId; amount: Money; fee: Money; chainType: ChainType; toAddress: string; }): WithdrawalOrder { // 验证金额 if (params.amount.value <= 0) { throw new DomainError('Withdrawal amount must be positive'); } // 验证手续费 if (params.fee.value < 0) { throw new DomainError('Withdrawal fee cannot be negative'); } // 验证净额大于0 if (params.amount.value <= params.fee.value) { throw new DomainError('Withdrawal amount must be greater than fee'); } // 验证地址格式 (简单的EVM地址检查) if (!params.toAddress.match(/^0x[a-fA-F0-9]{40}$/)) { throw new DomainError('Invalid withdrawal address format'); } return new WithdrawalOrder( BigInt(0), // Will be set by database this.generateOrderNo(), params.accountSequence, params.userId, params.amount, params.fee, params.chainType, params.toAddress, null, WithdrawalStatus.PENDING, null, null, null, null, new Date(), ); } /** * 从数据库重建 */ static reconstruct(params: { id: bigint; orderNo: string; accountSequence: string; userId: bigint; amount: Decimal; fee: Decimal; chainType: string; toAddress: string; txHash: string | null; status: string; errorMessage: string | null; frozenAt: Date | null; broadcastedAt: Date | null; confirmedAt: Date | null; createdAt: Date; }): WithdrawalOrder { return new WithdrawalOrder( params.id, params.orderNo, params.accountSequence, UserId.create(params.userId), Money.USDT(params.amount), Money.USDT(params.fee), params.chainType as ChainType, params.toAddress, params.txHash, params.status as WithdrawalStatus, params.errorMessage, params.frozenAt, params.broadcastedAt, params.confirmedAt, params.createdAt, ); } /** * 标记为已冻结 (资金已从可用余额冻结) */ markAsFrozen(): void { if (this._status !== WithdrawalStatus.PENDING) { throw new DomainError('Only pending withdrawals can be frozen'); } this._status = WithdrawalStatus.FROZEN; this._frozenAt = new Date(); } /** * 标记为已广播 */ markAsBroadcasted(txHash: string): void { if (this._status !== WithdrawalStatus.FROZEN) { throw new DomainError('Only frozen withdrawals can be broadcasted'); } this._status = WithdrawalStatus.BROADCASTED; this._txHash = txHash; this._broadcastedAt = new Date(); } /** * 标记为已确认 (链上确认) */ markAsConfirmed(): void { if (this._status !== WithdrawalStatus.BROADCASTED) { throw new DomainError('Only broadcasted withdrawals can be confirmed'); } this._status = WithdrawalStatus.CONFIRMED; this._confirmedAt = new Date(); } /** * 标记为失败 */ markAsFailed(errorMessage: string): void { if (this.isFinished) { throw new DomainError('Cannot fail a finished withdrawal'); } this._status = WithdrawalStatus.FAILED; this._errorMessage = errorMessage; } /** * 取消提现 */ cancel(): void { if (this._status !== WithdrawalStatus.PENDING && this._status !== WithdrawalStatus.FROZEN) { throw new DomainError('Only pending or frozen withdrawals can be cancelled'); } this._status = WithdrawalStatus.CANCELLED; } /** * 是否需要解冻资金 (失败或取消且已冻结) */ needsUnfreeze(): boolean { return (this._status === WithdrawalStatus.FAILED || this._status === WithdrawalStatus.CANCELLED) && this._frozenAt !== null; } }