# 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-service(2.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 → S0000000004(RWAD底池) │ │ │ ├─ 推荐奖励(调用 referral-service 已有 API): │ │ └─ SHARE_RIGHT: 720 → GET /api/v1/referral/chain/{accountSeq} │ │ ├─ 有推荐人且已认种 → 推荐人账户(立即到账) │ │ ├─ 有推荐人未认种 → 推荐人账户(PENDING,24h 内认种可领取) │ │ └─ 无推荐人 → 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(仅首次) │ ├─ 创建 PrePlantingRewardEntry(10 条记录,含每条的接收人 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** ### 流程 C:CDC 同步到 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 5:2.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 份 → 凑满合并