Эх сурвалжийг харах

price:完成满减送的价格计算~

YunaiV 2 жил өмнө
parent
commit
160d619d59

+ 4 - 0
yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/api/price/dto/PriceCalculateRespDTO.java

@@ -103,6 +103,10 @@ public class PriceCalculateRespDTO {
     @Data
     @Data
     public static class OrderItem {
     public static class OrderItem {
 
 
+        /**
+         * SPU 编号
+         */
+        private Long spuId;
         /**
         /**
          * SKU 编号
          * SKU 编号
          */
          */

+ 4 - 3
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/convert/price/PriceConvert.java

@@ -30,9 +30,10 @@ public interface PriceConvert {
         skuList.forEach(sku -> {
         skuList.forEach(sku -> {
             Integer count = skuIdCountMap.get(sku.getId());
             Integer count = skuIdCountMap.get(sku.getId());
             PriceCalculateRespDTO.OrderItem orderItem = new PriceCalculateRespDTO.OrderItem()
             PriceCalculateRespDTO.OrderItem orderItem = new PriceCalculateRespDTO.OrderItem()
-                    .setSkuId(sku.getId()).setCount(count).setOriginalUnitPrice(sku.getPrice())
-                    .setOriginalPrice(sku.getPrice() * count).setDiscountPrice(0).setOrderPartPrice(0);
-            orderItem.setPayPrice(orderItem.getOriginalPrice()).setOrderDividePrice(orderItem.getOrderDividePrice());
+                    .setSpuId(sku.getSpuId()).setSkuId(sku.getId()).setCount(count)
+                    .setOriginalUnitPrice(sku.getPrice()).setOriginalPrice(sku.getPrice() * count)
+                    .setDiscountPrice(0).setOrderPartPrice(0);
+            orderItem.setPayPrice(orderItem.getOriginalPrice()).setOrderDividePrice(orderItem.getOriginalPrice());
             priceCalculate.getOrder().getItems().add(orderItem);
             priceCalculate.getOrder().getItems().add(orderItem);
             // 补充价格信息到 Order 中
             // 补充价格信息到 Order 中
             order.setOriginalPrice(order.getOriginalPrice() + orderItem.getOriginalPrice()).setPayPrice(order.getOriginalPrice());
             order.setOriginalPrice(order.getOriginalPrice() + orderItem.getOriginalPrice()).setPayPrice(order.getOriginalPrice());

+ 2 - 2
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/dal/dataobject/reward/RewardActivityDO.java

@@ -92,7 +92,7 @@ public class RewardActivityDO extends BaseDO {
         /**
         /**
          * 优惠价格,单位:分
          * 优惠价格,单位:分
          */
          */
-        private Integer promotionPrice;
+        private Integer discountPrice;
         /**
         /**
          * 是否包邮
          * 是否包邮
          */
          */
@@ -100,7 +100,7 @@ public class RewardActivityDO extends BaseDO {
         /**
         /**
          * 赠送的积分
          * 赠送的积分
          */
          */
-        private Integer integral;
+        private Integer point;
         /**
         /**
          * 赠送的优惠劵编号的数组
          * 赠送的优惠劵编号的数组
          */
          */

+ 228 - 12
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceServiceImpl.java

@@ -1,14 +1,19 @@
 package cn.iocoder.yudao.module.market.service.price;
 package cn.iocoder.yudao.module.market.service.price;
 
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
 import cn.iocoder.yudao.module.market.convert.price.PriceConvert;
 import cn.iocoder.yudao.module.market.convert.price.PriceConvert;
 import cn.iocoder.yudao.module.market.dal.dataobject.discount.DiscountProductDO;
 import cn.iocoder.yudao.module.market.dal.dataobject.discount.DiscountProductDO;
+import cn.iocoder.yudao.module.market.dal.dataobject.reward.RewardActivityDO;
+import cn.iocoder.yudao.module.market.enums.common.PromotionConditionTypeEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionLevelEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionLevelEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.market.service.discount.DiscountService;
 import cn.iocoder.yudao.module.market.service.discount.DiscountService;
+import cn.iocoder.yudao.module.market.service.reward.RewardService;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import com.google.common.base.Suppliers;
 import com.google.common.base.Suppliers;
@@ -16,12 +21,15 @@ import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
 
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonList;
 
 
@@ -32,6 +40,11 @@ import static java.util.Collections.singletonList;
  * 参考文档:
  * 参考文档:
  * 1. <a href="https://help.youzan.com/displaylist/detail_4_4-1-60384">有赞文档:限时折扣、满减送、优惠券哪个优先计算?</a>
  * 1. <a href="https://help.youzan.com/displaylist/detail_4_4-1-60384">有赞文档:限时折扣、满减送、优惠券哪个优先计算?</a>
  *
  *
+ * TODO 芋艿:进一步完善
+ * 1. 限时折扣:指定金额、减免金额、折扣
+ * 2. 满减送:循环、折扣
+ * 3.
+ *
  * @author 芋道源码
  * @author 芋道源码
  */
  */
 @Service
 @Service
@@ -40,6 +53,8 @@ public class PriceServiceImpl implements PriceService {
 
 
     @Resource
     @Resource
     private DiscountService discountService;
     private DiscountService discountService;
+    @Resource
+    private RewardService rewardService;
 
 
     @Resource
     @Resource
     private ProductSkuApi productSkuApi;
     private ProductSkuApi productSkuApi;
@@ -53,7 +68,8 @@ public class PriceServiceImpl implements PriceService {
 
 
         // 计算商品级别的价格
         // 计算商品级别的价格
         calculatePriceForSkuLevel(calculateReqDTO.getUserId(), priceCalculate);
         calculatePriceForSkuLevel(calculateReqDTO.getUserId(), priceCalculate);
-        // 计算【满减送】促销 TODO 待实现
+        // 计算订单级别的价格
+        calculatePriceForOrderLevel(calculateReqDTO.getUserId(), priceCalculate);
         // 计算【优惠劵】促销 TODO 待实现
         // 计算【优惠劵】促销 TODO 待实现
         return priceCalculate;
         return priceCalculate;
     }
     }
@@ -75,10 +91,12 @@ public class PriceServiceImpl implements PriceService {
         return skus;
         return skus;
     }
     }
 
 
+    // ========== 计算商品级别的价格 ==========
+
     /**
     /**
      * 计算商品级别的价格,例如说:
      * 计算商品级别的价格,例如说:
      * 1. 会员折扣
      * 1. 会员折扣
-     * 2. 限时折扣
+     * 2. 限时折扣 {@link cn.iocoder.yudao.module.market.dal.dataobject.discount.DiscountActivityDO}
      *
      *
      * 其中,会员折扣、限时折扣取最低价
      * 其中,会员折扣、限时折扣取最低价
      *
      *
@@ -138,10 +156,126 @@ public class PriceServiceImpl implements PriceService {
         modifyOrderItemPayPrice(orderItem, promotionPrice, priceCalculate);
         modifyOrderItemPayPrice(orderItem, promotionPrice, priceCalculate);
     }
     }
 
 
+    // TODO 芋艿:提前实现
+    private Supplier<Double> getMemberDiscountSupplier(Long userId) {
+        return Suppliers.memoize(() -> {
+            if (userId == 1) {
+                return 90d;
+            }
+            if (userId == 2) {
+                return 80d;
+            }
+            return null; // 无优惠
+        });
+    }
+
+    // ========== 计算商品级别的价格 ==========
+
+    /**
+     * 计算订单级别的价格,例如说:
+     * 1. 满减送 {@link cn.iocoder.yudao.module.market.dal.dataobject.reward.RewardActivityDO}
+     *
+     * @param userId 用户编号
+     * @param priceCalculate 价格计算的结果
+     */
+    @SuppressWarnings("unused")
+    private void calculatePriceForOrderLevel(Long userId, PriceCalculateRespDTO priceCalculate) {
+        // 获取 SKU 级别的所有优惠信息
+        Set<Long> spuIds = convertSet(priceCalculate.getOrder().getItems(), PriceCalculateRespDTO.OrderItem::getSpuId);
+        Map<RewardActivityDO, Set<Long>> rewardActivities = rewardService.getMatchRewardActivities(spuIds);
+
+        // 处理满减送活动
+        if (CollUtil.isNotEmpty(rewardActivities)) {
+            rewardActivities.forEach((rewardActivity, activitySpuIds) -> {
+                List<PriceCalculateRespDTO.OrderItem> orderItems = CollectionUtils.filterList(priceCalculate.getOrder().getItems(),
+                        orderItem -> CollUtil.contains(activitySpuIds, orderItem.getSpuId()));
+                calculatePriceByRewardActivity(priceCalculate, orderItems, rewardActivity);
+            });
+        }
+    }
+
+    private void calculatePriceByRewardActivity(PriceCalculateRespDTO priceCalculate, List<PriceCalculateRespDTO.OrderItem> orderItems,
+                                                RewardActivityDO rewardActivity) {
+        // 获得最大匹配的满减送活动的规格
+        RewardActivityDO.Rule rule = getLastMatchRewardActivityRule(rewardActivity, orderItems);
+        if (rule == null) {
+            // 获取不到的情况下,记录不满足的优惠明细
+            addNotMeetPromotion(priceCalculate, orderItems, rewardActivity.getId(), rewardActivity.getName(),
+                    PromotionTypeEnum.REWARD_ACTIVITY.getType(), PromotionLevelEnum.ORDER.getLevel(),
+                    getRewardActivityNotMeetTip(rewardActivity));
+            return;
+        }
+
+        // 分摊金额
+        // TODO 芋艿:limit 不能超过最大价格
+        List<Integer> discountPartPrices = dividePrice(orderItems, rule.getDiscountPrice());
+        // 记录优惠明细
+        addPromotion(priceCalculate, orderItems, rewardActivity.getId(), rewardActivity.getName(),
+                PromotionTypeEnum.REWARD_ACTIVITY.getType(), PromotionLevelEnum.ORDER.getLevel(), discountPartPrices,
+                true, StrUtil.format("满减送:省 {} 元", formatPrice(rule.getDiscountPrice())));
+        // 修改 SKU 的分摊
+        for (int i = 0; i < orderItems.size(); i++) {
+            modifyOrderItemOrderPartPriceFromDiscountPrice(orderItems.get(i), discountPartPrices.get(i), priceCalculate);
+        }
+    }
+
+    /**
+     * 获得最大匹配的满减送活动的规格
+     *
+     * @param rewardActivity 满减送活动
+     * @param orderItems 商品项
+     * @return 匹配的活动规格
+     */
+    private RewardActivityDO.Rule getLastMatchRewardActivityRule(RewardActivityDO rewardActivity,
+                                                                 List<PriceCalculateRespDTO.OrderItem> orderItems) {
+        Integer count = CollectionUtils.getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getCount, Integer::sum);
+        // price 的计算逻辑,使用 orderDividePrice 的原因,主要考虑分摊后,这个才是该 SKU 当前真实的支付总价
+        Integer price = CollectionUtils.getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
+        assert count != null && price != null;
+        for (int i = rewardActivity.getRules().size() - 1; i >= 0; i--) {
+            RewardActivityDO.Rule rule = rewardActivity.getRules().get(i);
+            if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())
+                    && price >= rule.getLimit()) {
+                return rule;
+            }
+            if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())
+                    && count >= rule.getLimit()) {
+                return rule;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获得满减送活动部匹配时的提示
+     *
+     * @param rewardActivity 满减送活动
+     * @return 提示
+     */
+    private String getRewardActivityNotMeetTip(RewardActivityDO rewardActivity) {
+        return "TODO"; // TODO 芋艿:后面再想想
+    }
+
+    // ========== 其它相对通用的方法 ==========
+
+    /**
+     * 添加单个 OrderItem 的营销明细
+     *
+     * @param priceCalculate 价格计算结果
+     * @param orderItem 单个订单商品 SKU
+     * @param id 营销编号
+     * @param name 营销名字
+     * @param type 营销类型
+     * @param level 营销级别
+     * @param newPayPrice 新的单实付金额(总)
+     * @param meet 是否满足优惠条件
+     * @param meetTip 满足条件的提示
+     */
     private void addPromotion(PriceCalculateRespDTO priceCalculate, PriceCalculateRespDTO.OrderItem orderItem,
     private void addPromotion(PriceCalculateRespDTO priceCalculate, PriceCalculateRespDTO.OrderItem orderItem,
                               Long id, String name, Integer type, Integer level,
                               Long id, String name, Integer type, Integer level,
                               Integer newPayPrice, Boolean meet, String meetTip) {
                               Integer newPayPrice, Boolean meet, String meetTip) {
         // 创建营销明细 Item
         // 创建营销明细 Item
+        // TODO 芋艿:orderItem.getPayPrice() 要不要改成 orderDividePrice;同时,newPayPrice 要不要改成直接传递 discountPrice
         PriceCalculateRespDTO.PromotionItem promotionItem = new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId())
         PriceCalculateRespDTO.PromotionItem promotionItem = new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId())
                 .setOriginalPrice(orderItem.getPayPrice()).setDiscountPrice(orderItem.getPayPrice() - newPayPrice);
                 .setOriginalPrice(orderItem.getPayPrice()).setDiscountPrice(orderItem.getPayPrice() - newPayPrice);
         // 创建营销明细
         // 创建营销明细
@@ -152,6 +286,60 @@ public class PriceServiceImpl implements PriceService {
         priceCalculate.getPromotions().add(promotion);
         priceCalculate.getPromotions().add(promotion);
     }
     }
 
 
+    /**
+     * 添加多个 OrderItem 的营销明细
+     *
+     * @param priceCalculate 价格计算结果
+     * @param orderItems 多个订单商品 SKU
+     * @param id 营销编号
+     * @param name 营销名字
+     * @param type 营销类型
+     * @param level 营销级别
+     * @param discountPrices 多个订单商品 SKU 的优惠价格(总),和 orderItems 一一对应
+     * @param meet 是否满足优惠条件
+     * @param meetTip 满足条件的提示
+     */
+    private void addPromotion(PriceCalculateRespDTO priceCalculate, List<PriceCalculateRespDTO.OrderItem> orderItems,
+                              Long id, String name, Integer type, Integer level,
+                              List<Integer> discountPrices, Boolean meet, String meetTip) {
+        // 创建营销明细 Item
+        List<PriceCalculateRespDTO.PromotionItem> promotionItems = new ArrayList<>(discountPrices.size());
+        for (int i = 0; i < orderItems.size(); i++) {
+            PriceCalculateRespDTO.OrderItem orderItem = orderItems.get(i);
+            promotionItems.add(new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId())
+                    .setOriginalPrice(orderItem.getPayPrice()).setDiscountPrice(discountPrices.get(i)));
+        }
+        // 创建营销明细
+        PriceCalculateRespDTO.Promotion promotion = new PriceCalculateRespDTO.Promotion()
+                .setId(id).setName(name).setType(type).setLevel(level)
+                .setOriginalPrice(getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum))
+                .setDiscountPrice(getSumValue(discountPrices, value -> value, Integer::sum))
+                .setItems(promotionItems).setMeet(meet).setMeetTip(meetTip);
+        priceCalculate.getPromotions().add(promotion);
+    }
+
+    private void addNotMeetPromotion(PriceCalculateRespDTO priceCalculate, List<PriceCalculateRespDTO.OrderItem> orderItems,
+                                     Long id, String name, Integer type, Integer level, String meetTip) {
+        // 创建营销明细 Item
+        List<PriceCalculateRespDTO.PromotionItem> promotionItems = CollectionUtils.convertList(orderItems,
+                orderItem -> new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId())
+                        .setOriginalPrice(orderItem.getOrderDividePrice()).setDiscountPrice(0));
+        // 创建营销明细
+        Integer originalPrice = CollectionUtils.getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
+        PriceCalculateRespDTO.Promotion promotion = new PriceCalculateRespDTO.Promotion()
+                .setId(id).setName(name).setType(type).setLevel(level)
+                .setOriginalPrice(originalPrice).setDiscountPrice(0)
+                .setItems(promotionItems).setMeet(false).setMeetTip(meetTip);
+        priceCalculate.getPromotions().add(promotion);
+    }
+
+    /**
+     * 修改 OrderItem 的 payPrice 价格,同时会修改 Order 的 payPrice 价格
+     *
+     * @param orderItem 订单商品 SKU
+     * @param newPayPrice 新的 payPrice 价格
+     * @param priceCalculate 价格计算结果
+     */
     private void modifyOrderItemPayPrice(PriceCalculateRespDTO.OrderItem orderItem, Integer newPayPrice,
     private void modifyOrderItemPayPrice(PriceCalculateRespDTO.OrderItem orderItem, Integer newPayPrice,
                                          PriceCalculateRespDTO priceCalculate) {
                                          PriceCalculateRespDTO priceCalculate) {
         int diffPayPrice = orderItem.getPayPrice() - newPayPrice;
         int diffPayPrice = orderItem.getPayPrice() - newPayPrice;
@@ -163,17 +351,45 @@ public class PriceServiceImpl implements PriceService {
         priceCalculate.getOrder().setPayPrice(priceCalculate.getOrder().getPayPrice() - diffPayPrice);
         priceCalculate.getOrder().setPayPrice(priceCalculate.getOrder().getPayPrice() - diffPayPrice);
     }
     }
 
 
-    // TODO 芋艿:提前实现
-    private Supplier<Double> getMemberDiscountSupplier(Long userId) {
-        return Suppliers.memoize(() -> {
-            if (userId == 1) {
-                return 90d;
-            }
-            if (userId == 2) {
-                return 80d;
+    /**
+     * 修改 OrderItem 的 orderPartPrice 价格,同时会修改 Order 的 discountPrice 价格
+     *
+     * 本质:分摊 Order 的 discountPrice 价格,到对应的 OrderItem 的 orderPartPrice 价格中
+     *
+     * @param orderItem 订单商品 SKU
+     * @param addOrderPartPrice 新增的
+     * @param priceCalculate 价格计算结果
+     */
+    private void modifyOrderItemOrderPartPriceFromDiscountPrice(PriceCalculateRespDTO.OrderItem orderItem, Integer addOrderPartPrice,
+                                                                PriceCalculateRespDTO priceCalculate) {
+        // 设置 OrderItem 价格相关字段
+        orderItem.setOrderPartPrice(orderItem.getOrderPartPrice() + addOrderPartPrice);
+        orderItem.setOrderDividePrice(orderItem.getPayPrice() - orderItem.getOrderPartPrice());
+        // 设置 Order 相关相关字段
+        PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
+        order.setDiscountPrice(order.getDiscountPrice() + addOrderPartPrice);
+        order.setPayPrice(order.getPayPrice() - addOrderPartPrice);
+    }
+
+    private List<Integer> dividePrice(List<PriceCalculateRespDTO.OrderItem> orderItems, Integer price) {
+        List<Integer> prices = new ArrayList<>(orderItems.size());
+        Integer total = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
+        assert total != null;
+        int remainPrice = price;
+        // 遍历每一个,进行分摊
+        for (int i = 0; i < orderItems.size(); i++) {
+            PriceCalculateRespDTO.OrderItem orderItem = orderItems.get(i);
+            int partPrice;
+            if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减
+                partPrice = (int) (price * (1.0D * orderItem.getOrderDividePrice() / total));
+                remainPrice -= partPrice;
+            } else {
+                partPrice = remainPrice;
             }
             }
-            return null; // 无优惠
-        });
+            Assert.isTrue(partPrice > 0, "分摊金额必须大于 0");
+            prices.add(partPrice);
+        }
+        return prices;
     }
     }
 
 
     private String formatPrice(Integer price) {
     private String formatPrice(Integer price) {

+ 23 - 0
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/reward/RewardService.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.market.service.reward;
+
+import cn.iocoder.yudao.module.market.dal.dataobject.reward.RewardActivityDO;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 满减送 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface RewardService {
+
+    /**
+     * 基于指定的 SPU 编号数组,获得它们匹配的满减送活动
+     *
+     * @param spuIds SPU 编号数组
+     * @return 满减送活动,与对应的 SPU 编号的映射。即,value 就是 SPU 编号的集合
+     */
+    Map<RewardActivityDO, Set<Long>> getMatchRewardActivities(Set<Long> spuIds);
+
+}

+ 26 - 0
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/reward/RewardServiceImpl.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.market.service.reward;
+
+import cn.iocoder.yudao.module.market.dal.dataobject.reward.RewardActivityDO;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 满减送 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class RewardServiceImpl implements RewardService {
+
+    // TODO 芋艿:待实现
+    @Override
+    public Map<RewardActivityDO, Set<Long>> getMatchRewardActivities(Set<Long> spuIds) {
+        return Collections.emptyMap();
+    }
+
+}

+ 191 - 4
yudao-module-mall/yudao-module-market-biz/src/test/java/cn/iocoder/yudao/module/market/service/price/PriceServiceTest.java

@@ -1,20 +1,27 @@
 package cn.iocoder.yudao.module.market.service.price;
 package cn.iocoder.yudao.module.market.service.price;
 
 
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.map.MapUtil;
-import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
 import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
 import cn.iocoder.yudao.module.market.dal.dataobject.discount.DiscountProductDO;
 import cn.iocoder.yudao.module.market.dal.dataobject.discount.DiscountProductDO;
+import cn.iocoder.yudao.module.market.dal.dataobject.reward.RewardActivityDO;
+import cn.iocoder.yudao.module.market.enums.common.PromotionConditionTypeEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionLevelEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionLevelEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.market.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.market.service.discount.DiscountService;
 import cn.iocoder.yudao.module.market.service.discount.DiscountService;
+import cn.iocoder.yudao.module.market.service.reward.RewardService;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mock;
 
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static java.util.Arrays.asList;
 import static java.util.Arrays.asList;
@@ -36,6 +43,8 @@ public class PriceServiceTest extends BaseMockitoUnitTest {
     @Mock
     @Mock
     private DiscountService discountService;
     private DiscountService discountService;
     @Mock
     @Mock
+    private RewardService rewardService;
+    @Mock
     private ProductSkuApi productSkuApi;
     private ProductSkuApi productSkuApi;
 
 
     @Test
     @Test
@@ -46,7 +55,7 @@ public class PriceServiceTest extends BaseMockitoUnitTest {
                 .setItems(singletonList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2)));
                 .setItems(singletonList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2)));
         // mock 方法(商品 SKU 信息)
         // mock 方法(商品 SKU 信息)
         ProductSkuRespDTO productSku = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100));
         ProductSkuRespDTO productSku = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100));
-        when(productSkuApi.getSkuList(eq(SetUtils.asSet(10L)))).thenReturn(singletonList(productSku));
+        when(productSkuApi.getSkuList(eq(asSet(10L)))).thenReturn(singletonList(productSku));
 
 
         // 调用
         // 调用
         PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
         PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
@@ -96,13 +105,13 @@ public class PriceServiceTest extends BaseMockitoUnitTest {
         // mock 方法(商品 SKU 信息)
         // mock 方法(商品 SKU 信息)
         ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100));
         ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100));
         ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50));
         ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50));
-        when(productSkuApi.getSkuList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList(productSku01, productSku02));
+        when(productSkuApi.getSkuList(eq(asSet(10L, 20L)))).thenReturn(asList(productSku01, productSku02));
         // mock 方法(限时折扣 DiscountActivity 信息)
         // mock 方法(限时折扣 DiscountActivity 信息)
         DiscountProductDO discountProduct01 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(1000L).setActivityName("活动 1000 号")
         DiscountProductDO discountProduct01 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(1000L).setActivityName("活动 1000 号")
                 .setSkuId(10L).setPromotionPrice(80));
                 .setSkuId(10L).setPromotionPrice(80));
         DiscountProductDO discountProduct02 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(2000L).setActivityName("活动 2000 号")
         DiscountProductDO discountProduct02 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(2000L).setActivityName("活动 2000 号")
                 .setSkuId(20L).setPromotionPrice(40));
                 .setSkuId(20L).setPromotionPrice(40));
-        when(discountService.getMatchDiscountProducts(eq(SetUtils.asSet(10L, 20L)))).thenReturn(
+        when(discountService.getMatchDiscountProducts(eq(asSet(10L, 20L)))).thenReturn(
                 MapUtil.builder(10L, discountProduct01).put(20L, discountProduct02).map());
                 MapUtil.builder(10L, discountProduct01).put(20L, discountProduct02).map());
 
 
         // 调用
         // 调用
@@ -167,4 +176,182 @@ public class PriceServiceTest extends BaseMockitoUnitTest {
         assertEquals(promotionItem02.getDiscountPrice(), 30);
         assertEquals(promotionItem02.getDiscountPrice(), 30);
     }
     }
 
 
+    /**
+     * 测试满减送活动,匹配的情况
+     */
+    @Test
+    public void testCalculatePrice_rewardActivity() {
+        // 准备参数
+        PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(randomLongId())
+                .setItems(asList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2),
+                        new PriceCalculateReqDTO.Item().setSkuId(20L).setCount(3),
+                        new PriceCalculateReqDTO.Item().setSkuId(30L).setCount(4)));
+        // mock 方法(商品 SKU 信息)
+        ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100).setSpuId(1L));
+        ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50).setSpuId(2L));
+        ProductSkuRespDTO productSku03 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(30L).setPrice(30).setSpuId(3L));
+        when(productSkuApi.getSkuList(eq(asSet(10L, 20L, 30L)))).thenReturn(asList(productSku01, productSku02, productSku03));
+        // mock 方法(限时折扣 DiscountActivity 信息)
+        RewardActivityDO rewardActivity01 = randomPojo(RewardActivityDO.class, o -> o.setId(1000L).setName("活动 1000 号")
+                .setSpuIds(asList(10L, 20L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
+                .setRules(singletonList(new RewardActivityDO.Rule().setLimit(200).setDiscountPrice(70))));
+        RewardActivityDO rewardActivity02 = randomPojo(RewardActivityDO.class, o -> o.setId(2000L).setName("活动 2000 号")
+                .setSpuIds(singletonList(30L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType())
+                .setRules(asList(new RewardActivityDO.Rule().setLimit(1).setDiscountPrice(10),
+                        new RewardActivityDO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个
+                        new RewardActivityDO.Rule().setLimit(10).setDiscountPrice(100))));
+        Map<RewardActivityDO, Set<Long>> matchRewardActivities = new LinkedHashMap<>();
+        matchRewardActivities.put(rewardActivity01, asSet(1L, 2L));
+        matchRewardActivities.put(rewardActivity02, asSet(3L));
+        when(rewardService.getMatchRewardActivities(eq(asSet(1L, 2L, 3L)))).thenReturn(matchRewardActivities);
+
+        // 调用
+        PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
+        // 断言 Order 部分
+        PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
+        assertEquals(order.getOriginalPrice(), 470);
+        assertEquals(order.getDiscountPrice(), 130);
+        assertEquals(order.getPointPrice(), 0);
+        assertEquals(order.getDeliveryPrice(), 0);
+        assertEquals(order.getPayPrice(), 340);
+        assertNull(order.getCouponId());
+        // 断言 OrderItem 部分
+        assertEquals(order.getItems().size(), 3);
+        PriceCalculateRespDTO.OrderItem orderItem01 = order.getItems().get(0);
+        assertEquals(orderItem01.getSkuId(), 10L);
+        assertEquals(orderItem01.getCount(), 2);
+        assertEquals(orderItem01.getOriginalPrice(), 200);
+        assertEquals(orderItem01.getOriginalUnitPrice(), 100);
+        assertEquals(orderItem01.getDiscountPrice(), 0);
+        assertEquals(orderItem01.getPayPrice(), 200);
+        assertEquals(orderItem01.getOrderPartPrice(), 40);
+        assertEquals(orderItem01.getOrderDividePrice(), 160);
+        PriceCalculateRespDTO.OrderItem orderItem02 = order.getItems().get(1);
+        assertEquals(orderItem02.getSkuId(), 20L);
+        assertEquals(orderItem02.getCount(), 3);
+        assertEquals(orderItem02.getOriginalPrice(), 150);
+        assertEquals(orderItem02.getOriginalUnitPrice(), 50);
+        assertEquals(orderItem02.getDiscountPrice(), 0);
+        assertEquals(orderItem02.getPayPrice(), 150);
+        assertEquals(orderItem02.getOrderPartPrice(), 30);
+        assertEquals(orderItem02.getOrderDividePrice(), 120);
+        PriceCalculateRespDTO.OrderItem orderItem03 = order.getItems().get(2);
+        assertEquals(orderItem03.getSkuId(), 30L);
+        assertEquals(orderItem03.getCount(), 4);
+        assertEquals(orderItem03.getOriginalPrice(), 120);
+        assertEquals(orderItem03.getOriginalUnitPrice(), 30);
+        assertEquals(orderItem03.getDiscountPrice(), 0);
+        assertEquals(orderItem03.getPayPrice(), 120);
+        assertEquals(orderItem03.getOrderPartPrice(), 60);
+        assertEquals(orderItem03.getOrderDividePrice(), 60);
+        // 断言 Promotion 部分(第一个)
+        assertEquals(priceCalculate.getPromotions().size(), 2);
+        PriceCalculateRespDTO.Promotion promotion01 = priceCalculate.getPromotions().get(0);
+        assertEquals(promotion01.getId(), 1000L);
+        assertEquals(promotion01.getName(), "活动 1000 号");
+        assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
+        assertEquals(promotion01.getLevel(), PromotionLevelEnum.ORDER.getLevel());
+        assertEquals(promotion01.getOriginalPrice(), 350);
+        assertEquals(promotion01.getDiscountPrice(), 70);
+        assertTrue(promotion01.getMeet());
+        assertEquals(promotion01.getMeetTip(), "满减送:省 0.70 元");
+        assertEquals(promotion01.getItems().size(), 2);
+        PriceCalculateRespDTO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
+        assertEquals(promotionItem011.getSkuId(), 10L);
+        assertEquals(promotionItem011.getOriginalPrice(), 200);
+        assertEquals(promotionItem011.getDiscountPrice(), 40);
+        PriceCalculateRespDTO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
+        assertEquals(promotionItem012.getSkuId(), 20L);
+        assertEquals(promotionItem012.getOriginalPrice(), 150);
+        assertEquals(promotionItem012.getDiscountPrice(), 30);
+        // 断言 Promotion 部分(第二个)
+        PriceCalculateRespDTO.Promotion promotion02 = priceCalculate.getPromotions().get(1);
+        assertEquals(promotion02.getId(), 2000L);
+        assertEquals(promotion02.getName(), "活动 2000 号");
+        assertEquals(promotion02.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
+        assertEquals(promotion02.getLevel(), PromotionLevelEnum.ORDER.getLevel());
+        assertEquals(promotion02.getOriginalPrice(), 120);
+        assertEquals(promotion02.getDiscountPrice(), 60);
+        assertTrue(promotion02.getMeet());
+        assertEquals(promotion02.getMeetTip(), "满减送:省 0.60 元");
+        PriceCalculateRespDTO.PromotionItem promotionItem02 = promotion02.getItems().get(0);
+        assertEquals(promotion02.getItems().size(), 1);
+        assertEquals(promotionItem02.getSkuId(), 30L);
+        assertEquals(promotionItem02.getOriginalPrice(), 120);
+        assertEquals(promotionItem02.getDiscountPrice(), 60);
+    }
+
+    /**
+     * 测试满减送活动,不匹配的情况
+     */
+    @Test
+    public void testCalculatePrice_rewardActivityNotMeet() {
+        // 准备参数
+        PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(randomLongId())
+                .setItems(asList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2),
+                        new PriceCalculateReqDTO.Item().setSkuId(20L).setCount(3)));
+        // mock 方法(商品 SKU 信息)
+        ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100).setSpuId(1L));
+        ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50).setSpuId(2L));
+        when(productSkuApi.getSkuList(eq(asSet(10L, 20L)))).thenReturn(asList(productSku01, productSku02));
+        // mock 方法(限时折扣 DiscountActivity 信息)
+        RewardActivityDO rewardActivity01 = randomPojo(RewardActivityDO.class, o -> o.setId(1000L).setName("活动 1000 号")
+                .setSpuIds(asList(10L, 20L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
+                .setRules(singletonList(new RewardActivityDO.Rule().setLimit(351).setDiscountPrice(70))));
+        Map<RewardActivityDO, Set<Long>> matchRewardActivities = new LinkedHashMap<>();
+        matchRewardActivities.put(rewardActivity01, asSet(1L, 2L));
+        when(rewardService.getMatchRewardActivities(eq(asSet(1L, 2L)))).thenReturn(matchRewardActivities);
+
+        // 调用
+        PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
+        // 断言 Order 部分
+        PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
+        assertEquals(order.getOriginalPrice(), 350);
+        assertEquals(order.getDiscountPrice(), 0);
+        assertEquals(order.getPointPrice(), 0);
+        assertEquals(order.getDeliveryPrice(), 0);
+        assertEquals(order.getPayPrice(), 350);
+        assertNull(order.getCouponId());
+        // 断言 OrderItem 部分
+        assertEquals(order.getItems().size(), 2);
+        PriceCalculateRespDTO.OrderItem orderItem01 = order.getItems().get(0);
+        assertEquals(orderItem01.getSkuId(), 10L);
+        assertEquals(orderItem01.getCount(), 2);
+        assertEquals(orderItem01.getOriginalPrice(), 200);
+        assertEquals(orderItem01.getOriginalUnitPrice(), 100);
+        assertEquals(orderItem01.getDiscountPrice(), 0);
+        assertEquals(orderItem01.getPayPrice(), 200);
+        assertEquals(orderItem01.getOrderPartPrice(), 0);
+        assertEquals(orderItem01.getOrderDividePrice(), 200);
+        PriceCalculateRespDTO.OrderItem orderItem02 = order.getItems().get(1);
+        assertEquals(orderItem02.getSkuId(), 20L);
+        assertEquals(orderItem02.getCount(), 3);
+        assertEquals(orderItem02.getOriginalPrice(), 150);
+        assertEquals(orderItem02.getOriginalUnitPrice(), 50);
+        assertEquals(orderItem02.getDiscountPrice(), 0);
+        assertEquals(orderItem02.getPayPrice(), 150);
+        assertEquals(orderItem02.getOrderPartPrice(), 0);
+        assertEquals(orderItem02.getOrderDividePrice(), 150);
+        // 断言 Promotion 部分
+        assertEquals(priceCalculate.getPromotions().size(), 1);
+        PriceCalculateRespDTO.Promotion promotion01 = priceCalculate.getPromotions().get(0);
+        assertEquals(promotion01.getId(), 1000L);
+        assertEquals(promotion01.getName(), "活动 1000 号");
+        assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
+        assertEquals(promotion01.getLevel(), PromotionLevelEnum.ORDER.getLevel());
+        assertEquals(promotion01.getOriginalPrice(), 350);
+        assertEquals(promotion01.getDiscountPrice(), 0);
+        assertFalse(promotion01.getMeet());
+        assertEquals(promotion01.getMeetTip(), "TODO"); // TODO 芋艿:后面再想想
+        assertEquals(promotion01.getItems().size(), 2);
+        PriceCalculateRespDTO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
+        assertEquals(promotionItem011.getSkuId(), 10L);
+        assertEquals(promotionItem011.getOriginalPrice(), 200);
+        assertEquals(promotionItem011.getDiscountPrice(), 0);
+        PriceCalculateRespDTO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
+        assertEquals(promotionItem012.getSkuId(), 20L);
+        assertEquals(promotionItem012.getOriginalPrice(), 150);
+        assertEquals(promotionItem012.getDiscountPrice(), 0);
+    }
+
 }
 }