diff --git a/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/api/price/dto/PriceCalculateRespDTO.java b/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/api/price/dto/PriceCalculateRespDTO.java index 9cc3596cd..062b1eea3 100644 --- a/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/api/price/dto/PriceCalculateRespDTO.java +++ b/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/api/price/dto/PriceCalculateRespDTO.java @@ -52,7 +52,7 @@ public class PriceCalculateRespDTO { */ private Integer originalPrice; /** - * 商品优惠(总),单位:分 + * 订单优惠(总),单位:分 * * 订单级优惠:对主订单的优惠,常见如:订单满 200 元减 10 元;订单满 80 包邮。 * @@ -101,8 +101,12 @@ public class PriceCalculateRespDTO { * 订单商品 SKU */ @Data - public static class OrderItem extends PriceCalculateReqDTO.Item { + public static class OrderItem { + /** + * SKU 编号 + */ + private Long skuId; /** * 购买数量 */ @@ -141,14 +145,18 @@ public class PriceCalculateRespDTO { /** * 子订单分摊金额(总),单位:分 - * 需要分摊 {@link Order#discountPrice}、{@link Order#couponPrice} + * 需要分摊 {@link Order#discountPrice}、{@link Order#couponPrice}、{@link Order#pointPrice} * * 对应 taobao 的 order.part_mjz_discount 字段 + * 淘宝说明:子订单分摊优惠基础逻辑:一般正常优惠券和满减优惠按照子订单的金额进行分摊,特殊情况如果优惠券是指定商品使用的,只会分摊到对应商品子订单上不分摊。 */ private Integer orderPartPrice; /** * 分摊后子订单实付金额(总),单位:分 * + * = {@link #payPrice} + * - {@link #orderPartPrice} + * * 对应 taobao 的 divide_order_fee 字段 */ private Integer orderDividePrice; @@ -156,7 +164,7 @@ public class PriceCalculateRespDTO { } /** - * 营销活动 + * 营销明细 */ @Data public static class Promotion { @@ -186,11 +194,11 @@ public class PriceCalculateRespDTO { /** * 计算时的原价(总),单位:分 */ - private Integer beforePrice; + private Integer originalPrice; /** * 计算时的优惠(总),单位:分 */ - private Integer afterPrice; + private Integer discountPrice; /** * 匹配的商品 SKU 数组 */ @@ -225,11 +233,11 @@ public class PriceCalculateRespDTO { /** * 计算时的原价(总),单位:分 */ - private Integer beforePrice; + private Integer originalPrice; /** * 计算时的优惠(总),单位:分 */ - private Integer afterPrice; + private Integer discountPrice; } diff --git a/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/enums/common/PromotionTypeEnum.java b/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/enums/common/PromotionTypeEnum.java index d0ce9694a..ed0cc7834 100644 --- a/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/enums/common/PromotionTypeEnum.java +++ b/yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/enums/common/PromotionTypeEnum.java @@ -17,6 +17,8 @@ public enum PromotionTypeEnum implements IntArrayValuable { DISCOUNT_ACTIVITY(1, "限时折扣"), REWARD_ACTIVITY(2, "满减送"), + + MEMBER(3, "会员折扣"), ; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionTypeEnum::getType).toArray(); diff --git a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/convert/price/PriceConvert.java b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/convert/price/PriceConvert.java index 3f85258b9..d83bbca53 100644 --- a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/convert/price/PriceConvert.java +++ b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/convert/price/PriceConvert.java @@ -19,20 +19,23 @@ public interface PriceConvert { default PriceCalculateRespDTO convert(PriceCalculateReqDTO calculateReqDTO, List skuList) { // 创建 PriceCalculateRespDTO 对象 PriceCalculateRespDTO priceCalculate = new PriceCalculateRespDTO(); - priceCalculate.setOrder(new PriceCalculateRespDTO.Order().setOriginalPrice(0).setActivityPrice(0) - .setDeliveryPrice(0).setPayPrice(0).setItems(new ArrayList<>()) - .setCouponId(calculateReqDTO.getCouponId())); - priceCalculate.setPromotions(new ArrayList<>()); + // 创建它的 Order 属性 + PriceCalculateRespDTO.Order order = new PriceCalculateRespDTO.Order().setOriginalPrice(0).setDiscountPrice(0) + .setCouponPrice(0).setPointPrice(0).setDeliveryPrice(0).setPayPrice(0) + .setItems(new ArrayList<>()).setCouponId(calculateReqDTO.getCouponId()); + priceCalculate.setOrder(order).setPromotions(new ArrayList<>()); // 创建它的 OrderItem 属性 Map skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(), PriceCalculateReqDTO.Item::getSkuId, PriceCalculateReqDTO.Item::getCount); skuList.forEach(sku -> { Integer count = skuIdCountMap.get(sku.getId()); - PriceCalculateRespDTO.OrderItem orderItem = new PriceCalculateRespDTO.OrderItem().setCount(count) - .setOriginalUnitPrice(sku.getPrice()).setOriginalPrice(sku.getPrice() * count).setActivityPrice(0); - orderItem.setPayPrice(orderItem.getPayPrice()).setPayUnitPrice(orderItem.getOriginalUnitPrice()) - .setPayPrice(orderItem.getPayPrice()); + 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()); priceCalculate.getOrder().getItems().add(orderItem); + // 补充价格信息到 Order 中 + order.setOriginalPrice(order.getOriginalPrice() + orderItem.getOriginalPrice()).setPayPrice(order.getOriginalPrice()); }); return priceCalculate; } diff --git a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/DiscountService.java b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/DiscountService.java new file mode 100644 index 000000000..12c497d77 --- /dev/null +++ b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/DiscountService.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.market.service.discount; + +import cn.iocoder.yudao.module.market.dal.dataobject.discount.DiscountProductDO; + +import java.util.Collection; +import java.util.Map; + +/** + * 限时折扣 Service 接口 + * + * @author 芋道源码 + */ +public interface DiscountService { + + /** + * 基于指定 SKU 编号数组,获得匹配的限时折扣商品 + * + * 注意,匹配的条件,仅仅是日期符合,并且处于开启状态 + * + * @param skuIds SKU 编号数组 + * @return 匹配的限时折扣商品 + */ + Map getMatchDiscountProducts(Collection skuIds); + +} diff --git a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/DiscountServiceImpl.java b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/DiscountServiceImpl.java new file mode 100644 index 000000000..2b8440daa --- /dev/null +++ b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/DiscountServiceImpl.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.market.service.discount; + +import cn.iocoder.yudao.module.market.dal.dataobject.discount.DiscountProductDO; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * 限时折扣 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class DiscountServiceImpl implements DiscountService { + + // TODO 芋艿:待实现 + @Override + public Map getMatchDiscountProducts(Collection skuIds) { + Map products = new HashMap<>(); + products.put(1L, new DiscountProductDO().setPromotionPrice(100)); + products.put(2L, new DiscountProductDO().setPromotionPrice(50)); + return products; + } + +} diff --git a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/package-info.java b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/package-info.java deleted file mode 100644 index f52febd44..000000000 --- a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/discount/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO 占位 - */ -package cn.iocoder.yudao.module.market.service.discount; diff --git a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceServiceImpl.java b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceServiceImpl.java index 3e70093dd..6b9d60854 100644 --- a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceServiceImpl.java +++ b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceServiceImpl.java @@ -1,47 +1,64 @@ package cn.iocoder.yudao.module.market.service.price; +import cn.hutool.core.util.StrUtil; 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.PriceCalculateRespDTO; 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.enums.common.PromotionLevelEnum; +import cn.iocoder.yudao.module.market.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.market.service.discount.DiscountService; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import com.google.common.base.Suppliers; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import java.util.List; import java.util.Map; +import java.util.function.Supplier; 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.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS; +import static java.util.Collections.singletonList; /** * 价格计算 Service 实现类 * + * 优惠计算顺序:min(限时折扣, 会员折扣) > 满减送 > 优惠券。 + * 参考文档: + * 1. 有赞文档:限时折扣、满减送、优惠券哪个优先计算? + * * @author 芋道源码 */ @Service @Validated public class PriceServiceImpl implements PriceService { + @Resource + private DiscountService discountService; + @Resource private ProductSkuApi productSkuApi; @Override public PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO) { // 获得商品 SKU 数组 - List skuList = checkProductSkus(calculateReqDTO); + List skuList = checkSkus(calculateReqDTO); // 初始化 PriceCalculateRespDTO 对象 PriceCalculateRespDTO priceCalculate = PriceConvert.INSTANCE.convert(calculateReqDTO, skuList); - // 计算【限时折扣】促销 TODO 待实现 + // 计算商品级别的价格 + calculatePriceForSkuLevel(calculateReqDTO.getUserId(), priceCalculate); // 计算【满减送】促销 TODO 待实现 // 计算【优惠劵】促销 TODO 待实现 return priceCalculate; } - private List checkProductSkus(PriceCalculateReqDTO calculateReqDTO) { + private List checkSkus(PriceCalculateReqDTO calculateReqDTO) { // 获得商品 SKU 数组 Map skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(), PriceCalculateReqDTO.Item::getSkuId, PriceCalculateReqDTO.Item::getCount); @@ -58,4 +75,107 @@ public class PriceServiceImpl implements PriceService { return skus; } + /** + * 计算商品级别的价格,例如说: + * 1. 会员折扣 + * 2. 限时折扣 + * + * 其中,会员折扣、限时折扣取最低价 + * + * @param userId 用户编号 + * @param priceCalculate 价格计算的结果 + */ + private void calculatePriceForSkuLevel(Long userId, PriceCalculateRespDTO priceCalculate) { + // 获取 SKU 级别的所有优惠信息 + Supplier memberDiscountSupplier = getMemberDiscountSupplier(userId); + Map discountProducts = discountService.getMatchDiscountProducts( + convertSet(priceCalculate.getOrder().getItems(), PriceCalculateRespDTO.OrderItem::getSkuId)); + + // 处理每个 SKU 的优惠 + priceCalculate.getOrder().getItems().forEach(orderItem -> { + // 获取该 SKU 的优惠信息 + Double memberDiscount = memberDiscountSupplier.get(); + DiscountProductDO discountProduct = discountProducts.get(orderItem.getSkuId()); + if (discountProduct != null // 假设优惠价格更贵,则认为没优惠 + && discountProduct.getPromotionPrice() >= orderItem.getOriginalUnitPrice()) { + discountProduct = null; + } + if (memberDiscount == null && discountProduct == null) { + return; + } + // 计算价格,判断选择哪个折扣 + Integer memberPrice = memberDiscount != null ? (int) (orderItem.getPayPrice() * memberDiscount / 100) : null; + Integer promotionPrice = discountProduct != null ? discountProduct.getPromotionPrice() * orderItem.getCount() : null; + if (memberPrice == null) { + + } else if (promotionPrice == null) { + calculatePriceByMemberDiscount(orderItem, memberDiscount, memberPrice, priceCalculate); + } else if (memberPrice < promotionPrice) { + + } else { + calculatePriceByMemberDiscount(orderItem, memberDiscount, memberPrice, priceCalculate); + } + }); + } + + private void calculatePriceByMemberDiscount(PriceCalculateRespDTO.OrderItem orderItem, + Double memberDiscount, Integer memberPrice, + PriceCalculateRespDTO priceCalculate) { + // 记录优惠明细 + addPromotion(priceCalculate, orderItem, null, + PromotionTypeEnum.MEMBER.getName(), + PromotionTypeEnum.MEMBER.getType(), PromotionLevelEnum.SKU.getLevel(), memberPrice, + true, StrUtil.format("会员折扣:省 {} 元", formatPrice(orderItem.getPayPrice() - memberPrice))); + // 修改 SKU 的优惠 + modifyOrderItemPayPrice(orderItem, memberPrice, priceCalculate); + } + + private void calculatePriceByDiscountActivity(PriceCalculateRespDTO.OrderItem orderItem, + DiscountProductDO discountProduct, Integer promotionPrice, + PriceCalculateRespDTO priceCalculate) { + + } + + private void addPromotion(PriceCalculateRespDTO priceCalculate, PriceCalculateRespDTO.OrderItem orderItem, + Long id, String name, Integer type, Integer level, + Integer newPayPrice, Boolean meet, String meetTip) { + // 创建营销明细 Item + PriceCalculateRespDTO.PromotionItem promotionItem = new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId()) + .setOriginalPrice(orderItem.getPayPrice()).setDiscountPrice(orderItem.getPayPrice() - newPayPrice); + // 创建营销明细 + PriceCalculateRespDTO.Promotion promotion = new PriceCalculateRespDTO.Promotion() + .setId(id).setName(name).setType(type).setLevel(level) + .setOriginalPrice(promotionItem.getOriginalPrice()).setDiscountPrice(promotionItem.getDiscountPrice()) + .setItems(singletonList(promotionItem)).setMeet(meet).setMeetTip(meetTip); + priceCalculate.getPromotions().add(promotion); + } + + private void modifyOrderItemPayPrice(PriceCalculateRespDTO.OrderItem orderItem, Integer newPayPrice, + PriceCalculateRespDTO priceCalculate) { + int diffPayPrice = orderItem.getPayPrice() - newPayPrice; + // 设置 OrderItem 价格相关字段 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + diffPayPrice); + orderItem.setPayPrice(newPayPrice); + orderItem.setOrderDividePrice(orderItem.getPayPrice() - orderItem.getOrderPartPrice()); + // 设置 Order 相关相关字段 + priceCalculate.getOrder().setPayPrice(priceCalculate.getOrder().getPayPrice() - diffPayPrice); + } + + // TODO 芋艿:提前实现 + private Supplier getMemberDiscountSupplier(Long userId) { + return Suppliers.memoize(() -> { + if (userId == 1) { + return 90d; + } + if (userId == 2) { + return 80d; + } + return null; // 无优惠 + }); + } + + private String formatPrice(Integer price) { + return String.format("%.2f", price / 100d); + } + } diff --git a/yudao-module-mall/yudao-module-market-biz/src/test/java/cn/iocoder/yudao/module/market/service/price/PriceServiceTest.java b/yudao-module-mall/yudao-module-market-biz/src/test/java/cn/iocoder/yudao/module/market/service/price/PriceServiceTest.java new file mode 100644 index 000000000..b6a92bb40 --- /dev/null +++ b/yudao-module-mall/yudao-module-market-biz/src/test/java/cn/iocoder/yudao/module/market/service/price/PriceServiceTest.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.market.service.price; + +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +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.PriceCalculateRespDTO; +import cn.iocoder.yudao.module.market.enums.common.PromotionLevelEnum; +import cn.iocoder.yudao.module.market.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.market.service.discount.DiscountService; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PriceServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +public class PriceServiceTest extends BaseMockitoUnitTest { + + @InjectMocks + private PriceServiceImpl priceService; + + @Mock + private DiscountService discountService; + @Mock + private ProductSkuApi productSkuApi; + + @Test + public void testCalculatePrice_memberDiscount() { + // 准备参数 + // TODO 芋艿:userId = 1,实现 9 折;后续改成 mock + PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(1L) + .setItems(singletonList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2))); + // mock 方法(商品 SKU 信息) + ProductSkuRespDTO productSku = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100)); + when(productSkuApi.getSkuList(eq(SetUtils.asSet(10L)))).thenReturn(singletonList(productSku)); + + // 调用 + PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO); + // 断言 Order 部分 + PriceCalculateRespDTO.Order order = priceCalculate.getOrder(); + assertEquals(order.getOriginalPrice(), 200); + assertEquals(order.getDiscountPrice(), 0); + assertEquals(order.getPointPrice(), 0); + assertEquals(order.getDeliveryPrice(), 0); + assertEquals(order.getPayPrice(), 180); + assertNull(order.getCouponId()); + // 断言 OrderItem 部分 + PriceCalculateRespDTO.OrderItem orderItem = order.getItems().get(0); + assertEquals(order.getItems().size(), 1); + assertEquals(orderItem.getSkuId(), 10L); + assertEquals(orderItem.getCount(), 2); + assertEquals(orderItem.getOriginalPrice(), 200); + assertEquals(orderItem.getOriginalUnitPrice(), 100); + assertEquals(orderItem.getDiscountPrice(), 20); + assertEquals(orderItem.getPayPrice(), 180); + assertEquals(orderItem.getOrderPartPrice(), 0); + assertEquals(orderItem.getOrderDividePrice(), 180); + // 断言 Promotion 部分 + PriceCalculateRespDTO.Promotion promotion = priceCalculate.getPromotions().get(0); + assertEquals(priceCalculate.getPromotions().size(), 1); + assertNull(promotion.getId()); + assertEquals(promotion.getName(), "会员折扣"); + assertEquals(promotion.getType(), PromotionTypeEnum.MEMBER.getType()); + assertEquals(promotion.getLevel(), PromotionLevelEnum.SKU.getLevel()); + assertEquals(promotion.getOriginalPrice(), 200); + assertEquals(promotion.getDiscountPrice(), 20); + assertTrue(promotion.getMeet()); + assertEquals(promotion.getMeetTip(), "会员折扣:省 0.20 元"); + PriceCalculateRespDTO.PromotionItem promotionItem = promotion.getItems().get(0); + assertEquals(promotion.getItems().size(), 1); + assertEquals(promotionItem.getSkuId(), 10L); + assertEquals(promotionItem.getOriginalPrice(), 200); + assertEquals(promotionItem.getDiscountPrice(), 20); + } + +}