## 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，就只能进行整单退款。
