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

33 KiB
Raw Blame History

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 Topicpre-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 新增 PrePlantingCdcModuleCDC 消费预种表、1/5 算力、冻结逻辑)
mobile-app (Flutter) 新增预种功能模块(购买页、持仓页、合并流程、签约弹窗)

关键:除 app.module.ts 的 import 注册外,每个服务的现有 .ts 文件零修改


数据模型变更(全部是新增表,不修改任何现有表)

planting-service新增 4 张表)

// ===== 以下全部是新增表,写在 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_ordersplanting_positions 等表零修改注意:现有 PlantingOrder 聚合根代码零修改

admin-service新增配置表

// 追加到 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(新增同步表)

// 追加到 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")
}

预种分配金额配置(在预种模块新文件中定义)

// 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
// 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 },
      );
  }
}
// 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 精确匹配提现路由,检查 hasPlantedwallet-service 自己的字段,合并签约后由 PrePlantingModule 设置为 true

Middleware 的关键设计

  • 精确路由匹配:通过 forRoutes() 只对特定路由生效,其他路由零执行开销
  • 异常容错planting-service 不可达时默认放行(catch → next()),不影响现有功能
  • hasPrePlanting=false(纯认种用户)→ 直接放行,现有功能零影响
  • hasPrePlanting=truecanXxx=true → 放行
  • hasPrePlanting=truecanXxx=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

// 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 购买前校验):

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 流零影响:

{
  "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 份 → 凑满合并