Commit 7c4fbc63dc21f71ad62ea9c9a05af3e8acafbf50
1 parent
24111c15
trade:物流费用计算,增加金额 divide 分拆逻辑
Showing
3 changed files
with
61 additions
and
112 deletions
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.lang.Assert; |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 5 | 6 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 6 | 7 | import cn.iocoder.yudao.module.member.api.address.AddressApi; |
| 7 | 8 | import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO; |
| ... | ... | @@ -55,13 +56,13 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 55 | 56 | return; |
| 56 | 57 | } |
| 57 | 58 | if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { |
| 58 | - calculateByPickUp(param, result); | |
| 59 | + calculateByPickUp(param); | |
| 59 | 60 | } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { |
| 60 | 61 | calculateExpress(param, result); |
| 61 | 62 | } |
| 62 | 63 | } |
| 63 | 64 | |
| 64 | - private void calculateByPickUp(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { | |
| 65 | + private void calculateByPickUp(TradePriceCalculateReqBO param) { | |
| 65 | 66 | if (param.getPickUpStoreId() == null) { |
| 66 | 67 | throw exception(PRICE_CALCULATE_DELIVERY_PRICE_PICK_UP_STORE_IS_EMPTY); |
| 67 | 68 | } |
| ... | ... | @@ -82,12 +83,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 82 | 83 | Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId()); |
| 83 | 84 | |
| 84 | 85 | // 情况一:全局包邮 |
| 85 | - if (isGlobalExpressFree(param, result)) { | |
| 86 | + if (isGlobalExpressFree(result)) { | |
| 86 | 87 | return; |
| 87 | 88 | } |
| 88 | 89 | |
| 89 | - // 情况二: | |
| 90 | - // 2.1 过滤出已选中的商品SKU | |
| 90 | + // 情况二:快递模版 | |
| 91 | + // 2.1 过滤出已选中的商品 SKU | |
| 91 | 92 | List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected); |
| 92 | 93 | Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); |
| 93 | 94 | Map<Long, DeliveryExpressTemplateRespBO> expressTemplateMap = |
| ... | ... | @@ -103,11 +104,10 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 103 | 104 | /** |
| 104 | 105 | * 是否全局包邮 |
| 105 | 106 | * |
| 106 | - * @param param 计算信息 | |
| 107 | 107 | * @param result 计算结果 |
| 108 | 108 | * @return 是否包邮 |
| 109 | 109 | */ |
| 110 | - private boolean isGlobalExpressFree(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { | |
| 110 | + private boolean isGlobalExpressFree(TradePriceCalculateRespBO result) { | |
| 111 | 111 | TradeConfigDO config = tradeConfigService.getTradeConfig(); |
| 112 | 112 | return config != null |
| 113 | 113 | && Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮 |
| ... | ... | @@ -118,9 +118,9 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 118 | 118 | Map<Long, DeliveryExpressTemplateRespBO> expressTemplateMap, |
| 119 | 119 | TradePriceCalculateRespBO result) { |
| 120 | 120 | // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU |
| 121 | - Map<Long, List<OrderItem>> tplIdItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); | |
| 121 | + Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); | |
| 122 | 122 | // 依次计算快递运费 |
| 123 | - for (Map.Entry<Long, List<OrderItem>> entry : tplIdItemMap.entrySet()) { | |
| 123 | + for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) { | |
| 124 | 124 | Long templateId = entry.getKey(); |
| 125 | 125 | List<OrderItem> orderItems = entry.getValue(); |
| 126 | 126 | DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); |
| ... | ... | @@ -128,30 +128,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 128 | 128 | log.error("[calculateDeliveryPrice][不能计算快递运费,找不到 templateId({}) 对应的运费模板配置]", templateId); |
| 129 | 129 | continue; |
| 130 | 130 | } |
| 131 | - // 总件数, 总金额, 总重量, 总体积 | |
| 132 | - int totalCount = 0; | |
| 133 | - int totalPrice = 0; | |
| 134 | - double totalWeight = 0; | |
| 135 | - double totalVolume = 0; | |
| 136 | - for (OrderItem orderItem : orderItems) { | |
| 137 | - totalCount += orderItem.getCount(); | |
| 138 | - totalPrice += orderItem.getPayPrice(); | |
| 139 | - if (orderItem.getWeight() != null) { | |
| 140 | - totalWeight += totalWeight + orderItem.getWeight() * orderItem.getCount(); | |
| 141 | - } | |
| 142 | - if (orderItem.getVolume() != null) { | |
| 143 | - totalVolume += totalVolume + orderItem.getVolume() * orderItem.getCount(); | |
| 144 | - } | |
| 145 | - } | |
| 146 | - // 优先判断是否包邮. 如果包邮不计算快递运费 | |
| 147 | - if (isExpressFree(templateBO.getChargeMode(), totalCount, totalWeight, | |
| 148 | - totalVolume, totalPrice, templateBO.getFree())) { | |
| 131 | + // 1. 优先判断是否包邮。如果包邮不计算快递运费 | |
| 132 | + if (isExpressTemplateFree(orderItems, templateBO.getChargeMode(), templateBO.getFree())) { | |
| 149 | 133 | continue; |
| 150 | 134 | } |
| 151 | - // 计算快递运费 | |
| 152 | - calculateExpressFeeByChargeMode(totalCount, totalWeight, totalVolume, | |
| 153 | - templateBO.getChargeMode(), templateBO.getCharge(), orderItems); | |
| 154 | - | |
| 135 | + // 2. 计算快递运费 | |
| 136 | + calculateExpressFeeByChargeMode(orderItems, templateBO.getChargeMode(), templateBO.getCharge()); | |
| 155 | 137 | } |
| 156 | 138 | TradePriceCalculatorHelper.recountAllPrice(result); |
| 157 | 139 | } |
| ... | ... | @@ -159,73 +141,44 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 159 | 141 | /** |
| 160 | 142 | * 按配送方式来计算运费 |
| 161 | 143 | * |
| 162 | - * @param totalCount 总件数 | |
| 163 | - * @param totalWeight 总重量 | |
| 164 | - * @param totalVolume 总体积 | |
| 144 | + * @param orderItems SKU 商品项目 | |
| 165 | 145 | * @param chargeMode 配送计费方式 |
| 166 | 146 | * @param templateCharge 快递运费配置 |
| 167 | - * @param orderItems SKU 商品项目 | |
| 168 | 147 | */ |
| 169 | - private void calculateExpressFeeByChargeMode(double totalCount, double totalWeight, double totalVolume, | |
| 170 | - int chargeMode, DeliveryExpressTemplateRespBO.Charge templateCharge, | |
| 171 | - List<OrderItem> orderItems) { | |
| 148 | + private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode, | |
| 149 | + DeliveryExpressTemplateRespBO.Charge templateCharge) { | |
| 172 | 150 | if (templateCharge == null) { |
| 173 | 151 | log.error("[calculateExpressFeeByChargeMode][计算快递运费时,找不到 SKU({}) 对应的运费模版]", orderItems); |
| 174 | 152 | return; |
| 175 | 153 | } |
| 176 | - DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); | |
| 177 | - switch (chargeModeEnum) { | |
| 178 | - case PIECE: { | |
| 179 | - calculateExpressFee(totalCount, templateCharge, orderItems); | |
| 180 | - break; | |
| 181 | - } | |
| 182 | - case WEIGHT: { | |
| 183 | - calculateExpressFee(totalWeight, templateCharge, orderItems); | |
| 184 | - break; | |
| 185 | - } | |
| 186 | - case VOLUME: { | |
| 187 | - calculateExpressFee(totalVolume, templateCharge, orderItems); | |
| 188 | - break; | |
| 189 | - } | |
| 190 | - } | |
| 191 | - } | |
| 192 | - | |
| 193 | - /** | |
| 194 | - * 计算 SKU 商品快递费用 | |
| 195 | - * | |
| 196 | - * @param total 总件数/总重量/总体积 | |
| 197 | - * @param templateCharge 快递运费配置 | |
| 198 | - * @param orderItems SKU 商品项目 | |
| 199 | - */ | |
| 200 | - private void calculateExpressFee(double total, DeliveryExpressTemplateRespBO.Charge templateCharge, List<OrderItem> orderItems) { | |
| 154 | + double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); | |
| 155 | + // 1. 计算 SKU 商品快递费用 | |
| 201 | 156 | int deliveryPrice; |
| 202 | - if (total <= templateCharge.getStartCount()) { | |
| 157 | + if (totalChargeValue <= templateCharge.getStartCount()) { | |
| 203 | 158 | deliveryPrice = templateCharge.getStartPrice(); |
| 204 | 159 | } else { |
| 205 | - double remainWeight = total - templateCharge.getStartCount(); | |
| 160 | + double remainWeight = totalChargeValue - templateCharge.getStartCount(); | |
| 206 | 161 | // 剩余重量/ 续件 = 续件的次数. 向上取整 |
| 207 | 162 | int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount()); |
| 208 | 163 | int extraPrice = templateCharge.getExtraPrice() * extraNum; |
| 209 | 164 | deliveryPrice = templateCharge.getStartPrice() + extraPrice; |
| 210 | 165 | } |
| 211 | - // 分摊快递费用到 SKU. 退费的时候,可能按照 SKU 考虑退费金额 | |
| 212 | - divideDeliveryPrice(deliveryPrice, orderItems); | |
| 213 | - } | |
| 214 | 166 | |
| 215 | - /** | |
| 216 | - * 快递运费分摊到每个 SKU 商品上 | |
| 217 | - * | |
| 218 | - * @param deliveryPrice 快递运费 | |
| 219 | - * @param orderItems SKU 商品 | |
| 220 | - */ | |
| 221 | - private void divideDeliveryPrice(int deliveryPrice, List<OrderItem> orderItems) { | |
| 222 | - // TODO @jason:分摊的话,是不是要按照比例呀?重量、价格、数量等等, | |
| 223 | - // 按比例是不是有点复杂。后面看看是否需要; | |
| 224 | - // TODO 可以看看别的项目怎么搞的哈。 | |
| 225 | - int dividePrice = deliveryPrice / orderItems.size(); | |
| 226 | - for (OrderItem item : orderItems) { | |
| 167 | + // 2. 分摊快递费用到 SKU. 退费的时候,可能按照 SKU 考虑退费金额 | |
| 168 | + int remainPrice = deliveryPrice; | |
| 169 | + for (int i = 0; i < orderItems.size(); i++) { | |
| 170 | + TradePriceCalculateRespBO.OrderItem item = orderItems.get(i); | |
| 171 | + int partPrice; | |
| 172 | + double chargeValue = getChargeValue(item, chargeMode); | |
| 173 | + if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 | |
| 174 | + partPrice = (int) (deliveryPrice * (chargeValue / totalChargeValue)); | |
| 175 | + remainPrice -= partPrice; | |
| 176 | + } else { | |
| 177 | + partPrice = remainPrice; | |
| 178 | + } | |
| 179 | + Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0"); | |
| 227 | 180 | // 更新快递运费 |
| 228 | - item.setDeliveryPrice(dividePrice); | |
| 181 | + item.setDeliveryPrice(partPrice); | |
| 229 | 182 | TradePriceCalculatorHelper.recountPayPrice(item); |
| 230 | 183 | } |
| 231 | 184 | } |
| ... | ... | @@ -234,42 +187,38 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { |
| 234 | 187 | * 检查是否包邮 |
| 235 | 188 | * |
| 236 | 189 | * @param chargeMode 配送计费方式 |
| 237 | - * @param totalCount 总件数 | |
| 238 | - * @param totalWeight 总重量 | |
| 239 | - * @param totalVolume 总体积 | |
| 240 | - * @param totalPrice 总金额 | |
| 241 | 190 | * @param templateFree 包邮配置 |
| 242 | 191 | */ |
| 243 | - private boolean isExpressFree(Integer chargeMode, int totalCount, double totalWeight, | |
| 244 | - double totalVolume, int totalPrice, DeliveryExpressTemplateRespBO.Free templateFree) { | |
| 192 | + private boolean isExpressTemplateFree(List<OrderItem> orderItems, Integer chargeMode, | |
| 193 | + DeliveryExpressTemplateRespBO.Free templateFree) { | |
| 245 | 194 | if (templateFree == null) { |
| 246 | 195 | return false; |
| 247 | 196 | } |
| 197 | + double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); | |
| 198 | + double totalPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); | |
| 199 | + return totalChargeValue >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice(); | |
| 200 | + } | |
| 201 | + | |
| 202 | + private double getTotalChargeValue(List<OrderItem> orderItems, Integer chargeMode) { | |
| 203 | + double total = 0; | |
| 204 | + for (OrderItem orderItem : orderItems) { | |
| 205 | + total += getChargeValue(orderItem, chargeMode); | |
| 206 | + } | |
| 207 | + return total; | |
| 208 | + } | |
| 209 | + | |
| 210 | + private double getChargeValue(OrderItem orderItem, Integer chargeMode) { | |
| 248 | 211 | DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); |
| 249 | 212 | switch (chargeModeEnum) { |
| 250 | - case PIECE: | |
| 251 | - // 两个条件都满足才包邮 | |
| 252 | - if (totalCount >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice()) { | |
| 253 | - return true; | |
| 254 | - } | |
| 255 | - break; | |
| 213 | + case COUNT: | |
| 214 | + return orderItem.getCount(); | |
| 256 | 215 | case WEIGHT: |
| 257 | - // freeCount 是不是应该是 double ?? | |
| 258 | - // TODO @jason:要不配置的时候,把它的单位和商品对齐?到底是 kg、还是斤 | |
| 259 | - // TODO @芋艿 目前 包邮 件数/重量/体积 都用的是这个字段 | |
| 260 | - // TODO @jason:那要不快递模版也改成 kg?这样是不是就不用 double ? | |
| 261 | - if (totalWeight >= templateFree.getFreeCount() | |
| 262 | - && totalPrice >= templateFree.getFreePrice()) { | |
| 263 | - return true; | |
| 264 | - } | |
| 265 | - break; | |
| 216 | + return orderItem.getWeight() != null ? orderItem.getWeight() * orderItem.getCount() : 0; | |
| 266 | 217 | case VOLUME: |
| 267 | - if (totalVolume >= templateFree.getFreeCount() | |
| 268 | - && totalPrice >= templateFree.getFreePrice()) { | |
| 269 | - return true; | |
| 270 | - } | |
| 271 | - break; | |
| 218 | + return orderItem.getVolume() != null ? orderItem.getVolume() * orderItem.getCount() : 0; | |
| 219 | + default: | |
| 220 | + throw new IllegalArgumentException(StrUtil.format("未知的计费模式({})", chargeMode)); | |
| 272 | 221 | } |
| 273 | - return false; | |
| 274 | 222 | } |
| 223 | + | |
| 275 | 224 | } | ... | ... |
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java
| ... | ... | @@ -91,7 +91,7 @@ public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest { |
| 91 | 91 | item -> item.setFreeCount(20).setFreePrice(100)); |
| 92 | 92 | // 准备 SP 运费模板数据 |
| 93 | 93 | templateRespBO = randomPojo(DeliveryExpressTemplateRespBO.class, |
| 94 | - item -> item.setChargeMode(DeliveryExpressChargeModeEnum.PIECE.getType()) | |
| 94 | + item -> item.setChargeMode(DeliveryExpressChargeModeEnum.COUNT.getType()) | |
| 95 | 95 | .setCharge(chargeBO).setFree(freeBO)); |
| 96 | 96 | } |
| 97 | 97 | |
| ... | ... | @@ -144,11 +144,11 @@ public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest { |
| 144 | 144 | // 断言:SKU1 |
| 145 | 145 | assertThat(resultBO.getItems().get(0)) |
| 146 | 146 | .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") |
| 147 | - .containsExactly(100, 2, 0, 0, 0, 1500, 1700); | |
| 147 | + .containsExactly(100, 2, 0, 0, 0, 500, 700); | |
| 148 | 148 | // 断言:SKU2 |
| 149 | 149 | assertThat(resultBO.getItems().get(1)) |
| 150 | 150 | .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") |
| 151 | - .containsExactly(200, 10, 0, 0, 0, 1500, 3500); | |
| 151 | + .containsExactly(200, 10, 0, 0, 0, 2500, 4500); | |
| 152 | 152 | // 断言:SKU3 未选中 |
| 153 | 153 | assertThat(resultBO.getItems().get(2)) |
| 154 | 154 | .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") | ... | ... |