多商户平台接入分销体系后,资金分账的复杂度会指数级上升。
原来只有平台和商家两方分账时,逻辑还相对简单:订单金额按佣金比例拆成平台收入和商家货款。加上分销员之后,就变成了三级分润——平台佣金、商家货款、分销员佣金,每一笔订单都要同时拆给三方。更麻烦的是退款场景:用户退款时,已经拆分出去的资金还要按原路回退,差一分钱都会形成资金窟窿。
最近在基于 LikeShop 多商户 Java 版做汇付天下多级分账的落地,把完整的设计思路、接口对接、退款回退和踩坑经验整理出来,供做同类系统的同学参考。
一、为什么必须走通道分账,而不是平台自己算
在动手做之前,先把合规前提讲清楚。
很多团队第一反应是:不就是按比例算三个数吗,订单支付成功后我自己算好,分别转账给商家和分销员不就行了?
不行。这就是常说的二清——平台没有支付业务许可证,却归集了用户资金,再二次分配给商家和个人。这在监管层面属于无证从事资金清算业务,是多商户和分销模式最核心的合规风险。
合规的做法是:所有拆分动作在持牌支付通道层面完成。用户付款的瞬间,资金按预设比例直接路由到平台、商家、分销员各自的账户,平台不沉淀、不挪用、不二次划转。系统里的分账记录只是业务台账,用来对账和核算,真实资金链路在支付通道内闭环。
这也是我们选择汇付分账产品的根本原因——它支持多接收方分账,能一次性拆给平台、商家、分销员三方,资金链路全程在持牌体系内完成。
二、三级分润模型与整体架构
2.1 分润的三个角色
| 角色 | 资金性质 | 账户类型 |
|---|---|---|
| 平台方 | 类目佣金 | 平台主商户号 |
| 入驻商家 | 商品货款(扣除佣金后) | 商家子商户号 |
| 分销员 | 推广佣金 | 个人/商户账户 |
2.2 比例配置层级
比例不是全平台统一的,而是分层配置,越具体优先级越高:
- 平台类目佣金:每个经营类目设置平台抽成比例(如服饰 3%、食品 5%);
- 商家默认分销比例:商家维度设置一级/二级分销佣金比例;
- 商品单独分销比例:特定商品可单独设置分销佣金,覆盖商家默认值。
2.3 整体资金链路
用户支付订单金额
↓
汇付托管账户(支付通道内)
↓ 支付成功同时实时分账
├── 平台佣金 → 平台主商户号
├── 商家货款 → 商家子商户号
└── 分销员佣金 → 分销员对应账户
系统侧的流程则是:下单 → 计算三级分账明细 → 调用汇付下单接口并传入分账串 → 支付成功回调 → 更新本地分账快照 → 后续结算/对账。
三、分账快照:下单时刻锁定比例
分账最容易出的问题,就是比例变动导致历史订单分账错误。比如平台中途调整了类目佣金,或者商家改了分销比例,如果结算时再去查当前比例,历史订单就全算错了。
正确的做法是下单支付那一刻,生成分账快照并永久保存,后续任何比例调整都不影响历史订单。
3.1 数据库表设计
-- 订单分账快照表
CREATE TABLE ls_order_split (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL COMMENT '订单ID',
pay_order_no VARCHAR(32) NOT NULL COMMENT '支付单号',
merchant_id BIGINT NOT NULL,
distributor_id BIGINT DEFAULT NULL COMMENT '一级分销员ID',
second_distributor_id BIGINT DEFAULT NULL COMMENT '二级分销员ID',
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
platform_rate DECIMAL(5,2) NOT NULL COMMENT '平台佣金比例',
platform_amount DECIMAL(10,2) NOT NULL COMMENT '平台佣金金额',
merchant_rate DECIMAL(5,2) NOT NULL COMMENT '商家货款比例',
merchant_amount DECIMAL(10,2) NOT NULL COMMENT '商家货款金额',
dist1_rate DECIMAL(5,2) DEFAULT NULL COMMENT '一级分销比例',
dist1_amount DECIMAL(10,2) DEFAULT NULL COMMENT '一级分销佣金',
dist2_rate DECIMAL(5,2) DEFAULT NULL COMMENT '二级分销比例',
dist2_amount DECIMAL(10,2) DEFAULT NULL COMMENT '二级分销佣金',
split_status TINYINT NOT NULL COMMENT '分账状态:1待分账 2已分账 3已部分退回 4已全部退回',
split_time DATETIME COMMENT '分账完成时间',
create_time DATETIME NOT NULL,
INDEX idx_order_id(order_id),
INDEX idx_merchant_id(merchant_id)
);
3.2 分账计算逻辑
@Service
public class SplitCalcService {
/**
* 计算订单三级分账明细
*/
public SplitResult calculate(Long orderId, BigDecimal totalAmount,
Long merchantId, Long categoryId,
Long distributorId, Long goodsId) {
SplitResult result = new SplitResult();
result.setTotalAmount(totalAmount);
// 1. 平台佣金(类目优先,商家默认兜底)
BigDecimal platformRate = getPlatformCommissionRate(merchantId, categoryId);
BigDecimal platformAmount = totalAmount.multiply(platformRate)
.setScale(2, RoundingMode.HALF_UP);
result.setPlatformRate(platformRate);
result.setPlatformAmount(platformAmount);
// 2. 商家货款 = 总额 - 平台佣金
BigDecimal merchantAmount = totalAmount.subtract(platformAmount);
result.setMerchantAmount(merchantAmount);
// 3. 分销佣金(商品级优先,商家默认兜底)
if (distributorId != null) {
BigDecimal[] distRates = getDistributorRate(merchantId, goodsId);
BigDecimal dist1Rate = distRates[0];
BigDecimal dist2Rate = distRates[1];
// 分销佣金从商家货款里出,不是额外加
BigDecimal dist1Amount = merchantAmount.multiply(dist1Rate)
.setScale(2, RoundingMode.HALF_UP);
result.setDist1Rate(dist1Rate);
result.setDist1Amount(dist1Amount);
if (dist2Rate != null && dist2Rate.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal dist2Amount = merchantAmount.multiply(dist2Rate)
.setScale(2, RoundingMode.HALF_UP);
result.setDist2Rate(dist2Rate);
result.setDist2Amount(dist2Amount);
}
// 商家实际所得 = 货款 - 分销佣金
merchantAmount = merchantAmount.subtract(dist1Amount);
if (result.getDist2Amount() != null) {
merchantAmount = merchantAmount.subtract(result.getDist2Amount());
}
result.setMerchantAmount(merchantAmount);
}
return result;
}
}
这里有个容易踩坑的点:分销佣金是从商家货款里扣,不是在总额外额外增加。否则平台收入 + 商家货款 + 分销佣金会大于订单总额,资金对不上。
四、汇付分账接口对接
汇付的分账能力是通过下单接口里的 acct_infos(分账明细)字段传递的,支付成功后通道自动完成拆分。
4.1 分账明细构造
@Service
public class HuifuSplitService {
/**
* 构造汇付分账明细数组
*/
public JSONArray buildAcctInfos(SplitResult splitResult,
String platformHuifuId,
String merchantHuifuId,
String dist1HuifuId,
String dist2HuifuId) {
JSONArray acctInfos = new JSONArray();
// 1. 平台佣金
JSONObject platform = new JSONObject();
platform.put("huifu_id", platformHuifuId);
platform.put("div_amt", splitResult.getPlatformAmount().toString());
acctInfos.add(platform);
// 2. 商家货款
JSONObject merchant = new JSONObject();
merchant.put("huifu_id", merchantHuifuId);
merchant.put("div_amt", splitResult.getMerchantAmount().toString());
acctInfos.add(merchant);
// 3. 一级分销员
if (splitResult.getDist1Amount() != null
&& splitResult.getDist1Amount().compareTo(BigDecimal.ZERO) > 0) {
JSONObject dist1 = new JSONObject();
dist1.put("huifu_id", dist1HuifuId);
dist1.put("div_amt", splitResult.getDist1Amount().toString());
acctInfos.add(dist1);
}
// 4. 二级分销员
if (splitResult.getDist2Amount() != null
&& splitResult.getDist2Amount().compareTo(BigDecimal.ZERO) > 0) {
JSONObject dist2 = new JSONObject();
dist2.put("huifu_id", dist2HuifuId);
dist2.put("div_amt", splitResult.getDist2Amount().toString());
acctInfos.add(dist2);
}
return acctInfos;
}
}
4.2 下单接口传入分账串
在调用汇付统一下单接口时,把分账明细作为参数传入:
public String createPayOrder(Order order, SplitResult splitResult) {
// ... 基础参数组装
// 分账明细
JSONArray acctInfos = buildAcctInfos(splitResult,
config.getPlatformHuifuId(),
merchant.getHuifuId(),
distributor1.getHuifuId(),
distributor2.getHuifuId());
params.put("acct_infos", acctInfos.toJSONString());
// 延迟分账标记:如果需要先冻结再确认分账,传 Y
// params.put("delay_acct_flag", "Y");
// 调用汇付下单接口
return huifuClient.execute("v2/pay/jsapi", params);
}
4.3 支付回调更新分账状态
支付成功异步回调后,更新本地分账快照:
public void handlePayNotify(Map<String, String> notify) {
String payOrderNo = notify.get("order_id");
OrderSplit split = orderSplitMapper.getByPayOrderNo(payOrderNo);
if (split != null && split.getSplitStatus() == SplitStatus.PENDING) {
split.setSplitStatus(SplitStatus.SUCCESS);
split.setSplitTime(new Date());
orderSplitMapper.updateById(split);
}
}
五、退款与分账回退:最容易踩坑的环节
退款是多级分账里最容易出资金事故的地方。
普通退款只需要给用户退钱就行,但分账订单不一样:钱已经分给平台、商家、分销员三方了,退款时必须按原路从三方账户退回来,再统一退给用户。只退用户、不回分账,最后就会形成平台自己垫钱的窟窿。
5.1 全额退款回退
@Transactional(rollbackFor = Exception.class)
public void fullRefund(Long orderId, String reason) {
OrderSplit split = orderSplitMapper.getByOrderId(orderId);
if (split.getSplitStatus() != SplitStatus.SUCCESS) {
throw new BizException("分账未完成,无法退款");
}
// 1. 调用汇付分账回退接口,按原比例退回
Map<String, Object> params = new HashMap<>();
params.put("org_order_no", split.getPayOrderNo());
params.put("refund_amount", split.getTotalAmount().toString());
params.put("reason", reason);
// 回退分账资金
huifuClient.execute("v2/split/return", params);
// 2. 调用退款接口,给用户退款
huifuClient.execute("v2/pay/refund", params);
// 3. 更新本地分账状态
split.setSplitStatus(SplitStatus.RETURNED);
orderSplitMapper.updateById(split);
// 4. 记录退款日志
refundLogService.addRecord(orderId, split.getTotalAmount(), reason);
}
5.2 部分退款回退
部分退款更复杂,要按退款金额占原订单的比例,等比例回退三方分账资金:
public void partialRefund(Long orderId, BigDecimal refundAmount, String reason) {
OrderSplit split = orderSplitMapper.getByOrderId(orderId);
BigDecimal ratio = refundAmount.divide(
split.getTotalAmount(), 4, RoundingMode.HALF_UP);
// 按比例计算各方回退金额
BigDecimal platformReturn = split.getPlatformAmount().multiply(ratio)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal merchantReturn = split.getMerchantAmount().multiply(ratio)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal dist1Return = split.getDist1Amount() != null
? split.getDist1Amount().multiply(ratio)
.setScale(2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
// ...
// 构造回退分账明细,调用回退接口
// ...
}
5.3 手续费的回退
分账是有手续费的,退款时手续费也要按比例退回。汇付的规则是:反向退回手续费 =(退款金额 ÷ 原分账金额)× 原正向手续费。这部分如果忽略,长期下来会有小额资金差异,对账对不上。
六、自动对账
多级分账后,对账的复杂度也大大提升。不能只对总额,必须订单级逐笔核对。
每日凌晨跑定时任务:
- 下载汇付前一日的交易对账文件、分账对账文件、退款对账文件;
- 逐笔和本地
ls_order_split表核对:订单号、总金额、平台分账、商家分账、分销分账; - 差异订单写入差异表,自动告警;
- 生成日对账报表,财务可下载。
public void dailyReconcile() {
// 1. 下载通道分账明细
List<ChannelSplitRecord> channelList = huifuClient.downloadSplitDetail(
DateUtil.yesterday());
int match = 0;
int diff = 0;
for (ChannelSplitRecord channel : channelList) {
OrderSplit local = orderSplitMapper.getByPayOrderNo(channel.getOrderNo());
if (local == null) {
diff++;
reconcileLogService.addDiff(channel, "本地缺失分账记录");
continue;
}
// 逐字段核对
if (channel.getPlatformAmt().compareTo(local.getPlatformAmount()) != 0
|| channel.getMerchantAmt().compareTo(local.getMerchantAmount()) != 0) {
diff++;
reconcileLogService.addDiff(channel, "分账金额不一致");
continue;
}
match++;
}
// 告警
if (diff > 0) {
alertService.send("分账对账差异提醒", "当日差异:" + diff + " 笔");
}
}
七、常见坑与最佳实践
- 分账比例不做快照
坑:结算时再查当前比例,中途调佣导致历史订单错误。
最佳实践:支付时刻生成分账快照,所有结算对账以快照为准。 - 退款只退用户,不回分账
坑:用户钱退了,商家和分销员的分账没退,平台垫钱。
最佳实践:退款必须走分账回退接口,按比例原路退回三方资金。 - 分销佣金从总额里扣
坑:分销佣金在订单总额外额外算,导致分账总和大于订单金额。
最佳实践:分销佣金从商家货款里扣除,三方相加等于订单总额。 - 手续费承担方不明确
坑:分账手续费默认内扣(从接收方金额里扣),商家实际到账和预期不符,引起投诉。
最佳实践:提前明确手续费承担方,在商家后台公示。 - 没有幂等控制
坑:退款、分账回退接口重试,导致重复退回。
最佳实践:所有资金操作加幂等校验,按订单号+退款单号去重。 - 分销员账户合规问题
坑:分销员是个人,佣金直接打个人卡,涉及个税和结算合规。
最佳实践:分销员佣金走合法的劳务报酬结算路径,或通过具备资质的服务商代发。