14 KiB
mall4j微服务商城的资金流动
mall4j商城除了用户下单以外,最重要的东西便是结算。下面我们从几个方面来熟悉资金的流动返现,如果资金没算好会造成十分严重的后果。
平台会根据类目从用户的实际支付金额中抽取一部分佣金的。扣完佣金后剩余部分会加上平台优惠补贴的金额,全部给到了商家账户里的。
如何进行测试
要想知道你的结算金额是否正确,最基本的需要通过一个测试:
1. 先购买3件商品
2. 发货
3. 退一件,商家允许退款
4. 确认收货
5. 再退一件,商家允许退款
6. 修改订单确认收货、下单、支付、退款申请的时间为一个月前。
7. 调用强制取消未退款订单的定时任务。
8. 调用结算的定时任务,结算给商家。
如果在这几个流程,用户支付金额 + 平台优惠金额 - 平台佣金 = 商家收入
可以判断为基本正确,但会有很多特殊情况发生,所以要完整判断,还需要根据业务具体情况来确定
如何确定收支平衡
当商家上架商品的时候,一般会有一个销售价,这个销售价便是商家希望能够通过商品的销售可以获取的收益。
- 如果商品有一般的商家性的营销活动如:满减满折、优惠券之类的,商家可以获取的金额,实际上应该是用户支付的金额。即
商家收入 = 用户支付金额 - 平台佣金
- 如果涉及到平台性活动:平台优惠券、会员全平台折扣、积分等。因为这个是平台发放的活动,强制所有商家参与的时候,这部分优惠的金额要商家进行补贴,即
商家收入 = 用户支付金额 + 平台补贴支出金额 - 平台佣金
- 如果涉及退款
商家收入 = 用户支付金额 - 退款成功金额 - 未退款项平台佣金
综合上面三种情况得出:用户支付金额 + 平台支出金额 - 平台佣金 = 商家收入 + 用户退款收入
当有更多的营销活动,更多的收入支出方,只要知道这个平衡公式即可:支出a + 支出b +支出c + ... = 收入a + 收入b +收入c + ...
在上面的情况下,可以看出平台只收取了平台佣金,需要注意一点可能因为平台活动会亏损,需要妥善安排平台的活动。
支付
进行支付时,商家、平台获取待结算收入
简单的可以得出这个公式:
我们来看看这段代码在哪:
-
首先我们来看,默认接收支付成功消息进行处理的方法
com.mall4j.cloud.multishop.listener.OrderNotifyShopConsumer
这里做了一个操作
商家收入 = 用户支付金额 + 平台补贴支出金额 - 分销金额(暂无) - 平台佣金
待结算金额:
// 1. 商家应收 = 商品价格 - 商家优惠 - 分销金额(暂无)
// 2. 商家应收 = 用户支付 + 平台补贴 - 分销金额(暂无)
// 商家实际待结算金额 = 用户支付 + 平台补贴 - 订单分销佣金 - 平台佣金
Long changeAmount = orderSimpleAmountInfo.getActualTotal() + orderSimpleAmountInfo.getPlatformAmount() - orderSimpleAmountInfo.getDistributionAmount() - orderSimpleAmountInfo.getPlatformCommission();
// 商家添加未结算金额(因为添加了唯一索引,所以是不用怕多次加钱)
shopWalletMapper.addUnsettledAmount(orderSimpleAmountInfo.getShopId(), changeAmount);
- 然后这里还有平台未结算金额的计算
平加未结算金额 = 平台佣金 - 平台优惠分摊优惠金额 :
long platformChangeAmount = orderSimpleAmountInfo.getPlatformCommission() - orderSimpleAmountInfo.getPlatformAmount();
结算
注意:所有商品进行退款之后,也会进行结算的操作,可以看看退款这部分内容
1.结算
要确认收货15天之后进行结算,商家和平台的未结算金额转成结算金额。
当商品有平台性质的活动时,此时,有部分金额是平台进行补贴的参考上文中如何确定收支平衡 的这一部分得出,由于我们上一步已经计算得到商家未结算金额了,所以结算时只需要减去退款成功金额,结算金额 = 未结算金额 - 退款成功金额
我们来看看这段代码在哪:com.mall4j.cloud.multishop.listener.OrderNotifyShopConsumer
// 1. 商家结算金额 = 商家未结算金额 - 退款成功金额
// 2. 平台结算金额 = 平台未结算金额 - 退款成功金额
Long changeAmount = shopPayLog.getChangeAmount();
changeAmount = changeAmount - shopRefundLog.getChangeAmount();
至于为啥,还是看看退款这个文档。
退款
退款与结算息息相关,而且是整个系统最复杂的地方
刚才在确认收货的时候有提及退款的功能,退款的逻辑稍微复杂一点点,首先要根据用户的退款时间来进行判断。首先我们确定的是,用户能退款的金额上线,是用户支付的金额。对于退款来说,用户的金额即所支付的金额在商家待结算金额。
也就是说当商家同意退款的时候,需要从商家未结算金额里减去对应的金额。
这里的退款有一点需要值得注意:
2. 部分退款
因为我们的退款是支持部分退款的。如果用户将分商品进行了部分退款,那么退款的钱从平台出多少,商家出多少呢?
这么一想问题又会复杂化,我们从前面已经规定了,平台根据商品类目收取佣金,所以在部分退款后,平台补贴和平台的佣金都要进行改变,此时商家和平台的未结算金额改变可以参考com.mall4j.cloud.multishop.listener.OrderRefundShopConsumer#subShopSettlementAmount(OrderRefundDto)
// 商家改变金额 = 原商家应收(下单时商家应收) - 现在商家应收 - 平台佣金应退
// = (用户支付 + 平台补贴) - 分销金额 - (实付金额 + 平台补贴) - 平台佣金 *(退款金额 / 实付金额)
// = (用户支付 + 平台补贴)*(退款金额 / 实付金额) - 分销金额
// = (用户支付 + 平台补贴 - 平台佣金) * (退款金额 / 实付金额) - 分销金额
// = 退款金额 + (平台补贴 - 平台佣金)* (退款金额 / 实付金额) - 分销金额
// = 退款金额 + 平台补贴改变量 - 平台佣金改变量 - 分销金额
// 平台补贴改变量 = 平台补贴金额 * 退款金额 / 商品实际金额
Long changePlatformAmount = PriceUtil.divideByBankerRounding(orderChangeShopWalletAmountBO.getPlatformAllowanceAmount() * orderChangeShopWalletAmountBO.getRefundAmount(), orderChangeShopWalletAmountBO.getActualTotal());
// 平台佣金改变量 = 平台佣金 * 退款金额 / 商品实际金额
Long changePlatformCommission = PriceUtil.divideByBankerRounding(orderChangeShopWalletAmountBO.getPlatformCommission() * orderChangeShopWalletAmountBO.getRefundAmount(), orderChangeShopWalletAmountBO.getActualTotal());
// 商家改变金额 = 退款金额 + 平台补贴金额改变量 - 平台佣金 - 分销金额(暂无)
Long shopRealRefundAmount = orderChangeShopWalletAmountBO.getRefundAmount() + changePlatformAmount - changePlatformCommission - orderChangeShopWalletAmountBO.getDistributionAmount();
// 用户申请的退款金额
shopWalletLog.setUserAmount(orderChangeShopWalletAmountBO.getRefundAmount());
// 回退的平台补贴
shopWalletLog.setPlatformAmount(changePlatformAmount);
// 回退的平台佣金
shopWalletLog.setPlatformCommission(changePlatformCommission);
// 商家改变金额
shopWalletLog.setChangeAmount(shopRealRefundAmount);
...
// 平台实际上的改变金额 = 当前退款改变的平台佣金 - 当前退款退款改变的平台补贴金额
// 订单退款,补贴肯定是减少了,但是对于平台来说是赚钱了
// 订单退款了,平台的佣金也是减少了,所以对于平台来说是亏钱的。
long platformRealRefundAmount = shopWalletLog.getPlatformAmount() - shopWalletLog.getPlatformCommission();
5. 强制取消申请超时的退款订单
根据 部分退款 这部分内容,我们可以得出,结算给商家,需要在退款流程结束之后。如果退款一直不处理的话,是不是商家就一直不用结算呢?系统当然不能这样,所以我们规定了申请退款的最大时限,同时规定了几个时间:1)最大确认收货退款时间7天 2)退款最长申请时间,当申请时间过了这个时间段之后,会取消退款申请
与取消退款申请超时的订单相关的定时任务:com.mall4j.cloud.order.task.OrderTask.OrderRefundTask#cancelWhenTimeOut()
与退款有关的几个常量com.mall4j.cloud.common.constant.Constant
:
/**
* 最大确认收货退款时间7天
*/
public static final int MAX_FINALLY_REFUND_TIME = 7;
/**
* 退款最长申请时间,当申请时间过了这个时间段之后,会取消退款申请
*/
public static final int MAX_REFUND_APPLY_TIME = 7;
/**
* 离即将退款超时x小时时提醒
*/
public static final int MAX_REFUND_HOUR = 12;
6. 参与商家活动时,订单项分摊优惠金额
当商品A(90元)和商品B(10元)进行购买的时候,两者相加为100元,此时有一个满100减10的店铺活动。用户支付了90元买走了100元的商品。然后进行B商品的退款,请问,这个用户可以申请退10元吗?
答:很明显是不能退10元的,因为如果B商品可以退10元,A商品就可以退90元,那么他如果只选择退A商品,岂不是能白白获得B商品?难道有优惠活动就不允许退款吗?这明显是不合理的,毕竟常见的优惠活动,别的平台都能退款,凭啥你不能退款?所以我们要引入一个概念:分摊金额
分摊金额,简单的解释就是:当A和B同时有优惠的时候,那么这个优惠就会按照比例,分摊到每个订单项(不是商品,是订单项)上即订单项A分摊优惠金额 = 优惠金额 * (订单项A/ 参与该活动的订单项目金额之和)
根据等式可得:
商品A分摊金额 = 优惠金额10元 * (商品A金额90元/ (商品A金额90元 + 商品B金额10元)) = 9元
商品B分摊金额 = 优惠金额10元 * (商品B金额10元/ (商品B金额90元 + 商品B金额10元)) = 1元
此时该订单项可退款金额 = 商品金额 - 商品分摊金额
根据等式可以知道,用户不能退10元,只能退 商品B金额10元 - 商品B分摊金额1元 = 9元
7. 参与平台活动时,订单项分摊优惠金额
当用户参与平台活动,如满100减10,因为这个是平台发放的活动,强制所有商家参与的时候,这部分优惠的金额要商家进行补贴。
当商品A(90元)和商品B(10元)进行购买的时候,两者相加为100元,用户只要支付90元,商家收到100元。平台亏10元。
用户进行B商品的退款,请问,这个用户可以申请退10元吗?
不可以,因为按照上面的 参与商家活动时,订单项分摊优惠金额 来说用户只为这件商品付款了9元所以只能退9元。但是当用户选择退9元的时候,商家要从他的钱包当中扣减10元。因为其中1元是平台支出的,用户选择了退款,应该归还这1元给平台。
注意:
我们是有协商部分退款的说法的,也就是当用户选择部分退款的时候,这部分应该刚按照比例退回给平台,并扣除商家的这部分金额。
可以参考com.mall4j.cloud.order.controller.app.OrderRefundController#apply(OrderRefundDto)
// 计算该订单的分销金额
newOrderRefund.setDistributionTotalAmount(orderService.sumTotalDistributionAmountByOrderItem(orderItemList));
// 计算平台退款金额(退款时将这部分钱退回给平台,所以商家要扣除从平台这里获取的金额)
newOrderRefund.setPlatformRefundAmount(order.getPlatformAmount());
8. 退款导致订单结算
由于我们系统支持部分金额退款,所以当100元的商品,用户选择退90元的时候,要结算10元给商家。我们并不是每退一次就结算一次,而是当订单里面的订单项都退完的时候,该订单会被取消,此时结算给商家。而这里的结算,用的是在接收到退款成功通知之后进行的。发送部分退款完成的。
可以参考com.yami.shop.service.impl.OrderRefundServiceImpl#verifyRefund(OrderRefundDto,String)
// 订单在已支付,已发货,确认收货的情况下,退款导致订单关闭,需要判断下是否要进行结算给商家的操作,这个结算的计算是退款完成后结算的,所以不要随便改变顺序
boolean canRefundFlag =
Objects.equals(dbOrder.getStatus(),OrderStatus.PAYED.value())
|| Objects.equals(dbOrder.getStatus(), OrderStatus.CONSIGNMENT.value())
|| Objects.equals(dbOrder.getStatus(), OrderStatus.SUCCESS.value());
if (canRefundFlag && Objects.equals(order.getStatus(),OrderStatus.CLOSE.value())) {
// 通知还原优惠券
refundCoupon(dbOrder, order);
handlePartialRefund(dbOrder);
}
...
// 2.发送消息给商家,将需要结算的钱进行结算
// 减少商家待结算金额,增加已结算金额
SendStatus sendStatus = orderRefundSuccessSettlementTemplate.syncSend(RocketMqConstant.ORDER_REFUND_SUCCESS_SETTLEMENT_TOPIC, new GenericMessage<>(orderChangeShopWalletAmountBO)).getSendStatus();
提现的流程
- 当商家选择进行提现申请的时候,会冻结申请中的金额。
- 平台管理员线下银行卡转账。
- 平台上提交转账信息,完成。
退款说明
- 由于退款时,必须订单有支付钱或者积分才能走流程,所以如果整个订单如果有一个订单项积分并且钱为0,就只能进行整单退款。