Ver código fonte

docs(cf): API 文档补充 CF 分佣/转让/阶梯配置接口 + 单元测试

Sisyphus 2 semanas atrás
pai
commit
b928550cc9

+ 126 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceTest.java

@@ -0,0 +1,126 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.CfRateTier;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * CF 值分佣阶梯匹配边界测试。
+ *
+ * 复刻 CfReferralService.matchRateTier 的匹配语义:
+ *   WHERE enabled = 1 AND min_team_size <= teamSize
+ *   ORDER BY min_team_size DESC LIMIT 1
+ *
+ * 种子数据(DatabaseInitializer 迁移279):
+ *   铜牌 min=0  5% / 银牌 min=3  10% / 金牌 min=10 15%
+ *   铂金 min=30 20% / 钻石 min=100 25%
+ *
+ * 预期边界映射:
+ *   0/1/2  → 铜牌 5
+ *   3~9    → 银牌 10
+ *   10~29  → 金牌 15
+ *   30~99  → 铂金 20
+ *   100+   → 钻石 25
+ */
+class CfCommissionServiceTest {
+
+    private static final List<CfRateTier> SEED_TIERS = buildSeedTiers();
+
+    private static List<CfRateTier> buildSeedTiers() {
+        List<CfRateTier> list = new ArrayList<>();
+        list.add(tier("铜牌", 0, 5, 1));
+        list.add(tier("银牌", 3, 10, 2));
+        list.add(tier("金牌", 10, 15, 3));
+        list.add(tier("铂金", 30, 20, 4));
+        list.add(tier("钻石", 100, 25, 5));
+        return list;
+    }
+
+    private static CfRateTier tier(String name, int minTeamSize, int ratePercent, int sortOrder) {
+        CfRateTier t = new CfRateTier();
+        t.setTierName(name);
+        t.setMinTeamSize(minTeamSize);
+        t.setRatePercent(ratePercent);
+        t.setSortOrder(sortOrder);
+        t.setEnabled(1);
+        return t;
+    }
+
+    /**
+     * 等价于 CfReferralService.matchRateTier 的查询语义。
+     * 在内存中对启用的档位按 min_team_size <= teamSize 过滤后取最大 min_team_size 档位。
+     */
+    private CfRateTier matchRateTier(int teamSize) {
+        CfRateTier best = null;
+        for (CfRateTier t : SEED_TIERS) {
+            if (t.getEnabled() != 1 || t.getMinTeamSize() > teamSize) {
+                continue;
+            }
+            if (best == null || t.getMinTeamSize() > best.getMinTeamSize()) {
+                best = t;
+            }
+        }
+        return best;
+    }
+
+    @Test
+    void teamSize0_shouldMatchBronze5() {
+        assertTier(0, 5, "铜牌");
+    }
+
+    @Test
+    void teamSize2_shouldMatchBronze5() {
+        assertTier(2, 5, "铜牌");
+    }
+
+    @Test
+    void teamSize3_shouldMatchSilver10() {
+        assertTier(3, 10, "银牌");
+    }
+
+    @Test
+    void teamSize9_shouldMatchSilver10() {
+        assertTier(9, 10, "银牌");
+    }
+
+    @Test
+    void teamSize10_shouldMatchGold15() {
+        assertTier(10, 15, "金牌");
+    }
+
+    @Test
+    void teamSize29_shouldMatchGold15() {
+        assertTier(29, 15, "金牌");
+    }
+
+    @Test
+    void teamSize30_shouldMatchPlatinum20() {
+        assertTier(30, 20, "铂金");
+    }
+
+    @Test
+    void teamSize99_shouldMatchPlatinum20() {
+        assertTier(99, 20, "铂金");
+    }
+
+    @Test
+    void teamSize100_shouldMatchDiamond25() {
+        assertTier(100, 25, "钻石");
+    }
+
+    @Test
+    void teamSize100plus_shouldMatchDiamond25() {
+        assertTier(1000, 25, "钻石");
+    }
+
+    private void assertTier(int teamSize, int expectedRate, String expectedName) {
+        CfRateTier tier = matchRateTier(teamSize);
+        assertNotNull(tier, "teamSize=" + teamSize + " 应匹配到档位");
+        assertEquals(expectedRate, tier.getRatePercent(), "teamSize=" + teamSize + " 返佣比例");
+        assertEquals(expectedName, tier.getTierName(), "teamSize=" + teamSize + " 档位名");
+    }
+}

+ 47 - 1
docs/superpowers/api/API_REFERENCE.md

@@ -69,6 +69,9 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `ArticleController` | `/api/articles` | 文章 | — |
 | `NotificationController` | `/api/notification` | 通知 | — |
 | `StatsController` | `/api/stats` | 统计 | — |
+| `CfCommissionController` | `/api/commission/cf` | CF 分佣(团队规模/比例/钱包/流水) | — |
+| `CfTransferController` | `/api/cf/transfer` | CF 成员间转让 | — |
+| `AdminCfRateTierController` | `/api/admin/cf-rate-tier` | CF 返佣阶梯配置(admin) | — |
 
 ---
 
@@ -884,6 +887,49 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 
 ---
 
+### 4.33 CF 值分佣(`/api/commission/cf/*`)
+
+> CF 值(平台积分)统一分佣体系:普通订单双返(当前消费人 + 推荐人按团队规模阶梯分润),会员/订阅只返推荐人,同家庭互推跳过本人上溯。
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/commission/cf/rate` | 我的团队规模 + 当前返佣比例 + 档位名。返回 `{ totalTeamSize, ratePercent, tierName }`(未入档时 ratePercent=0、tierName="未入档") |
+| `POST /api/commission/cf/summary` | 我的 CF 钱包汇总,返回 `PlatformBalanceService.getBalance(userId)` 结果 |
+| `POST /api/commission/cf/list` | 我的 CF 流水(分页),请求 `{ page, size }`,返回 `Page<PlatformBalanceLog>` |
+
+**阶梯档位(cf_rate_tier,管理端可配置):**
+
+| 团队规模(min_team_size) | 档位 | 返佣比例 |
+|:---:|:---:|:---:|
+| 0~2 | 铜牌 | 5% |
+| 3~9 | 银牌 | 10% |
+| 10~29 | 金牌 | 15% |
+| 30~99 | 铂金 | 20% |
+| 100+ | 钻石 | 25% |
+
+> 匹配语义:`enabled=1 AND min_team_size <= teamSize ORDER BY min_team_size DESC LIMIT 1`。
+
+### 4.34 CF 成员间转让(`/api/cf/transfer/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/cf/transfer/send` | 成员间转让 CF,请求 `{ toUserId, amount }`。仅限同一家庭(校验 familyId 一致);amount 必须为正数。转出扣减 + 转入增加在同一事务 |
+| `POST /api/cf/transfer/list` | 转让记录(分页,按当前用户家庭过滤),请求 `{ page, size }`,返回 `Page<CfTransferRecord>` |
+
+### 4.35 CF 返佣阶梯配置(admin,`/api/admin/cf-rate-tier/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/cf-rate-tier/list` | 阶梯列表(按 sort_order 升序),返回 `List<CfRateTier>` |
+| `POST /api/admin/cf-rate-tier/save` | 新增/更新阶梯,请求体为 `CfRateTier`(含 id 则更新,否则新增,默认 enabled=1),返回 "已保存" |
+| `POST /api/admin/cf-rate-tier/delete` | 删除阶梯,请求 `{ id }`,返回 "已删除" |
+
+**`CfRateTier` 字段:** `id` / `tierName`(档位名)/ `minTeamSize`(团队规模下限含)/ `ratePercent`(返佣比例%)/ `sortOrder`(排序,越大越高)/ `enabled`(1启用/0停用)/ `createdAt` / `updatedAt`。
+
+> 注意:家庭券接口为 `POST /api/coupon/my` 与 `POST /api/coupon/list`(见 4.32),不存在 `/api/coupon/family/list` 路由。
+
+---
+
 ## 六、待清理的废弃接口
 
 | Controller | 废弃接口 | 替代方案 | 当前状态 | 清理条件 |
@@ -898,4 +944,4 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 
 ---
 
-*文档最后更新:2026-08-31*
+*文档最后更新:2026-09-02*