259 lines
7.6 KiB
TypeScript
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;
|
|
}
|
|
}
|