415 lines
12 KiB
TypeScript
415 lines
12 KiB
TypeScript
import { PlantingOrderStatus } from '../value-objects/planting-order-status.enum';
|
||
import { ProvinceCitySelection } from '../value-objects/province-city-selection.vo';
|
||
import { FundAllocation } from '../value-objects/fund-allocation.vo';
|
||
import { TreeCount } from '../value-objects/tree-count.vo';
|
||
import { PRICE_PER_TREE } from '../value-objects/fund-allocation-target-type.enum';
|
||
import { DomainEvent } from '../events/domain-event.interface';
|
||
import { PlantingOrderCreatedEvent } from '../events/planting-order-created.event';
|
||
import { ProvinceCityConfirmedEvent } from '../events/province-city-confirmed.event';
|
||
import { PlantingOrderPaidEvent } from '../events/planting-order-paid.event';
|
||
import { FundsAllocatedEvent } from '../events/funds-allocated.event';
|
||
import { PoolInjectedEvent } from '../events/pool-injected.event';
|
||
import { MiningEnabledEvent } from '../events/mining-enabled.event';
|
||
|
||
export interface PlantingOrderData {
|
||
id?: bigint;
|
||
orderNo: string;
|
||
userId: bigint;
|
||
treeCount: number;
|
||
totalAmount: number;
|
||
status: PlantingOrderStatus;
|
||
selectedProvince?: string | null;
|
||
selectedCity?: string | null;
|
||
provinceCitySelectedAt?: Date | null;
|
||
provinceCityConfirmedAt?: Date | null;
|
||
poolInjectionBatchId?: bigint | null;
|
||
poolInjectionScheduledTime?: Date | null;
|
||
poolInjectionActualTime?: Date | null;
|
||
poolInjectionTxHash?: string | null;
|
||
miningEnabledAt?: Date | null;
|
||
createdAt?: Date;
|
||
paidAt?: Date | null;
|
||
fundAllocatedAt?: Date | null;
|
||
}
|
||
|
||
export class PlantingOrder {
|
||
private _id: bigint | null;
|
||
private readonly _orderNo: string;
|
||
private readonly _userId: bigint;
|
||
private readonly _treeCount: TreeCount;
|
||
private readonly _totalAmount: number;
|
||
private _provinceCitySelection: ProvinceCitySelection | null;
|
||
private _status: PlantingOrderStatus;
|
||
private _fundAllocations: FundAllocation[];
|
||
private _poolInjectionBatchId: bigint | null;
|
||
private _poolInjectionScheduledTime: Date | null;
|
||
private _poolInjectionActualTime: Date | null;
|
||
private _poolInjectionTxHash: string | null;
|
||
private _miningEnabledAt: Date | null;
|
||
private readonly _createdAt: Date;
|
||
private _paidAt: Date | null;
|
||
private _fundAllocatedAt: Date | null;
|
||
|
||
// 领域事件
|
||
private _domainEvents: DomainEvent[] = [];
|
||
|
||
private constructor(
|
||
orderNo: string,
|
||
userId: bigint,
|
||
treeCount: TreeCount,
|
||
totalAmount: number,
|
||
createdAt?: Date,
|
||
) {
|
||
this._id = null;
|
||
this._orderNo = orderNo;
|
||
this._userId = userId;
|
||
this._treeCount = treeCount;
|
||
this._totalAmount = totalAmount;
|
||
this._status = PlantingOrderStatus.CREATED;
|
||
this._provinceCitySelection = null;
|
||
this._fundAllocations = [];
|
||
this._poolInjectionBatchId = null;
|
||
this._poolInjectionScheduledTime = null;
|
||
this._poolInjectionActualTime = null;
|
||
this._poolInjectionTxHash = null;
|
||
this._miningEnabledAt = null;
|
||
this._createdAt = createdAt || new Date();
|
||
this._paidAt = null;
|
||
this._fundAllocatedAt = null;
|
||
}
|
||
|
||
// Getters
|
||
get id(): bigint | null {
|
||
return this._id;
|
||
}
|
||
get orderNo(): string {
|
||
return this._orderNo;
|
||
}
|
||
get userId(): bigint {
|
||
return this._userId;
|
||
}
|
||
get treeCount(): TreeCount {
|
||
return this._treeCount;
|
||
}
|
||
get totalAmount(): number {
|
||
return this._totalAmount;
|
||
}
|
||
get status(): PlantingOrderStatus {
|
||
return this._status;
|
||
}
|
||
get provinceCitySelection(): ProvinceCitySelection | null {
|
||
return this._provinceCitySelection;
|
||
}
|
||
get fundAllocations(): ReadonlyArray<FundAllocation> {
|
||
return this._fundAllocations;
|
||
}
|
||
get poolInjectionBatchId(): bigint | null {
|
||
return this._poolInjectionBatchId;
|
||
}
|
||
get poolInjectionScheduledTime(): Date | null {
|
||
return this._poolInjectionScheduledTime;
|
||
}
|
||
get poolInjectionActualTime(): Date | null {
|
||
return this._poolInjectionActualTime;
|
||
}
|
||
get poolInjectionTxHash(): string | null {
|
||
return this._poolInjectionTxHash;
|
||
}
|
||
get miningEnabledAt(): Date | null {
|
||
return this._miningEnabledAt;
|
||
}
|
||
get isMiningEnabled(): boolean {
|
||
return this._miningEnabledAt !== null;
|
||
}
|
||
get createdAt(): Date {
|
||
return this._createdAt;
|
||
}
|
||
get paidAt(): Date | null {
|
||
return this._paidAt;
|
||
}
|
||
get fundAllocatedAt(): Date | null {
|
||
return this._fundAllocatedAt;
|
||
}
|
||
get domainEvents(): ReadonlyArray<DomainEvent> {
|
||
return this._domainEvents;
|
||
}
|
||
|
||
/**
|
||
* 工厂方法:创建认种订单
|
||
*/
|
||
static create(userId: bigint, treeCount: number): PlantingOrder {
|
||
if (treeCount <= 0) {
|
||
throw new Error('认种数量必须大于0');
|
||
}
|
||
|
||
const orderNo = `PLT${Date.now()}${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
|
||
const tree = TreeCount.create(treeCount);
|
||
const totalAmount = treeCount * PRICE_PER_TREE;
|
||
|
||
const order = new PlantingOrder(orderNo, userId, tree, totalAmount);
|
||
|
||
// 发布领域事件
|
||
order._domainEvents.push(
|
||
new PlantingOrderCreatedEvent(orderNo, {
|
||
orderNo: order.orderNo,
|
||
userId: order.userId.toString(),
|
||
treeCount: order.treeCount.value,
|
||
totalAmount: order.totalAmount,
|
||
}),
|
||
);
|
||
|
||
return order;
|
||
}
|
||
|
||
/**
|
||
* 选择省市 (5秒倒计时前)
|
||
*/
|
||
selectProvinceCity(
|
||
provinceCode: string,
|
||
provinceName: string,
|
||
cityCode: string,
|
||
cityName: string,
|
||
): void {
|
||
this.ensureStatus(PlantingOrderStatus.CREATED);
|
||
|
||
if (this._provinceCitySelection?.isConfirmed) {
|
||
throw new Error('省市已确认,不可修改');
|
||
}
|
||
|
||
this._provinceCitySelection = ProvinceCitySelection.create(
|
||
provinceCode,
|
||
provinceName,
|
||
cityCode,
|
||
cityName,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 确认省市选择 (5秒后)
|
||
*/
|
||
confirmProvinceCity(): void {
|
||
this.ensureStatus(PlantingOrderStatus.CREATED);
|
||
|
||
if (!this._provinceCitySelection) {
|
||
throw new Error('请先选择省市');
|
||
}
|
||
|
||
if (!this._provinceCitySelection.canConfirm()) {
|
||
throw new Error('请等待5秒确认时间');
|
||
}
|
||
|
||
this._provinceCitySelection = this._provinceCitySelection.confirm();
|
||
this._status = PlantingOrderStatus.PROVINCE_CITY_CONFIRMED;
|
||
|
||
this._domainEvents.push(
|
||
new ProvinceCityConfirmedEvent(this.orderNo, {
|
||
orderNo: this.orderNo,
|
||
userId: this.userId.toString(),
|
||
provinceCode: this._provinceCitySelection.provinceCode,
|
||
provinceName: this._provinceCitySelection.provinceName,
|
||
cityCode: this._provinceCitySelection.cityCode,
|
||
cityName: this._provinceCitySelection.cityName,
|
||
}),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 标记为已支付
|
||
*/
|
||
markAsPaid(): void {
|
||
this.ensureStatus(PlantingOrderStatus.PROVINCE_CITY_CONFIRMED);
|
||
|
||
this._status = PlantingOrderStatus.PAID;
|
||
this._paidAt = new Date();
|
||
|
||
this._domainEvents.push(
|
||
new PlantingOrderPaidEvent(this.orderNo, {
|
||
orderNo: this.orderNo,
|
||
userId: this.userId.toString(),
|
||
treeCount: this.treeCount.value,
|
||
totalAmount: this.totalAmount,
|
||
provinceCode: this._provinceCitySelection!.provinceCode,
|
||
cityCode: this._provinceCitySelection!.cityCode,
|
||
}),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 分配资金
|
||
*/
|
||
allocateFunds(allocations: FundAllocation[]): void {
|
||
this.ensureStatus(PlantingOrderStatus.PAID);
|
||
|
||
// 验证分配总额
|
||
const totalAllocated = allocations.reduce((sum, a) => sum + a.amount, 0);
|
||
if (Math.abs(totalAllocated - this.totalAmount) > 0.01) {
|
||
throw new Error(
|
||
`资金分配总额不匹配: 期望 ${this.totalAmount}, 实际 ${totalAllocated}`,
|
||
);
|
||
}
|
||
|
||
this._fundAllocations = allocations;
|
||
this._status = PlantingOrderStatus.FUND_ALLOCATED;
|
||
this._fundAllocatedAt = new Date();
|
||
|
||
this._domainEvents.push(
|
||
new FundsAllocatedEvent(this.orderNo, {
|
||
orderNo: this.orderNo,
|
||
allocations: allocations.map((a) => a.toDTO()),
|
||
}),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 安排底池注入
|
||
* 注意:资金分配已移至 reward-service 异步处理,所以支付后即可安排底池注入
|
||
*/
|
||
schedulePoolInjection(batchId: bigint, scheduledTime: Date): void {
|
||
this.ensureStatus(PlantingOrderStatus.PAID);
|
||
|
||
this._poolInjectionBatchId = batchId;
|
||
this._poolInjectionScheduledTime = scheduledTime;
|
||
this._status = PlantingOrderStatus.POOL_SCHEDULED;
|
||
}
|
||
|
||
/**
|
||
* 确认底池注入完成
|
||
*/
|
||
confirmPoolInjection(txHash: string): void {
|
||
this.ensureStatus(PlantingOrderStatus.POOL_SCHEDULED);
|
||
|
||
this._poolInjectionActualTime = new Date();
|
||
this._poolInjectionTxHash = txHash;
|
||
this._status = PlantingOrderStatus.POOL_INJECTED;
|
||
|
||
this._domainEvents.push(
|
||
new PoolInjectedEvent(this.orderNo, {
|
||
orderNo: this.orderNo,
|
||
userId: this.userId.toString(),
|
||
amount: this.treeCount.value * 800, // 800 USDT/棵
|
||
txHash,
|
||
}),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 开启挖矿
|
||
*/
|
||
enableMining(): void {
|
||
this.ensureStatus(PlantingOrderStatus.POOL_INJECTED);
|
||
|
||
this._miningEnabledAt = new Date();
|
||
this._status = PlantingOrderStatus.MINING_ENABLED;
|
||
|
||
this._domainEvents.push(
|
||
new MiningEnabledEvent(this.orderNo, {
|
||
orderNo: this.orderNo,
|
||
userId: this.userId.toString(),
|
||
treeCount: this.treeCount.value,
|
||
}),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 取消订单
|
||
*/
|
||
cancel(): void {
|
||
if (
|
||
this._status !== PlantingOrderStatus.CREATED &&
|
||
this._status !== PlantingOrderStatus.PROVINCE_CITY_CONFIRMED
|
||
) {
|
||
throw new Error('只有未支付的订单才能取消');
|
||
}
|
||
this._status = PlantingOrderStatus.CANCELLED;
|
||
}
|
||
|
||
/**
|
||
* 清除领域事件
|
||
*/
|
||
clearDomainEvents(): void {
|
||
this._domainEvents = [];
|
||
}
|
||
|
||
private ensureStatus(...allowedStatuses: PlantingOrderStatus[]): void {
|
||
if (!allowedStatuses.includes(this._status)) {
|
||
throw new Error(
|
||
`订单状态错误: 当前 ${this._status}, 期望 ${allowedStatuses.join(' 或 ')}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置ID(用于持久化后回填)
|
||
*/
|
||
setId(id: bigint): void {
|
||
if (this._id !== null) {
|
||
throw new Error('ID已设置,不可修改');
|
||
}
|
||
this._id = id;
|
||
}
|
||
|
||
/**
|
||
* 用于从数据库重建
|
||
*/
|
||
static reconstitute(data: PlantingOrderData): PlantingOrder {
|
||
const order = new PlantingOrder(
|
||
data.orderNo,
|
||
data.userId,
|
||
TreeCount.create(data.treeCount),
|
||
data.totalAmount,
|
||
data.createdAt,
|
||
);
|
||
|
||
if (data.id) {
|
||
order._id = data.id;
|
||
}
|
||
order._status = data.status;
|
||
order._paidAt = data.paidAt || null;
|
||
order._fundAllocatedAt = data.fundAllocatedAt || null;
|
||
order._poolInjectionBatchId = data.poolInjectionBatchId || null;
|
||
order._poolInjectionScheduledTime = data.poolInjectionScheduledTime || null;
|
||
order._poolInjectionActualTime = data.poolInjectionActualTime || null;
|
||
order._poolInjectionTxHash = data.poolInjectionTxHash || null;
|
||
order._miningEnabledAt = data.miningEnabledAt || null;
|
||
|
||
if (data.selectedProvince && data.selectedCity) {
|
||
order._provinceCitySelection = ProvinceCitySelection.reconstitute(
|
||
data.selectedProvince,
|
||
'',
|
||
data.selectedCity,
|
||
'',
|
||
data.provinceCitySelectedAt || new Date(),
|
||
data.provinceCityConfirmedAt || null,
|
||
);
|
||
}
|
||
|
||
return order;
|
||
}
|
||
|
||
/**
|
||
* 转换为可持久化的数据对象
|
||
*/
|
||
toPersistence(): PlantingOrderData {
|
||
return {
|
||
id: this._id || undefined,
|
||
orderNo: this._orderNo,
|
||
userId: this._userId,
|
||
treeCount: this._treeCount.value,
|
||
totalAmount: this._totalAmount,
|
||
status: this._status,
|
||
selectedProvince: this._provinceCitySelection?.provinceCode || null,
|
||
selectedCity: this._provinceCitySelection?.cityCode || null,
|
||
provinceCitySelectedAt: this._provinceCitySelection?.selectedAt || null,
|
||
provinceCityConfirmedAt: this._provinceCitySelection?.confirmedAt || null,
|
||
poolInjectionBatchId: this._poolInjectionBatchId,
|
||
poolInjectionScheduledTime: this._poolInjectionScheduledTime,
|
||
poolInjectionActualTime: this._poolInjectionActualTime,
|
||
poolInjectionTxHash: this._poolInjectionTxHash,
|
||
miningEnabledAt: this._miningEnabledAt,
|
||
createdAt: this._createdAt,
|
||
paidAt: this._paidAt,
|
||
fundAllocatedAt: this._fundAllocatedAt,
|
||
};
|
||
}
|
||
}
|