2026-06-26 20b96473f2520590a0dca6b775b81e3ea06a77a0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
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<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
        Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
        Map<Long, DeliveryExpressTemplateRespBO> 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<OrderItem> selectedSkus,
                                        Map<Long, DeliveryExpressTemplateRespBO> expressTemplateMap,
                                        TradePriceCalculateRespBO result) {
        // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU
        Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
        // 依次计算快递运费
        for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) {
            Long templateId = entry.getKey();
            List<OrderItem> 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<OrderItem> 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<OrderItem> 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<OrderItem> 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));
        }
    }
 
}