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

415 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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