# Planting Service 开发指导 ## 项目概述 Planting Service 是 RWA 榴莲皇后平台的认种微服务,负责管理认种订单生命周期、省市选择确认、资金分配规则计算、底池注入批次管理和挖矿资格管理。 ## 技术栈 - **框架**: NestJS - **数据库**: PostgreSQL + Prisma ORM - **架构**: DDD + Hexagonal Architecture (六边形架构) - **语言**: TypeScript ## 架构参考 请参考 `identity-service` 的架构模式,保持一致性: ``` planting-service/ ├── prisma/ │ └── schema.prisma # 数据库模型 ├── src/ │ ├── api/ # Presentation Layer (API层) │ │ ├── controllers/ │ │ │ ├── planting-order.controller.ts │ │ │ ├── planting-position.controller.ts │ │ │ └── pool-batch.controller.ts │ │ ├── dto/ │ │ │ ├── create-planting-order.dto.ts │ │ │ ├── select-province-city.dto.ts │ │ │ ├── planting-order.dto.ts │ │ │ └── planting-position.dto.ts │ │ └── api.module.ts │ │ │ ├── application/ # Application Layer (应用层) │ │ ├── commands/ │ │ │ ├── create-planting-order.command.ts │ │ │ ├── select-province-city.command.ts │ │ │ ├── confirm-province-city.command.ts │ │ │ ├── pay-planting-order.command.ts │ │ │ ├── inject-pool.command.ts │ │ │ └── enable-mining.command.ts │ │ ├── queries/ │ │ │ ├── get-user-planting-orders.query.ts │ │ │ ├── get-user-position.query.ts │ │ │ └── get-pool-batch-info.query.ts │ │ ├── handlers/ │ │ │ ├── create-planting-order.handler.ts │ │ │ ├── select-province-city.handler.ts │ │ │ ├── confirm-province-city.handler.ts │ │ │ └── pay-planting-order.handler.ts │ │ └── services/ │ │ └── planting-application.service.ts │ │ │ ├── domain/ # Domain Layer (领域层) │ │ ├── aggregates/ │ │ │ ├── planting-order.aggregate.ts │ │ │ ├── planting-position.aggregate.ts │ │ │ └── pool-injection-batch.aggregate.ts │ │ ├── value-objects/ │ │ │ ├── order-id.vo.ts │ │ │ ├── order-no.vo.ts │ │ │ ├── tree-count.vo.ts │ │ │ ├── money.vo.ts │ │ │ ├── province-code.vo.ts │ │ │ ├── city-code.vo.ts │ │ │ ├── province-city-selection.vo.ts │ │ │ ├── fund-allocation.vo.ts │ │ │ ├── pool-injection-info.vo.ts │ │ │ ├── planting-order-status.enum.ts │ │ │ ├── fund-allocation-target-type.enum.ts │ │ │ └── batch-status.enum.ts │ │ ├── events/ │ │ │ ├── planting-order-created.event.ts │ │ │ ├── province-city-confirmed.event.ts │ │ │ ├── planting-order-paid.event.ts │ │ │ ├── funds-allocated.event.ts │ │ │ ├── pool-injected.event.ts │ │ │ └── mining-enabled.event.ts │ │ ├── repositories/ │ │ │ ├── planting-order.repository.interface.ts │ │ │ ├── planting-position.repository.interface.ts │ │ │ └── pool-injection-batch.repository.interface.ts │ │ └── services/ │ │ ├── fund-allocation.service.ts │ │ └── pool-injection-scheduler.service.ts │ │ │ ├── infrastructure/ # Infrastructure Layer (基础设施层) │ │ ├── persistence/ │ │ │ ├── mappers/ │ │ │ │ ├── planting-order.mapper.ts │ │ │ │ ├── planting-position.mapper.ts │ │ │ │ └── pool-injection-batch.mapper.ts │ │ │ └── repositories/ │ │ │ ├── planting-order.repository.impl.ts │ │ │ ├── planting-position.repository.impl.ts │ │ │ └── pool-injection-batch.repository.impl.ts │ │ ├── external/ │ │ │ ├── wallet-service.client.ts │ │ │ ├── referral-service.client.ts │ │ │ └── blockchain-service.client.ts │ │ └── infrastructure.module.ts │ │ │ ├── app.module.ts │ └── main.ts ├── .env.development ├── .env.example ├── package.json └── tsconfig.json ``` --- ## 第一阶段:项目初始化 ### 1.1 创建 NestJS 项目 ```bash cd backend/services npx @nestjs/cli new planting-service --skip-git --package-manager npm cd planting-service ``` ### 1.2 安装依赖 ```bash npm install @nestjs/config @prisma/client class-validator class-transformer uuid npm install -D prisma @types/uuid ``` ### 1.3 配置环境变量 创建 `.env.development`: ```env DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_planting?schema=public" NODE_ENV=development PORT=3003 # 外部服务 WALLET_SERVICE_URL=http://localhost:3002 IDENTITY_SERVICE_URL=http://localhost:3001 ``` --- ## 第二阶段:数据库设计 (Prisma Schema) ### 2.1 创建 prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================ // 认种订单表 (状态表) // ============================================ model PlantingOrder { id BigInt @id @default(autoincrement()) @map("order_id") orderNo String @unique @map("order_no") @db.VarChar(50) userId BigInt @map("user_id") // 认种信息 treeCount Int @map("tree_count") totalAmount Decimal @map("total_amount") @db.Decimal(20, 8) // 省市选择 (不可修改) selectedProvince String? @map("selected_province") @db.VarChar(10) selectedCity String? @map("selected_city") @db.VarChar(10) provinceCitySelectedAt DateTime? @map("province_city_selected_at") provinceCityConfirmedAt DateTime? @map("province_city_confirmed_at") // 订单状态 status String @default("CREATED") @map("status") @db.VarChar(30) // 底池信息 poolInjectionBatchId BigInt? @map("pool_injection_batch_id") poolInjectionScheduledTime DateTime? @map("pool_injection_scheduled_time") poolInjectionActualTime DateTime? @map("pool_injection_actual_time") poolInjectionTxHash String? @map("pool_injection_tx_hash") @db.VarChar(100) // 挖矿 miningEnabledAt DateTime? @map("mining_enabled_at") // 时间戳 createdAt DateTime @default(now()) @map("created_at") paidAt DateTime? @map("paid_at") fundAllocatedAt DateTime? @map("fund_allocated_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 fundAllocations FundAllocation[] batch PoolInjectionBatch? @relation(fields: [poolInjectionBatchId], references: [id]) @@map("planting_orders") @@index([userId]) @@index([orderNo]) @@index([status]) @@index([poolInjectionBatchId]) @@index([selectedProvince, selectedCity]) @@index([createdAt]) @@index([paidAt]) } // ============================================ // 资金分配明细表 (行为表, append-only) // ============================================ model FundAllocation { id BigInt @id @default(autoincrement()) @map("allocation_id") orderId BigInt @map("order_id") // 分配信息 targetType String @map("target_type") @db.VarChar(50) amount Decimal @map("amount") @db.Decimal(20, 8) targetAccountId String? @map("target_account_id") @db.VarChar(100) // 元数据 metadata Json? @map("metadata") createdAt DateTime @default(now()) @map("created_at") // 关联 order PlantingOrder @relation(fields: [orderId], references: [id]) @@map("fund_allocations") @@index([orderId]) @@index([targetType, targetAccountId]) @@index([createdAt]) } // ============================================ // 用户持仓表 (状态表) // ============================================ model PlantingPosition { id BigInt @id @default(autoincrement()) @map("position_id") userId BigInt @unique @map("user_id") // 持仓统计 totalTreeCount Int @default(0) @map("total_tree_count") effectiveTreeCount Int @default(0) @map("effective_tree_count") pendingTreeCount Int @default(0) @map("pending_tree_count") // 挖矿状态 firstMiningStartAt DateTime? @map("first_mining_start_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 distributions PositionDistribution[] @@map("planting_positions") @@index([userId]) @@index([totalTreeCount]) } // ============================================ // 持仓省市分布表 // ============================================ model PositionDistribution { id BigInt @id @default(autoincrement()) @map("distribution_id") userId BigInt @map("user_id") // 省市信息 provinceCode String? @map("province_code") @db.VarChar(10) cityCode String? @map("city_code") @db.VarChar(10) // 数量 treeCount Int @default(0) @map("tree_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 position PlantingPosition @relation(fields: [userId], references: [userId]) @@unique([userId, provinceCode, cityCode]) @@map("position_province_city_distribution") @@index([userId]) @@index([provinceCode]) @@index([cityCode]) } // ============================================ // 底池注入批次表 (状态表) // ============================================ model PoolInjectionBatch { id BigInt @id @default(autoincrement()) @map("batch_id") batchNo String @unique @map("batch_no") @db.VarChar(50) // 批次时间窗口 (5天) startDate DateTime @map("start_date") @db.Date endDate DateTime @map("end_date") @db.Date // 统计信息 orderCount Int @default(0) @map("order_count") totalAmount Decimal @default(0) @map("total_amount") @db.Decimal(20, 8) // 注入状态 status String @default("PENDING") @map("status") @db.VarChar(20) scheduledInjectionTime DateTime? @map("scheduled_injection_time") actualInjectionTime DateTime? @map("actual_injection_time") injectionTxHash String? @map("injection_tx_hash") @db.VarChar(100) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 orders PlantingOrder[] @@map("pool_injection_batches") @@index([batchNo]) @@index([startDate, endDate]) @@index([status]) @@index([scheduledInjectionTime]) } // ============================================ // 认种事件表 (行为表, append-only) // ============================================ model PlantingEvent { id BigInt @id @default(autoincrement()) @map("event_id") eventType String @map("event_type") @db.VarChar(50) // 聚合根信息 aggregateId String @map("aggregate_id") @db.VarChar(100) aggregateType String @map("aggregate_type") @db.VarChar(50) // 事件数据 eventData Json @map("event_data") // 元数据 userId BigInt? @map("user_id") occurredAt DateTime @default(now()) @map("occurred_at") version Int @default(1) @map("version") @@map("planting_events") @@index([aggregateType, aggregateId]) @@index([eventType]) @@index([userId]) @@index([occurredAt]) } ``` ### 2.2 初始化数据库 ```bash npx prisma migrate dev --name init npx prisma generate ``` --- ## 第三阶段:领域层实现 ### 3.1 值对象 (Value Objects) #### 3.1.1 planting-order-status.enum.ts ```typescript export enum PlantingOrderStatus { CREATED = 'CREATED', // 已创建 PROVINCE_CITY_CONFIRMED = 'PROVINCE_CITY_CONFIRMED', // 省市已确认 PAID = 'PAID', // 已支付 FUND_ALLOCATED = 'FUND_ALLOCATED', // 资金已分配 POOL_SCHEDULED = 'POOL_SCHEDULED', // 底池已排期 POOL_INJECTED = 'POOL_INJECTED', // 底池已注入 MINING_ENABLED = 'MINING_ENABLED', // 挖矿已开启 CANCELLED = 'CANCELLED', // 已取消 } ``` #### 3.1.2 fund-allocation-target-type.enum.ts ```typescript export enum FundAllocationTargetType { COST_ACCOUNT = 'COST_ACCOUNT', // 400 USDT - 成本账户 OPERATION_ACCOUNT = 'OPERATION_ACCOUNT', // 300 USDT - 运营账户 HEADQUARTERS_COMMUNITY = 'HEADQUARTERS_COMMUNITY',// 9 USDT - 总部社区 REFERRAL_RIGHTS = 'REFERRAL_RIGHTS', // 500 USDT - 分享权益 PROVINCE_AREA_RIGHTS = 'PROVINCE_AREA_RIGHTS', // 15 USDT - 省区域权益 PROVINCE_TEAM_RIGHTS = 'PROVINCE_TEAM_RIGHTS', // 20 USDT - 省团队权益 CITY_AREA_RIGHTS = 'CITY_AREA_RIGHTS', // 35 USDT - 市区域权益 CITY_TEAM_RIGHTS = 'CITY_TEAM_RIGHTS', // 40 USDT - 市团队权益 COMMUNITY_RIGHTS = 'COMMUNITY_RIGHTS', // 80 USDT - 社区权益 RWAD_POOL = 'RWAD_POOL', // 800 USDT - RWAD底池 } // 每棵树的资金分配规则 (总计 2199 USDT) export const FUND_ALLOCATION_AMOUNTS: Record = { [FundAllocationTargetType.COST_ACCOUNT]: 400, [FundAllocationTargetType.OPERATION_ACCOUNT]: 300, [FundAllocationTargetType.HEADQUARTERS_COMMUNITY]: 9, [FundAllocationTargetType.REFERRAL_RIGHTS]: 500, [FundAllocationTargetType.PROVINCE_AREA_RIGHTS]: 15, [FundAllocationTargetType.PROVINCE_TEAM_RIGHTS]: 20, [FundAllocationTargetType.CITY_AREA_RIGHTS]: 35, [FundAllocationTargetType.CITY_TEAM_RIGHTS]: 40, [FundAllocationTargetType.COMMUNITY_RIGHTS]: 80, [FundAllocationTargetType.RWAD_POOL]: 800, }; // 验证总额 const TOTAL = Object.values(FUND_ALLOCATION_AMOUNTS).reduce((a, b) => a + b, 0); if (TOTAL !== 2199) { throw new Error(`资金分配配置错误: 总额 ${TOTAL} != 2199`); } ``` #### 3.1.3 batch-status.enum.ts ```typescript export enum BatchStatus { PENDING = 'PENDING', // 待注入 (收集订单中) SCHEDULED = 'SCHEDULED', // 已排期 INJECTING = 'INJECTING', // 注入中 INJECTED = 'INJECTED', // 已注入 } ``` #### 3.1.4 tree-count.vo.ts ```typescript export class TreeCount { private constructor(public readonly value: number) { if (value <= 0 || !Number.isInteger(value)) { throw new Error('认种数量必须是正整数'); } } static create(value: number): TreeCount { return new TreeCount(value); } multiply(factor: number): number { return this.value * factor; } } ``` #### 3.1.5 province-city-selection.vo.ts ```typescript export class ProvinceCitySelection { private constructor( public readonly provinceCode: string, public readonly provinceName: string, public readonly cityCode: string, public readonly cityName: string, public readonly selectedAt: Date, public readonly isConfirmed: boolean, ) {} static create( provinceCode: string, provinceName: string, cityCode: string, cityName: string, ): ProvinceCitySelection { return new ProvinceCitySelection( provinceCode, provinceName, cityCode, cityName, new Date(), false, ); } confirm(): ProvinceCitySelection { if (this.isConfirmed) { throw new Error('省市已确认,不可重复确认'); } return new ProvinceCitySelection( this.provinceCode, this.provinceName, this.cityCode, this.cityName, this.selectedAt, true, ); } /** * 检查是否已过5秒确认时间 */ canConfirm(): boolean { const elapsed = Date.now() - this.selectedAt.getTime(); return elapsed >= 5000; // 5秒 } } ``` #### 3.1.6 fund-allocation.vo.ts ```typescript import { FundAllocationTargetType } from './fund-allocation-target-type.enum'; export class FundAllocation { constructor( public readonly targetType: FundAllocationTargetType, public readonly amount: number, public readonly targetAccountId: string | null, public readonly metadata?: Record, ) {} toDTO() { return { targetType: this.targetType, amount: this.amount, targetAccountId: this.targetAccountId, metadata: this.metadata, }; } } ``` ### 3.2 聚合根 (Aggregates) #### 3.2.1 planting-order.aggregate.ts ```typescript import { v4 as uuidv4 } from 'uuid'; 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'; 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: any[] = []; private constructor( orderNo: string, userId: bigint, treeCount: TreeCount, totalAmount: number, ) { 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 = 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 { return this._fundAllocations; } get poolInjectionBatchId(): bigint | null { return this._poolInjectionBatchId; } get miningEnabledAt(): Date | null { return this._miningEnabledAt; } get isMiningEnabled(): boolean { return this._miningEnabledAt !== null; } get domainEvents(): any[] { return this._domainEvents; } /** * 工厂方法:创建认种订单 */ static create(userId: bigint, treeCount: number): PlantingOrder { const PRICE_PER_TREE = 2199; if (treeCount <= 0) { throw new Error('认种数量必须大于0'); } const orderNo = `PLT${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`; const tree = TreeCount.create(treeCount); const totalAmount = treeCount * PRICE_PER_TREE; const order = new PlantingOrder(orderNo, userId, tree, totalAmount); // 发布领域事件 order._domainEvents.push({ type: 'PlantingOrderCreated', data: { 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({ type: 'ProvinceCityConfirmed', data: { 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({ type: 'PlantingOrderPaid', data: { 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({ type: 'FundsAllocated', data: { orderNo: this.orderNo, allocations: allocations.map(a => a.toDTO()), }, }); } /** * 安排底池注入 */ schedulePoolInjection(batchId: bigint, scheduledTime: Date): void { this.ensureStatus(PlantingOrderStatus.FUND_ALLOCATED); 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({ type: 'PoolInjected', data: { 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({ type: 'MiningEnabled', data: { orderNo: this.orderNo, userId: this.userId.toString(), treeCount: this.treeCount.value, }, }); } /** * 清除领域事件 */ clearDomainEvents(): void { this._domainEvents = []; } private ensureStatus(...allowedStatuses: PlantingOrderStatus[]): void { if (!allowedStatuses.includes(this._status)) { throw new Error(`订单状态错误: 当前 ${this._status}, 期望 ${allowedStatuses.join(' 或 ')}`); } } // 用于从数据库重建 static reconstitute(data: any): PlantingOrder { const order = new PlantingOrder( data.orderNo, data.userId, TreeCount.create(data.treeCount), Number(data.totalAmount), ); order._id = data.id; order._status = data.status; order._paidAt = data.paidAt; order._fundAllocatedAt = data.fundAllocatedAt; order._poolInjectionBatchId = data.poolInjectionBatchId; order._poolInjectionScheduledTime = data.poolInjectionScheduledTime; order._poolInjectionActualTime = data.poolInjectionActualTime; order._poolInjectionTxHash = data.poolInjectionTxHash; order._miningEnabledAt = data.miningEnabledAt; if (data.selectedProvince && data.selectedCity) { order._provinceCitySelection = ProvinceCitySelection.create( data.selectedProvince, data.selectedProvinceName || '', data.selectedCity, data.selectedCityName || '', ); if (data.provinceCityConfirmedAt) { order._provinceCitySelection = order._provinceCitySelection.confirm(); } } return order; } } ``` ### 3.3 领域服务 #### 3.3.1 fund-allocation.service.ts ```typescript import { Injectable } from '@nestjs/common'; import { FundAllocation } from '../value-objects/fund-allocation.vo'; import { FundAllocationTargetType, FUND_ALLOCATION_AMOUNTS, } from '../value-objects/fund-allocation-target-type.enum'; import { PlantingOrder } from '../aggregates/planting-order.aggregate'; @Injectable() export class FundAllocationService { /** * 计算认种订单的资金分配 * 核心业务规则: 2199 USDT 的 10 个去向 */ calculateAllocations( order: PlantingOrder, referralChain: string[], nearestProvinceAuth: string | null, nearestCityAuth: string | null, nearestCommunity: string | null, ): FundAllocation[] { const treeCount = order.treeCount.value; const allocations: FundAllocation[] = []; const selection = order.provinceCitySelection!; // 1. 成本账户: 400 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.COST_ACCOUNT, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.COST_ACCOUNT] * treeCount, 'SYSTEM_COST_ACCOUNT', )); // 2. 运营账户: 300 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.OPERATION_ACCOUNT, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.OPERATION_ACCOUNT] * treeCount, 'SYSTEM_OPERATION_ACCOUNT', )); // 3. 总部社区: 9 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.HEADQUARTERS_COMMUNITY, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.HEADQUARTERS_COMMUNITY] * treeCount, 'SYSTEM_HEADQUARTERS_COMMUNITY', )); // 4. 分享权益: 500 USDT/棵 (分配给推荐链) allocations.push(new FundAllocation( FundAllocationTargetType.REFERRAL_RIGHTS, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.REFERRAL_RIGHTS] * treeCount, referralChain.length > 0 ? referralChain[0] : 'SYSTEM_HEADQUARTERS_COMMUNITY', { referralChain }, )); // 5. 省区域权益: 15 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.PROVINCE_AREA_RIGHTS, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.PROVINCE_AREA_RIGHTS] * treeCount, `SYSTEM_PROVINCE_${selection.provinceCode}`, )); // 6. 省团队权益: 20 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.PROVINCE_TEAM_RIGHTS, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.PROVINCE_TEAM_RIGHTS] * treeCount, nearestProvinceAuth || 'SYSTEM_HEADQUARTERS_COMMUNITY', )); // 7. 市区域权益: 35 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.CITY_AREA_RIGHTS, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.CITY_AREA_RIGHTS] * treeCount, `SYSTEM_CITY_${selection.cityCode}`, )); // 8. 市团队权益: 40 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.CITY_TEAM_RIGHTS, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.CITY_TEAM_RIGHTS] * treeCount, nearestCityAuth || 'SYSTEM_HEADQUARTERS_COMMUNITY', )); // 9. 社区权益: 80 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.COMMUNITY_RIGHTS, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.COMMUNITY_RIGHTS] * treeCount, nearestCommunity || 'SYSTEM_HEADQUARTERS_COMMUNITY', )); // 10. RWAD底池: 800 USDT/棵 allocations.push(new FundAllocation( FundAllocationTargetType.RWAD_POOL, FUND_ALLOCATION_AMOUNTS[FundAllocationTargetType.RWAD_POOL] * treeCount, 'SYSTEM_RWAD_POOL', )); // 验证总额 const total = allocations.reduce((sum, a) => sum + a.amount, 0); const expected = 2199 * treeCount; if (Math.abs(total - expected) > 0.01) { throw new Error(`资金分配计算错误: 总额 ${total} != ${expected}`); } return allocations; } } ``` ### 3.4 仓储接口 #### 3.4.1 planting-order.repository.interface.ts ```typescript import { PlantingOrder } from '../aggregates/planting-order.aggregate'; import { PlantingOrderStatus } from '../value-objects/planting-order-status.enum'; export interface IPlantingOrderRepository { save(order: PlantingOrder): Promise; findById(orderId: bigint): Promise; findByOrderNo(orderNo: string): Promise; findByUserId(userId: bigint, page?: number, pageSize?: number): Promise; findByStatus(status: PlantingOrderStatus, limit?: number): Promise; findPendingPoolScheduling(): Promise; findByBatchId(batchId: bigint): Promise; findReadyForMining(): Promise; countTreesByUserId(userId: bigint): Promise; } export const PLANTING_ORDER_REPOSITORY = Symbol('IPlantingOrderRepository'); ``` --- ## 第四阶段:应用层实现 ### 4.1 应用服务 ```typescript // application/services/planting-application.service.ts import { Injectable, Inject } from '@nestjs/common'; import { PlantingOrder } from '../../domain/aggregates/planting-order.aggregate'; import { IPlantingOrderRepository, PLANTING_ORDER_REPOSITORY } from '../../domain/repositories/planting-order.repository.interface'; import { IPlantingPositionRepository, PLANTING_POSITION_REPOSITORY } from '../../domain/repositories/planting-position.repository.interface'; import { FundAllocationService } from '../../domain/services/fund-allocation.service'; import { WalletServiceClient } from '../../infrastructure/external/wallet-service.client'; @Injectable() export class PlantingApplicationService { constructor( @Inject(PLANTING_ORDER_REPOSITORY) private readonly orderRepository: IPlantingOrderRepository, @Inject(PLANTING_POSITION_REPOSITORY) private readonly positionRepository: IPlantingPositionRepository, private readonly fundAllocationService: FundAllocationService, private readonly walletService: WalletServiceClient, ) {} /** * 创建认种订单 */ async createOrder(userId: bigint, treeCount: number) { // 风控检查 await this.checkRiskControl(userId, treeCount); // 创建订单 const order = PlantingOrder.create(userId, treeCount); await this.orderRepository.save(order); return { orderNo: order.orderNo, treeCount: order.treeCount.value, totalAmount: order.totalAmount, status: order.status, }; } /** * 选择省市 */ async selectProvinceCity( orderNo: string, provinceCode: string, provinceName: string, cityCode: string, cityName: string, ) { const order = await this.orderRepository.findByOrderNo(orderNo); if (!order) throw new Error('订单不存在'); order.selectProvinceCity(provinceCode, provinceName, cityCode, cityName); await this.orderRepository.save(order); return { success: true }; } /** * 确认省市选择 (5秒后调用) */ async confirmProvinceCity(orderNo: string) { const order = await this.orderRepository.findByOrderNo(orderNo); if (!order) throw new Error('订单不存在'); order.confirmProvinceCity(); await this.orderRepository.save(order); return { success: true }; } /** * 支付认种订单 */ async payOrder(orderNo: string, userId: bigint) { const order = await this.orderRepository.findByOrderNo(orderNo); if (!order) throw new Error('订单不存在'); if (order.userId !== userId) throw new Error('无权操作此订单'); // 调用钱包服务扣款 await this.walletService.deductForPlanting({ userId: userId.toString(), amount: order.totalAmount, orderId: order.orderNo, }); // 标记已支付 order.markAsPaid(); // 计算资金分配 const allocations = this.fundAllocationService.calculateAllocations( order, [], // referralChain - 需要从推荐服务获取 null, // nearestProvinceAuth null, // nearestCityAuth null, // nearestCommunity ); // 分配资金 order.allocateFunds(allocations); await this.orderRepository.save(order); // 调用钱包服务执行资金分配 await this.walletService.allocateFunds({ orderId: order.orderNo, allocations: allocations.map(a => a.toDTO()), }); // 更新用户持仓 const position = await this.positionRepository.getOrCreate(userId); position.addPlanting( order.treeCount.value, order.provinceCitySelection!.provinceCode, order.provinceCitySelection!.cityCode, ); await this.positionRepository.save(position); return { orderNo: order.orderNo, status: order.status, allocations: allocations.map(a => a.toDTO()), }; } /** * 查询用户订单列表 */ async getUserOrders(userId: bigint, page = 1, pageSize = 10) { const orders = await this.orderRepository.findByUserId(userId, page, pageSize); return orders.map(o => ({ orderNo: o.orderNo, treeCount: o.treeCount.value, totalAmount: o.totalAmount, status: o.status, provinceName: o.provinceCitySelection?.provinceName, cityName: o.provinceCitySelection?.cityName, isMiningEnabled: o.isMiningEnabled, })); } /** * 查询用户持仓 */ async getUserPosition(userId: bigint) { const position = await this.positionRepository.findByUserId(userId); if (!position) { return { totalTreeCount: 0, effectiveTreeCount: 0, pendingTreeCount: 0, }; } return { totalTreeCount: position.totalTreeCount, effectiveTreeCount: position.effectiveTreeCount, pendingTreeCount: position.pendingTreeCount, }; } private async checkRiskControl(userId: bigint, treeCount: number) { // 检查用户限购等风控规则 const existingCount = await this.orderRepository.countTreesByUserId(userId); if (existingCount + treeCount > 1000) { throw new Error('超过个人最大认种数量限制'); } } } ``` --- ## 第五阶段:API层实现 ### 5.1 DTO 定义 ```typescript // api/dto/create-planting-order.dto.ts import { IsInt, Min } from 'class-validator'; export class CreatePlantingOrderDto { @IsInt() @Min(1) treeCount: number; } // api/dto/select-province-city.dto.ts import { IsString, IsNotEmpty } from 'class-validator'; export class SelectProvinceCityDto { @IsString() @IsNotEmpty() provinceCode: string; @IsString() @IsNotEmpty() provinceName: string; @IsString() @IsNotEmpty() cityCode: string; @IsString() @IsNotEmpty() cityName: string; } ``` ### 5.2 控制器 ```typescript // api/controllers/planting-order.controller.ts import { Controller, Post, Get, Body, Param, Query, UseGuards, Req } from '@nestjs/common'; import { PlantingApplicationService } from '../../application/services/planting-application.service'; import { CreatePlantingOrderDto } from '../dto/create-planting-order.dto'; import { SelectProvinceCityDto } from '../dto/select-province-city.dto'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; @Controller('planting') @UseGuards(JwtAuthGuard) export class PlantingOrderController { constructor(private readonly plantingService: PlantingApplicationService) {} @Post('orders') async createOrder(@Req() req: any, @Body() dto: CreatePlantingOrderDto) { const userId = BigInt(req.user.id); return this.plantingService.createOrder(userId, dto.treeCount); } @Post('orders/:orderNo/select-province-city') async selectProvinceCity( @Param('orderNo') orderNo: string, @Body() dto: SelectProvinceCityDto, ) { return this.plantingService.selectProvinceCity( orderNo, dto.provinceCode, dto.provinceName, dto.cityCode, dto.cityName, ); } @Post('orders/:orderNo/confirm-province-city') async confirmProvinceCity(@Param('orderNo') orderNo: string) { return this.plantingService.confirmProvinceCity(orderNo); } @Post('orders/:orderNo/pay') async payOrder(@Req() req: any, @Param('orderNo') orderNo: string) { const userId = BigInt(req.user.id); return this.plantingService.payOrder(orderNo, userId); } @Get('orders') async getUserOrders( @Req() req: any, @Query('page') page = 1, @Query('pageSize') pageSize = 10, ) { const userId = BigInt(req.user.id); return this.plantingService.getUserOrders(userId, page, pageSize); } @Get('position') async getUserPosition(@Req() req: any) { const userId = BigInt(req.user.id); return this.plantingService.getUserPosition(userId); } } ``` --- ## 关键业务规则 (不变式) 1. **省市选择后不可修改**: 一旦确认省市,终生不可修改 2. **省市确认需要5秒倒计时**: 防止用户误操作 3. **资金分配规则**: 2199 USDT 必须精确分配到10个去向 4. **800 USDT 必须进底池**: 每棵树的800 USDT 必须注入 RWAD 底池 5. **底池注入后才能挖矿**: 只有底池注入完成才能开启挖矿 --- ## API 端点汇总 | 方法 | 路径 | 描述 | 认证 | |------|------|------|------| | POST | /planting/orders | 创建认种订单 | 需要 | | POST | /planting/orders/:orderNo/select-province-city | 选择省市 | 需要 | | POST | /planting/orders/:orderNo/confirm-province-city | 确认省市(5秒后) | 需要 | | POST | /planting/orders/:orderNo/pay | 支付订单 | 需要 | | GET | /planting/orders | 查询我的订单列表 | 需要 | | GET | /planting/position | 查询我的持仓 | 需要 | --- ## 开发顺序建议 1. 项目初始化和 Prisma Schema 2. 值对象和枚举实现 3. 聚合根实现 (PlantingOrder) 4. 领域服务实现 (FundAllocationService) 5. 仓储接口和实现 6. 应用服务实现 7. 外部服务客户端 (WalletServiceClient) 8. DTO 和控制器实现 9. 模块配置和测试 --- ## 与前端页面对应关系 ### 认种--选择数量 页面 - 调用余额API: `GET /api/deposit/balances` (Wallet Service) - 计算最大可认种数量: `余额 / 2199` - 用户选择数量后,调用: `POST /planting/orders` ### 认种--选择省市 页面 - 使用 `city_pickers` 包选择省市 - 调用: `POST /planting/orders/:orderNo/select-province-city` ### 认种--确认弹窗 - 显示5秒倒计时 - 倒计时结束后调用: `POST /planting/orders/:orderNo/confirm-province-city` - 确认后自动调用: `POST /planting/orders/:orderNo/pay` --- ## 注意事项 1. 所有金额使用 `Decimal(20, 8)` 存储 2. 省市代码使用国家统计局标准代码 3. 资金分配明细表是 append-only,不允许修改 4. 使用事务确保订单状态和资金分配的一致性 5. 5秒倒计时由前端控制,后端再次验证时间