## mall4j微服务商城的资金流动 mall4j商城除了用户下单以外,最重要的东西便是结算。下面我们从几个方面来熟悉资金的流动返现,如果资金没算好会造成十分严重的后果。 > 平台会根据类目从用户的实际支付金额中抽取一部分佣金的。扣完佣金后剩余部分会加上平台优惠补贴的金额,全部给到了商家账户里的。 ### 如何进行测试 要想知道你的结算金额是否正确,最基本的需要通过一个测试: ``` 1. 先购买3件商品 2. 发货 3. 退一件,商家允许退款 4. 确认收货 5. 再退一件,商家允许退款 6. 修改订单确认收货、下单、支付、退款申请的时间为一个月前。 7. 调用强制取消未退款订单的定时任务。 8. 调用结算的定时任务,结算给商家。 ``` 如果在这几个流程,`用户支付金额 + 平台优惠金额 - 平台佣金 = 商家收入 ` 可以判断为基本正确,但会有很多特殊情况发生,所以要完整判断,还需要根据业务具体情况来确定 ### 如何确定收支平衡 当商家上架商品的时候,一般会有一个销售价,这个销售价便是商家希望能够通过商品的销售可以获取的收益。 1. 如果商品有一般的商家性的营销活动如:满减满折、优惠券之类的,商家可以获取的金额,实际上应该是用户支付的金额。即`商家收入 = 用户支付金额 - 平台佣金` 3. 如果涉及到平台性活动:平台优惠券、会员全平台折扣、积分等。因为这个是平台发放的活动,强制所有商家参与的时候,这部分优惠的金额要商家进行补贴,即`商家收入 = 用户支付金额 + 平台补贴支出金额 - 平台佣金` 4. 如果涉及退款 `商家收入 = 用户支付金额 - 退款成功金额 - 未退款项平台佣金` 综合上面三种情况得出:`用户支付金额 + 平台支出金额 - 平台佣金 = 商家收入 + 用户退款收入` 当有更多的营销活动,更多的收入支出方,只要知道这个平衡公式即可:`支出a + 支出b +支出c + ... = 收入a + 收入b +收入c + ... ` 在上面的情况下,可以看出平台只收取了平台佣金,需要注意一点可能因为平台活动会亏损,需要妥善安排平台的活动。 ### 支付 > 进行支付时,商家、平台获取待结算收入 简单的可以得出这个公式: 我们来看看这段代码在哪: 1. 首先我们来看,默认接收支付成功消息进行处理的方法`com.mall4j.cloud.multishop.listener.OrderNotifyShopConsumer` 这里做了一个操作`商家收入 = 用户支付金额 + 平台补贴支出金额 - 分销金额(暂无) - 平台佣金`待结算金额: ``` // 1. 商家应收 = 商品价格 - 商家优惠 - 分销金额(暂无) // 2. 商家应收 = 用户支付 + 平台补贴 - 分销金额(暂无) // 商家实际待结算金额 = 用户支付 + 平台补贴 - 订单分销佣金 - 平台佣金 Long changeAmount = orderSimpleAmountInfo.getActualTotal() + orderSimpleAmountInfo.getPlatformAmount() - orderSimpleAmountInfo.getDistributionAmount() - orderSimpleAmountInfo.getPlatformCommission(); // 商家添加未结算金额(因为添加了唯一索引,所以是不用怕多次加钱) shopWalletMapper.addUnsettledAmount(orderSimpleAmountInfo.getShopId(), changeAmount); ``` 2. 然后这里还有平台未结算金额的计算 平加未结算金额 = 平台佣金 - 平台优惠分摊优惠金额 : ```java 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)` ```java // 商家改变金额 = 原商家应收(下单时商家应收) - 现在商家应收 - 平台佣金应退 // = (用户支付 + 平台补贴) - 分销金额 - (实付金额 + 平台补贴) - 平台佣金 *(退款金额 / 实付金额) // = (用户支付 + 平台补贴)*(退款金额 / 实付金额) - 分销金额 // = (用户支付 + 平台补贴 - 平台佣金) * (退款金额 / 实付金额) - 分销金额 // = 退款金额 + (平台补贴 - 平台佣金)* (退款金额 / 实付金额) - 分销金额 // = 退款金额 + 平台补贴改变量 - 平台佣金改变量 - 分销金额 // 平台补贴改变量 = 平台补贴金额 * 退款金额 / 商品实际金额 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`: ```java /** * 最大确认收货退款时间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)` ```java // 计算该订单的分销金额 newOrderRefund.setDistributionTotalAmount(orderService.sumTotalDistributionAmountByOrderItem(orderItemList)); // 计算平台退款金额(退款时将这部分钱退回给平台,所以商家要扣除从平台这里获取的金额) newOrderRefund.setPlatformRefundAmount(order.getPlatformAmount()); ``` #### 8. 退款导致订单结算 由于我们系统支持部分金额退款,所以当100元的商品,用户选择退90元的时候,要结算10元给商家。我们并不是每退一次就结算一次,而是当订单里面的订单项都退完的时候,该订单会被取消,此时结算给商家。而这里的结算,用的是在接收到退款成功通知之后进行的。发送部分退款完成的。 可以参考`com.yami.shop.service.impl.OrderRefundServiceImpl#verifyRefund(OrderRefundDto,String)` ```java // 订单在已支付,已发货,确认收货的情况下,退款导致订单关闭,需要判断下是否要进行结算给商家的操作,这个结算的计算是退款完成后结算的,所以不要随便改变顺序 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(); ``` ### 提现的流程 1. 当商家选择进行提现申请的时候,会冻结申请中的金额。 2. 平台管理员线下银行卡转账。 3. 平台上提交转账信息,完成。 ### 退款说明 1. 由于退款时,必须订单有支付钱或者积分才能走流程,所以如果整个订单如果有一个订单项积分并且钱为0,就只能进行整单退款。