|
|
@@ -0,0 +1,309 @@
|
|
|
+# CF 分润模型 v2 增量设计(架构师产出)
|
|
|
+
|
|
|
+> 需求基线:`2026-11-14-cf-reward-model-v2.md`(dc04a6b4c,需求已裁决)
|
|
|
+> 设计基线:`2026-10-24-cf-value-unification-plan.md` + 现行代码实证
|
|
|
+> 产出者:架构师(software-architect)| 状态:**待主理人/产品确认 §9 四项后进入实现**
|
|
|
+
|
|
|
+## 0. 重大发现:现存 ProfitPoolService 旧体系须处置(先读)
|
|
|
+
|
|
|
+worktree 中存在**已在产**的旧团队分润体系,与 v2 新团队池口径冲突,必须纳入迁移方案:
|
|
|
+
|
|
|
+| 项 | 旧体系(ProfitPoolService) | v2 新团队池(本文设计) |
|
|
|
+|----|------------------------------|--------------------------|
|
|
|
+| 表 | profit_pool_order / team_share_record / team_award_record / team_star_snapshot(迁移458-461) | 新建 cf_team_pool_order / cf_team_pool_share |
|
|
|
+| 池来源 | T = roundPct(D=毛利−运营−CF成本, teamShare=30%),**从毛利池切** | T = 成本 × T%(**从成本切,与毛利脱钩**) |
|
|
|
+| 瓜分规则 | 星级份数(8份=T×80% + 团队奖=T×20%)+ CronTask 冷静期结算 | referral_tree 层级树 + 销售档位权重(复用 F03a 档位) |
|
|
|
+| 调用点 | PaymentService:270、ProductOrderService/MembershipService/AssessmentOrderService/MemberSubscriptionService 多处 + TeamProfitCronTask @Scheduled | 由 CfCommissionService.settle 内新方法承载 |
|
|
|
+| 已核实 | ProfitPoolService.java / TeamProfitService / TeamProfitFlowService / StarScoreCalculator / TeamProfitAdminController / TeamProfitCronTask 均在产 | — |
|
|
|
+
|
|
|
+**结论**:v2 落地时旧 ProfitPoolService 应**加开关停用**(配置 `referral_profit.enabled=false`,调用点内部守卫即可灰度停用),避免"毛利池团队奖 + 成本池团队池"双发。此点需产品确认(是否彻底废弃旧团队分润)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 账目公式全集(金额单位:分;CF 单位:CF;1 元 = 100 CF)
|
|
|
+
|
|
|
+### 1.1 单商品订单(售价 S、成本 C、实付 A、体系 T_bps)
|
|
|
+```
|
|
|
+R0 平台抽成 = floor(A × TAKE_BPS / 10000) // TAKE_BPS=1000 → 10%,新增
|
|
|
+R1 消费返 CF = floor(A/100) × 100 CF // 现状不变,走 F02
|
|
|
+R2 直推销售奖 = basePpoints × directRate% // basePpoints = max(0, A − C×qty),现状 calcOrderPpoints 不变
|
|
|
+R2 间推销售奖 = basePpoints × indirectRate%
|
|
|
+R2' 团队池 = floor(C × qty × T_bps / 10000) // 新增,从成本切
|
|
|
+R3 分享注册 = +20 CF/人(日限10),不变
|
|
|
+剩余 = A − R0 − R2直推 − R2间推 − R2'池 // 归商家/平台交付成本与利润,不落账(记日志)
|
|
|
+```
|
|
|
+### 1.2 多商品订单(按行分摊,沿用现状方案A)
|
|
|
+```
|
|
|
+basePpoints = Σ max(0, allocPaid_i − C_i×qty_i) // allocPaid_i = A × base_i/Σbase,现状不变(R2)
|
|
|
+R2'池 = Σ floor(C_i × qty_i × T_bps / 10000) // 按行成本加总(新增,无分摊歧义)
|
|
|
+R0/R1 = 整单口径(不按行)
|
|
|
+```
|
|
|
+### 1.3 非商品订单(会员/订阅/测评/套餐)
|
|
|
+```
|
|
|
+R0 = floor(A×TAKE_BPS/10000);R1 不变;
|
|
|
+R2 沿用 calcOrderPpoints 非商品分支(金额×serviceRateBps),不变;
|
|
|
+R2' = 0(无成本概念,默认不进团队池,需产品确认)
|
|
|
+```
|
|
|
+### 1.4 边界条件
|
|
|
+| 场景 | 处理 |
|
|
|
+|------|------|
|
|
|
+| 亏损仓(A < C×qty) | R2=0(现状 max(0,...));R2' 默认仍按成本×T% 发放(成本切出与毛利无关)——配置 `cf.team_pool.on_loss=payout\|zero`,默认 payout,待产品确认 |
|
|
|
+| 实付=0 | R0=0、R1=0、R2=0;R2' 仍按成本×T% |
|
|
|
+| T=0 / 未配置 | 团队池跳过,不影响 R2/R1/R0 |
|
|
|
+| 同家庭互推 | R2 上溯跳过(现状);R2' 瓜分成员也跳过同家庭 |
|
|
|
+| 池金额小、权重分配尾差 | 尾差守恒(见 §6) |
|
|
|
+
|
|
|
+### 1.5 幂等键设计(全部复用"同一事务+唯一键")
|
|
|
+| 通道 | 表/键 |
|
|
|
+|------|-------|
|
|
|
+| R1 | cf_balance_log uk_ref_type(member_id, 'earn', 'cf_consume_order', ref_id=orderId×100+ns),现状不变 |
|
|
|
+| R2 | cf_balance_log uk_ref_type(member_id, 'earn', 'F03a'/'F03b', ref_id=orderId),现状不变 |
|
|
|
+| R2' | cf_team_pool_order.uk_order(order_type, order_id) + cf_team_pool_share.uk_pool_member(pool_id, member_id) + cf_balance_log(member_id,'earn','F03c', ref_id=pool_id) 三层兜底 |
|
|
|
+| R0 | cf_platform_revenue_log.uk_order(order_type, order_id) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 表结构变更清单(迁移编号建议 462-465)
|
|
|
+
|
|
|
+### 2.1 T% 配置落位 —— 推荐独立表 `cf_team_share_config`(新,迁移462)
|
|
|
+
|
|
|
+**候选对比**:
|
|
|
+| 方案 | 优点 | 缺点 | 结论 |
|
|
|
+|------|------|------|------|
|
|
|
+| 商品/品类表加 team_share_percent | 粒度最细 | 配置分散、与体系维度叠加复杂、改商品热表风险大 | ❌ |
|
|
|
+| distribution_systems 表加列 | 与销售体系天然对齐 | 只覆盖 SYSTEM 维度,无法表达 miniapp 维度;ALTER 热表 | ❌ |
|
|
|
+| products 按 distribution_system_id 外部引用 | 同 distribution | 同上 | ❌ |
|
|
|
+| **独立 cf_team_share_config(多作用域)** | GLOBAL/SYSTEM/MINIAPP 三作用域一张表,优先级链清晰,不碰现有热表 | 多一张表、需解析服务 | ✅ 推荐 |
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE IF NOT EXISTS cf_team_share_config (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ owner_type VARCHAR(16) NOT NULL COMMENT '作用域: GLOBAL/SYSTEM/MINIAPP',
|
|
|
+ owner_id VARCHAR(64) COMMENT 'SYSTEM=distribution_systems.id / MINIAPP=miniapp_config.mp_code / GLOBAL=NULL',
|
|
|
+ t_bps INT NOT NULL COMMENT '团队池比例(bps, 如3000=30%)',
|
|
|
+ enabled TINYINT NOT NULL DEFAULT 1,
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY uk_scope (owner_type, owner_id),
|
|
|
+ KEY idx_enabled (enabled)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='团队分润池比例配置(作用域GLOBAL/SYSTEM/MINIAPP)';
|
|
|
+```
|
|
|
+- **优先级链**:MINIAPP(mp_code) → SYSTEM(商品所属 distribution_system_id) → GLOBAL → 代码兜底 3000bps。与 P0 的 `miniapp_config.mp_code` 呼应(只引用短码,不在 P0 热改表上加列,避免冲突)。
|
|
|
+- **不用 miniapp_config 加列的原因**:miniapp_config 是多小程序凭据表(语义"谁"),团队池比例是运营参数;独立表同时服务 SYSTEM 维度,且避开 P0 正在改 schema.sql/DatabaseInitializer 的交集。
|
|
|
+
|
|
|
+### 2.2 团队池整单表 `cf_team_pool_order`(新,迁移463)
|
|
|
+```sql
|
|
|
+CREATE TABLE IF NOT EXISTS cf_team_pool_order (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ order_id BIGINT NOT NULL COMMENT '来源订单ID',
|
|
|
+ order_type VARCHAR(32) NOT NULL,
|
|
|
+ product_id BIGINT NULL,
|
|
|
+ buyer_user_id BIGINT NOT NULL,
|
|
|
+ pool_cent INT NOT NULL DEFAULT 0 COMMENT '团队池金额(分)',
|
|
|
+ t_bps INT NOT NULL COMMENT '实际采用T(bps)',
|
|
|
+ config_scope VARCHAR(16) COMMENT '命中作用域 GLOBAL/SYSTEM/MINIAPP',
|
|
|
+ status VARCHAR(16) NOT NULL DEFAULT 'pooled' COMMENT 'pooled=已建池/share_issued=已瓜分/unclaimed=无成员可瓜分',
|
|
|
+ remark VARCHAR(255),
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ settled_at DATETIME NULL,
|
|
|
+ UNIQUE KEY uk_order (order_type, order_id),
|
|
|
+ KEY idx_status (status)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='团队分润池整单(成本×T%)';
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 团队池成员流水 `cf_team_pool_share`(新,迁移464)
|
|
|
+```sql
|
|
|
+CREATE TABLE IF NOT EXISTS cf_team_pool_share (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ pool_id BIGINT NOT NULL COMMENT 'cf_team_pool_order.id',
|
|
|
+ member_id BIGINT NOT NULL COMMENT 'family_members.id',
|
|
|
+ user_id BIGINT NOT NULL COMMENT 'users.id(推荐链成员)',
|
|
|
+ layer INT NOT NULL COMMENT '推荐层级1..8(买家上溯)',
|
|
|
+ rate_percent INT NOT NULL COMMENT '该成员直推档位率(%)',
|
|
|
+ weight INT NOT NULL DEFAULT 0 COMMENT '档位权重(=rate_percent, 0=无档位跳过)',
|
|
|
+ share_cent INT NOT NULL COMMENT '分得金额(分)',
|
|
|
+ status VARCHAR(16) NOT NULL DEFAULT 'issued',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY uk_pool_member (pool_id, member_id),
|
|
|
+ KEY idx_user (user_id)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='团队池成员瓜分明细';
|
|
|
+```
|
|
|
+
|
|
|
+### 2.4 平台抽成流水 `cf_platform_revenue_log`(新,迁移465)
|
|
|
+```sql
|
|
|
+CREATE TABLE IF NOT EXISTS cf_platform_revenue_log (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ order_id BIGINT NOT NULL,
|
|
|
+ order_type VARCHAR(32) NOT NULL,
|
|
|
+ amount_cent INT NOT NULL COMMENT '平台抽成(分)=实付×10%',
|
|
|
+ take_bps INT NOT NULL DEFAULT 1000,
|
|
|
+ remark VARCHAR(255),
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY uk_order (order_type, order_id),
|
|
|
+ KEY idx_created (created_at)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台抽成R0流水(独立入平台账)';
|
|
|
+```
|
|
|
+> 不复用 cf_balance_log:平台无 member 维度,cf_balance_log 主键 member_id 不适合平台账;独立表便于财务对账。
|
|
|
+
|
|
|
+### 2.5 迁移方式
|
|
|
+- 全部走 `DatabaseInitializer.runMigrations()` 新增私有方法 `migrateCfRewardV2TeamPool()`:`CREATE TABLE IF NOT EXISTS`(幂等)+ `ensureServiceConfig` 播种 + schema.sql 同步(项目铁律)。
|
|
|
+- 不与 P0 冲突:P0 也在改 schema.sql / DatabaseInitializer → 实现须等 P0 合入后另开 worktree(已排期),本设计按 P0 合入后的环境续编号。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 结算链路改造点(方法级清单)
|
|
|
+
|
|
|
+### 3.1 CfCommissionService(主改造)
|
|
|
+| 方法 | 改动 |
|
|
|
+|------|------|
|
|
|
+| `settle(...)` (138-146) | 保留 earnBuyerCf(R1)+ distributeToReferrers(R2),新增 `settlePlatformTake`(R0)+ `settleTeamPool`(R2');同一 @Transactional |
|
|
|
+| `settleReferrerOnly` (166-171) | 同上加 R0/R2'(会员/订阅单 R2'=0 自动跳过) |
|
|
|
+| `settlePlatformTake`(新) | floor(paid×1000/10000) 写 cf_platform_revenue_log(uk_order 幂等);实付 null 时用 orderAmountCent |
|
|
|
+| `settleTeamPool`(新) | 解析 T(TeamShareConfigService)→ 算池 → 建 cf_team_pool_order(uk_order 幂等)→ distributeTeamPool |
|
|
|
+| `distributeTeamPool`(新) | §6 伪代码:层级树+档位权重瓜分 |
|
|
|
+| `distributeToReferrers` (196-240) | **零改动** |
|
|
|
+| `calcOrderPpoints` (344-396) | **零改动**(毛利口径 R2 专用) |
|
|
|
+| 常量 | 新增 `RULE_TEAM_POOL = "F03c"` |
|
|
|
+
|
|
|
+### 3.2 新增 TeamShareConfigService(新类)
|
|
|
+- `resolveTbps(String mpCode, Long distributionSystemId)` → 优先级 MINIAPP>SYSTEM>GLOBAL>3000 兜底;本地缓存 + reload(对齐 ConfigService TTL 60s 模式)。
|
|
|
+- 入参来源:`MiniAppContextHolder`(P0 提供)取 mp_code;商品 `products.distribution_system_id` 取体系。
|
|
|
+
|
|
|
+### 3.3 cf_earn_rule 新增 F03c 种子(DatabaseInitializer.seedCfEarnRules 追加)
|
|
|
+```
|
|
|
+{"F03c", "团队分润池(成本×T%池内瓜分)", "consume", "per_item", "",
|
|
|
+ "", "{\"source\":\"cf_team_pool_share.share_cent\",\"note\":\"amount=每人分得CF,按池金额1分=1CF\"}",
|
|
|
+ "", "", "", "F03c", "25",
|
|
|
+ "团队池=成本×T%,按referral_tree层级树+直推档位权重瓜分;幂等ref_type=F03c,ref_id=pool_id"}
|
|
|
+```
|
|
|
+- 发放仍走 `CfBalanceService.earnByRule(memberId,"F03c",poolId,{amount:share_cent})`(per_item 由 ctx.amount 驱动)→ 三道闸/幂等自动生效。
|
|
|
+- **单位说明**:R2/R2' 均以 CF 发放(1 分=1 CF 参照,与现状 F03a/b 一致);R0 为货币分入平台账;R1 为 CF。账目示例"金额"在落库语义为分→CF 对照。
|
|
|
+
|
|
|
+### 3.4 旧体系停用(最小侵入)
|
|
|
+- `ProfitPoolService` 加配置 `referral_profit.enabled`(默认 false),所有 settle 入口首行守卫:disabled 时直接 return null(幂等不破坏);CronTask 同样守卫。
|
|
|
+- 调用点(PaymentService:270、ProductOrderService、MembershipService、AssessmentOrderService、MemberSubscriptionService)**不改代码**,仅靠 ProfitPoolService 内部开关灰度停用。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 配置键(新增,进 CfConfigKeys + DatabaseInitializer.seedCfServiceConfig)
|
|
|
+| 键 | 默认 | 类型 | 说明 |
|
|
|
+|----|------|------|------|
|
|
|
+| `cf.platform.take_rate_bps` | 1000 | int | R0 平台抽成(成交价 10%) |
|
|
|
+| `cf.team_pool.enabled` | true | bool | 团队池总开关 |
|
|
|
+| `cf.team_pool.default_t_bps` | 3000 | int | GLOBAL 兜底 T(30%) |
|
|
|
+| `cf.team_pool.on_loss` | payout | string | 亏损仓是否仍发(payout/zero) |
|
|
|
+| `cf.team_pool.max_depth` | 8 | int | 池瓜分最大层级(默认 8) |
|
|
|
+| `referral_profit.enabled` | false | bool | 旧 ProfitPoolService 停用开关 |
|
|
|
+- 播种点:`seedCfServiceConfig()` 内 ensureServiceConfig 追加;CfConfigKeys 静态常量 + FALLBACKS 同步。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 层级树瓜分算法(池金额 × 档位占比加权伪代码)
|
|
|
+
|
|
|
+```
|
|
|
+function distributeTeamPool(orderId, orderType, buyerUserId, buyerFamilyId, paidCent, productId):
|
|
|
+ // 1. 算池金额(单商品/多商品分行)
|
|
|
+ poolCent = 0
|
|
|
+ for each line (product, qty) in order:
|
|
|
+ if product has purchase_price C:
|
|
|
+ poolCent += floor(C * qty * T_bps / 10000)
|
|
|
+ if poolCent <= 0 or !cf.team_pool.enabled: return
|
|
|
+
|
|
|
+ // 2. 幂等建池(uk_order)
|
|
|
+ pool = cf_team_pool_order.insert_if_absent(orderId, orderType, poolCent, T_bps)
|
|
|
+ if pool existed: return // 重复结算直接返回
|
|
|
+
|
|
|
+ // 3. 取层级树成员(买家上溯,跳过同家庭,≤max_depth)
|
|
|
+ members = []
|
|
|
+ for ref in referral_tree(buyerUserId) order by level asc:
|
|
|
+ if ref.parent in same family as buyer(user): continue // 同家庭跳过
|
|
|
+ members.append(ref.parent)
|
|
|
+ if len(members) >= max_depth: break
|
|
|
+
|
|
|
+ // 4. 档位权重(与 F03a 直推档位同一口径)
|
|
|
+ totalW = 0; items = []
|
|
|
+ for m in members:
|
|
|
+ teamPp = cfReferralService.getTeamAnnualPpoints(m, 8) // 复用
|
|
|
+ tier = cfReferralService.matchDirectSalesTier(teamPp) // 复用,ratePercent 作权重
|
|
|
+ w = (tier == null) ? 0 : tier.ratePercent
|
|
|
+ totalW += w
|
|
|
+ items.append({user:m, layer:..., rate:w, memberId: resolveMemberId(m)})
|
|
|
+ if totalW <= 0:
|
|
|
+ pool.status = 'unclaimed'; update(); return // 无人有档位 → 池归商家(记日志)
|
|
|
+
|
|
|
+ // 5. 加权瓜分 + 尾差守恒(最后一人补齐)
|
|
|
+ issued = 0
|
|
|
+ for i, it in items.enumerate():
|
|
|
+ raw = floor(poolCent * it.w / totalW)
|
|
|
+ if i == last: raw = poolCent - issued // 守恒
|
|
|
+ if raw <= 0 or it.memberId == null: continue
|
|
|
+ earnByRule(it.memberId, "F03c", pool.id, {amount: raw}) // 幂等兜底
|
|
|
+ cf_team_pool_share.insert(pool.id, ..., raw) // uk_pool_member 兜底
|
|
|
+ issued += raw
|
|
|
+ pool.status = 'share_issued'; pool.settled_at = now; update()
|
|
|
+```
|
|
|
+
|
|
|
+**与直推档位累计口径衔接**:
|
|
|
+- 权重数据源 = `CfReferralService.getTeamAnnualPpoints(m, 8)`(团队 8 层滚动一年 P 点)+ `matchDirectSalesTier(...).ratePercent`,与 F03a 完全同源、同表、同窗口;
|
|
|
+- 因此"团队成员拿到的团队池份额"与其"作为直推时的档位率"成正比(高业绩成员权重高),运营调整档位表即同时影响 F03a 与 R2',无需两套规则;
|
|
|
+- 差异点仅:F03a 基数=本单毛利 P 点,R2' 基数=池金额;F03a 只奖第 1/2 个非同家庭引荐人,R2' 奖 ≤8 层所有非同家庭引荐人(member 维度)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 任务分解 T01-T08(有序,含 P0 文件交集)
|
|
|
+
|
|
|
+> 约定:★=与多小程序 P0 有文件交集;实现顺序维持"P0 合入 → 本序列"。
|
|
|
+
|
|
|
+| 任务 | 内容 | 主要文件 | 依赖 | 能力键(TDD) |
|
|
|
+|------|------|----------|------|----------------|
|
|
|
+| **T01** 配置与迁移(表+键+种子) | 新建 4 表 + F03c 种子 + 6 键播种 + ProfitPoolService 停用开关 | DatabaseInitializer★★、schema.sql★★、CfConfigKeys、CfEarnRuleService 测试 | 无(等 P0 合入) | `cf.rewardv2.config.migrate`、`cf.rewardv2.config.seed` |
|
|
|
+| **T02** TeamShareConfigService | 多作用域解析 + 优先级链 + 缓存 | TeamShareConfigService(新)、CfTeamShareConfigMapper(新)、entity.CfTeamShareConfig(新) | T01 | `cf.teamshare.config.resolve`、`cf.teamshare.config.priority` |
|
|
|
+| **T03** R0 平台抽成记账 | settlePlatformTake + cf_platform_revenue_log | CfCommissionService、CfPlatformRevenueLog(新)、PaymentController 依赖不变 | T01 | `cf.rewardv2.r0.platform-take`、`cf.rewardv2.r0.idempotent` |
|
|
|
+| **T04** 团队池建池+结算分叉 | settleTeamPool + 建池幂等 + settle 入口接线 | CfCommissionService、CfTeamPoolOrder(新)、CfTeamPoolOrderMapper(新)、PaymentService★(仅依赖链,无签名变化) | T01,T02 | `cf.teamshare.pool.create`、`cf.teamshare.pool.idempotent` |
|
|
|
+| **T05** 层级树瓜分算法 | distributeTeamPool + share 流水 + 尾差守恒 + 同家庭跳过 | CfCommissionService、CfTeamPoolShare(新)、CfReferralService(复用不改)、CfBalanceServiceImpl(复用不改) | T04 | `cf.teamshare.share.weighted`、`cf.teamshare.share.family-skip`、`cf.teamshare.share.remainder` |
|
|
|
+| **T06** F03c 规则 + 发放接入 | earnByRule("F03c") per_item + 三道闸验证 | CfEarnRuleService、DatabaseInitializer(种子已T01)、CfBalanceServiceImpl(仅验证) | T04 | `cf.rewardv2.f03c.dispatch` |
|
|
|
+| **T07** 旧 ProfitPoolService 停用验证 | enabled=false 灰度、CronTask 守卫、回归 | ProfitPoolService、TeamProfitCronTask、ProfitPoolService 测试 | T01 | `cf.rewardv2.legacy.disable` |
|
|
|
+| **T08** 端到端 + 测试矩阵 | 账目示例/多商品/亏损/幂等/回退断言 | CfCommissionServiceTest、PaymentServiceORDRoutingTest★、新 RewardV2E2ETest | T03-T07 | `cf.rewardv2.e2e.example-200-100` 等 |
|
|
|
+
|
|
|
+P0 文件交集标注:T01 与 P0 冲突面最大(DatabaseInitializer/schema.sql)——必须 P0 合入后实现;PaymentService 仅 T04 有依赖引用(不改签名),UserService/application.yml 无交集。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 风险清单
|
|
|
+
|
|
|
+1. **幂等**:三层兜底(uk_order → uk_pool_member → uk_ref_type),重复结算任一环节直接短路;settle 整体 @Transactional,任一步失败整体回滚。
|
|
|
+2. **并发结算**:同订单并发回调时 uk_order 唯一键竞争——先插入者胜,后者捕获 DuplicateKeyException 直接返回(现状 settle 已如此)。
|
|
|
+3. **旧体系双发**:ProfitPoolService 未停用前与新团队池同时发放 → 必须 T07 灰度开关先行;上线顺序 旧停用 → 新启用。
|
|
|
+4. **既有数据兼容**:profit_pool_order/team_share_record/team_award_record 全部保留只读;CronTask 守卫后不再写新数据。
|
|
|
+5. **T 未配置降级**:GLOBAL 行缺失时代码兜底 3000bps;`cf.team_pool.enabled=false` 时全链路跳过(R2/R1/R0 不受影响)。
|
|
|
+6. **亏损仓语义**:R2' 从成本切出,亏损单仍发池(默认 payout)可能造成商家亏损加倍——配置 on_loss=zero 可关;待产品确认默认值。
|
|
|
+7. **单位口径**:R2/R2' 发 CF(1分=1CF 参照)、R0 货币分、R1 CF——财务对账需按类型区分,避免把 CF 当货币分合计。
|
|
|
+8. **"成交价"口径**:R0 基数 = 实付(paidAmountCent)按现状传参;若决策"成交价=划线价",需另加参数,待确认。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 测试用例设计(至少覆盖)
|
|
|
+
|
|
|
+| # | 用例 | 断言 |
|
|
|
+|---|------|------|
|
|
|
+| 1 | 账目示例 200/100/T=30%(直推70%档、间推20%档、2名团队成员各70/50权重) | R0=20、R2直推=70、R2间推=20、池=30、R1=200CF;池内瓜分 30×70/120=17、30×50/120=13(守恒) |
|
|
|
+| 2 | 多商品分摊:商品A(100/60×2) + 商品B(80/40) | R2 按行分摊现状断言;R2'池 = (60×2+40)×30% = 48 |
|
|
|
+| 3 | 亏损仓:售价100/成本120 | R2=0;R2'=120×30%=36(payout 默认)或 0(on_loss=zero) |
|
|
|
+| 4 | 团队池幂等:同一订单重复 settle 2 次 | 池建 1 次、share 不重复、CF 不重复(earned/duplicated 断言) |
|
|
|
+| 5 | 同家庭跳过:买家直推为同家庭 | 直推不拿 F03a;R2' 成员列表剔除同家庭,池分给下一非同家庭或 unclaimed |
|
|
|
+| 6 | T 未配置回退:cf_team_share_config 空表 | 命中 GLOBAL 兜底 3000bps;无 GLOBAL 行时走代码兜底 3000 |
|
|
|
+| 7 | 作用域优先级:MINIAPP=2000 且 SYSTEM=4000 | 命中 2000(MINIAPP 优先) |
|
|
|
+| 8 | T=0 / enabled=false | 池不建、不瓜分,R2/R1 照常 |
|
|
|
+| 9 | 尾差守恒 | Σshare_cent == pool_cent(最后一人补齐,无金额蒸发) |
|
|
|
+| 10 | R0 不参与基数 | R0=实付×10% 单列,R2 基数仍为毛利全额(示例中 100 毛利不被抽成 10% 缩小) |
|
|
|
+| 11 | 旧体系停用 | referral_profit.enabled=false 后 ProfitPoolService.settle 返回 null,profit_pool_order 不再新增 |
|
|
|
+| 12 | 并发回调 | 两线程同时 settle 同订单,恰一张池单、全部发放一次 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 待产品/主理人确认(写入风险,不阻塞设计)
|
|
|
+
|
|
|
+1. 旧 ProfitPoolService/团队8份体系是否彻底废弃(默认方案:开关停用、表保留只读)?
|
|
|
+2. 非商品订单是否要进团队池(默认 0)?如需,需定义非商品成本模型。
|
|
|
+3. 亏损仓默认 payout 还是 zero(默认 payout 与"成本切出"语义一致)?
|
|
|
+4. R0 基数"成交价"= 实付还是划线价(默认实付)?
|