案例 29:分销返佣/上下级分润
本案例对应 JQuick-Java 仓库 USE-CASES 场景 29「分销返佣/上下级分润」,属于电商分销领域。系统对一笔分销订单完成自购佣金、直推上级奖励、间推上级奖励与团长服务费的多级分润核算,并执行活动让利基数折减、30% 合计封顶熔断、退款没收与大额单人工审核等风控约束。
社交分销体系中,一笔订单的佣金往往要同时分给成交分销商本人、其一级上线(直推)、二级上线(间推)以及带队成团的团长,且费率随商品类目毛利(美妆高、家电薄)、分销商等级(L1/L2/L3/PARTNER)与活动让利幅度变化。运营还会按成团规模给团长额外服务费。
核算从可佣基数开始:订单实付金额先乘以活动让利折减系数(让利占比 ≥50% 基数按 50% 计、≥30% 按 70%、≥10% 按 90%,无让利全额),避免商家深度打折后仍按原价计提佣金。自购佣金 = 基数 × 类目基础费率(美妆 20%、标品 10%、生鲜 6%、家电等其余 3%)× 等级系数(PARTNER 1.5、L3 1.3、L2 1.1、L1 1.0);直推奖励 = 基数 × 上线身份费率(PARTNER 5%、L3 4%、L2 3%、L1 无);间推奖励 = 基数 × 二级上线费率(PARTNER 2%、L3 1.5%,其余无);团长服务费仅团长可获,月成团 ≥20 单为 2%、≥10 单为 1%。
Service 侧承担三项财务与风控约束:其一,四项分润合计不得超过可佣基数的 30%,超过即按上限熔断并标记 capped;其二,订单已退款则各级分润全部归零、状态置为 FORFEITED;其三,单笔金额达到 5 万元的批发单佣金照算但冻结为 PENDING,转人工核验交易真实性后再结算。所有金额按 BigDecimal HALF_UP 保留两位小数。
决策输出物为 DistributionCommissionDecision:可佣基数、四项分润金额、合计金额、是否触发熔断、结算状态(SETTLED/PENDING/FORFEITED)与逐行分润明细(breakdown),可直接作为结算单与对账依据。
- 类目费率、等级系数、直推/间推费率、团长门槛随招商政策频繁调整,费率硬编码在结算代码里,每次政策变更都要赶结算窗口发版。
- 多级叠加容易把薄利商品「分穿」:美妆高费率叠加上下级与团长奖可能超过商家让利空间,必须有合计上限熔断且可对账说明。
- 活动大促期间订单深度让利,佣金基数若仍按实付甚至原价计提会造成补贴资损,需要独立可配的基数折减口径。
- 退款追缴、大额批发刷单等风控约束与正常分润在同一链路,需要清晰的前置短路(退款直接没收)与冻结审核(5 万元 PENDING)。
- 分润涉及真金白银,double 连乘易出尾差纠纷,财务要求分项两位小数取整、每一级金额都可审计回溯。
- 分销系统多为运行多年的 JDK 8 老系统,并需适配信创 JDK,难以引入独立部署的重型佣金规则平台。
本案例采用「入参模型 + 决策模型 + 规则接口 + XML 规则脚本 + Service 编排 + JUnit 测试」六件套结构:六个规则方法各自封装一类费率/系数,全部外置 XML,Service 负责基数折减、多级连乘、BigDecimal 取整、30% 封顶熔断与状态短路。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型层 | 承载分销订单与各级分销商身份要素,输出四项分润、熔断标记与结算状态 | DistributionOrder.java、DistributionCommissionDecision.java |
| 规则接口层 | 声明类目费率、等级系数、直推/间推费率、基数系数、团长费率六个方法 | DistributionCommissionRules.java |
| 规则脚本层 | 全部费率、系数与成团门槛外置,支持热更新 | distribution-commission.xml |
| 编排与测试层 | 基数折减、多级连乘、两位取整、30% 熔断、退款/大额短路;6 组形态回归 | DistributionCommissionService.java、DistributionCommissionServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
| categoryRate | 类目基础佣金率:BEAUTY 0.20、STANDARD 0.10、FRESH 0.06、其余(家电)0.03 | 0.03-0.20 |
| levelMultiplier | 分销商等级系数:PARTNER 1.5、L3 1.3、L2 1.1、L1 等其余 1.0 | 1.0-1.5 |
| directReferRate | 直推(一级上线)奖励率:PARTNER 0.05、L3 0.04、L2 0.03、其余 0 | 0-0.05 |
| indirectReferRate | 间推(二级上线)奖励率:PARTNER 0.02、L3 0.015、其余 0 | 0-0.02 |
| baseFactor | 活动让利折减:让利 ≥50% 取 0.5、≥30% 取 0.7、≥10% 取 0.9、无让利 1.0 | 0.5-1.0 |
| groupBonusRate | 团长且月成团 ≥20 单为 0.02、≥10 单为 0.01,非团长或不足 10 单为 0 | 0-0.02 |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将下列文件按原始包名放入 JQuick-Java 测试工程:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试类放入 src/test/java/com/github/paohaijiao/demo/,Service 放入 src/test/java/com/github/paohaijiao/demo/service/,XML 脚本放入 src/test/resources/demo/。在工程根目录执行 mvn test -Dtest=DistributionCommissionServiceTest 即可运行。
DistributionOrder.java
Section titled “DistributionOrder.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 分销订单与各级分销商身份要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic class DistributionOrder {
/** * 订单号 */ private String orderId;
/** * 商品编码 */ private String sku;
/** * 实付金额(元) */ private double orderAmount;
/** * 商品类目:BEAUTY 美妆 / STANDARD 标品 / FRESH 生鲜 / APPLIANCE 家电 */ private String productCategory;
/** * 成交分销商等级:L1 初级 / L2 中级 / L3 高级 / PARTNER 合伙人 */ private String distributorLevel;
/** * 直推上级(一级上线)等级 */ private String uplineLevel;
/** * 间推上级(二级上线)等级 */ private String rootLevel;
/** * 是否团长 */ private boolean groupLeader;
/** * 近 30 天成团数 */ private int groupCount30d;
/** * 活动让利占比(整数百分比) */ private int promotionCostRate;
/** * 订单后续是否发生退款 */ private boolean refunded;}DistributionCommissionDecision.java
Section titled “DistributionCommissionDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 多级分润决策 */@Datapublic class DistributionCommissionDecision {
/** * 订单号 */ private String orderId;
/** * 可佣基数(活动让利后) */ private double baseAmount;
/** * 自购/直销佣金 */ private double selfCommission;
/** * 直推上级奖励 */ private double directCommission;
/** * 间推上级奖励 */ private double indirectCommission;
/** * 团长团队服务费 */ private double groupBonus;
/** * 分润合计(受 30% 封顶约束) */ private double totalCommission;
/** * 是否触发 30% 分润上限熔断 */ private boolean capped;
/** * 状态:SETTLED 可结算 / PENDING 待人工审核 / FORFEITED 退款没收 */ private String status;
/** * 分润明细与说明 */ private List<String> breakdown = new ArrayList<String>();
public void addBreakdown(String line) { this.breakdown.add(line); }}DistributionCommissionRules.java
Section titled “DistributionCommissionRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 29:分销多级分润的类目费率与身份系数 */public interface DistributionCommissionRules {
/** * 类目基础佣金率 */ double categoryRate(@Param("category") String category);
/** * 分销商等级系数 */ double levelMultiplier(@Param("level") String level);
/** * 直推奖励率(一级上线) */ double directReferRate(@Param("uplineLevel") String uplineLevel);
/** * 间推奖励率(二级上线) */ double indirectReferRate(@Param("rootLevel") String rootLevel);
/** * 活动订单可佣基数系数(让利折减) */ double baseFactor(@Param("promotionCostRate") int promotionCostRate);
/** * 团长服务费费率 */ double groupBonusRate(@Param("groupLeader") boolean groupLeader, @Param("groupCount") int groupCount);}distribution-commission.xml
Section titled “distribution-commission.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.DistributionCommissionRules">
<java name="categoryRate" returnClass="double"> <![CDATA[ double def categoryRate(str:category) { if (category == "BEAUTY") { return 0.20; } else if (category == "STANDARD") { return 0.10; } else if (category == "FRESH") { return 0.06; } else { return 0.03; } } ]]> </java>
<java name="levelMultiplier" returnClass="double"> <![CDATA[ double def levelMultiplier(str:level) { if (level == "PARTNER") { return 1.5; } else if (level == "L3") { return 1.3; } else if (level == "L2") { return 1.1; } else { return 1.0; } } ]]> </java>
<java name="directReferRate" returnClass="double"> <![CDATA[ double def directReferRate(str:uplineLevel) { if (uplineLevel == "PARTNER") { return 0.05; } else if (uplineLevel == "L3") { return 0.04; } else if (uplineLevel == "L2") { return 0.03; } else { return 0.0; } } ]]> </java>
<java name="indirectReferRate" returnClass="double"> <![CDATA[ double def indirectReferRate(str:rootLevel) { if (rootLevel == "PARTNER") { return 0.02; } else if (rootLevel == "L3") { return 0.015; } else { return 0.0; } } ]]> </java>
<java name="baseFactor" returnClass="double"> <![CDATA[ double def baseFactor(int:promotionCostRate) { if (promotionCostRate >= 50) { return 0.5; } else if (promotionCostRate >= 30) { return 0.7; } else if (promotionCostRate >= 10) { return 0.9; } else { return 1.0; } } ]]> </java>
<java name="groupBonusRate" returnClass="double"> <![CDATA[ double def groupBonusRate(boolean:groupLeader, int:groupCount) { if (groupLeader) { if (groupCount >= 20) { return 0.02; } else if (groupCount >= 10) { return 0.01; } else { return 0.0; } } else { return 0.0; } } ]]> </java>
</javas>DistributionCommissionService.java
Section titled “DistributionCommissionService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.DistributionCommissionRules;import com.github.paohaijiao.demo.model.DistributionCommissionDecision;import com.github.paohaijiao.demo.model.DistributionOrder;
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 场景 29:分销自购 + 直推 + 间推 + 团长服务费多级分润 */public class DistributionCommissionService {
private static final BigDecimal TOTAL_CAP_RATIO = new BigDecimal("0.30");
private final DistributionCommissionRules rules;
public DistributionCommissionService(DistributionCommissionRules rules) { this.rules = rules; }
public DistributionCommissionDecision settle(DistributionOrder order) { DistributionCommissionDecision d = new DistributionCommissionDecision(); d.setOrderId(order.getOrderId());
if (order.isRefunded()) { d.setBaseAmount(0.0); d.setSelfCommission(0.0); d.setDirectCommission(0.0); d.setIndirectCommission(0.0); d.setGroupBonus(0.0); d.setTotalCommission(0.0); d.setStatus("FORFEITED"); d.addBreakdown("订单已退款,各级分润全部不予结算"); return d; }
BigDecimal base = money(order.getOrderAmount() * rules.baseFactor(order.getPromotionCostRate())); BigDecimal self = money(base.doubleValue() * rules.categoryRate(order.getProductCategory()) * rules.levelMultiplier(order.getDistributorLevel())); BigDecimal direct = money(base.doubleValue() * rules.directReferRate(order.getUplineLevel())); BigDecimal indirect = money(base.doubleValue() * rules.indirectReferRate(order.getRootLevel())); BigDecimal group = money(base.doubleValue() * rules.groupBonusRate(order.isGroupLeader(), order.getGroupCount30d()));
BigDecimal total = self.add(direct).add(indirect).add(group); BigDecimal cap = money(base.multiply(TOTAL_CAP_RATIO).doubleValue()); boolean capped = total.compareTo(cap) > 0; if (capped) { total = cap; }
d.setBaseAmount(base.doubleValue()); d.setSelfCommission(self.doubleValue()); d.setDirectCommission(direct.doubleValue()); d.setIndirectCommission(indirect.doubleValue()); d.setGroupBonus(group.doubleValue()); d.setTotalCommission(total.doubleValue()); d.setCapped(capped);
d.addBreakdown("可佣基数: " + base.toPlainString() + " 元"); d.addBreakdown("自购佣金: " + self.toPlainString() + " 元"); d.addBreakdown("直推奖励: " + direct.toPlainString() + " 元"); d.addBreakdown("间推奖励: " + indirect.toPlainString() + " 元"); d.addBreakdown("团长服务费: " + group.toPlainString() + " 元"); if (capped) { d.addBreakdown("多级分润合计超过可佣基数 30%,已按上限 " + cap.toPlainString() + " 元熔断"); }
if (order.getOrderAmount() >= 50000) { d.setStatus("PENDING"); d.addBreakdown("单笔金额超 5 万元,转人工核验交易真实性后再行结算"); } else { d.setStatus("SETTLED"); } return d; }
private BigDecimal money(double v) { return BigDecimal.valueOf(v).setScale(2, RoundingMode.HALF_UP); }}DistributionCommissionServiceTest.java
Section titled “DistributionCommissionServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.DistributionCommissionDecision;import com.github.paohaijiao.demo.model.DistributionOrder;import com.github.paohaijiao.demo.service.DistributionCommissionService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 29:分销多级返佣与分润熔断 * <p> * 6 组形态:美妆高级分销团长触顶熔断、家电薄利两级分润、 * 活动让利基数折减、退款全额没收、大额批发人工审核、生鲜团长成团临界。 */public class DistributionCommissionServiceTest {
private DistributionCommissionService service;
@Before public void setUp() { DistributionCommissionRules rules = JQuickJava.create() .createApi(DistributionCommissionRules.class, "demo/distribution-commission.xml"); this.service = new DistributionCommissionService(rules); }
/** * 美妆 L3 + 合伙人直推 + L3 间推 + 月成团 25 团长 → 345 元触 300 元上限 */ @Test public void shouldCapTotalCommissionAtThirtyPercent() { DistributionOrder o = base("DIS001", 1000, "BEAUTY", "L3"); o.setUplineLevel("PARTNER"); o.setRootLevel("L3"); o.setGroupLeader(true); o.setGroupCount30d(25);
DistributionCommissionDecision d = service.settle(o);
Assert.assertEquals(1000.0, d.getBaseAmount(), 0.0001); Assert.assertEquals(260.0, d.getSelfCommission(), 0.0001); Assert.assertEquals(50.0, d.getDirectCommission(), 0.0001); Assert.assertEquals(15.0, d.getIndirectCommission(), 0.0001); Assert.assertEquals(20.0, d.getGroupBonus(), 0.0001); Assert.assertEquals(300.0, d.getTotalCommission(), 0.0001); Assert.assertTrue(d.isCapped()); Assert.assertEquals("SETTLED", d.getStatus()); }
/** * 家电低毛利 L1 分销 + L2 直推 → 60 + 60,无间推与团长奖 */ @Test public void shouldSettleApplianceThinMargin() { DistributionOrder o = base("DIS002", 2000, "APPLIANCE", "L1"); o.setUplineLevel("L2"); o.setRootLevel("L1");
DistributionCommissionDecision d = service.settle(o);
Assert.assertEquals(60.0, d.getSelfCommission(), 0.0001); Assert.assertEquals(60.0, d.getDirectCommission(), 0.0001); Assert.assertEquals(0.0, d.getIndirectCommission(), 0.0001); Assert.assertEquals(0.0, d.getGroupBonus(), 0.0001); Assert.assertEquals(120.0, d.getTotalCommission(), 0.0001); Assert.assertFalse(d.isCapped()); Assert.assertEquals("SETTLED", d.getStatus()); }
/** * 标品 L2 活动让利 35% → 基数 350,自购 38.5、直推 14、间推 7 */ @Test public void shouldDiscountCommissionBaseForPromotion() { DistributionOrder o = base("DIS003", 500, "STANDARD", "L2"); o.setUplineLevel("L3"); o.setRootLevel("PARTNER"); o.setPromotionCostRate(35);
DistributionCommissionDecision d = service.settle(o);
Assert.assertEquals(350.0, d.getBaseAmount(), 0.0001); Assert.assertEquals(38.5, d.getSelfCommission(), 0.0001); Assert.assertEquals(14.0, d.getDirectCommission(), 0.0001); Assert.assertEquals(7.0, d.getIndirectCommission(), 0.0001); Assert.assertEquals(59.5, d.getTotalCommission(), 0.0001); Assert.assertEquals("SETTLED", d.getStatus()); }
/** * 订单退款 → 所有分润没收 */ @Test public void shouldForfeitAllWhenRefunded() { DistributionOrder o = base("DIS004", 1000, "BEAUTY", "L3"); o.setUplineLevel("PARTNER"); o.setRefunded(true);
DistributionCommissionDecision d = service.settle(o);
Assert.assertEquals("FORFEITED", d.getStatus()); Assert.assertEquals(0.0, d.getTotalCommission(), 0.0001); Assert.assertEquals(0.0, d.getSelfCommission(), 0.0001); Assert.assertTrue(d.getBreakdown().contains("订单已退款,各级分润全部不予结算")); }
/** * 单笔 6 万元批发单 → 佣金预估照算但冻结待人工审核 */ @Test public void shouldHoldLargeWholesaleOrderForReview() { DistributionOrder o = base("DIS005", 60000, "STANDARD", "L1"); o.setUplineLevel("L1"); o.setRootLevel("L1");
DistributionCommissionDecision d = service.settle(o);
Assert.assertEquals("PENDING", d.getStatus()); Assert.assertEquals(60000.0, d.getBaseAmount(), 0.0001); Assert.assertEquals(6000.0, d.getSelfCommission(), 0.0001); Assert.assertEquals(6000.0, d.getTotalCommission(), 0.0001); Assert.assertTrue(d.getBreakdown().contains( "单笔金额超 5 万元,转人工核验交易真实性后再行结算")); }
/** * 生鲜 L1 团长:成团 10 单拿 1% 服务费(21 元),9 单无服务费(18 元) */ @Test public void shouldPayGroupBonusAtTenOrdersThreshold() { DistributionOrder o = base("DIS006", 300, "FRESH", "L1"); o.setUplineLevel("L1"); o.setRootLevel("L1"); o.setGroupLeader(true); o.setGroupCount30d(10);
DistributionCommissionDecision d1 = service.settle(o); Assert.assertEquals(18.0, d1.getSelfCommission(), 0.0001); Assert.assertEquals(3.0, d1.getGroupBonus(), 0.0001); Assert.assertEquals(21.0, d1.getTotalCommission(), 0.0001); Assert.assertEquals("SETTLED", d1.getStatus());
o.setGroupCount30d(9); DistributionCommissionDecision d2 = service.settle(o); Assert.assertEquals(0.0, d2.getGroupBonus(), 0.0001); Assert.assertEquals(18.0, d2.getTotalCommission(), 0.0001); }
private DistributionOrder base(String id, double amount, String category, String level) { DistributionOrder o = new DistributionOrder(); o.setOrderId(id); o.setSku("SKU200"); o.setOrderAmount(amount); o.setProductCategory(category); o.setDistributorLevel(level); o.setUplineLevel("L1"); o.setRootLevel("L1"); o.setGroupLeader(false); o.setGroupCount30d(0); o.setPromotionCostRate(0); o.setRefunded(false); return o; }}在工程根目录执行:
mvn test -Dtest=DistributionCommissionServiceTest测试覆盖形态(共 6 个 @Test 方法):
| 测试方法 | 业务形态 | 关键断言/期望决策 |
|---|---|---|
| shouldCapTotalCommissionAtThirtyPercent | 美妆 1000 元、L3 自购、PARTNER 直推、L3 间推、团长月成团 25 单 | 基数 1000;自购 260、直推 50、间推 15、团长 20(合计 345);total 被熔断为 300、capped=true、SETTLED |
| shouldSettleApplianceThinMargin | 家电 2000 元薄利、L1 自购、L2 直推、无间推无团长 | 自购 60、直推 60、间推 0、团长 0;total=120、capped=false、SETTLED |
| shouldDiscountCommissionBaseForPromotion | 标品 500 元、L2、让利 35%、L3 直推、PARTNER 间推 | 基数折减为 350;自购 38.5、直推 14、间推 7;total=59.5、SETTLED |
| shouldForfeitAllWhenRefunded | 美妆 L3 订单后续发生退款 | status=FORFEITED;total 与 self 均为 0;明细含「订单已退款,各级分润全部不予结算」 |
| shouldHoldLargeWholesaleOrderForReview | 标品 6 万元批发大单、L1 | status=PENDING;基数 60000、自购 6000、total=6000(佣金照算但冻结);含「单笔金额超 5 万元,转人工核验交易真实性后再行结算」 |
| shouldPayGroupBonusAtTenOrdersThreshold | 生鲜 300 元 L1 团长:月成团 10 单 vs 改为 9 单 | 10 单时自购 18、团长 3、total=21;9 单时团长费 0、total=18,验证成团门槛临界 |
- 五类费率一方法一口径、外置热更新:类目费率、等级系数、直推率、间推率、团长率各占一个规则方法,招商改费率只改 XML,结算主程序不发版。
- 基数 × 多系数交叉相乘:自购佣金以「让利后基数 × 类目费率 × 等级系数」串联,直推/间推/团长各以基数乘以独立费率,多级关系通过方法组合表达而非嵌套分支。
- Java 侧财务级约束:分项金额统一
BigDecimalHALF_UP 保留两位后再加总;合计超可佣基数 30% 即截断到 cap 并写熔断明细,防止多级叠加分穿商家毛利。 - 硬短路分层:退款最前置,全部归零 FORFEITED;5 万元大额单佣金预估照算但置 PENDING 冻结人工审核,风控与结算解耦且金额可见。
- 身份按字符串枚举传参:
@Param按名绑定 L1/L2/L3/PARTNER,XML 以==匹配身份档,新增等级只需在脚本加一档。 - ASM 代理、Java 8 无重型依赖:规则接口动态代理实现并可复用,无独立结算规则服务,可直接嵌入 JDK 8 老分销系统与信创环境。