rwadurian/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts

259 lines
7.6 KiB
TypeScript

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