mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-11-04 12:18:42 +08:00 
			
		
		
		
	物流运费计算 review 修改 + 单元测试
This commit is contained in:
		@@ -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,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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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());
 | 
			
		||||
            if (spu == null) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            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);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        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)
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -164,6 +164,15 @@ public class TradePriceCalculateRespBO {
 | 
			
		||||
         */
 | 
			
		||||
        private Integer payPrice;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * 商品重量,单位:kg 千克
 | 
			
		||||
         */
 | 
			
		||||
        private Double weight;
 | 
			
		||||
        /**
 | 
			
		||||
         * 商品体积,单位:m^3 平米
 | 
			
		||||
         */
 | 
			
		||||
        private Double volume;
 | 
			
		||||
 | 
			
		||||
        // ========== 商品信息 ==========
 | 
			
		||||
        /**
 | 
			
		||||
         * 商品名
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
            }
 | 
			
		||||
            // 总件数, 总金额, 总重量, 总体积
 | 
			
		||||
@@ -94,21 +89,15 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
 | 
			
		||||
            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();
 | 
			
		||||
                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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user