Forráskód Böngészése

物流运费计算 review 修改 + 单元测试

jason 2 éve
szülő
commit
dde89d51d5
11 módosított fájl, 304 hozzáadás és 95 törlés
  1. 4 2
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  2. 6 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/delivery/DeliveryExpressTemplateConvert.java
  3. 2 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateService.java
  4. 23 33
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java
  5. 29 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/bo/DeliveryExpressTemplateChargeBO.java
  6. 26 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/bo/DeliveryExpressTemplateFreeBO.java
  7. 2 19
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/bo/SpuDeliveryExpressTemplateRespBO.java
  8. 9 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java
  9. 33 37
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
  10. 2 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
  11. 168 0
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java

+ 4 - 2
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java

@@ -50,8 +50,10 @@ public interface ErrorCodeConstants {
     // TODO @jason:最好每个模块一段哈。express 一个;exmpresstemplate 一个;pickup 一个
     ErrorCode EXPRESS_CODE_DUPLICATE = new ErrorCode(1011003001, "已经存在该编码的快递公司");
     ErrorCode EXPRESS_TEMPLATE_NOT_EXISTS = new ErrorCode(1011003002, "运费模板不存在");
-    ErrorCode EXPRESS_TEMPLATE_NAME_DUPLICATE = new ErrorCode(1011003002, "已经存在该运费模板名");
-    ErrorCode PICK_UP_STORE_NOT_EXISTS = new ErrorCode(1011003003, "自提门店不存在");
+    ErrorCode EXPRESS_TEMPLATE_NAME_DUPLICATE = new ErrorCode(1011003003, "已经存在该运费模板名");
+    ErrorCode DELIVERY_EXPRESS_USER_ADDRESS_IS_EMPTY = new ErrorCode(1011003004, "计算快递运费时,收件人地址编号为空");
+    ErrorCode PRODUCT_EXPRESS_TEMPLATE_NOT_FOUND = new ErrorCode(1011003005, "找不到到商品对应的运费模板");
+    ErrorCode PICK_UP_STORE_NOT_EXISTS = new ErrorCode(1011003006, "自提门店不存在");
 
     // ========== Price 相关 1011004000 ============
     ErrorCode PRICE_CALCULATE_PAY_PRICE_ILLEGAL = new ErrorCode(1011004000, "支付价格计算异常,原因:价格小于等于 0");

+ 6 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/delivery/DeliveryExpressTemplateConvert.java

@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.expresstemplat
 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.service.delivery.bo.DeliveryExpressTemplateChargeBO;
+import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplateFreeBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
@@ -47,6 +49,8 @@ public interface DeliveryExpressTemplateConvert {
 
     DeliveryExpressTemplateChargeDO convertTemplateCharge(DeliveryExpressTemplateUpdateReqVO.ExpressTemplateChargeUpdateVO vo);
 
+    DeliveryExpressTemplateChargeBO convertTemplateCharge(DeliveryExpressTemplateChargeDO bean);
+
     default List<DeliveryExpressTemplateChargeDO> convertTemplateChargeList(Long templateId, Integer chargeMode, List<ExpressTemplateChargeBaseVO> list) {
         return CollectionUtils.convertList(list, vo -> convertTemplateCharge(templateId, chargeMode, vo));
     }
@@ -57,6 +61,8 @@ public interface DeliveryExpressTemplateConvert {
 
     DeliveryExpressTemplateFreeDO convertTemplateFree(DeliveryExpressTemplateUpdateReqVO.ExpressTemplateFreeUpdateVO vo);
 
+    DeliveryExpressTemplateFreeBO convertTemplateFree(DeliveryExpressTemplateFreeDO bean);
+
     List<ExpressTemplateChargeBaseVO> convertTemplateChargeList(List<DeliveryExpressTemplateChargeDO> list);
 
     List<ExpressTemplateFreeBaseVO> convertTemplateFreeList(List<DeliveryExpressTemplateFreeDO> list);

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

@@ -83,14 +83,13 @@ public interface DeliveryExpressTemplateService {
      */
     DeliveryExpressTemplateDO validateDeliveryExpressTemplate(Long templateId);
 
-    // TODO @jason 方法名可以改成 getExpressTemplateMapBySpuIdsAndArea
     /**
      * 基于指定的 SPU 编号数组和收件人地址区域编号. 获取匹配运费模板
      *
-     * @param ids    SPU 编号列表 // TODO @jason:模版编号?
+     * @param spuIds    SPU 编号列表
      * @param areaId 区域编号
      * @return Map (spuId -> 运费模板设置)
      */
-    Map<Long, SpuDeliveryExpressTemplateRespBO> getExpressTemplateBySpuIdsAndArea(Collection<Long> ids, Integer areaId);
+    Map<Long, SpuDeliveryExpressTemplateRespBO> getExpressTemplateMapBySpuIdsAndArea(Collection<Long> spuIds, Integer areaId);
 
 }

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

@@ -15,6 +15,8 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressTemp
 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.dal.mysql.delivery.DeliveryExpressTemplateMapper;
+import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplateChargeBO;
+import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplateFreeBO;
 import cn.iocoder.yudao.module.trade.service.delivery.bo.SpuDeliveryExpressTemplateRespBO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -24,8 +26,7 @@ import javax.annotation.Resource;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.trade.convert.delivery.DeliveryExpressTemplateConvert.INSTANCE;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_TEMPLATE_NAME_DUPLICATE;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_TEMPLATE_NOT_EXISTS;
@@ -227,7 +228,7 @@ public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTempla
     }
 
     @Override
-    public Map<Long, SpuDeliveryExpressTemplateRespBO> getExpressTemplateBySpuIdsAndArea(Collection<Long> spuIds, Integer areaId) {
+    public Map<Long, SpuDeliveryExpressTemplateRespBO> getExpressTemplateMapBySpuIdsAndArea(Collection<Long> spuIds, Integer areaId) {
         Assert.notNull(areaId, "区域编号 {} 不能为空", areaId);
         List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
         if (CollUtil.isEmpty(spuList)) {
@@ -237,43 +238,32 @@ public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTempla
         List<DeliveryExpressTemplateDO> templateList = expressTemplateMapper.selectBatchIds(spuMap.keySet());
         Map<Long, SpuDeliveryExpressTemplateRespBO> result = new HashMap<>(templateList.size());
         templateList.forEach(item -> {
-            // TODO @jason:if return ,更简洁哈;
-            if (spuMap.containsKey(item.getId())) {
-                ProductSpuRespDTO spu = spuMap.get(item.getId());
-                SpuDeliveryExpressTemplateRespBO bo = new SpuDeliveryExpressTemplateRespBO()
-                        .setSpuId(spu.getId()).setAreaId(areaId)
-                        .setChargeMode(item.getChargeMode())
-                        // TODO @jason:是不是只要查询到一个,就不用查询下一个了;TemplateCharge 和 TemplateFree
-                        .setTemplateCharge(findMatchExpressTemplateCharge(item.getId(), areaId))
-                        .setTemplateFree(findMatchExpressTemplateFree(item.getId(), areaId));
-                result.put(spu.getId(), bo);
+            ProductSpuRespDTO spu = spuMap.get(item.getId());
+            if (spu == null) {
+                return;
             }
+            SpuDeliveryExpressTemplateRespBO bo = new SpuDeliveryExpressTemplateRespBO()
+                    .setChargeMode(item.getChargeMode())
+                    // TODO @jason:是不是只要查询到一个,就不用查询下一个了;TemplateCharge 和 TemplateFree
+                    //  @芋艿 包邮的优先级> 费用的优先级 所以两个都要查询
+                    .setTemplateCharge(findMatchExpressTemplateCharge(item.getId(), areaId))
+                    .setTemplateFree(findMatchExpressTemplateFree(item.getId(), areaId));
+            result.put(spu.getId(), bo);
         });
         return result;
     }
 
-    private DeliveryExpressTemplateChargeDO findMatchExpressTemplateCharge(Long templateId, Integer areaId) {
-        List<DeliveryExpressTemplateChargeDO> list = expressTemplateChargeMapper.selectListByTemplateId(templateId);
-        // TODO @jason:可以使用 CollectionUtils.findFirst()
-        for (DeliveryExpressTemplateChargeDO item : list) {
-            // 第一个匹配的返回。 areaId 不能重复
-            if (item.getAreaIds().contains(areaId)) {
-                return item;
-            }
-        }
-        return null;
+    private DeliveryExpressTemplateChargeBO findMatchExpressTemplateCharge(Long templateId, Integer areaId) {
+        return INSTANCE.convertTemplateCharge(findFirst(
+                        expressTemplateChargeMapper.selectListByTemplateId(templateId), item -> item.getAreaIds().contains(areaId)
+                )
+        );
     }
 
-    private DeliveryExpressTemplateFreeDO findMatchExpressTemplateFree(Long templateId, Integer areaId) {
-        List<DeliveryExpressTemplateFreeDO> list = expressTemplateFreeMapper.selectListByTemplateId(templateId);
-        // TODO @jason:可以使用 CollectionUtils.findFirst()
-        for (DeliveryExpressTemplateFreeDO item : list) {
-            // 第一个匹配的返回。 areaId 不能重复
-            if (item.getAreaIds().contains(areaId)) {
-                return item;
-            }
-        }
-        return null;
+    private DeliveryExpressTemplateFreeBO findMatchExpressTemplateFree(Long templateId, Integer areaId) {
+        return INSTANCE.convertTemplateFree(findFirst(
+                expressTemplateFreeMapper.selectListByTemplateId(templateId), item -> item.getAreaIds().contains(areaId)
+        ));
     }
 
 }

+ 29 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/bo/DeliveryExpressTemplateChargeBO.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.trade.service.delivery.bo;
+
+import lombok.Data;
+
+/**
+ * 快递运费模板费用配置 BO
+ *
+ * @author jason
+ */
+@Data
+public class DeliveryExpressTemplateChargeBO {
+
+    /**
+     * 首件数量(件数,重量,或体积)
+     */
+    private Double startCount;
+    /**
+     * 起步价,单位:分
+     */
+    private Integer startPrice;
+    /**
+     * 续件数量(件, 重量,或体积)
+     */
+    private Double extraCount;
+    /**
+     * 额外价,单位:分
+     */
+    private Integer extraPrice;
+}

+ 26 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/bo/DeliveryExpressTemplateFreeBO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.trade.service.delivery.bo;
+
+import lombok.Data;
+
+/**
+ * 快递运费模板包邮配置 BO
+ *
+ * @author jason
+ */
+@Data
+public class DeliveryExpressTemplateFreeBO {
+
+    /**
+     * 包邮金额,单位:分
+     *
+     * 订单总金额 > 包邮金额时,才免运费
+     */
+    private Integer freePrice;
+
+    /**
+     * 包邮件数
+     *
+     * 订单总件数 > 包邮件数时,才免运费
+     */
+    private Integer freeCount;
+}

+ 2 - 19
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/bo/SpuDeliveryExpressTemplateRespBO.java

@@ -1,7 +1,5 @@
 package cn.iocoder.yudao.module.trade.service.delivery.bo;
 
-import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO;
-import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO;
 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum;
 import lombok.Data;
 
@@ -13,8 +11,6 @@ import lombok.Data;
 @Data
 public class SpuDeliveryExpressTemplateRespBO {
 
-    // TODO @jason:是不是可以简单一点;是否包邮;然后直接把  templateCharge、templateFree 需要的字段平铺开
-
     /**
      * 配送计费方式
      *
@@ -25,24 +21,11 @@ public class SpuDeliveryExpressTemplateRespBO {
     /**
      * 运费模板快递运费设置
      */
-    private DeliveryExpressTemplateChargeDO templateCharge;
+    private DeliveryExpressTemplateChargeBO templateCharge;
 
     /**
      * 运费模板包邮设置
      */
-    private DeliveryExpressTemplateFreeDO templateFree;
-
-    // TODO @jason:下面两个字段不用返回也可以呀
-    /**
-     * SPU 编号
-     * <p>
-     * 关联  ProductSpuDO 的 id 编号
-     */
-    private Long spuId;
-
-    /**
-     * 区域编号
-     */
-    private Integer areaId;
+    private DeliveryExpressTemplateFreeBO templateFree;
 
 }

+ 9 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java

@@ -164,6 +164,15 @@ public class TradePriceCalculateRespBO {
          */
         private Integer payPrice;
 
+        /**
+         * 商品重量,单位:kg 千克
+         */
+        private Double weight;
+        /**
+         * 商品体积,单位:m^3 平米
+         */
+        private Double volume;
+
         // ========== 商品信息 ==========
         /**
          * 商品名

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

@@ -4,17 +4,16 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 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.DeliveryExpressTemplateFreeDO;
 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.delivery.bo.DeliveryExpressTemplateChargeBO;
+import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplateFreeBO;
 import cn.iocoder.yudao.module.trade.service.delivery.bo.SpuDeliveryExpressTemplateRespBO;
 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 lombok.extern.slf4j.Slf4j;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
@@ -23,7 +22,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.DELIVERY_EXPRESS_USER_ADDRESS_IS_EMPTY;
+import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRODUCT_EXPRESS_TEMPLATE_NOT_FOUND;
 
 /**
  * 运费的 {@link TradePriceCalculator} 实现类
@@ -32,13 +34,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  */
 @Component
 @Order(TradePriceCalculator.ORDER_DELIVERY)
+@Slf4j
 public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
 
     @Resource
     private AddressApi addressApi;
     @Resource
-    private ProductSkuApi productSkuApi;
-    @Resource
     private DeliveryExpressTemplateService deliveryExpressTemplateService;
 
     @Override
@@ -47,10 +48,10 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
         if (param.getDeliveryType() == null || DeliveryTypeEnum.PICK_UP.getMode().equals(param.getDeliveryType())) {
             return;
         }
-        // 1.2 得到收件地址区域
         if (param.getAddressId() == null) {
-            return;
+            throw exception(DELIVERY_EXPRESS_USER_ADDRESS_IS_EMPTY);
         }
+        // 1.2 得到收件地址区域
         AddressRespDTO address = addressApi.getAddress(param.getAddressId(), param.getUserId());
         Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId());
 
@@ -58,33 +59,27 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
         List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
         Set<Long> spuIds = convertSet(selectedItem, OrderItem::getSpuId);
         Map<Long, SpuDeliveryExpressTemplateRespBO> spuExpressTemplateMap =
-                deliveryExpressTemplateService.getExpressTemplateBySpuIdsAndArea(spuIds, address.getAreaId());
-
+                deliveryExpressTemplateService.getExpressTemplateMapBySpuIdsAndArea(spuIds, address.getAreaId());
         // 3. 计算配送费用
-        // TODO @jason:这里应该不能判断空;如果找不到模版,就要报错;不然配送费就亏了
-        if (CollUtil.isNotEmpty(spuExpressTemplateMap)) {
-            calculateDeliveryPrice(selectedItem, spuExpressTemplateMap, result);
+        if (CollUtil.isEmpty(spuExpressTemplateMap)) {
+            log.error("找不到商品 SPU ID {}, area Id {} ,对应的运费模板", spuIds, address.getAreaId());
+            throw exception(PRODUCT_EXPRESS_TEMPLATE_NOT_FOUND);
         }
+        calculateDeliveryPrice(selectedItem, spuExpressTemplateMap, result);
     }
 
     private void calculateDeliveryPrice(List<OrderItem> selectedSkus,
                                         Map<Long, SpuDeliveryExpressTemplateRespBO> spuExpressTemplateMap,
                                         TradePriceCalculateRespBO result) {
-        // 得到 SKU 详情
-        // TODO @jason:可以去掉这里的读取;在 TradePriceCalculateRespBO 初始化的时候,把 weight、volume 拿到
-        Set<Long> skuIds = convertSet(selectedSkus, OrderItem::getSkuId);
-        Map<Long, ProductSkuRespDTO> skuRespMap = convertMap(productSkuApi.getSkuList(skuIds), ProductSkuRespDTO::getId);
         // 按 SPU 来计算商品的运费:一个 spuId 可能对应多条订单商品 SKU
         Map<Long, List<OrderItem>> spuIdItemMap = convertMultiMap(selectedSkus, OrderItem::getSpuId);
-
         // 依次计算每个 SPU 的快递运费
         for (Map.Entry<Long, List<OrderItem>> entry : spuIdItemMap.entrySet()) {
             Long spuId  = entry.getKey();
             List<OrderItem> orderItems = entry.getValue();
-            // TODO @jason:如果找不到,则打印 error log
             SpuDeliveryExpressTemplateRespBO templateBO = spuExpressTemplateMap.get(spuId);
             if (templateBO == null) {
-                // 记录错误日志
+                log.error("不能计算快递运费。不能找到 spuId : {}. 对应的运费模板配置 Resp BO", spuId);
                 continue;
             }
             // 总件数, 总金额, 总重量, 总体积
@@ -93,22 +88,16 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
             double totalWeight = 0;
             double totalVolume = 0;
             for (OrderItem orderItem : orderItems) {
-                totalCount += orderItem.getCount();
-                totalPrice += orderItem.getPayPrice(); // 先按应付总金额来算,后面确认一下 TODO @jason:是的哈
-                ProductSkuRespDTO skuResp = skuRespMap.get(orderItem.getSkuId());
-                // TODO @jason:是不是要保持风格统一,都用 +=
-                totalWeight = totalWeight + skuResp.getWeight() * orderItem.getCount();
-                totalVolume = totalVolume + skuResp.getVolume() * orderItem.getCount();
+                totalCount  += orderItem.getCount();
+                totalPrice  += orderItem.getPayPrice();
+                totalWeight += totalWeight + orderItem.getWeight() * orderItem.getCount();
+                totalVolume += totalVolume + orderItem.getVolume() * orderItem.getCount();
             }
             // 优先判断是否包邮. 如果包邮不计算快递运费
-            if (checkExpressFree(templateBO.getChargeMode(), totalCount, totalWeight,
+            if (isExpressFree(templateBO.getChargeMode(), totalCount, totalWeight,
                             totalVolume, totalPrice, templateBO.getTemplateFree())) {
                 continue;
             }
-            // TODO @jason:这块判断,可以收到 calculateExpressFeeByChargeMode 里;另外找不到,要打 error log
-            if (templateBO.getTemplateCharge() == null) {
-                continue;
-            }
             // 计算快递运费
             calculateExpressFeeByChargeMode(totalCount, totalWeight, totalVolume,
                     templateBO.getChargeMode(), templateBO.getTemplateCharge(), orderItems);
@@ -128,8 +117,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
      * @param orderItems SKU 商品项目
      */
     private void calculateExpressFeeByChargeMode(double totalCount, double totalWeight, double totalVolume,
-                                                 int chargeMode, DeliveryExpressTemplateChargeDO templateCharge,
+                                                 int chargeMode, DeliveryExpressTemplateChargeBO templateCharge,
                                                  List<OrderItem> orderItems) {
+        if (templateCharge == null) {
+            log.error("计算快递运费时,不能找到对应的快递运费模板费用配置。无法计算以下商品 SKU 项目运费: {}", orderItems);
+            return;
+        }
         DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode);
         switch (chargeModeEnum) {
             case PIECE: {
@@ -154,7 +147,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
      * @param templateCharge 快递运费配置
      * @param orderItems     SKU 商品项目
      */
-    private void calculateExpressFee(double total, DeliveryExpressTemplateChargeDO templateCharge, List<OrderItem> orderItems) {
+    private void calculateExpressFee(double total, DeliveryExpressTemplateChargeBO templateCharge, List<OrderItem> orderItems) {
         int deliveryPrice;
         if (total <= templateCharge.getStartCount()) {
             deliveryPrice = templateCharge.getStartPrice();
@@ -176,11 +169,14 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
      * @param orderItems    SKU 商品
      */
     private void divideDeliveryPrice(int deliveryPrice, List<OrderItem> orderItems) {
-        // TODO @jason:分摊的话,是不是要按照比例呀?重量、价格、数量等等
+        // TODO @jason:分摊的话,是不是要按照比例呀?重量、价格、数量等等,
+        //  按比例是不是有点复杂。后面看看是否需要
         int dividePrice = deliveryPrice / orderItems.size();
         for (OrderItem item : orderItems) {
             // 更新快递运费
             item.setDeliveryPrice(dividePrice);
+
+            TradePriceCalculatorHelper.recountPayPrice(item);
         }
     }
 
@@ -194,9 +190,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
      * @param totalPrice   总金额
      * @param templateFree 包邮配置
      */
-    // TODO @jason:isExpressFree 更合适;因为 check 是一种校验,往往抛出异常;
-    private boolean checkExpressFree(Integer chargeMode, int totalCount, double totalWeight,
-                                     double totalVolume, int totalPrice, DeliveryExpressTemplateFreeDO templateFree) {
+    private boolean isExpressFree(Integer chargeMode, int totalCount, double totalWeight,
+                                  double totalVolume, int totalPrice, DeliveryExpressTemplateFreeBO templateFree) {
         if (templateFree == null) {
             return false;
         }
@@ -211,6 +206,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
             case WEIGHT:
                 // freeCount 是不是应该是 double ??
                 // TODO @jason:要不配置的时候,把它的单位和商品对齐?到底是 kg、还是斤
+                // TODO @芋艿 目前 包邮 件数/重量/体积 都用的是这个字段
                 if (totalWeight >= templateFree.getFreeCount()
                         && totalPrice >= templateFree.getFreePrice()) {
                     return true;

+ 2 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java

@@ -53,7 +53,8 @@ public class TradePriceCalculatorHelper {
             orderItem.setPrice(sku.getPrice()).setPayPrice(sku.getPrice() * item.getCount())
                     .setDiscountPrice(0).setDeliveryPrice(0).setCouponPrice(0).setPointPrice(0);
             // sku 信息
-            orderItem.setPicUrl(sku.getPicUrl()).setProperties(sku.getProperties());
+            orderItem.setPicUrl(sku.getPicUrl()).setProperties(sku.getProperties())
+                    .setWeight(sku.getWeight()).setVolume(sku.getVolume());
             // spu 信息
             orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId());
             if (orderItem.getPicUrl() == null) {

+ 168 - 0
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java

@@ -0,0 +1,168 @@
+package cn.iocoder.yudao.module.trade.service.price.calculator;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.member.api.address.AddressApi;
+import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
+import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressTemplateService;
+import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplateChargeBO;
+import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplateFreeBO;
+import cn.iocoder.yudao.module.trade.service.delivery.bo.SpuDeliveryExpressTemplateRespBO;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum.PIECE;
+import static cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum.EXPRESS;
+import static java.util.Arrays.asList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author jason
+ */
+public class TradeDeliveryPriceCalculatorTest  extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private TradeDeliveryPriceCalculator calculator;
+    @Mock
+    private AddressApi addressApi;
+    @Mock
+    private DeliveryExpressTemplateService deliveryExpressTemplateService;
+
+    private TradePriceCalculateReqBO reqBO;
+    private TradePriceCalculateRespBO resultBO;
+    private AddressRespDTO addressResp;
+    private DeliveryExpressTemplateChargeBO chargeBO;
+    private DeliveryExpressTemplateFreeBO freeBO;
+    private SpuDeliveryExpressTemplateRespBO spuTemplateRespBO;
+
+    @BeforeEach
+    public void init(){
+        // 准备参数
+        reqBO = new TradePriceCalculateReqBO()
+                .setDeliveryType(EXPRESS.getMode())
+                .setAddressId(10L)
+                .setUserId(1L)
+                .setItems(asList(
+                        new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true),
+                        new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(10).setSelected(true),
+                        new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(false)
+                ));
+        resultBO = new TradePriceCalculateRespBO()
+                .setPrice(new TradePriceCalculateRespBO.Price())
+                .setPromotions(new ArrayList<>())
+                .setItems(asList(
+                        new TradePriceCalculateRespBO.OrderItem().setSpuId(1L).setSkuId(10L).setCount(2).setSelected(true)
+                                .setWeight(10d).setVolume(10d).setPrice(100),
+                        new TradePriceCalculateRespBO.OrderItem().setSpuId(1L).setSkuId(20L).setCount(10).setSelected(true)
+                                .setWeight(10d).setVolume(10d).setPrice(200),
+                        new TradePriceCalculateRespBO.OrderItem().setSpuId(1L).setSkuId(30L).setCount(1).setSelected(false)
+                                .setWeight(10d).setVolume(10d).setPrice(300)
+                ));
+        // 保证价格被初始化上
+        TradePriceCalculatorHelper.recountPayPrice(resultBO.getItems());
+        TradePriceCalculatorHelper.recountAllPrice(resultBO);
+        // 准备收件地址数据
+        addressResp = randomPojo(AddressRespDTO.class, item -> item.setAreaId(10));
+        // 准备运费模板费用配置数据
+        chargeBO = randomPojo(DeliveryExpressTemplateChargeBO.class,
+                item -> item.setStartCount(10D).setStartPrice(1000).setExtraCount(10D).setExtraPrice(2000));
+        // 准备运费模板包邮配置数据 订单总件数 < 包邮件数时 12 < 20
+        freeBO = randomPojo(DeliveryExpressTemplateFreeBO.class,
+                item -> item.setFreeCount(20).setFreePrice(100));
+        // 准备 SP 运费模板 数据
+        spuTemplateRespBO = randomPojo(SpuDeliveryExpressTemplateRespBO.class,
+                item -> item.setChargeMode(PIECE.getType())
+                        .setTemplateCharge(chargeBO).setTemplateFree(freeBO));
+    }
+
+    @Test
+    @DisplayName("按件计算运费不包邮的情况")
+    public void testCalculateByExpressTemplateCharge() {
+        // SKU 1 : 100 * 2  = 200
+        // SKU 2 :200 * 10 = 2000
+        // 运费  首件 1000 +  续件 2000 = 3000
+        Map<Long, SpuDeliveryExpressTemplateRespBO> respMap = new HashMap<>();
+        respMap.put(1L, spuTemplateRespBO);
+
+        // mock 方法
+        when(addressApi.getAddress(eq(10L), eq(1L))).thenReturn(addressResp);
+        when(deliveryExpressTemplateService.getExpressTemplateMapBySpuIdsAndArea(eq(asSet(1L)), eq(10)))
+                .thenReturn(respMap);
+
+        calculator.calculate(reqBO, resultBO);
+
+        TradePriceCalculateRespBO.Price price = resultBO.getPrice();
+
+        assertThat(price)
+                .extracting("totalPrice","discountPrice","couponPrice","pointPrice","deliveryPrice","payPrice")
+                .containsExactly(2200, 0, 0, 0, 3000,  5200);
+        // 断言:SKU
+        assertThat(resultBO.getItems()).hasSize(3);
+        // SKU1
+        assertThat(resultBO.getItems().get(0))
+                .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")
+                .containsExactly(100, 2, 0, 0, 0, 1500, 1700);
+        // SKU2
+        assertThat(resultBO.getItems().get(1))
+                .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")
+                .containsExactly(200, 10, 0, 0, 0, 1500, 3500);
+        // SKU3 未选中
+        assertThat(resultBO.getItems().get(2))
+                .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")
+                .containsExactly(300, 1, 0, 0, 0, 0, 300);
+    }
+
+    @Test
+    @DisplayName("按件计算运费包邮的情况")
+    public void testCalculateByExpressTemplateFree() {
+        // SKU 1 : 100 * 2  = 200
+        // SKU 2 :200 * 10 = 2000
+        // 运费  0
+        Map<Long, SpuDeliveryExpressTemplateRespBO> respMap = new HashMap<>();
+        respMap.put(1L, spuTemplateRespBO);
+        // 准备运费模板包邮配置数据 包邮 订单总件数 > 包邮件数时 12 > 10
+        freeBO = randomPojo(DeliveryExpressTemplateFreeBO.class,
+                item -> item.setFreeCount(10).setFreePrice(1000));
+        spuTemplateRespBO.setTemplateFree(freeBO);
+        // mock 方法
+        when(addressApi.getAddress(eq(10L), eq(1L))).thenReturn(addressResp);
+        when(deliveryExpressTemplateService.getExpressTemplateMapBySpuIdsAndArea(eq(asSet(1L)), eq(10)))
+                .thenReturn(respMap);
+
+        calculator.calculate(reqBO, resultBO);
+
+        TradePriceCalculateRespBO.Price price = resultBO.getPrice();
+
+        // 断言price
+        assertThat(price)
+                .extracting("totalPrice","discountPrice","couponPrice","pointPrice","deliveryPrice","payPrice")
+                .containsExactly(2200, 0, 0, 0, 0,  2200);
+        // 断言:SKU
+        assertThat(resultBO.getItems()).hasSize(3);
+        // SKU1
+        assertThat(resultBO.getItems().get(0))
+                .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")
+                .containsExactly(100, 2, 0, 0, 0, 0, 200);
+        // SKU2
+        assertThat(resultBO.getItems().get(1))
+                .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")
+                .containsExactly(200, 10, 0, 0, 0, 0, 2000);
+        // SKU3 未选中
+        assertThat(resultBO.getItems().get(2))
+                .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")
+                .containsExactly(300, 1, 0, 0, 0, 0, 300);
+    }
+}