Browse Source

mall - trade - 新增 TradeDeliveryPriceCalculator

jason 1 year ago
parent
commit
f1fa8eadd2

+ 1 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/spu/ProductSpuDO.java

@@ -141,7 +141,7 @@ public class ProductSpuDO extends BaseDO {
     /**
      * 物流配置模板编号
      *
-     * 关联 { TradeDeliveryExpressTemplateDO#getId()}
+     * 对应 { TradeDeliveryExpressTemplateDO 的 id 编号}
      */
     private Long deliveryTemplateId;
 

+ 5 - 0
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.trade.enums.delivery;
 
+import cn.hutool.core.util.ArrayUtil;
 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
@@ -35,4 +36,8 @@ public enum DeliveryExpressChargeModeEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
+    public static DeliveryExpressChargeModeEnum valueOf(Integer value) {
+        return ArrayUtil.firstMatch(chargeMode -> chargeMode.getType().equals(value), DeliveryExpressChargeModeEnum.values());
+    }
+
 }

+ 7 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateService.java

@@ -63,4 +63,11 @@ public interface DeliveryExpressTemplateService {
      * @return 快递运费模板分页
      */
     PageResult<DeliveryExpressTemplateDO> getDeliveryExpressTemplatePage(DeliveryExpressTemplatePageReqVO pageReqVO);
+
+    /**
+     * 校验快递运费模板
+     * @param templateId 模板编号
+     * @return DeliveryExpressTemplateDO 非空
+     */
+    DeliveryExpressTemplateDO validateDeliveryExpressTemplate(Long templateId);
 }

+ 9 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java

@@ -202,4 +202,13 @@ public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTempla
         return expressTemplateMapper.selectPage(pageReqVO);
     }
 
+    @Override
+    public DeliveryExpressTemplateDO validateDeliveryExpressTemplate(Long templateId) {
+        DeliveryExpressTemplateDO template = expressTemplateMapper.selectById(templateId);
+        if (template == null) {
+            throw exception(EXPRESS_TEMPLATE_NOT_EXISTS);
+        }
+        return template;
+    }
+
 }

+ 16 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateReqBO.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.trade.service.price.bo;
 
+import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO;
+import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import lombok.Data;
 
@@ -45,6 +47,20 @@ public class TradePriceCalculateReqBO {
     private Long addressId;
 
     /**
+     * 配送方式
+     *
+     * 枚举 {@link DeliveryTypeEnum}
+     */
+    private Integer deliveryType;
+
+    /**
+     * 配送模板编号
+     *
+     * 关联 {@link DeliveryExpressTemplateDO#getId()}
+     */
+    private Long templateId;
+
+    /**
      * 商品 SKU 数组
      */
     @NotNull(message = "商品数组不能为空")
@@ -82,5 +98,4 @@ public class TradePriceCalculateReqBO {
         private Boolean selected;
 
     }
-
 }

+ 228 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java

@@ -0,0 +1,228 @@
+package cn.iocoder.yudao.module.trade.service.price.calculator;
+
+import cn.iocoder.yudao.module.member.api.address.AddressApi;
+import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
+import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO;
+import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO;
+import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO;
+import cn.iocoder.yudao.module.trade.dal.mysql.delivery.DeliveryExpressTemplateChargeMapper;
+import cn.iocoder.yudao.module.trade.dal.mysql.delivery.DeliveryExpressTemplateFreeMapper;
+import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum;
+import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
+import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressTemplateService;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+
+/**
+ * 运费的 {@link TradePriceCalculator} 实现类
+ *
+ * @author jason
+ */
+@Component
+@Order(TradePriceCalculator.ORDER_DELIVERY)
+public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
+    @Resource
+    private AddressApi addressApi;
+    @Resource
+    private ProductSkuApi productSkuApi;
+    @Resource
+    private DeliveryExpressTemplateService deliveryExpressTemplateService;
+    @Resource
+    private DeliveryExpressTemplateChargeMapper templateChargeMapper;
+    @Resource
+    private DeliveryExpressTemplateFreeMapper templateFreeMapper;
+
+    @Override
+    public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
+        // 1.1 判断配送方式
+        if (param.getDeliveryType() == null || DeliveryTypeEnum.PICK_UP.getMode().equals(param.getDeliveryType())) {
+            return;
+        }
+
+        if (param.getTemplateId() == null || param.getAddressId() == null) {
+            return;
+        }
+        // 1.2 校验运费模板是否存在
+        DeliveryExpressTemplateDO template = deliveryExpressTemplateService.validateDeliveryExpressTemplate(param.getTemplateId());
+
+        // 得到包邮配置
+        List<DeliveryExpressTemplateFreeDO> expressTemplateFreeList = templateFreeMapper.selectListByTemplateId(template.getId());
+        Map<Integer, DeliveryExpressTemplateFreeDO> areaTemplateFreeMap = new HashMap<>();
+        expressTemplateFreeList.forEach(item -> {
+            for (Integer areaId : item.getAreaIds()) {
+                // TODO 需要保证 areaId 不能重复
+                if (!areaTemplateFreeMap.containsKey(areaId)) {
+                    areaTemplateFreeMap.put(areaId, item);
+                }
+            }
+        });
+        // 得到快递运费配置
+        List<DeliveryExpressTemplateChargeDO> expressTemplateChargeList = templateChargeMapper.selectListByTemplateId(template.getId());
+        Map<Integer, DeliveryExpressTemplateChargeDO> areaTemplateChargeMap = new HashMap<>();
+        expressTemplateChargeList.forEach(item -> {
+            for (Integer areaId : item.getAreaIds()) {
+                // areaId 不能重复
+                if (!areaTemplateChargeMap.containsKey(areaId)) {
+                    areaTemplateChargeMap.put(areaId, item);
+                }
+            }
+        });
+        // 得到收件地址区域
+        AddressRespDTO address = addressApi.getAddress(param.getAddressId(), param.getUserId());
+        // 1.3 计算快递费用
+        calculateDeliveryPrice(address.getAreaId(), template.getChargeMode(),
+                areaTemplateFreeMap, areaTemplateChargeMap, result);
+    }
+
+    /**
+     * 校验订单是否满足包邮条件
+     *
+     * @param receiverAreaId        收件人地区的区域编号
+     * @param chargeMode            配送计费方式
+     * @param areaTemplateFreeMap   运费模板包邮区域设置 Map
+     * @param areaTemplateChargeMap 运费模板快递费用设置 Map
+     */
+    private void calculateDeliveryPrice(Integer receiverAreaId,
+                                        Integer chargeMode,
+                                        Map<Integer, DeliveryExpressTemplateFreeDO> areaTemplateFreeMap,
+                                        Map<Integer, DeliveryExpressTemplateChargeDO> areaTemplateChargeMap,
+                                        TradePriceCalculateRespBO result) {
+        // 过滤出已选中的商品SKU
+        List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
+        Set<Long> skuIds = convertSet(selectedItem, OrderItem::getSkuId);
+        // 得到SKU 详情。得到 重量体积
+        Map<Long, ProductSkuRespDTO> skuRespMap = convertMap(productSkuApi.getSkuList(skuIds), ProductSkuRespDTO::getId);
+        // 一个 spuId 可能对应多条订单商品 SKU
+        Map<Long, List<OrderItem>> spuIdItemMap = convertMultiMap(selectedItem, OrderItem::getSpuId);
+        // 依次计算每个 SPU 的快递运费
+        for (Map.Entry<Long, List<OrderItem>> entry : spuIdItemMap.entrySet()) {
+            List<OrderItem> orderItems = entry.getValue();
+            // 总件数, 总金额, 总重量, 总体积
+            int totalCount = 0, totalPrice = 0;
+            double totalWeight = 0;
+            double totalVolume = 0;
+            for (OrderItem orderItem : orderItems) {
+                totalCount += orderItem.getCount();
+                totalPrice += orderItem.getPrice();
+                ProductSkuRespDTO skuResp = skuRespMap.get(orderItem.getSkuId());
+                if (skuResp != null) {
+                    totalWeight = totalWeight + skuResp.getWeight();
+                    totalVolume = totalVolume + skuResp.getVolume();
+                }
+            }
+            // 优先判断是否包邮. 如果包邮不计算快递运费
+            if (areaTemplateFreeMap.containsKey(receiverAreaId) &&
+                    checkExpressFree(chargeMode, totalCount, totalWeight,
+                            totalVolume, totalPrice, areaTemplateFreeMap.get(receiverAreaId))) {
+                continue;
+            }
+            // 计算快递运费
+            if (areaTemplateChargeMap.containsKey(receiverAreaId)) {
+                DeliveryExpressTemplateChargeDO templateCharge = areaTemplateChargeMap.get(receiverAreaId);
+                DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode);
+                switch (chargeModeEnum) {
+                    case PIECE: {
+                        calculateExpressFeeBySpu(totalCount, templateCharge, orderItems);
+                        break;
+                    }
+                    case WEIGHT: {
+                        calculateExpressFeeBySpu(totalWeight, templateCharge, orderItems);
+                        break;
+                    }
+                    case VOLUME: {
+                        calculateExpressFeeBySpu(totalVolume, templateCharge, orderItems);
+                        break;
+                    }
+                }
+            }
+        }
+        TradePriceCalculatorHelper.recountAllPrice(result);
+    }
+
+    /**
+     * 按 spu 来计算快递费用
+     *
+     * @param total          总件数/总重量/总体积
+     * @param templateCharge 快递运费配置
+     * @param orderItems     SKU 商品项目
+     */
+    private void calculateExpressFeeBySpu(double total, DeliveryExpressTemplateChargeDO templateCharge, List<OrderItem> orderItems) {
+        int deliveryPrice;
+        if (total <= templateCharge.getStartCount()) {
+            deliveryPrice = templateCharge.getStartPrice();
+        } else {
+            double remainWeight = total - templateCharge.getStartCount();
+            // 剩余重量/ 续件 = 续件的次数. 向上取整
+            int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount());
+            int extraPrice = templateCharge.getExtraPrice() * extraNum;
+            deliveryPrice = templateCharge.getStartPrice() + extraPrice;
+        }
+        //
+        // TODO @芋艿 分摊快递费用到 SKU. 是不是搞复杂了
+        divideDeliveryPrice(deliveryPrice, orderItems);
+    }
+
+    /**
+     * 快递运费分摊到每个 SKU 商品上
+     *
+     * @param deliveryPrice 快递运费
+     * @param orderItems    SKU 商品
+     */
+    private void divideDeliveryPrice(int deliveryPrice, List<OrderItem> orderItems) {
+        int dividePrice = deliveryPrice / orderItems.size();
+        for (OrderItem item : orderItems) {
+            // 更新快递运费
+            item.setDeliveryPrice(dividePrice);
+        }
+    }
+
+    /**
+     * 检查是否包邮
+     *
+     * @param chargeMode   配送计费方式
+     * @param totalCount   总件数
+     * @param totalWeight  总重量
+     * @param totalVolume  总体积
+     * @param totalPrice   总金额
+     * @param templateFree 包邮配置
+     */
+    private boolean checkExpressFree(Integer chargeMode, int totalCount, double totalWeight,
+                                     double totalVolume, int totalPrice, DeliveryExpressTemplateFreeDO templateFree) {
+        DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode);
+        switch (chargeModeEnum) {
+            case PIECE:
+                // 两个条件都满足才包邮
+                if (totalCount >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice()) {
+                    return true;
+                }
+                break;
+            case WEIGHT:
+                //  freeCount 是不是应该是 double ??
+                if (totalWeight >= templateFree.getFreeCount()
+                        && totalPrice >= templateFree.getFreePrice()) {
+                    return true;
+                }
+                break;
+            case VOLUME:
+                if (totalVolume >= templateFree.getFreeCount()
+                        && totalPrice >= templateFree.getFreePrice()) {
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+}

+ 4 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculator.java

@@ -11,6 +11,10 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
 public interface TradePriceCalculator {
 
     int ORDER_DISCOUNT_ACTIVITY = 10;
+    /**
+     * TODO @芋艿 快递运费的计算在满减之前。 例如有满多少包邮
+     */
+    int ORDER_DELIVERY = 15;
     int ORDER_REWARD_ACTIVITY = 20;
     int ORDER_COUPON = 30;
 

+ 1 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/address/MemberAddressDO.java

@@ -39,7 +39,7 @@ public class MemberAddressDO extends BaseDO {
     /**
      * 地区编号
      */
-    private Long areaId;
+    private Integer areaId;
     /**
      * 收件详细地址
      */