rwadurian/docs/pre-planting-implementation...

746 lines
33 KiB
Markdown
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.

# 3171 预种计划(拼种/团购计划)— 实现方案
## Context
RWADurian 平台需要新增"3171 预种计划"功能。这本质是一个**拼种团购计划**:用户以 3171 USDT/份1 棵树的 1/5 价格)参与认种,每购买一份立即享有分配、推荐奖励和算力收益;累计 5 份自动合成 1 棵树,触发合同签署和挖矿开启,同时解除交易和提现限制。此功能需无缝融入现有 1.0 认种分配和 2.0 算力挖矿体系,不破坏任何已有功能。
---
## 架构决策
### 核心原则:纯新增,零侵入
**现有代码文件一行不改。** 所有预种逻辑通过以下方式实现:
- 新增独立文件(新 controller、新 service、新 handler、新 Guard
- 新增数据库表(不修改现有表结构)
- 新增 Kafka Topic不修改现有 Topic 的生产/消费逻辑)
- 唯一的"接触"是在各服务的 `app.module.ts` 中 import 新模块(纯注册,不改现有逻辑)
### 决策 1在 planting-service 中新增独立模块(非新服务)
预种作为 planting-service 内的一个**完全独立的 NestJS Module**
- 有自己的 Controller、Service、Repository、Events
- 有自己的 Prisma 表(不触碰现有 planting_orders 等表)
- 通过自己的 Kafka Topic 通信(不复用 `planting.planting.created` 等现有 Topic
- 与现有 PlantingOrder 聚合根**零耦合**
**不创建新微服务的原因**:共享同一 PostgreSQL (rwa_planting) + 同一 Redis + 同一 Kafka避免新增基础设施和运维成本。
### 决策 2预种有自己完整的独立事件流
预种使用全新的 Kafka Topic`pre-planting.*`),现有消费者不会收到这些事件,所以:
- referral-service 的现有 `planting-created.handler.ts` **完全不受影响**
- reward-service 的现有 `reward-calculation.service.ts` **完全不受影响**
- contract-signing 的现有流程 **完全不受影响**
### 决策 3合并产生的"树"不进入现有 PlantingOrder 表
5 份合并后,**不创建 PlantingOrder**(避免触发现有分配流程)。而是:
- 在预种模块自己的 `pre_planting_merges` 表中记录合并
- 预种模块自己处理合同签署(复用签署逻辑但在新文件中)
- 预种模块通过自己的事件通知 contribution-service2.0
- 预种模块直接调用 wallet-service 内部 API 设置 `hasPlanted=true`
**好处**:现有 PlantingOrder 的整个生命周期管道(事件 → referral → contract → reward → contribution完全不被触碰。
### 决策 4从第 1 份起即产生分配与算力,附带限制条件
**从第 1 份预种开始,以下功能立即生效:**
- 10 类权益分配1/5 比例)立即触发并入账
- 推荐人立即获得 720 推荐奖励
- 算力(贡献值)立即产生并参与 2.0 挖矿分配
- 团队统计立即更新(预种份额计入团队业绩)
**附带的限制条件(未累积满 5 份前):**
- 不可卖出 eUSDT只能看不能卖
- 不可申请/授权社区、市公司、省公司
- 不可使用对公账户提现
- 若 1 年内未满 5 份 → 算力暂停分配(满 5 份后恢复,恢复起算 2 年后失效)
**合并后解锁全部限制 + 触发合同签署和挖矿开启。**
### 决策 5推荐奖励取决于"被推荐人买了什么",与推荐人自身无关
推荐奖励规则(与现有逻辑完全一致,只是新增预种产品类型):
| 场景 | 推荐人 A | 被推荐人 B | A 获得奖励 |
|------|----------|------------|------------|
| B 认种 1 棵树 | 买了 1 份预种 | 认种 1 棵树 | **3600** 绿积分 |
| B 买 1 份预种 | 买了 1 份预种 | 买 1 份预种 | **720** 绿积分 |
| B 买 1 份预种 | 认种了 1 棵树 | 买 1 份预种 | **720** 绿积分 |
| B 认种 1 棵树 | 认种了 1 棵树 | 认种 1 棵树 | **3600** 绿积分(原有逻辑不变) |
规则本质:奖励金额 = 被推荐人购买产品对应的 SHARE_RIGHT树=3600预种份=720
---
## 涉及服务清单(纯新增方式)
| 服务 | 现有代码改动 | 新增内容 |
|------|-------------|----------|
| **planting-service** | `app.module.ts` 加 1 行 import | **新增 PrePlantingModule**独立聚合根、Controller、Service、Repository、Events、合并逻辑、开关 |
| **referral-service** | `app.module.ts` 加 1 行 import | **新增 PrePlantingStatsModule**(新 handler 消费预种事件更新团队统计) |
| **reward-service** | 无 | **不涉及**。预种的 1/5 分配由 planting-service 的预种模块直接调用 wallet-service 完成 |
| **authorization-service** | `app.module.ts` 加 1 行 import | **新增 PrePlantingGuardModule**(路由级 Middleware仅对授权申请路由生效 |
| **trading-service** | `app.module.ts` 加 1 行 import | **新增 PrePlantingGuardModule**(路由级 Middleware仅对卖单路由生效 |
| **wallet-service** | `app.module.ts` 加 1 行 import | **新增 PrePlantingGuardModule**(路由级 Middleware仅对提现路由生效 |
| **admin-service** | `app.module.ts` 加 1 行 import | **新增 PrePlantingAdminModule**(开关管理 + 预种订单查询视图) |
| **contribution-service (2.0)** | `app.module.ts` 加 1 行 import | **新增 PrePlantingCdcModule**CDC 消费预种表、1/5 算力、冻结逻辑) |
| **mobile-app (Flutter)** | 无 | **新增预种功能模块**(购买页、持仓页、合并流程、签约弹窗) |
**关键**:除 `app.module.ts` 的 import 注册外,**每个服务的现有 .ts 文件零修改**。
---
## 数据模型变更(全部是新增表,不修改任何现有表)
### planting-service新增 4 张表)
```prisma
// ===== 以下全部是新增表,写在 schema.prisma 末尾 =====
// 预种订单
model PrePlantingOrder {
id BigInt @id @default(autoincrement())
orderNo String @unique // PPL{timestamp}{random}
userId BigInt
accountSequence String
portionCount Int @default(1)
pricePerPortion Decimal @default(3171)
totalAmount Decimal // portionCount × 3171
provinceCode String // 省代码(购买时选择)
cityCode String // 市代码(购买时选择)
status String @default("CREATED") // CREATED → PAID → MERGED
mergedToMergeId BigInt? // 合并后指向 PrePlantingMerge.id
mergedAt DateTime?
createdAt DateTime @default(now())
paidAt DateTime?
updatedAt DateTime @updatedAt
@@map("pre_planting_orders")
}
// 预种持仓(每用户一条)
model PrePlantingPosition {
id BigInt @id @default(autoincrement())
userId BigInt @unique
accountSequence String @unique
totalPortions Int @default(0) // 累计购买份数(含已合并)
availablePortions Int @default(0) // 待合并份数0-4
mergedPortions Int @default(0) // 已合并份数
totalTreesMerged Int @default(0) // 已合成的树数
provinceCode String? // 首次购买时选择的省代码(后续复用)
cityCode String? // 首次购买时选择的市代码(后续复用)
firstPurchaseAt DateTime? // 首次购买时间1年冻结起点
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("pre_planting_positions")
}
// 合并记录(预种的"树",不进入现有 planting_orders 表)
model PrePlantingMerge {
id BigInt @id @default(autoincrement())
mergeNo String @unique // PMG{timestamp}{random}
userId BigInt
accountSequence String
sourceOrderNos Json // 5 笔预种订单号数组
treeCount Int @default(1) // 合并产生的树数(固定 1
// 省市选择(合并后由用户选择)
selectedProvince String?
selectedCity String?
provinceCityConfirmedAt DateTime?
// 合同签署
contractStatus String @default("PENDING") // PENDING → SIGNED → EXPIRED
contractSignedAt DateTime?
// 挖矿
miningEnabledAt DateTime?
mergedAt DateTime @default(now())
@@map("pre_planting_merges")
}
// 预种分配记录(预种模块自己管理分配,不经过 reward-service
model PrePlantingRewardEntry {
id BigInt @id @default(autoincrement())
sourceOrderNo String // 来源预种订单号
sourceAccountSequence String // 购买者 accountSequence
recipientAccountSequence String // 接收者 accountSequence推荐人/社区/省市公司/系统账户)
rightType String // 权益类型(同 RightType 枚举)
usdtAmount Decimal
rewardStatus String @default("SETTLED") // SETTLED / PENDING / EXPIRED
memo String? // 分配原因说明
createdAt DateTime @default(now())
@@map("pre_planting_reward_entries")
}
```
**注意**:现有 `planting_orders`、`planting_positions` 等表**零修改**。
**注意**:现有 `PlantingOrder` 聚合根代码**零修改**。
### admin-service新增配置表
```prisma
// 追加到 schema.prisma 末尾
model PrePlantingConfig {
id String @id @default(uuid())
isActive Boolean @default(false)
activatedAt DateTime?
updatedAt DateTime @updatedAt
updatedBy String?
@@map("pre_planting_configs")
}
```
### contribution-service 2.0(新增同步表)
```prisma
// 追加到 schema.prisma 末尾
model SyncedPrePlantingAdoption {
id BigInt @id @default(autoincrement())
originalOrderId BigInt @unique
accountSequence String
portionCount Int
purchaseDate DateTime @db.Date
contributionPerPortion Decimal
contributionFrozen Boolean @default(false)
freezeExpiresAt DateTime?
contributionDistributed Boolean @default(false)
syncedAt DateTime @default(now())
@@map("synced_pre_planting_adoptions")
}
```
### 预种分配金额配置(在预种模块新文件中定义)
```typescript
// planting-service/src/pre-planting/domain/value-objects/pre-planting-right-amounts.ts新文件
// 基准reward-service 的 RIGHT_AMOUNTS实际生效的分配金额
// 预种每份 = 整棵树金额 / 5差额 4.8 归入总部社区
export const PRE_PLANTING_RIGHT_AMOUNTS = {
COST_FEE: 576, // 2880/5
OPERATION_FEE: 420, // 2100/5
HEADQUARTERS_BASE_FEE: 29.4, // 123/5 + 4.8 差额3171 - 3166.2 = 4.8
RWAD_POOL_INJECTION: 1152, // 5760/5
SHARE_RIGHT: 720, // 3600/5 = 720推荐奖励金额
PROVINCE_AREA_RIGHT: 21.6, // 108/5
PROVINCE_TEAM_RIGHT: 28.8, // 144/5
CITY_AREA_RIGHT: 50.4, // 252/5
CITY_TEAM_RIGHT: 57.6, // 288/5
COMMUNITY_RIGHT: 115.2, // 576/5
} as const;
// 合计 = 576 + 420 + 29.4 + 1152 + 720 + 21.6 + 28.8 + 50.4 + 57.6 + 115.2 = 3171.0 ✓
```
---
## 核心事件流
### 流程 A预种购买每份 — 全部在预种模块内完成
**重要**:购买时即选择省市(与现有认种流程一致),后续购买复用同一省市。
这样 10 类权益全部可在购买时立即分配。
```
用户购买 1 份预种3171 USDT
[planting-service / PrePlantingModule](全新代码,不触碰现有代码)
├─ Step 1前置校验
│ ├─ 开关 ON或续购规则通过
│ ├─ 余额 ≥ 3171
│ └─ 省市信息(首次购买:用户选择;续购:自动复用 PrePlantingPosition 中的省市)
├─ Step 2冻结余额
│ └─ wallet.freezeForPlanting(3171)(调用已有内部 API
├─ Step 3确定 10 类权益的分配对象pre-planting-reward.service.ts
│ │
│ ├─ 4 类系统费用(硬编码,无需查询):
│ │ ├─ COST_FEE: 576 → S0000000002成本账户
│ │ ├─ OPERATION_FEE: 420 → S0000000003运营账户
│ │ ├─ HQ_BASE_FEE: 29.4 → S0000000001总部社区
│ │ └─ RWAD_POOL: 1152 → S0000000004RWAD底池
│ │
│ ├─ 推荐奖励(调用 referral-service 已有 API
│ │ └─ SHARE_RIGHT: 720 → GET /api/v1/referral/chain/{accountSeq}
│ │ ├─ 有推荐人且已认种 → 推荐人账户(立即到账)
│ │ ├─ 有推荐人未认种 → 推荐人账户PENDING24h 内认种可领取)
│ │ └─ 无推荐人 → S0000000005分享权益池
│ │
│ ├─ 社区权益(调用 authorization-service 已有 API
│ │ └─ COMMUNITY: 115.2 → GET /authorization/community-reward-distribution
│ │ ├─ 有前有效社区 → 社区领导账户
│ │ └─ 无社区 → S0000000001总部
│ │
│ └─ 省市 4 类权益(调用 authorization-service 已有 API需要 provinceCode/cityCode
│ ├─ PROVINCE_AREA: 21.6 → GET /authorization/province-area-reward-distribution?provinceCode=XX
│ │ └─ 有正式省公司 → 省公司账户 | 无 → 9{provinceCode}(系统省账户)
│ ├─ PROVINCE_TEAM: 28.8 → GET /authorization/province-team-reward-distribution
│ │ └─ 有授权省团队 → 团队领导账户 | 无 → S0000000001总部
│ ├─ CITY_AREA: 50.4 → GET /authorization/city-area-reward-distribution?cityCode=XX
│ │ └─ 有正式市公司 → 市公司账户 | 无 → 8{cityCode}(系统市账户)
│ └─ CITY_TEAM: 57.6 → GET /authorization/city-team-reward-distribution
│ └─ 有授权市团队 → 团队领导账户 | 无 → S0000000001总部
├─ Step 4事务内持久化
│ ├─ 创建 PrePlantingOrder(status=PAID, provinceCode, cityCode)
│ ├─ 更新 PrePlantingPosition(totalPortions++, availablePortions++)
│ ├─ 设置 firstPurchaseAt + provinceCode/cityCode仅首次
│ ├─ 创建 PrePlantingRewardEntry10 条记录,含每条的接收人 accountSequence
│ └─ Outbox: "pre-planting.portion.purchased"
├─ Step 5执行资金转账
│ ├─ wallet.confirmDeduction(3171)(调用已有 API
│ └─ wallet.allocateFunds(10 条分配)(调用已有 API与 reward-service 调用格式完全一致)
└─ Step 6检查是否触发合并
└─ availablePortions == 5 ? → 触发合并(流程 B
[referral-service / PrePlantingStatsModule](新增 handler不改现有文件
├─ 消费 "pre-planting.portion.purchased"
└─ 更新 TeamStatistics团队认种统计预种份额按 1/5 棵树计入)
```
**与现有系统的隔离**
- 预种模块**直接调用** referral-service 和 authorization-service 的**已有内部 API** 确定分配对象
- 确定对象后,调用 wallet-service 的 `allocateFunds` 执行转账(与 reward-service 调同一个 API
- **reward-service 完全不感知预种的存在**——预种模块自己做了 reward-service 做的事情
- 推荐奖励 720 由预种模块计算后直接写入推荐人钱包
### 流程 B自动合并5 份 → 1 棵树) — 不创建 PlantingOrder
```
availablePortions == 5
[planting-service / PrePlantingModule] 同一事务内:
├─ 创建 PrePlantingMerge 记录mergeNo, 5个sourceOrderNos, contractStatus=PENDING
│ └─ 注意:不创建 PlantingOrder合并的树记录在自己的表中
├─ 标记 5 笔 PrePlantingOrder → MERGED
├─ 更新 PrePlantingPosition(available-=5, merged+=5, treesMerged++)
└─ Outbox: "pre-planting.merged"
前端收到合并通知 → 弹出合同签署流程(省市已在首次购买时确定,无需再选)
[planting-service / PrePlantingModule] 处理合同签署:
├─ 省市从 PrePlantingPosition 直接带入 PrePlantingMerge无需用户重新选择
├─ 生成合同 PDF复用 MinIO 上传逻辑)
├─ 用户签约 → contractStatus = SIGNED
├─ wallet.setHasPlanted(accountSequence, true)(调用已有 API
├─ Outbox: "pre-planting.contract.signed"
└─ 开启挖矿miningEnabledAt = now()
[contribution-service / PrePlantingCdcModule](新增 handler
├─ 消费 CDC 或 "pre-planting.contract.signed"
└─ 同步合并记录,标记为 MINING_ENABLED
```
**关键隔离点**
- 不创建 PlantingOrder → referral-service 的现有 handler 不会被触发
- 不发布 `planting.planting.created` → reward-service 的现有 handler 不会被触发
- 不发布 `planting-events` → contract-signing 的现有 handler 不会被触发
- **所有预种事件走 `pre-planting.*` 独立 Topic**
### 流程 CCDC 同步到 2.0(算力 — 从第 1 份即生效)
```
每份预种购买支付成功后Debezium 立即捕获 pre_planting_orders 变更
[contribution-service] CDC Consumer
├─ 同步到 SyncedPrePlantingAdoption
├─ 立即计算算力 = contributionPerTree / 5 × portionCount
├─ 算力立即参与挖矿分配(从第 1 份起就有收益)
├─ 解锁条件同正常认种15 级 + 3 阶梯,按预种份额判断)
├─ 1 年冻结逻辑(定时任务检查):
│ ├─ 正常状态firstPurchaseAt + 1 年内 → 算力正常分配
│ ├─ 触发冻结firstPurchaseAt + 1 年后仍未满 5 份 → 所有算力暂停分配
│ └─ 解冻恢复:此 ID 后续累计满 5 份 → 算力恢复分配
│ └─ 恢复后的失效期 = 恢复日起算 + 2 年
└─ 正常到期:首次产生挖矿收益日 + 2 年后失效(未被冻结过的情况)
```
**关键时间线示例:**
```
Day 0: 买第 1 份 → 算力 = tree算力/5立即开始挖矿分配
Day 30: 买第 2 份 → 算力 += tree算力/5
Day 200: 买第 3 份 → 算力 += tree算力/5
Day 365: 仍只有 3 份 → 所有 3 份算力冻结(暂停分配)
Day 400: 买第 4 份 → 仍冻结(未满 5 份)
Day 420: 买第 5 份 → 自动合并成 1 棵树
→ 所有 5 份算力解冻,恢复分配
→ 解冻起算 + 2 年后失效
```
---
## 跨服务限制实现(全部通过新增 NestJS Middleware不改现有代码
### 实现模式NestJS Middleware + forRoutes 精确路由匹配 + 内部 API
每个需要限制的服务新增一个 `PrePlantingGuardModule`(新文件),通过 `NestModule.configure()` + `forRoutes()` 注册路由级别 Middleware。
**只有特定路由才会执行检查逻辑**其他路由完全无感知、零执行开销。Middleware 调用 planting-service 暴露的预种资格 API。
**planting-service 新增内部 API**(在 PrePlantingModule 内):
```
GET /internal/pre-planting/eligibility/{accountSequence}
返回:{
hasPrePlanting: boolean, // 是否有预种记录
totalPortions: number, // 累计份数
totalTreesMerged: number, // 已合成树数
canApplyAuthorization: boolean, // 可申请授权
canTrade: boolean, // 可卖出 eUSDT
}
```
逻辑:
- `canApplyAuthorization = totalTreesMerged >= 1`(已有认种记录的老用户不受影响,因为 hasPrePlanting=false 时 Guard 直接放行)
- `canTrade = totalTreesMerged >= 1`(同上)
### 授权锁authorization-service — 新增文件)
```
新增文件authorization-service/src/pre-planting/pre-planting-guard.module.ts
新增文件authorization-service/src/pre-planting/pre-planting-authorization.middleware.ts
新增文件authorization-service/src/pre-planting/pre-planting.client.ts
```
```typescript
// pre-planting-guard.module.ts新文件不触碰现有代码
@Module({ providers: [PrePlantingClient] })
export class PrePlantingGuardModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(PrePlantingAuthorizationMiddleware)
.forRoutes(
{ path: 'authorization/apply*', method: RequestMethod.POST },
);
}
}
```
```typescript
// pre-planting-authorization.middleware.ts新文件
@Injectable()
export class PrePlantingAuthorizationMiddleware implements NestMiddleware {
constructor(private readonly prePlantingClient: PrePlantingClient) {}
async use(req: Request, res: Response, next: NextFunction) {
try {
const accountSequence = req['user']?.accountSequence;
if (!accountSequence) return next(); // 无用户信息,放行
const eligibility = await this.prePlantingClient.getEligibility(accountSequence);
// 没有预种记录(纯认种用户)→ 直接放行,现有逻辑零影响
if (!eligibility.hasPrePlanting) return next();
// 有预种记录但未满 5 份合并 → 拦截
if (!eligibility.canApplyAuthorization) {
return res.status(403).json({
message: '须累积购买5份预种计划合并成树后方可申请授权',
});
}
return next();
} catch (error) {
// planting-service 不可达时,默认放行(不影响现有功能)
return next();
}
}
}
```
### 交易锁trading-service — 新增文件)
```
新增文件trading-service/src/pre-planting/pre-planting-guard.module.ts
新增文件trading-service/src/pre-planting/pre-planting-trading.middleware.ts
新增文件trading-service/src/pre-planting/pre-planting.client.ts
```
同理Middleware 通过 `forRoutes({ path: 'trading/orders', method: POST })` 精确匹配卖单路由。其他路由**完全不经过此 Middleware**。对无预种记录的用户直接放行。
### 提现锁wallet-service — 新增文件)
```
新增文件wallet-service/src/pre-planting/pre-planting-guard.module.ts
新增文件wallet-service/src/pre-planting/pre-planting-withdrawal.middleware.ts
新增文件wallet-service/src/pre-planting/pre-planting.client.ts
```
Middleware 通过 `forRoutes` 精确匹配提现路由,检查 `hasPlanted`wallet-service 自己的字段,合并签约后由 PrePlantingModule 设置为 true
**Middleware 的关键设计**
- **精确路由匹配**:通过 `forRoutes()` 只对特定路由生效,其他路由零执行开销
- **异常容错**planting-service 不可达时默认放行(`catch → next()`),不影响现有功能
- `hasPrePlanting=false`(纯认种用户)→ **直接放行**,现有功能零影响
- `hasPrePlanting=true``canXxx=true` → 放行
- `hasPrePlanting=true``canXxx=false` → 拦截
### 1 年算力冻结contribution-service — 新增文件)
```
新增文件contribution-service/src/pre-planting/pre-planting-cdc.module.ts
新增文件contribution-service/src/pre-planting/pre-planting-contribution.service.ts
新增文件contribution-service/src/pre-planting/pre-planting-freeze-scheduler.ts
```
冻结逻辑在全新的 service 文件中实现,不触碰现有 `contribution-calculation.service.ts`
```typescript
// pre-planting-freeze-scheduler.ts新文件
@Cron('0 0 * * *') // 每日检查
async checkContributionFreeze() {
// 查找所有 firstPurchaseAt + 1年 < NOW() 且 totalPortions < 5 的用户
// 冻结其预种算力记录
// 查找所有 totalPortions >= 5 且仍被冻结的用户
// 解冻,设 expireDate = 解冻日 + 2年
}
```
---
## 后台开关逻辑
修改文件:`admin-service` 新增 `PrePlantingConfigController` + `PrePlantingConfigEntity`
### 核心原则
- **随时可开**:打开后所有用户均可购买预种
- **随时可关**:关闭后限制新购买,但**已成交的一切保持不变**
- 已支付的预种订单:状态不变、分配不变、算力不变
- 已获得的推荐奖励:不回收
- 已产生的算力:继续参与挖矿分配(除非触发 1 年冻结规则)
- 待合并的份额:允许继续购买至下一个 5 的整数倍(凑满合并)
- **开关仅控制"能否发起新购买",不影响任何已完成的业务流程**
### 开关规则planting-service 购买前校验):
```typescript
async validatePrePlantingPurchase(userId, portionCount) {
const config = await adminClient.getPrePlantingConfig();
const position = await prePositionRepo.findByUserId(userId);
if (config.isActive) {
// 开关打开:任何人都可以购买
return;
}
// 开关关闭:已成交的保持不变,仅限制新购买
if (!position || position.totalPortions === 0) {
// 从未购买过的用户:显示"待开启"
throw new BusinessError('预种功能待开启');
}
// 已有未凑满的份额:允许继续购买至下一个 5 的倍数
const remainder = position.availablePortions % 5;
if (remainder === 0) {
// 已凑满当前轮次(如 5/10/15...),不可开启新一轮
throw new BusinessError('预种功能已关闭,您当前份额已满,无法继续购买');
}
const maxAdditional = 5 - remainder;
if (portionCount > maxAdditional) {
throw new BusinessError(`当前只可再购买 ${maxAdditional} 份以凑满5份`);
}
}
```
### 前端展示逻辑
| 开关状态 | 用户状态 | 前端表现 |
|----------|----------|----------|
| ON | 任何用户 | 正常显示购买入口 |
| OFF | 从未购买 | 显示"待开启",购买按钮置灰 |
| OFF | 有 1-4 份未合并 | 显示"可继续购买 N 份凑满合并",购买按钮可用 |
| OFF | 份额恰好为 5 的倍数 | 显示"预种功能已暂停",购买按钮置灰 |
---
## 新增 Kafka Topics
| Topic | 生产者 | 消费者 | 触发时机 |
|-------|--------|--------|----------|
| `pre-planting.portion.purchased` | planting-service | referral-service | 每份购买支付成功 |
| `pre-planting.order.paid` | referral-service | reward-service | referral 处理完团队统计后 |
| `pre-planting.merged` | planting-service | contribution-service, referral-service | 5 份合并完成 |
**注意:不复用任何现有 Topic**。预种的所有事件(购买、合并、签约)全部走 `pre-planting.*` 独立 Topic现有消费者完全不受影响。
---
## 新增 Debezium CDC Connector独立连接器不修改现有连接器
**不修改现有 Debezium 连接器**。为预种表创建一个**全新的独立连接器**,确保现有 CDC 流零影响:
```json
{
"name": "pre-planting-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres-planting",
"database.port": "5432",
"database.dbname": "rwa_planting",
"database.server.name": "cdc.pre-planting",
"table.include.list": "public.pre_planting_orders,public.pre_planting_positions,public.pre_planting_merges",
"slot.name": "pre_planting_slot",
"publication.name": "pre_planting_publication",
"topic.prefix": "cdc.pre-planting"
}
}
```
Topic 命名(与现有 CDC Topic 完全隔离):
- `cdc.pre-planting.public.pre_planting_orders`
- `cdc.pre-planting.public.pre_planting_positions`
- `cdc.pre-planting.public.pre_planting_merges`
contribution-service 新增对应 CDC Consumer在 PrePlantingCdcModule 内)。
**隔离保证**
- 独立 replication slot → 不影响现有 slot 的 WAL 消费
- 独立 publication → 不影响现有 publication
- 独立 topic prefix → 现有消费者不会收到预种 CDC 事件
---
## 关键文件索引
### 需要触碰的现有文件(仅 import 注册,不改业务逻辑)
| 文件 | 改动 |
|------|------|
| `planting-service/src/app.module.ts` | `imports: [..., PrePlantingModule]` |
| `planting-service/prisma/schema.prisma` | 文件末尾追加 4 张新表定义 |
| `referral-service/src/app.module.ts` | `imports: [..., PrePlantingStatsModule]` |
| `authorization-service/src/app.module.ts` | `imports: [..., PrePlantingGuardModule]` |
| `trading-service/src/app.module.ts` | `imports: [..., PrePlantingGuardModule]` |
| `wallet-service/src/app.module.ts` | `imports: [..., PrePlantingGuardModule]` |
| `admin-service/src/app.module.ts` | `imports: [..., PrePlantingAdminModule]` |
| `admin-service/prisma/schema.prisma` | 文件末尾追加 1 张新表定义 |
| `contribution-service/src/app.module.ts` | `imports: [..., PrePlantingCdcModule]` |
| `contribution-service/prisma/schema.prisma` | 文件末尾追加 1 张新表定义 |
**以上改动总量**:每个文件仅增加 1 行 import。现有业务逻辑文件**零修改**。
### 全部新增的文件
**planting-service/src/pre-planting/**(核心模块,约 15-20 个新文件)
| 文件 | 内容 |
|------|------|
| `pre-planting.module.ts` | 模块注册 |
| `domain/aggregates/pre-planting-order.aggregate.ts` | PrePlantingOrder 聚合根 |
| `domain/aggregates/pre-planting-position.aggregate.ts` | PrePlantingPosition 聚合根 |
| `domain/aggregates/pre-planting-merge.aggregate.ts` | PrePlantingMerge 聚合根(合并的树) |
| `domain/events/pre-planting-*.event.ts` | 领域事件purchased/merged/contract-signed |
| `domain/value-objects/pre-planting-order-status.enum.ts` | 预种状态枚举 |
| `domain/value-objects/pre-planting-right-amounts.ts` | 1/5 分配金额常量 |
| `application/services/pre-planting-application.service.ts` | 购买 + 合并应用服务 |
| `application/services/pre-planting-reward.service.ts` | 预种分配服务(调用 wallet-service |
| `application/services/pre-planting-contract.service.ts` | 合并后合同签署服务 |
| `api/controllers/pre-planting.controller.ts` | 用户端 API |
| `api/controllers/internal-pre-planting.controller.ts` | 内部资格查询 API |
| `infrastructure/repositories/pre-planting-order.repository.ts` | 订单仓储 |
| `infrastructure/repositories/pre-planting-position.repository.ts` | 持仓仓储 |
| `infrastructure/repositories/pre-planting-merge.repository.ts` | 合并记录仓储 |
| `infrastructure/kafka/pre-planting-outbox-publisher.service.ts` | Outbox 事件发布 |
**referral-service/src/pre-planting/**(新 handler
| 文件 | 内容 |
|------|------|
| `pre-planting-stats.module.ts` | 模块注册 |
| `pre-planting-purchased.handler.ts` | 消费预种购买事件,更新团队统计 |
**authorization-service/src/pre-planting/**(路由级 Middleware
| 文件 | 内容 |
|------|------|
| `pre-planting-guard.module.ts` | Middleware 模块注册forRoutes 精确匹配) |
| `pre-planting-authorization.middleware.ts` | 授权申请拦截 Middleware |
| `pre-planting.client.ts` | 调用 planting-service 资格 API 的 HTTP Client |
**trading-service/src/pre-planting/**(路由级 Middleware
| 文件 | 内容 |
|------|------|
| `pre-planting-guard.module.ts` | Middleware 模块注册forRoutes 精确匹配) |
| `pre-planting-trading.middleware.ts` | 卖单拦截 Middleware |
| `pre-planting.client.ts` | HTTP Client |
**wallet-service/src/pre-planting/**(路由级 Middleware
| 文件 | 内容 |
|------|------|
| `pre-planting-guard.module.ts` | Middleware 模块注册forRoutes 精确匹配) |
| `pre-planting-withdrawal.middleware.ts` | 提现拦截 Middleware |
| `pre-planting.client.ts` | HTTP Client |
**admin-service/src/pre-planting/**(开关管理)
| 文件 | 内容 |
|------|------|
| `pre-planting-admin.module.ts` | 模块注册 |
| `pre-planting-config.entity.ts` | 开关实体 |
| `pre-planting-config.controller.ts` | 开关 API |
| `pre-planting-config.repository.ts` | 开关仓储 |
**contribution-service/src/pre-planting/**CDC + 算力)
| 文件 | 内容 |
|------|------|
| `pre-planting-cdc.module.ts` | 模块注册 |
| `pre-planting-cdc.consumer.ts` | 预种表 CDC 消费 |
| `pre-planting-contribution.service.ts` | 1/5 算力计算 |
| `pre-planting-freeze-scheduler.ts` | 1 年冻结定时任务 |
---
## 实施阶段
### Phase 1预种购买基础planting-service + admin-service
- Schema 迁移3 新表 + 1 字段)
- PrePlantingOrder 聚合根 + PrePlantingPosition
- 购买 API + 开关配置
- Outbox 事件发布
### Phase 2分配与推荐reward-service + referral-service
- 1/5 分配逻辑 + 720 推荐奖励
- 预种事件消费 + 团队统计
- 合并订单跳过分配逻辑
### Phase 3自动合并planting-service 核心)
- 5 份合并 → PrePlantingMerge 创建(不创建 PlantingOrder
- 合并后省市选择 + 合同签署流程
- 前端合并通知 + 签约弹窗
### Phase 4跨服务限制authorization + trading + wallet
- 授权申请资格校验
- eUSDT 卖出限制
- 提现 hasPlanted 校验
- 内部 API 端点
### Phase 52.0 算力集成contribution-service
- CDC 新表消费
- 1/5 算力计算
- 1 年冻结 / 解冻逻辑
- Debezium 连接器配置
### Phase 6前端 + 测试
- Flutter 预种购买/持仓页面
- 合并提示与合同签署
- admin-web 开关管理
- 集成测试(购买 → 分配 → 合并 → 签约 → 挖矿)
---
## 验证方案
1. **单元测试**PrePlantingOrder 聚合根状态机、PRE_PLANTING_RIGHT_AMOUNTS 总额校验(= 3171
2. **集成测试**:购买 5 份 → 验证 5 次分配(各 3171→ 验证自动合并 → 验证 PrePlantingMerge 创建
3. **E2E 测试**:合并后签约 → hasPlanted=true → 可申请授权 → 可卖出 eUSDT → 可提现
4. **CDC 测试**:预种订单 → contribution-service 同步 → 算力 = tree算力/5 → 1 年冻结验证
5. **开关测试**:关闭开关 → 新用户不可购买 → 已有 3 份的用户可继续购买 2 份 → 凑满合并