Commit 7c4fbc63dc21f71ad62ea9c9a05af3e8acafbf50

Authored by YunaiV
1 parent 24111c15

trade:物流费用计算,增加金额 divide 分拆逻辑

yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java
@@ -16,7 +16,7 @@ import java.util.Arrays; @@ -16,7 +16,7 @@ import java.util.Arrays;
16 @Getter 16 @Getter
17 public enum DeliveryExpressChargeModeEnum implements IntArrayValuable { 17 public enum DeliveryExpressChargeModeEnum implements IntArrayValuable {
18 18
19 - PIECE(1, "按件"), 19 + COUNT(1, "按件"),
20 WEIGHT(2,"按重量"), 20 WEIGHT(2,"按重量"),
21 VOLUME(3, "按体积"); 21 VOLUME(3, "按体积");
22 22
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,6 +2,7 @@ package cn.iocoder.yudao.module.trade.service.price.calculator;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 import cn.iocoder.yudao.module.member.api.address.AddressApi; 7 import cn.iocoder.yudao.module.member.api.address.AddressApi;
7 import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO; 8 import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
@@ -55,13 +56,13 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -55,13 +56,13 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
55 return; 56 return;
56 } 57 }
57 if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { 58 if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) {
58 - calculateByPickUp(param, result); 59 + calculateByPickUp(param);
59 } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { 60 } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) {
60 calculateExpress(param, result); 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 if (param.getPickUpStoreId() == null) { 66 if (param.getPickUpStoreId() == null) {
66 throw exception(PRICE_CALCULATE_DELIVERY_PRICE_PICK_UP_STORE_IS_EMPTY); 67 throw exception(PRICE_CALCULATE_DELIVERY_PRICE_PICK_UP_STORE_IS_EMPTY);
67 } 68 }
@@ -82,12 +83,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -82,12 +83,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
82 Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId()); 83 Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId());
83 84
84 // 情况一:全局包邮 85 // 情况一:全局包邮
85 - if (isGlobalExpressFree(param, result)) { 86 + if (isGlobalExpressFree(result)) {
86 return; 87 return;
87 } 88 }
88 89
89 - // 情况二:  
90 - // 2.1 过滤出已选中的商品SKU 90 + // 情况二:快递模版
  91 + // 2.1 过滤出已选中的商品 SKU
91 List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected); 92 List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
92 Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); 93 Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
93 Map<Long, DeliveryExpressTemplateRespBO> expressTemplateMap = 94 Map<Long, DeliveryExpressTemplateRespBO> expressTemplateMap =
@@ -103,11 +104,10 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -103,11 +104,10 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
103 /** 104 /**
104 * 是否全局包邮 105 * 是否全局包邮
105 * 106 *
106 - * @param param 计算信息  
107 * @param result 计算结果 107 * @param result 计算结果
108 * @return 是否包邮 108 * @return 是否包邮
109 */ 109 */
110 - private boolean isGlobalExpressFree(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { 110 + private boolean isGlobalExpressFree(TradePriceCalculateRespBO result) {
111 TradeConfigDO config = tradeConfigService.getTradeConfig(); 111 TradeConfigDO config = tradeConfigService.getTradeConfig();
112 return config != null 112 return config != null
113 && Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮 113 && Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮
@@ -118,9 +118,9 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -118,9 +118,9 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
118 Map<Long, DeliveryExpressTemplateRespBO> expressTemplateMap, 118 Map<Long, DeliveryExpressTemplateRespBO> expressTemplateMap,
119 TradePriceCalculateRespBO result) { 119 TradePriceCalculateRespBO result) {
120 // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU 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 Long templateId = entry.getKey(); 124 Long templateId = entry.getKey();
125 List<OrderItem> orderItems = entry.getValue(); 125 List<OrderItem> orderItems = entry.getValue();
126 DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); 126 DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId);
@@ -128,30 +128,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -128,30 +128,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
128 log.error("[calculateDeliveryPrice][不能计算快递运费,找不到 templateId({}) 对应的运费模板配置]", templateId); 128 log.error("[calculateDeliveryPrice][不能计算快递运费,找不到 templateId({}) 对应的运费模板配置]", templateId);
129 continue; 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 continue; 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 TradePriceCalculatorHelper.recountAllPrice(result); 138 TradePriceCalculatorHelper.recountAllPrice(result);
157 } 139 }
@@ -159,73 +141,44 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -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 * @param chargeMode 配送计费方式 145 * @param chargeMode 配送计费方式
166 * @param templateCharge 快递运费配置 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 if (templateCharge == null) { 150 if (templateCharge == null) {
173 log.error("[calculateExpressFeeByChargeMode][计算快递运费时,找不到 SKU({}) 对应的运费模版]", orderItems); 151 log.error("[calculateExpressFeeByChargeMode][计算快递运费时,找不到 SKU({}) 对应的运费模版]", orderItems);
174 return; 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 int deliveryPrice; 156 int deliveryPrice;
202 - if (total <= templateCharge.getStartCount()) { 157 + if (totalChargeValue <= templateCharge.getStartCount()) {
203 deliveryPrice = templateCharge.getStartPrice(); 158 deliveryPrice = templateCharge.getStartPrice();
204 } else { 159 } else {
205 - double remainWeight = total - templateCharge.getStartCount(); 160 + double remainWeight = totalChargeValue - templateCharge.getStartCount();
206 // 剩余重量/ 续件 = 续件的次数. 向上取整 161 // 剩余重量/ 续件 = 续件的次数. 向上取整
207 int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount()); 162 int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount());
208 int extraPrice = templateCharge.getExtraPrice() * extraNum; 163 int extraPrice = templateCharge.getExtraPrice() * extraNum;
209 deliveryPrice = templateCharge.getStartPrice() + extraPrice; 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 TradePriceCalculatorHelper.recountPayPrice(item); 182 TradePriceCalculatorHelper.recountPayPrice(item);
230 } 183 }
231 } 184 }
@@ -234,42 +187,38 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { @@ -234,42 +187,38 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
234 * 检查是否包邮 187 * 检查是否包邮
235 * 188 *
236 * @param chargeMode 配送计费方式 189 * @param chargeMode 配送计费方式
237 - * @param totalCount 总件数  
238 - * @param totalWeight 总重量  
239 - * @param totalVolume 总体积  
240 - * @param totalPrice 总金额  
241 * @param templateFree 包邮配置 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 if (templateFree == null) { 194 if (templateFree == null) {
246 return false; 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 DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); 211 DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode);
249 switch (chargeModeEnum) { 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 case WEIGHT: 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 case VOLUME: 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,7 +91,7 @@ public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest {
91 item -> item.setFreeCount(20).setFreePrice(100)); 91 item -> item.setFreeCount(20).setFreePrice(100));
92 // 准备 SP 运费模板数据 92 // 准备 SP 运费模板数据
93 templateRespBO = randomPojo(DeliveryExpressTemplateRespBO.class, 93 templateRespBO = randomPojo(DeliveryExpressTemplateRespBO.class,
94 - item -> item.setChargeMode(DeliveryExpressChargeModeEnum.PIECE.getType()) 94 + item -> item.setChargeMode(DeliveryExpressChargeModeEnum.COUNT.getType())
95 .setCharge(chargeBO).setFree(freeBO)); 95 .setCharge(chargeBO).setFree(freeBO));
96 } 96 }
97 97
@@ -144,11 +144,11 @@ public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest { @@ -144,11 +144,11 @@ public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest {
144 // 断言:SKU1 144 // 断言:SKU1
145 assertThat(resultBO.getItems().get(0)) 145 assertThat(resultBO.getItems().get(0))
146 .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") 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 // 断言:SKU2 148 // 断言:SKU2
149 assertThat(resultBO.getItems().get(1)) 149 assertThat(resultBO.getItems().get(1))
150 .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") 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 // 断言:SKU3 未选中 152 // 断言:SKU3 未选中
153 assertThat(resultBO.getItems().get(2)) 153 assertThat(resultBO.getItems().get(2))
154 .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") 154 .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice")