跳转到内容

案例 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
规则方法 口径 分值/输出
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

将下列文件按原始包名放入 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 即可运行。

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 分销订单与各级分销商身份要素
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 多级分润决策
*/
@Data
public 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);
}
}
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);
}
<?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>
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);
}
}
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;
}
}

在工程根目录执行:

Terminal window
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 侧财务级约束:分项金额统一 BigDecimal HALF_UP 保留两位后再加总;合计超可佣基数 30% 即截断到 cap 并写熔断明细,防止多级叠加分穿商家毛利。
  • 硬短路分层:退款最前置,全部归零 FORFEITED;5 万元大额单佣金预估照算但置 PENDING 冻结人工审核,风控与结算解耦且金额可见。
  • 身份按字符串枚举传参:@Param 按名绑定 L1/L2/L3/PARTNER,XML 以 == 匹配身份档,新增等级只需在脚本加一档。
  • ASM 代理、Java 8 无重型依赖:规则接口动态代理实现并可复用,无独立结算规则服务,可直接嵌入 JDK 8 老分销系统与信创环境。