Procházet zdrojové kódy

feat(specs): CF值分佣体系重构设计(cfclub分支)

新增 docs/superpowers/specs/2026-09-01-cf-commission-redesign.md

设计范围:
- 全部订单统一走 CF 值分佣,废弃旧佣金体系
- 纯团队规模阶梯分佣(cf_rate_tier 动态可配置)
- 同家庭互推上溯到第一个非同家庭引荐人
- 返佣返个人 CF 钱包,兑换优惠券挂家庭
- 新增 CfCommissionService + cf_transfer_record
- 用户可查团队规模与返佣比例
- 订单迁移点 7 处 + 前端改造方案
Sisyphus před 2 týdny
rodič
revize
2e1dc597d6

+ 322 - 0
docs/superpowers/specs/2026-09-01-cf-commission-redesign.md

@@ -0,0 +1,322 @@
+# CF 值分佣体系重构设计文档
+
+> 日期:2026-09-01
+> 状态:待评审
+> 分支:cfc 主分支
+
+## 1. 背景与目标
+
+### 1.1 现状问题
+
+当前系统存在两套并行的返利/分佣体系:
+
+| 体系 | 服务 | 表 | 作用域 | 用途 | 状态 |
+|------|------|-----|--------|------|------|
+| 新 CF 值分佣 | `CommissionDistService` | `commission_dist_log` / `user_platform_balance` | 个人 | 商品订单 P点分润 | 仅商品订单 |
+| 旧佣金体系 | `CommissionService.settle/settleTwoLevel` | `commission_records` / `family_earnings` | 个人+家庭 | 套餐/测评/会员/订阅订单 L1/L2 佣金 | `@Deprecated`,仍被调用 |
+
+问题:
+1. 旧佣金体系 `settleTwoLevel` 仍被 5 个订单服务调用(套餐/测评/会员/订阅),与需求"全部走 CF 值体系"冲突。
+2. 团队规模统计 `PromotionTierService.updateTeamSize()` 全库无调用者,等级评估链路断裂。
+3. `promotion_tier_config` 表列(`min_team_size_1st/2nd + commission_rate_l1/l2`)与实体 `PromotionTierConfig`(`min_team_size + profit_share_percent + enabled`)不一致,等级配置实际未接通。
+4. 用户无法查询自己的团队规模和返佣比例。
+5. CF 值不能成员间转让;优惠券不绑定家庭。
+
+### 1.2 目标
+
+1. 全部订单统一走 CF 值分佣,废弃旧佣金体系写入。
+2. 返佣比例由团队规模(不限层级)决定,规模越大比例越高。
+3. 用户可查自己的团队规模与返佣比例。
+4. 家庭成员间可转让 CF 值;CF 值兑换的优惠券绑定家庭共享。
+
+### 1.3 关键决策(已与需求方确认)
+
+| # | 决策点 | 结论 |
+|---|--------|------|
+| D1 | 返佣比例模型 | **纯团队规模阶梯**(`cf_rate_tier` 动态可配置) |
+| D2 | CF 值归属维度 | **返给个人钱包**(`user_platform_balance`),不进家庭池 |
+| D3 | 会员升级特例 | 仅 membership/subscription 订单不返当前人,只返推荐人 |
+| D4 | 优惠券绑定 | 用 CF 兑换的券**放进家庭共享池**(券绑 `family_id`,全家可用) |
+| D5 | 同家庭互推 | **跳过本人,上溯到第一个非同家庭的引荐人** |
+| D6 | 分润结算主体 | **个人维度**(A/B 引荐人不同则各自独立结算) |
+| D7 | 团队规模口径 | 推荐人全层级下线总人数(`promotion_tier.total_team_size`) |
+
+## 2. 数据模型设计
+
+### 2.1 新建表
+
+#### `cf_rate_tier` — 分佣阶梯配置
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_rate_tier (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tier_name VARCHAR(50) NOT NULL COMMENT '档位名称: 铜牌/银牌/金牌/铂金/钻石',
+    min_team_size INT NOT NULL DEFAULT 0 COMMENT '团队规模下限(含)',
+    rate_percent INT NOT NULL DEFAULT 0 COMMENT '返佣比例(%),如10表示10%',
+    sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,越大越高',
+    enabled TINYINT DEFAULT 1 COMMENT '1启用/0停用',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_min_size (min_team_size)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值返佣阶梯配置';
+```
+
+种子数据(默认 5 档,管理端可动态增删改):
+
+| tier_name | min_team_size | rate_percent |
+|-----------|--------------|--------------|
+| 铜牌 | 0 | 5 |
+| 银牌 | 3 | 10 |
+| 金牌 | 10 | 15 |
+| 铂金 | 30 | 20 |
+| 钻石 | 100 | 25 |
+
+#### 个人 CF 钱包 — **复用 `user_platform_balance`(不新建)**
+
+经核对,`user_platform_balance` 表已具备 `family_id` 列及完整字段(`available/frozen/withdrawn/exchanged/total_earned`),`PlatformPointsService` 即为个人 CF 钱包服务(`earn/spend/freeze/unfreeze/withdraw`)。**直接复用,无需新建 `user_cf_wallet` 表。**
+
+转让时个人钱包变动复用 `PlatformPointsService.spend/earn`,通过 `cf_transfer_record` 记录流转明细。
+
+#### `cf_transfer_record` — CF 流转记录
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_transfer_record (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    from_user_id BIGINT COMMENT '转出用户ID(null=平台/家庭池)',
+    to_user_id BIGINT COMMENT '转入用户ID',
+    family_id BIGINT COMMENT '所属家庭ID',
+    amount INT NOT NULL COMMENT 'CF值数量',
+    type VARCHAR(16) NOT NULL COMMENT 'transfer成员转让/allocate管理员分配/refund退款回补',
+    ref_type VARCHAR(50) COMMENT '关联业务类型',
+    ref_id BIGINT COMMENT '关联业务ID',
+    remark VARCHAR(255),
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_from (from_user_id),
+    INDEX idx_to (to_user_id),
+    INDEX idx_family (family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流转记录';
+```
+
+### 2.2 迁移现有表
+
+| 表 | 操作 | 说明 |
+|----|------|------|
+| `promotion_tier` | 删除 `team_size_1st/2nd/3rd`;新增 `rate_percent INT DEFAULT 0` | 团队规模只留 `total_team_size` 一个口径 |
+| `promotion_tier_config` | 废弃(保留历史数据,业务不再读取) | 替代者为 `cf_rate_tier` |
+| `coupon` | 新增 `family_id BIGINT NULL` | 券绑定家庭(NULL=不绑定) |
+| `user_platform_balance` | **复用,不改表** | 已有 `family_id` 列及完整字段,直接作为个人 CF 钱包 |
+
+### 2.3 保留不变
+
+- `referral_tree`:已支持全层级(`path` 物化路径)。
+- `user_platform_balance` / `platform_balance_log`:个人 CF 值余额与流水(复用为个人钱包)。
+- `commission_dist_log`:商品订单分润流水(迁移后并入通用分润流水体系)。
+- `family_platform_balance` / `family_platform_exchange_record`:**仅作优惠券兑换用途**(用个人 CF 兑换后券挂家庭),不再承接返佣。
+
+## 3. 分佣核心逻辑
+
+### 3.1 统一服务 `CfCommissionService`(新建)
+
+```java
+@Service
+public class CfCommissionService {
+    /** 通用分佣入口(商品/套餐/测评订单:当前人+推荐人双返) */
+    void settle(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                Integer orderAmountCent, Long productId);
+
+    /** 会员/订阅专用(只返推荐人,不返当前人) */
+    void settleReferrerOnly(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                Integer orderAmountCent);
+}
+```
+
+### 3.2 推荐链查询与同家庭上溯
+
+```java
+List<ReferralNode> getEffectiveReferrers(Long buyerUserId, Long buyerFamilyId) {
+    // 1. 查 referral_tree WHERE child_id=buyerUserId ORDER BY level ASC
+    // 2. 对每个 referrer R:
+    //    a. R.familyId == buyerFamilyId → 跳过(同家庭不返),查 R 的引荐人继续上溯
+    //    b. R.familyId != buyerFamilyId → 加入结果集
+    // 3. 上溯终止:无更上级 / 已找到非同家庭引荐人
+}
+```
+
+> 决策 D5:同家庭互推时跳过本人,上溯到第一个非同家庭的引荐人。避免"自家消费返自己家人"的无效内耗与刷单。
+
+### 3.3 返佣比例计算(纯团队规模阶梯)
+
+```java
+int getRatePercent(Long referrerUserId) {
+    // 1. 查 promotion_tier.total_team_size(全层级下线人数)
+    // 2. SELECT rate_percent FROM cf_rate_tier
+    //    WHERE enabled=1 AND min_team_size <= total_team_size
+    //    ORDER BY min_team_size DESC LIMIT 1
+    // 3. 无匹配 → 0
+}
+```
+
+### 3.4 分润公式
+
+```
+分润CF = floor(orderAmountCent / 100) × 分润基数比例 × rate_percent / 100
+
+其中:
+- 商品订单:分润基数比例 = 商品 P点 ppointConfig(如 sourcePpoint 已含此义)
+- 非商品订单(套餐/测评/会员/订阅):分润基数比例 = sys_config.commission_service_rate(默认 10%)
+```
+
+简化统一为:
+
+```
+商品:cf = floor(sourcePpoint) × rate_percent / 100   (sourcePpoint 来自 PpointConfigService)
+非商品:cf = floor(orderAmountCent / 100) × serviceRateBps / 10000 × rate_percent / 100
+```
+
+### 3.5 返现路径
+
+- **当前人返 CF**(商品/套餐/测评):`platformPointsService.earn(buyerUserId, cf, "order_consume", orderId, ...)` → 个人钱包
+- **推荐人分润**:`platformPointsService.earn(referrerUserId, cf, "referral_dist", orderId, ...)` → 个人钱包
+- **流水**:`cf_transfer_record` 记录 from=平台 to=受益人(type=allocate)
+
+### 3.6 幂等
+
+以 `(ref_type='referral_dist'|'order_consume', ref_id=orderId)` 为幂等键,`platform_balance_log` 存在即跳过。`platformPointsService.earn` 当前实现无幂等检查,实现时需补(仿 `FamilyPlatformPointsService.earn` 的 `(ref_type, ref_id)` 去重)。
+
+## 4. 订单结算迁移点
+
+| 订单类型 | 文件:行 | 原调用 | 新调用 | 当前人返 CF | 推荐人分润 |
+|---|---|---|---|---|---|
+| 测评 | `AssessmentOrderService.java:76` | `commissionService.settle(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 套餐 | `PackagePaymentService.java:220` | `commissionService.settle(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 套餐 | `PaymentService.java:240` | `commissionService.settle(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 商品 | `ProductOrderService.java:645` | `commissionDistService.distribute(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 会员 | `MembershipService.java:784` | `commissionService.settleTwoLevel(...)` | `cfCommissionService.settleReferrerOnly(...)` | ❌ | ✅ |
+| 订阅 | `MemberSubscriptionService.java:174` | `commissionService.settleTwoLevel(...)` | `cfCommissionService.settleReferrerOnly(...)` | ❌ | ✅ |
+| 订阅 | `MemberSubscriptionService.java:255` | `commissionService.settleTwoLevel(...)` | `cfCommissionService.settleReferrerOnly(...)` | ❌ | ✅ |
+| 活动 | `ActivityOrderService` | 无 | 无 | ❌ | ❌ |
+
+### 4.1 商品订单处理
+
+商品订单当前 `ProductOrderService:633` 用 `platformPointsService.earn(buyerId, cfValue, "product_order", ...)` 返个人钱包,`:827 confirmReceive` 用 `familyPlatformPointsService.earn(familyId, ...)` 返家庭池。
+
+**本次设计决定:**
+- `:633` 个人 CF 返现:**保留**(属于分佣双返中的"当前人返")。
+- `:827 confirmReceive` 家庭池返 CF:**保留不动**(属于"确认收货额外积分",非本次推荐分佣范畴)。若后续需统一口径,另行提任务。
+
+> 待产品确认项:确认收货返家庭池 CF 是否仍需保留(与"返佣只进个人钱包"是否冲突)。
+
+## 5. 等级评估触发链路修复
+
+### 5.1 团队规模写入
+
+```
+推荐绑定:CommissionService.bindReferral(userId, referralCode)
+  → 建立 referral_tree 关系(path 物化路径)
+  → promotionTierService.updateTeamSize(referrerId, 全链路上溯, +1)
+     (对 referrer 及其所有祖先 promotion_tier.total_team_size +1)
+```
+
+### 5.2 等级/比例刷新
+
+```
+PromotionTierService.refreshRate(referrerUserId)
+  → 查 total_team_size → JOIN cf_rate_tier → rate_percent
+  → 写回 promotion_tier.rate_percent + tier(档位名)
+```
+
+触发时机:
+1. `bindReferral` 后(实时)
+2. 每日定时任务 `PromotionTierCheckScheduledTask`(现有,改调 refreshRate 批量刷新)
+
+### 5.3 废弃旧逻辑
+
+- `CommissionService.settle/settleTwoLevel` → 标 `@Deprecated` + 保留(历史读取),删除全部调用点。
+- `PromotionTierEvalService.evaluateTier`(基于 promotion_tier_config)→ 改读 `cf_rate_tier`。
+- `PromotionTierConfig` 实体 → 不再使用。
+
+## 6. 查询接口
+
+### 6.1 用户查询
+
+| 接口 | 说明 | 返回 |
+|------|------|------|
+| `POST /api/commission/cf/rate` | 我的团队规模 + 当前返佣比例 | `{ totalTeamSize, ratePercent, tierName }` |
+| `POST /api/commission/cf/summary` | 我的 CF 钱包汇总 | `{ available, frozen, totalEarned, totalSpent }` |
+| `POST /api/commission/cf/list` | CF 分润/消费流水(分页) | `Page<PlatformBalanceLog>` |
+| `POST /api/commission/cf/wallet` | 个人 CF 余额 | `PlatformPointsService.getBalance` |
+
+### 6.2 CF 转让
+
+| 接口 | 说明 | 约束 |
+|------|------|------|
+| `POST /api/cf/transfer/send` | 成员间转让 CF | 双方同家庭;转出余额足;type=transfer |
+| `POST /api/cf/transfer/list` | 转让记录(分页) | 按 family_id 或 userId 查询 |
+
+### 6.3 家庭券库
+
+| 接口 | 说明 |
+|------|------|
+| `POST /api/coupon/family/list` | 查询家庭共享券(`user_coupon.family_id=familyId`) |
+| 复用 | `POST /api/family-platform/exchange-coupon`(兑换后券挂 family_id) |
+
+### 6.4 管理端
+
+| 接口 | 说明 |
+|------|------|
+| `POST /api/admin/cf-rate-tier/list` | 阶梯配置列表 |
+| `POST /api/admin/cf-rate-tier/save` | 新增/更新档位 |
+| `POST /api/admin/cf-rate-tier/delete` | 删除档位 |
+
+## 7. 前端改造
+
+### 7.1 推广中心(`pages/promotion/*`)
+
+- `team.vue`:展示全层级团队总人数 + 当前返佣比例 + 档位名(调 `/api/commission/cf/rate`)
+- `commission.vue`:改用 CF 分润流水(调 `/api/commission/cf/list`)
+- `index.vue`:头部汇总改用个人 CF 钱包(调 `/api/commission/cf/summary`)
+
+### 7.2 家庭积分页
+
+- 新增"成员间转让"入口:调 `/api/cf/transfer/send`
+- 显示个人 CF 余额与转让记录
+
+### 7.3 优惠券
+
+- 兑换后的券进入家庭共享券库;下单可选择家庭券
+
+## 8. 错误处理与边界
+
+| 场景 | 处理 |
+|------|------|
+| 同家庭互推 | 跳过本人,上溯到非同家庭引荐人(D5) |
+| 无有效推荐人 | 不产生分润,结束 |
+| 余额不足 | `spend` 抛异常,前端提示"CF值不足" |
+| 重复结算 | `earn` 补幂等(`ref_type+ref_id` 去重) |
+| 团队规模未更新 | 定时任务兜底刷新 |
+| 孤儿用户(无 family) | 分润照常走个人钱包,family_id 冗余为空 |
+
+## 9. 测试计划
+
+- 单元:`getRatePercent` 阶梯匹配(边界 0/2/3/9/10);同家庭上溯逻辑;幂等防重。
+- 集成:各订单支付成功 → 分润到账;会员订单只返推荐人;转让同家庭校验。
+- E2E:推广中心展示团队规模与比例;成员间转让;家庭券库兑换与使用。
+
+## 10. 实施顺序(阶段)
+
+1. 数据库迁移:`cf_rate_tier` 建表 + 种子;`promotion_tier` 精简加列;`coupon` 加 `family_id`;`user_platform_balance` 补 `family_id`。
+2. `CfCommissionService` 核心 + `PlatformPointsService.earn` 幂等补丁。
+3. 等级评估:`updateTeamSize` 调用链 + `refreshRate` + 定时任务改造。
+4. 各订单服务迁移点切换(7 处)。
+5. 旧 `settle/settleTwoLevel` 调用点摘除 + `@Deprecated`。
+6. 查询接口 + CF 转让接口 + 家庭券库接口。
+7. 管理端阶梯配置接口。
+8. 前端改造。
+9. 测试 + 部署。
+
+## 11. 明确不做(YAGNI)
+
+- 不重建家庭级分润系统(家庭公共账户 `family_earnings` 不在本次范围)。
+- 不改 `invite_milestone` / `referral_leaderboard` / `onboarding` 现有逻辑。
+- 不迁移历史 `commission_records` 数据到 CF 体系(历史只读)。
+- 不做积分商城实物兑换(现有 `PointsExchangeProduct` 不动)。