1295 lines
41 KiB
Markdown
1295 lines
41 KiB
Markdown
# 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, number> = {
|
||
[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<string, any>,
|
||
) {}
|
||
|
||
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<FundAllocation> { 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<void>;
|
||
findById(orderId: bigint): Promise<PlantingOrder | null>;
|
||
findByOrderNo(orderNo: string): Promise<PlantingOrder | null>;
|
||
findByUserId(userId: bigint, page?: number, pageSize?: number): Promise<PlantingOrder[]>;
|
||
findByStatus(status: PlantingOrderStatus, limit?: number): Promise<PlantingOrder[]>;
|
||
findPendingPoolScheduling(): Promise<PlantingOrder[]>;
|
||
findByBatchId(batchId: bigint): Promise<PlantingOrder[]>;
|
||
findReadyForMining(): Promise<PlantingOrder[]>;
|
||
countTreesByUserId(userId: bigint): Promise<number>;
|
||
}
|
||
|
||
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秒倒计时由前端控制,后端再次验证时间
|