## b2b2c商城的资金流动 b2b2c商城除了用户下单以外,最重要的东西便是结算。下面我们从几个方面来熟悉资金的流动返现,如果资金没算好会造成十分严重的后果。 > 平台是不从用户的支付金额中多抽取任何一分钱的。从用户的支付金额全部给到了商家和分销等活动中 ### 如何进行测试 要想知道你的结算金额是否正确,最基本的需要通过一个测试: ``` 1. 先购买3件商品 2. 发货 3. 退一件,商家允许退款 4. 确认收货(结算) 5. 再退一件,商家允许退款 6. 修改订单确认收货、下单、支付、退款申请的时间为一个月前。 7. 调用强制取消未退款订单的定时任务。 8. 调用结算的定时任务,结算给分销员 ``` 如果在这几个流程,`用户支付金额 + 平台支出金额 = 商家收入 + 分销员收入` 可以判断为基本正确,但会有很多特殊情况发生,所以要完整判断,还需要根据业务具体情况来确定 ### 如何确定收支平衡 当商家上架商品的时候,一般会有一个销售价,这个销售价便是商家希望能够通过商品的销售可以获取的收益。 1. 如果商品有一般的商家性的营销活动如:满减满折、优惠券之类的,商家可以获取的金额,实际上应该是用户支付的金额。即`商家收入 = 用户支付金额` 2. 如果商品是进行的是分销商品时,商家创建分销商品的时候,就会有一部分钱,给到分销员,一部分在自己的手上。既`商家收入 + 分销员收入 = 用户支付金额` 3. 如果涉及到平台性活动:平台优惠券、会员全平台折扣等。pi因为这个是平台发放的活动,强制所有商家参与的时候,这部分优惠的金额要商家进行补贴,即`商家收入 = 用户支付金额 + 平台补贴支出金额` 4. 如果涉及退款 `商家收入 = 用户支付金额 - 退款成功金额` 综合上面三种情况得出:`用户支付金额 + 平台支出金额 = 商家收入 + 分销员收入 + 用户退款收入` 当有更多的营销活动,更多的收入支出方,只要知道这个平衡公式即可:`支出a + 支出b +支出c + ... = 收入a + 收入b +收入c + ... ` 在上面的情况下,可以看出平台没有赚钱,甚至因为平台活动会亏损,那么该如何赚取收益呢?可以和商户签订协议,在提现的时候收取10%手续费,具体如何签订,签订何种协议,平台如何抽佣,这个并不在系统关心的访问内 ### 支付 > 进行支付时,商家、分销员获取待结算收入 简单的可以得出这个公式: 我们来看看这段代码在哪: 1. 首先我们来看,默认的支付成功监听 `com.yami.shop.api.listener.PaySuccessOrderListener` 这里做了一个操作`商家收入 = 用户支付金额 + 平台补贴支出金额`待结算金额: ``` // 商家未结算金额 = 商家原未结算金额 + 订单实付金额 + 订单平台补贴金额 newShopWallet.setUnsettledAmount(Arith.add(shopWallet.getUnsettledAmount(), Arith.add(order.getActualTotal(),order.getPlatformAmount()))); ``` 2. 如果订单项中含有分销的商品,那么还会有这么一个监听`com.yami.shop.distribution.api.listener.PaySuccessOrderListener` 商家待结算金额 = 商家待结算金额 - 订单分销佣金: ```java // 订单分销金额 double totalDistributionAmount = orderService.sumTotalDistributionAmountByOrderItem(distributionOrderItems); // 商家待结算金额 = 商家待结算金额 - 订单分销佣金 newShopWallet.setUnsettledAmount(Arith.sub(shopWallet.getUnsettledAmount(), totalDistributionAmount)); ``` 分销员添加待结算金额: ```java /** * 创建订单项收入记录添加待结算金额到钱包中 */ private void createIncomeByOrderItem(DistributionBindSet distributionBindSet, DistributionUser shareUser, DistributionUser bindUser, OrderItem orderItem) {} ``` ### 确认收货与结算 确认收货,商家进行结算,未结算金额转成结算金额。分销员要在确认收货15天之后,才能获取结算金额,至于为啥分销员要15天才结算,可以看看退款这部分的内容。 **注意:所有商品进行退款之后,也会进行结算的操作,可以看看退款这部分内容** #### 1. 商家结算 有两个动作会触发确认收货: 1. 定时任务`com.yami.shop.platform.task.OrderTask#confirmOrder()` 会将15天前待收货的订单,进行确认收货的操作。 2. 用户手动点击确认收货按钮,进行确认收货。 当商品有平台性质的活动时,此时,有部分金额是平台进行补贴的参考上文中**如何确定收支平衡** 这一部分得出`结算金额 = (用户支付金额 + 平台补贴金额) - 退款成功金额 - 分销占用金额` 我们来看看这段代码在哪:`com.yami.shop.listener.ReceiptOrderListener` ```java // 退款成功金额 double refundSuccessAmount = orderRefundService.sumRefundSuccessAmountByOrderId(order.getOrderId()); // 分销占用金额 double distributionAmount = orderService.sumTotalDistributionAmountByOrderItem(order.getOrderItems()); // 平台占用金额 double platformAmount = orderRefundService.sumRefundSuccessPlatformAmountByOrderId(order.getOrderId()); // 结算金额 = (用户支付金额 + 平台补贴金额) - 退款成功金额 - 分销占用金额 double shopAmount = Arith.add(order.getActualTotal(), order.getPlatformAmount()); double settledAmount = Arith.sub(Arith.sub(Arith.sub(shopAmount, refundSuccessAmount), distributionAmount),platformAmount); ``` #### 2.分销结算 退款涉及到分销员的订单,会使订单失效。所以什么时候结算给分销员是一个值得关注的地方。 有关分销员的结算,这个关乎到一个定时任务 `com.yami.shop.distribution.platform.task.DistributionCommissionSettlementTask#distributionCommissionSettlement()` ,这里面有个常量`Constant.DISTRIBUTION_SETTLEMENT_TIME` 而仔细看源码可以看到,这个结算的时间为: ```java /** * 分销佣金结算在确认收货后的时间,维权期过后(7+7+1) */ public static final int DISTRIBUTION_SETTLEMENT_TIME = MAX_FINALLY_REFUND_TIME + MAX_REFUND_APPLY_TIME + 1; ``` 至于为啥,还是看看退款这个文档。 ### 退款 > 退款与结算息息相关,而且是整个系统最复杂的地方 刚才在确认收货的时候有提及退款的功能,退款的逻辑稍微复杂一点点,首先要根据用户的退款时间来进行判断。首先我们确定的是,用户能退款的金额上线,是用户支付的金额。对于退款来说,用户的金额处于两个阶段: 1. 用户没有确认收货,所支付的金额在商家待结算金额。如果有分销的话一部分钱在分销员待结算的金额。 2. 用户进行了确认收货,所支付的金额在商家已结算。如果有分销的话一部分钱在分销员待结算的金额。 处于两个阶段,也就是说明当商家同意退款的时候,需要从商家里减去对应的金额,同时要在分销员的待结算金额也减去对应的金额。 这里的退款有几点需要值得注意: #### 1. 用户确认收货发起退款 用户确认收货之后,商城已结算给商家,商家将金额进行提现,用户进行退款的时候因已结算金额不足,将无法扣除,所以无法退款成功。值得注意的是,用户提交退款申请时,商家进行审核是有可能会拒绝用户的申请的。所以退款的订单是当商家点击确认的退款的时候才会去扣除商家钱包对应金额,微信发起退款。 #### 2. 退款显示的提示 买家进行退款的申请的时候,商城不应该提示买家该商家结算金额不足的问题。这会导致用户以为商家要卷款跑路,平台会遭到投诉!!!应该在商家点击确认退款时,提示商家自己的结算金额不足,无法完成退款的操作。 #### 3. 团购当中未成团的退款 关于团购当中未成团的退款,未成团的时候,用户肯定是不能确认收货的,所以这里的钱,都是从商家待结算里面出。 #### 4. 部分退款与分销结算 因为我们的退款是支持部分退款的。如果用户将分销的商品进行了部分退款,那么退款的钱从分销员出多少,商家出多少呢? 这么一想问题又会复杂话,我们从前面已经规定了,平台不拿商家一分钱,那么只要将分销的待结算金额返回给商家后,一次性将退款退回给用户。分销员查看自己分销的订单时该分销订单项将会变成无效。所以 **分销的结算要在无法进行退款之后** 可以参考`com.yami.shop.service.impl.OrderRefundServiceImpl#subShopSettlementAmount(OrderRefundDto)` ```java // 店铺真正退款的金额是不用退分销员的那部分的 // 如果订单100元,分销10元,10元的分销费用由商家出,此时商家结算时,拿到90元。 // 当用户进行退款,退1块的时候,分销会无效,此时商家拿回给出去的10元分销费用,并将1元给到退款用户,此时商家因为退款赚9元。 shopRealRefundAmount = -9 // 当用户进行退款,退11块的时候,分销会无效,此时商家拿回给出去的10元分销费用,并将11元给到退款用户,此时商家因为退款亏1元。shopRealRefundAmount = 1 if (orderRefundDto.getDistributionTotalAmount() != null) { shopRealRefundAmount = Arith.sub(shopRealRefundAmount, orderRefundDto.getDistributionTotalAmount()); } ``` #### 5. 强制取消申请超时的退款订单 根据 **部分退款与分销结算** 这部分内容,我们可以得出,结算给分销员,需要在退款流程结束之后。如果退款一直不处理的话,是不是分销员就一直不用结算呢?系统当然不能这样,所以我们规定了**申请退款的最大时限**,同时规定了几个时间:1)最大确认收货退款时间7天 2)退款最长申请时间,当申请时间过了这个时间段之后,会取消退款申请 与取消退款申请超时的订单相关的定时任务:`com.yami.shop.platform.task.OrderRefundTask#cancelWhenTimeOut()` 与退款有关的几个常量`com.yami.shop.common.config.Constant`: ```java /** * 最大确认收货退款时间7天 */ public static final int MAX_FINALLY_REFUND_TIME = 7; /** * 退款最长申请时间,当申请时间过了这个时间段之后,会取消退款申请 */ public static final int MAX_REFUND_APPLY_TIME = 7; ``` #### 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元给平台。 注意: 我们是有协商部分退款的说法的,也就是当用户选择部分退款的时候,这部分应该刚按照比例退回给平台,并扣除商家的这部分金额。但是如果用户可以退100元的商品,选择退99.99元,此时就会有个0.01如果有其他的比例,就会有其他的小数,因为除法的问题,会导致商家结算异常。所以我们**不允许当用户进行平台优惠后选择部分金额退款,只允许整单退款或整订单项退款,不允许整订单项部分退款** 可以参考`com.yami.shop.service.impl.OrderRefundServiceImpl#subShopSettlementAmount(OrderRefundDto)` ```java // 订单发起退款时,扣除商家金额应该加上平台部分 // 如果订单100元,平台出10元给商家(如平台10元优惠券,会员折扣9折),用户支付90元,此时商家结算时,要拿到100元,。 // 当用户进行退款,有一部分钱是平台出的,平台出的这部分钱,要将平台出的这部分钱按数据库存的平台优惠金额从商家那里扣除,如用户退90块,商家结算减少100块。 if(Objects.nonNull(order.getPlatformAmount())&& order.getPlatformAmount() > 0){ if (Objects.equals(orderRefundDto.getRefundType(), RefundType.ALL.value())) { shopRealRefundAmount = Arith.add(shopRealRefundAmount,order.getPlatformAmount()); } else { OrderItem item = orderItemService.getById(orderRefundDto.getOrderItemId()); shopRealRefundAmount = Arith.add(shopRealRefundAmount,item.getPlatformShareReduce()); } } ``` #### 8. 退款导致订单结算 由于我们系统支持部分金额退款,所以当100元的商品,用户选择退90元的时候,要结算10元给商家。我们并不是没退一次就结算一次,而是当订单里面的订单项都退完的时候,改订单会被取消。此时结算给商家。而这里的结算,用的是确认收货的监听。在退款回调之后进行。发送确认收货事件。 可以参考`com.yami.shop.service.impl.OrderRefundServiceImpl#verifyRefund(OrderRefundDto,String)` ```java // 非整单退款 && 以前订单没有结算(确认收货)&& 现在订单关闭了 && 因为退款导致订单关闭 // 如果订单因为退款关闭,要进行结算给商家的操作,这个结算的计算是退款完成后结算的,所以不要随便改变顺序 if (!Objects.equals(orderRefundDto.getRefundType(), RefundType.ALL.value()) && !Objects.equals(oldOrderStatus, OrderStatus.SUCCESS.value()) && Objects.equals(order.getStatus(), OrderStatus.CLOSE.value()) && Objects.equals(order.getCloseType(), OrderCloseType.REFUND.value())) { applicationContext.publishEvent(new ReceiptOrderEvent(order)); } ``` ### 退款说明 1. 由于退款时,必须订单有支付钱或者积分才能走流程,所以如果整个order如果有一个orderItem积分并且钱为0,就只能进行整单退款。