package cn.iocoder.yudao.module.trade.service.price.calculator; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; 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.config.TradeConfigService; import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressTemplateService; import cn.iocoder.yudao.module.trade.service.delivery.DeliveryPickUpStoreService; import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; 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 jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; 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.*; /** * 运费的 {@link TradePriceCalculator} 实现类 * * @author jason */ @Component @Order(TradePriceCalculator.ORDER_DELIVERY) @Slf4j public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @Resource private MemberAddressApi addressApi; @Resource private DeliveryPickUpStoreService deliveryPickUpStoreService; @Resource private DeliveryExpressTemplateService deliveryExpressTemplateService; @Resource private TradeConfigService tradeConfigService; @Override public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { if (param.getDeliveryType() == null) { return; } // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL); } if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { calculateByPickUp(param); } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { calculateExpress(param, result); } } private void calculateByPickUp(TradePriceCalculateReqBO param) { if (param.getPickUpStoreId() == null) { // 价格计算时,如果为空就不算~最终下单,会校验该字段不允许空 return; } DeliveryPickUpStoreDO pickUpStore = deliveryPickUpStoreService.getDeliveryPickUpStore(param.getPickUpStoreId()); if (pickUpStore == null || CommonStatusEnum.DISABLE.getStatus().equals(pickUpStore.getStatus())) { throw exception(PICK_UP_STORE_NOT_EXISTS); } } // ========= 快递发货 ========== private void calculateExpress(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { // 0. 得到收件地址区域 if (param.getAddressId() == null) { // 价格计算时,如果为空就不算~最终下单,会校验该字段不允许空 return; } MemberAddressRespDTO address = addressApi.getAddress(param.getAddressId(), param.getUserId()); Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId()); // 情况一:全局包邮 if (isGlobalExpressFree(result)) { return; } // 情况二:活动包邮 if (Boolean.TRUE.equals(result.getFreeDelivery())) { return; } // 情况三:快递模版 // 2.1 过滤出已选中的商品 SKU List selectedItem = filterList(result.getItems(), OrderItem::getSelected); Set deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); Map expressTemplateMap = deliveryExpressTemplateService.getExpressTemplateMapByIdsAndArea(deliveryTemplateIds, address.getAreaId()); // 2.2 计算配送费用 if (CollUtil.isEmpty(expressTemplateMap)) { log.error("[calculate][找不到商品 templateIds {} areaId{} 对应的运费模板]", deliveryTemplateIds, address.getAreaId()); throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND); } calculateDeliveryPrice(selectedItem, expressTemplateMap, result); } /** * 是否全局包邮 * * @param result 计算结果 * @return 是否包邮 */ private boolean isGlobalExpressFree(TradePriceCalculateRespBO result) { TradeConfigDO config = tradeConfigService.getTradeConfig(); // 情况一:交易中心配置不存在默认不包邮 if (config == null) { return false; } // 情况二:开启了全局包邮 && 满足包邮金额 return Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) && result.getPrice().getPayPrice() >= config.getDeliveryExpressFreePrice(); } private void calculateDeliveryPrice(List selectedSkus, Map expressTemplateMap, TradePriceCalculateRespBO result) { // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU Map> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); // 依次计算快递运费 for (Map.Entry> entry : template2ItemMap.entrySet()) { Long templateId = entry.getKey(); List orderItems = entry.getValue(); DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); if (templateBO == null) { log.error("[calculateDeliveryPrice][不能计算快递运费,找不到 templateId({}) 对应的运费模板配置]", templateId); continue; } // 1. 优先判断是否包邮。如果包邮不计算快递运费 if (isExpressTemplateFree(orderItems, templateBO.getChargeMode(), templateBO.getFree())) { continue; } // 2. 计算快递运费 calculateExpressFeeByChargeMode(orderItems, templateBO.getChargeMode(), templateBO.getCharge()); } TradePriceCalculatorHelper.recountAllPrice(result); } /** * 按配送方式来计算运费 * * @param orderItems SKU 商品项目 * @param chargeMode 配送计费方式 * @param templateCharge 快递运费配置 */ private void calculateExpressFeeByChargeMode(List orderItems, Integer chargeMode, DeliveryExpressTemplateRespBO.Charge templateCharge) { if (templateCharge == null) { log.error("[calculateExpressFeeByChargeMode][计算快递运费时,找不到 SKU({}) 对应的运费模版]", orderItems); return; } double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); // 1. 计算 SKU 商品快递费用 int deliveryPrice; if (totalChargeValue <= templateCharge.getStartCount()) { deliveryPrice = templateCharge.getStartPrice(); } else { double remainWeight = totalChargeValue - templateCharge.getStartCount(); // 剩余重量/ 续件 = 续件的次数. 向上取整 int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount()); int extraPrice = templateCharge.getExtraPrice() * extraNum; deliveryPrice = templateCharge.getStartPrice() + extraPrice; } // 2. 分摊快递费用到 SKU. 退费的时候,可能按照 SKU 考虑退费金额 int remainPrice = deliveryPrice; for (int i = 0; i < orderItems.size(); i++) { TradePriceCalculateRespBO.OrderItem item = orderItems.get(i); int partPrice; double chargeValue = getChargeValue(item, chargeMode); if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 partPrice = (int) (deliveryPrice * (chargeValue / totalChargeValue)); remainPrice -= partPrice; } else { partPrice = remainPrice; } Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0"); // 更新快递运费 item.setDeliveryPrice(partPrice); TradePriceCalculatorHelper.recountPayPrice(item); } } /** * 检查是否包邮 * * @param chargeMode 配送计费方式 * @param templateFree 包邮配置 */ private boolean isExpressTemplateFree(List orderItems, Integer chargeMode, DeliveryExpressTemplateRespBO.Free templateFree) { if (templateFree == null) { return false; } double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); double totalPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); return totalChargeValue >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice(); } private double getTotalChargeValue(List orderItems, Integer chargeMode) { double total = 0; for (OrderItem orderItem : orderItems) { total += getChargeValue(orderItem, chargeMode); } return total; } private double getChargeValue(OrderItem orderItem, Integer chargeMode) { DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); switch (chargeModeEnum) { case COUNT: return orderItem.getCount(); case WEIGHT: return orderItem.getWeight() != null ? orderItem.getWeight() * orderItem.getCount() : 0; case VOLUME: return orderItem.getVolume() != null ? orderItem.getVolume() * orderItem.getCount() : 0; default: throw new IllegalArgumentException(StrUtil.format("未知的计费模式({})", chargeMode)); } } }