From 5122b68aca1aee44e759c959761750688c820085 Mon Sep 17 00:00:00 2001 From: YunaiV <> Date: Fri, 14 Aug 2020 19:09:17 +0800 Subject: [PATCH] =?UTF-8?q?Price=20=E4=BB=B7=E6=A0=BC=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=9A=84=E7=BC=96=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/cart/UsersCartController.java | 5 +- .../order/biz/service/CartServiceImpl.java | 373 ++---------------- .../enums/sku/ProductSkuDetailFieldEnum.java | 28 ++ .../sku/dto/ProductSkuListQueryReqDTO.java | 8 + .../productservice/rpc/spu/ProductSpuRpc.java | 2 +- .../manager/sku/ProductSkuManager.java | 7 + .../manager/spu/ProductSpuManager.java | 2 +- .../rpc/spu/ProductSpuRpcImpl.java | 2 +- .../service/spu/ProductSpuService.java | 3 +- .../coupon/dto/card/CouponCardRespDTO.java} | 8 +- .../dto/template/CouponTemplateRespDTO.java} | 7 +- .../promotion/api/rpc/price/PriceRpc.java | 12 +- .../rpc/price/dto/PriceProductCalcReqDTO.java | 2 +- .../price/dto/PriceProductCalcRespDTO.java | 37 +- .../convert/coupon/CouponCardConvert.java | 10 +- .../coupon/card/CouponCardConvert.java | 15 + .../{ => card}/CouponTemplateConvert.java | 20 +- .../manager/price/PriceManager.java | 337 ++++++++++++++-- .../promotionservice/rpc/package-info.java | 1 + .../rpc/price/PriceRpcImpl.java | 24 ++ .../service/coupon/CouponCardService.java | 34 ++ .../service/coupon/CouponService.java | 11 +- .../service/coupon/CouponTemplateService.java | 24 ++ .../src/main/resources/application.yaml | 2 +- .../manager/package-info.java | 1 + .../manager/price/PriceManagerTest.java | 30 ++ shop-web-app/pom.xml | 6 + .../controller/order/CartController.java | 7 + .../order/vo/cart/CartDetailVO.java | 213 ++++++++++ .../shopweb/convert/order/CartConvert.java | 11 + .../manager/order/cart/CartManager.java | 42 ++ .../src/main/resources/application.yml | 2 + 32 files changed, 858 insertions(+), 428 deletions(-) create mode 100644 product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/enums/sku/ProductSkuDetailFieldEnum.java rename promotion-service-project/{promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponCardBO.java => promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/card/CouponCardRespDTO.java} (90%) rename promotion-service-project/{promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponTemplateBO.java => promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/template/CouponTemplateRespDTO.java} (94%) create mode 100644 promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/card/CouponCardConvert.java rename promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/{ => card}/CouponTemplateConvert.java (68%) create mode 100644 promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/package-info.java create mode 100644 promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/price/PriceRpcImpl.java create mode 100644 promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponCardService.java create mode 100644 promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponTemplateService.java create mode 100644 promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/package-info.java create mode 100644 promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/price/PriceManagerTest.java create mode 100644 shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/vo/cart/CartDetailVO.java create mode 100644 shop-web-app/src/main/java/cn/iocoder/mall/shopweb/convert/order/CartConvert.java diff --git a/order/order-rest/src/main/java/cn/iocoder/mall/order/rest/controller/cart/UsersCartController.java b/order/order-rest/src/main/java/cn/iocoder/mall/order/rest/controller/cart/UsersCartController.java index 863915417..4ca9d94c2 100644 --- a/order/order-rest/src/main/java/cn/iocoder/mall/order/rest/controller/cart/UsersCartController.java +++ b/order/order-rest/src/main/java/cn/iocoder/mall/order/rest/controller/cart/UsersCartController.java @@ -38,10 +38,7 @@ public class UsersCartController { // } // -// @GetMapping("/list") -// public CommonResult list() { // TODO 芋艿,先暂用这个 VO 。等促销活动出来后,做调整 -// return getCartDetail(); -// } + // // private CommonResult getCartDetail() { // // 获得购物车中选中的 diff --git a/order/order-service-impl/src/main/java/cn/iocoder/mall/order/biz/service/CartServiceImpl.java b/order/order-service-impl/src/main/java/cn/iocoder/mall/order/biz/service/CartServiceImpl.java index fdaae5d32..344a544ce 100644 --- a/order/order-service-impl/src/main/java/cn/iocoder/mall/order/biz/service/CartServiceImpl.java +++ b/order/order-service-impl/src/main/java/cn/iocoder/mall/order/biz/service/CartServiceImpl.java @@ -1,351 +1,54 @@ package cn.iocoder.mall.order.biz.service; -import cn.iocoder.common.framework.enums.CommonStatusEnum; -import cn.iocoder.common.framework.util.ServiceExceptionUtil; -import cn.iocoder.mall.order.api.CartService; -import cn.iocoder.mall.order.api.bo.CalcOrderPriceBO; -import cn.iocoder.mall.order.api.bo.CalcSkuPriceBO; -import cn.iocoder.mall.order.api.bo.CartItemBO; -import cn.iocoder.mall.order.api.constant.CartItemStatusEnum; -import cn.iocoder.mall.order.api.constant.OrderErrorCodeEnum; -import cn.iocoder.mall.order.api.dto.CalcOrderPriceDTO; -import cn.iocoder.mall.order.biz.convert.CartConvert; -import cn.iocoder.mall.order.biz.dao.CartMapper; -import cn.iocoder.mall.order.biz.dataobject.CartItemDO; -import cn.iocoder.mall.product.api.ProductSpuService; -import cn.iocoder.mall.product.api.bo.ProductSkuBO; -import cn.iocoder.mall.product.api.bo.ProductSkuDetailBO; -import cn.iocoder.mall.promotion.api.CouponService; -import cn.iocoder.mall.promotion.api.PromotionActivityService; -import cn.iocoder.mall.promotion.api.bo.CouponCardDetailBO; -import cn.iocoder.mall.promotion.api.bo.PromotionActivityBO; -import cn.iocoder.mall.promotion.api.enums.*; -import org.apache.dubbo.config.annotation.Reference; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.util.Assert; - -import java.util.*; -import java.util.stream.Collectors; /** * 购物车服务 Service 实现类 */ @Service @org.apache.dubbo.config.annotation.Service(validation = "true", version = "${dubbo.provider.CartService.version}") -public class CartServiceImpl implements CartService { - - @Reference(validation = "true", version = "${dubbo.consumer.PromotionActivityService.version}") - private ProductSpuService productSpuService; - @Reference(validation = "true", version = "${dubbo.consumer.PromotionActivityService.version}") - private PromotionActivityService promotionActivityService; - @Reference(validation = "true", version = "${dubbo.consumer.CouponService.version}") - private CouponService couponService; - - @Autowired - private CartMapper cartMapper; - - @Override - public CalcOrderPriceBO calcOrderPrice(CalcOrderPriceDTO calcOrderPriceDTO) { - // 2. 计算【限时折扣】促销 - - // 3. 计算【满减送】促销 - List itemGroups = groupByFullPrivilege(items, activityList); - calcOrderPriceBO.setItemGroups(itemGroups); - // 4. 计算优惠劵 - if (calcOrderPriceDTO.getCouponCardId() != null) { - Integer result = modifyPriceByCouponCard(calcOrderPriceDTO.getUserId(), calcOrderPriceDTO.getCouponCardId(), itemGroups); - calcOrderPriceBO.setCouponCardDiscountTotal(result); - } - // 5. 计算最终的价格 - int buyTotal = 0; - int discountTotal = 0; - int presentTotal = 0; - for (CalcOrderPriceBO.ItemGroup itemGroup : calcOrderPriceBO.getItemGroups()) { - buyTotal += itemGroup.getItems().stream().mapToInt(item -> item.getSelected() ? item.getBuyTotal() : 0).sum(); - discountTotal += itemGroup.getItems().stream().mapToInt(item -> item.getSelected() ? item.getDiscountTotal() : 0).sum(); - presentTotal += itemGroup.getItems().stream().mapToInt(item -> item.getSelected() ? item.getPresentTotal() : 0).sum(); - } - Assert.isTrue(buyTotal - discountTotal == presentTotal, - String.format("价格合计( %d - %d == %d )不正确", buyTotal, discountTotal, presentTotal)); - calcOrderPriceBO.setFee(new CalcOrderPriceBO.Fee(buyTotal, discountTotal, 0, presentTotal)); - // 返回 - return calcOrderPriceBO; - } - - @Override - @SuppressWarnings("Duplicates") - public CalcSkuPriceBO calcSkuPrice(Integer skuId) { - // 查询 SKU 是否合法 - ProductSkuBO sku = productSpuService.getProductSku(skuId); - if (sku == null - || CommonStatusEnum.DISABLE.getValue().equals(sku.getStatus())) { // sku 被禁用 - throw ServiceExceptionUtil.exception(OrderErrorCodeEnum.CARD_ITEM_SKU_NOT_FOUND.getCode()); - } - // 查询促销活动 - List activityList = promotionActivityService.getPromotionActivityListBySpuId(sku.getSpuId(), - Arrays.asList(PromotionActivityStatusEnum.WAIT.getValue(), PromotionActivityStatusEnum.RUN.getValue())); - if (activityList.isEmpty()) { // 如果无促销活动,则直接返回默认结果即可 - return new CalcSkuPriceBO().setOriginalPrice(sku.getPrice()).setBuyPrice(sku.getPrice()); - } - // 如果有促销活动,则开始做计算 TODO 芋艿,因为现在暂时只有限时折扣 + 满减送。所以写的比较简单先 - PromotionActivityBO fullPrivilege = findPromotionActivityByType(activityList, PromotionActivityTypeEnum.FULL_PRIVILEGE); - PromotionActivityBO timeLimitedDiscount = findPromotionActivityByType(activityList, PromotionActivityTypeEnum.TIME_LIMITED_DISCOUNT); - Integer presentPrice = calcSkuPriceByTimeLimitDiscount(sku, timeLimitedDiscount); - // 返回结果 - return new CalcSkuPriceBO().setFullPrivilege(fullPrivilege).setTimeLimitedDiscount(timeLimitedDiscount) - .setOriginalPrice(sku.getPrice()).setBuyPrice(presentPrice); - } +public class CartServiceImpl { - - private List groupByFullPrivilege(List items, List activityList) { - List itemGroups = new ArrayList<>(); - // 获得所有满减送促销 - List fullPrivileges = activityList.stream() - .filter(activity -> PromotionActivityTypeEnum.FULL_PRIVILEGE.getValue().equals(activity.getActivityType())) - .collect(Collectors.toList()); - // 基于满减送促销,进行分组 - if (!fullPrivileges.isEmpty()) { - items = new ArrayList<>(items); // 因为下面会修改数组,进行浅拷贝,避免影响传入的 items 。 - for (PromotionActivityBO fullPrivilege : fullPrivileges) { - // 创建 fullPrivilege 对应的分组 - CalcOrderPriceBO.ItemGroup itemGroup = new CalcOrderPriceBO.ItemGroup() - .setActivity(fullPrivilege) - .setItems(new ArrayList<>()); - // 筛选商品到分组中 - for (Iterator iterator = items.iterator(); iterator.hasNext(); ) { - CalcOrderPriceBO.Item item = iterator.next(); - if (!isSpuMatchFullPrivilege(item.getSpu().getId(), fullPrivilege)) { - continue; - } - itemGroup.getItems().add(item); - iterator.remove(); - } - // 如果匹配到,则添加到 itemGroups 中 - if (!itemGroup.getItems().isEmpty()) { - itemGroups.add(itemGroup); - } - } - } - // 处理未参加活动的商品,形成一个分组 - if (!items.isEmpty()) { - itemGroups.add(new CalcOrderPriceBO.ItemGroup().setItems(items)); - } - // 计算每个分组的价格 - for (CalcOrderPriceBO.ItemGroup itemGroup : itemGroups) { - itemGroup.setActivityDiscountTotal(calcSkuPriceByFullPrivilege(itemGroup)); - } - // 返回结果 - return itemGroups; - } - - private Integer modifyPriceByCouponCard(Integer userId, Integer couponCardId, List itemGroups) { - Assert.isTrue(couponCardId != null, "优惠劵编号不能为空"); - // 查询优惠劵 - CouponCardDetailBO couponCard = couponService.getCouponCardDetail(userId, couponCardId); - // 获得匹配的商品 - List items = new ArrayList<>(); - if (RangeTypeEnum.ALL.getValue().equals(couponCard.getRangeType())) { -// totalPrice = spus.stream().mapToInt(spu -> spu.getPrice() * spu.getQuantity()).sum(); - itemGroups.forEach(itemGroup -> items.addAll(itemGroup.getItems())); - } else if (RangeTypeEnum.PRODUCT_INCLUDE_PART.getValue().equals(couponCard.getRangeType())) { - itemGroups.forEach(itemGroup -> items.forEach(item -> { - if (couponCard.getRangeValues().contains(item.getSpu().getId())) { - items.add(item); - } - })); - } else if (RangeTypeEnum.PRODUCT_EXCLUDE_PART.getValue().equals(couponCard.getRangeType())) { - itemGroups.forEach(itemGroup -> items.forEach(item -> { - if (!couponCard.getRangeValues().contains(item.getSpu().getId())) { - items.add(item); - } - })); - } else if (RangeTypeEnum.CATEGORY_INCLUDE_PART.getValue().equals(couponCard.getRangeType())) { - itemGroups.forEach(itemGroup -> items.forEach(item -> { - if (couponCard.getRangeValues().contains(item.getSpu().getCid())) { - items.add(item); - } - })); - } else if (RangeTypeEnum.CATEGORY_EXCLUDE_PART.getValue().equals(couponCard.getRangeType())) { - itemGroups.forEach(itemGroup -> items.forEach(item -> { - if (!couponCard.getRangeValues().contains(item.getSpu().getCid())) { - items.add(item); - } - })); - } - // 判断是否符合条件 - int originalTotal = items.stream().mapToInt(CalcOrderPriceBO.Item::getPresentTotal).sum(); // 此处,指的是以优惠劵视角的原价 - if (originalTotal == 0 || originalTotal < couponCard.getPriceAvailable()) { - throw ServiceExceptionUtil.exception(PromotionErrorCodeConstants.COUPON_CARD_NOT_MATCH.getCode()); // TODO 芋艿,这种情况,会出现错误码的提示,无法格式化出来。另外,这块的最佳实践,找人讨论下。 - } - // 计算价格 - // 获得到优惠信息,进行价格计算 - int presentTotal; - if (PreferentialTypeEnum.PRICE.getValue().equals(couponCard.getPreferentialType())) { // 减价 - // 计算循环次数。这样,后续优惠的金额就是相乘了 - presentTotal = originalTotal - couponCard.getPriceOff(); - Assert.isTrue(presentTotal > 0, "计算后,价格为负数:" + presentTotal); - } else if (PreferentialTypeEnum.DISCOUNT.getValue().equals(couponCard.getPreferentialType())) { // 打折 - presentTotal = originalTotal * couponCard.getPercentOff() / 100; - if (couponCard.getDiscountPriceLimit() != null // 空,代表不限制优惠上限 - && originalTotal - presentTotal > couponCard.getDiscountPriceLimit()) { - presentTotal = originalTotal - couponCard.getDiscountPriceLimit(); - } - } else { - throw new IllegalArgumentException(String.format("优惠劵(%s) 的优惠类型不正确", couponCard.toString())); - } - int discountTotal = originalTotal - presentTotal; - Assert.isTrue(discountTotal > 0, "计算后,不产生优惠:" + discountTotal); - // 按比例,拆分 presentTotal - splitDiscountPriceToItems(items, discountTotal, presentTotal); - // 返回优惠金额 - return originalTotal - presentTotal; - } - - /** - * 计算指定 SKU 在限时折扣下的价格 - * - * @param sku SKU - * @param timeLimitedDiscount 限时折扣促销。 - * 传入的该活动,要保证该 SKU 在该促销下一定有优惠。 - * @return 计算后的价格 - */ - private Integer calcSkuPriceByTimeLimitDiscount(ProductSkuBO sku, PromotionActivityBO timeLimitedDiscount) { - if (timeLimitedDiscount == null) { - return sku.getPrice(); - } - // 获得对应的优惠项 - PromotionActivityBO.TimeLimitedDiscount.Item item = timeLimitedDiscount.getTimeLimitedDiscount().getItems().stream() - .filter(item0 -> item0.getSpuId().equals(sku.getSpuId())) - .findFirst().orElse(null); - if (item == null) { - throw new IllegalArgumentException(String.format("折扣活动(%s) 不存在商品(%s) 的优惠配置", - timeLimitedDiscount.toString(), sku.toString())); - } - // 计算价格 - if (PreferentialTypeEnum.PRICE.getValue().equals(item.getPreferentialType())) { // 减价 - int presentPrice = sku.getPrice() - item.getPreferentialValue(); - return presentPrice >= 0 ? presentPrice : sku.getPrice(); // 如果计算优惠价格小于 0 ,则说明无法使用优惠。 - } - if (PreferentialTypeEnum.DISCOUNT.getValue().equals(item.getPreferentialType())) { // 打折 - return sku.getPrice() * item.getPreferentialValue() / 100; - } - throw new IllegalArgumentException(String.format("折扣活动(%s) 的优惠类型不正确", timeLimitedDiscount.toString())); - } - - private Integer calcSkuPriceByFullPrivilege(CalcOrderPriceBO.ItemGroup itemGroup) { - if (itemGroup.getActivity() == null) { - return null; - } - PromotionActivityBO activity = itemGroup.getActivity(); - Assert.isTrue(PromotionActivityTypeEnum.FULL_PRIVILEGE.getValue().equals(activity.getActivityType()), - "传入的必须的满减送活动必须是满减送"); - // 获得优惠信息 - List items = itemGroup.getItems().stream().filter(CalcOrderPriceBO.Item::getSelected) - .collect(Collectors.toList()); - Integer itemCnt = items.stream().mapToInt(CalcOrderPriceBO.Item::getBuyQuantity).sum(); - Integer originalTotal = items.stream().mapToInt(CalcOrderPriceBO.Item::getPresentTotal).sum(); - List privileges = activity.getFullPrivilege().getPrivileges().stream() - .filter(privilege -> { - if (MeetTypeEnum.PRICE.getValue().equals(privilege.getMeetType())) { - return originalTotal >= privilege.getMeetValue(); - } - if (MeetTypeEnum.QUANTITY.getValue().equals(privilege.getMeetType())) { - return itemCnt >= privilege.getMeetValue(); - } - throw new IllegalArgumentException(String.format("满减送活动(%s) 的匹配(%s)不正确", itemGroup.getActivity().toString(), privilege.toString())); - }).collect(Collectors.toList()); - // 获得不到优惠信息,返回原始价格 - if (privileges.isEmpty()) { - return null; - } - // 获得到优惠信息,进行价格计算 - PromotionActivityBO.FullPrivilege.Privilege privilege = privileges.get(privileges.size() - 1); - Integer presentTotal; - if (PreferentialTypeEnum.PRICE.getValue().equals(privilege.getPreferentialType())) { // 减价 - // 计算循环次数。这样,后续优惠的金额就是相乘了 - Integer cycleCount = 1; - if (activity.getFullPrivilege().getCycled()) { - if (MeetTypeEnum.PRICE.getValue().equals(privilege.getMeetType())) { - cycleCount = originalTotal / privilege.getMeetValue(); - } else if (MeetTypeEnum.QUANTITY.getValue().equals(privilege.getMeetType())) { - cycleCount = itemCnt / privilege.getMeetValue(); - } - } - presentTotal = originalTotal - cycleCount * privilege.getMeetValue(); - if (presentTotal < 0) { // 如果计算优惠价格小于 0 ,则说明无法使用优惠。 - presentTotal = originalTotal; - } - } else if (PreferentialTypeEnum.DISCOUNT.getValue().equals(privilege.getPreferentialType())) { // 打折 - presentTotal = originalTotal * privilege.getPreferentialValue() / 100; - } else { - throw new IllegalArgumentException(String.format("满减送促销(%s) 的优惠类型不正确", activity.toString())); - } - int discountTotal = originalTotal - presentTotal; - if (discountTotal == 0) { - return null; - } - // 按比例,拆分 presentTotal -// for (int i = 0; i < items.size(); i++) { -// CalcOrderPriceBO.Item item = items.get(i); -// Integer discountPart; -// if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 -// discountPart = (int) (discountTotal * (1.0D * item.getPresentTotal() / presentTotal)); -// discountTotal -= discountPart; -// } else { -// discountPart = discountTotal; -// } -// Assert.isTrue(discountPart > 0, "优惠金额必须大于 0"); -// item.setDiscountTotal(item.getDiscountTotal() + discountPart); -// item.setPresentTotal(item.getBuyTotal() - item.getDiscountTotal()); -// item.setPresentPrice(item.getPresentTotal() / item.getBuyQuantity()); +// @Override +// @SuppressWarnings("Duplicates") +// public CalcSkuPriceBO calcSkuPrice(Integer skuId) { +// // 查询 SKU 是否合法 +// ProductSkuBO sku = productSpuService.getProductSku(skuId); +// if (sku == null +// || CommonStatusEnum.DISABLE.getValue().equals(sku.getStatus())) { // sku 被禁用 +// throw ServiceExceptionUtil.exception(OrderErrorCodeEnum.CARD_ITEM_SKU_NOT_FOUND.getCode()); // } - splitDiscountPriceToItems(items, discountTotal, presentTotal); - // 返回优惠金额 - return originalTotal - presentTotal; - } +// // 查询促销活动 +// List activityList = promotionActivityService.getPromotionActivityListBySpuId(sku.getSpuId(), +// Arrays.asList(PromotionActivityStatusEnum.WAIT.getValue(), PromotionActivityStatusEnum.RUN.getValue())); +// if (activityList.isEmpty()) { // 如果无促销活动,则直接返回默认结果即可 +// return new CalcSkuPriceBO().setOriginalPrice(sku.getPrice()).setBuyPrice(sku.getPrice()); +// } +// // 如果有促销活动,则开始做计算 TODO 芋艿,因为现在暂时只有限时折扣 + 满减送。所以写的比较简单先 +// PromotionActivityBO fullPrivilege = findPromotionActivityByType(activityList, PromotionActivityTypeEnum.FULL_PRIVILEGE); +// PromotionActivityBO timeLimitedDiscount = findPromotionActivityByType(activityList, PromotionActivityTypeEnum.TIME_LIMITED_DISCOUNT); +// Integer presentPrice = calcSkuPriceByTimeLimitDiscount(sku, timeLimitedDiscount); +// // 返回结果 +// return new CalcSkuPriceBO().setFullPrivilege(fullPrivilege).setTimeLimitedDiscount(timeLimitedDiscount) +// .setOriginalPrice(sku.getPrice()).setBuyPrice(presentPrice); +// } +// +// +// +// +// private PromotionActivityBO findPromotionActivityByType(List activityList, PromotionActivityTypeEnum type) { +// return activityList.stream() +// .filter(activity -> type.getValue().equals(activity.getActivityType())) +// .findFirst().orElse(null); +// } +// +// private List findPromotionActivityListByType(List activityList, PromotionActivityTypeEnum type) { +// return activityList.stream() +// .filter(activity -> type.getValue().equals(activity.getActivityType())) +// .collect(Collectors.toList()); +// } - private void splitDiscountPriceToItems(List items, Integer discountTotal, Integer presentTotal) { - for (int i = 0; i < items.size(); i++) { - CalcOrderPriceBO.Item item = items.get(i); - Integer discountPart; - if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 - discountPart = (int) (discountTotal * (1.0D * item.getPresentTotal() / presentTotal)); - discountTotal -= discountPart; - } else { - discountPart = discountTotal; - } - Assert.isTrue(discountPart > 0, "优惠金额必须大于 0"); - item.setDiscountTotal(item.getDiscountTotal() + discountPart); - item.setPresentTotal(item.getBuyTotal() - item.getDiscountTotal()); - item.setPresentPrice(item.getPresentTotal() / item.getBuyQuantity()); - } - } - private PromotionActivityBO findPromotionActivityByType(List activityList, PromotionActivityTypeEnum type) { - return activityList.stream() - .filter(activity -> type.getValue().equals(activity.getActivityType())) - .findFirst().orElse(null); - } - - private List findPromotionActivityListByType(List activityList, PromotionActivityTypeEnum type) { - return activityList.stream() - .filter(activity -> type.getValue().equals(activity.getActivityType())) - .collect(Collectors.toList()); - } - - private boolean isSpuMatchFullPrivilege(Integer spuId, PromotionActivityBO activity) { - Assert.isTrue(PromotionActivityTypeEnum.FULL_PRIVILEGE.getValue().equals(activity.getActivityType()), - "传入的必须的促销活动必须是满减送"); - PromotionActivityBO.FullPrivilege fullPrivilege = activity.getFullPrivilege(); - if (RangeTypeEnum.ALL.getValue().equals(fullPrivilege.getRangeType())) { - return true; - } else if (RangeTypeEnum.PRODUCT_INCLUDE_PART.getValue().equals(fullPrivilege.getRangeType())) { - return fullPrivilege.getRangeValues().contains(spuId); - } else { - throw new IllegalArgumentException(String.format("促销活动(%s) 可用范围的类型是不正确", activity.toString())); - } - } } diff --git a/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/enums/sku/ProductSkuDetailFieldEnum.java b/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/enums/sku/ProductSkuDetailFieldEnum.java new file mode 100644 index 000000000..6e35823d1 --- /dev/null +++ b/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/enums/sku/ProductSkuDetailFieldEnum.java @@ -0,0 +1,28 @@ +package cn.iocoder.mall.productservice.enums.sku; + +import cn.iocoder.mall.productservice.rpc.sku.dto.ProductSkuRespDTO; + +/** + * 商品 SKU 明细的字段枚举 + * + * @see ProductSkuRespDTO + */ +public enum ProductSkuDetailFieldEnum { + + SPU("spu"), + ATTR("attr"); + + /** + * 字段 + */ + private final String field; + + ProductSkuDetailFieldEnum(String field) { + this.field = field; + } + + public String getField() { + return field; + } + +} diff --git a/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/sku/dto/ProductSkuListQueryReqDTO.java b/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/sku/dto/ProductSkuListQueryReqDTO.java index 401a1d125..e1bf68605 100644 --- a/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/sku/dto/ProductSkuListQueryReqDTO.java +++ b/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/sku/dto/ProductSkuListQueryReqDTO.java @@ -1,10 +1,12 @@ package cn.iocoder.mall.productservice.rpc.sku.dto; +import cn.iocoder.mall.productservice.enums.sku.ProductSkuDetailFieldEnum; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.Collection; +import java.util.Collections; /** * 商品 SKU 列表查询 DTO @@ -26,5 +28,11 @@ public class ProductSkuListQueryReqDTO implements Serializable { */ private Integer productSpuId; + /** + * 额外返回字段 + * + * @see ProductSkuDetailFieldEnum + */ + private Collection fields = Collections.emptySet(); } diff --git a/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpc.java b/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpc.java index 0076212e8..792e16a89 100644 --- a/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpc.java +++ b/product-service-project/product-service-api/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpc.java @@ -41,7 +41,7 @@ public interface ProductSpuRpc { * @param productSpuIds 商品 SPU 编号列表 * @return 商品 SPU 列表 */ - CommonResult> listProductSpus(List productSpuIds); + CommonResult> listProductSpus(Collection productSpuIds); /** * 获得商品 SPU 分页 diff --git a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/sku/ProductSkuManager.java b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/sku/ProductSkuManager.java index a262370df..bafa60ffe 100644 --- a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/sku/ProductSkuManager.java +++ b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/sku/ProductSkuManager.java @@ -1,5 +1,6 @@ package cn.iocoder.mall.productservice.manager.sku; +import cn.iocoder.common.framework.util.CollectionUtils; import cn.iocoder.mall.productservice.convert.sku.ProductSkuConvert; import cn.iocoder.mall.productservice.rpc.sku.dto.ProductSkuListQueryReqDTO; import cn.iocoder.mall.productservice.rpc.sku.dto.ProductSkuRespDTO; @@ -8,6 +9,7 @@ import cn.iocoder.mall.productservice.service.sku.bo.ProductSkuBO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; /** @@ -37,8 +39,13 @@ public class ProductSkuManager { * @return 商品 SKU列表 */ public List listProductSkus(ProductSkuListQueryReqDTO queryReqDTO) { + // 获得商品 SKU 列表 List productSkuBOs = productSkuService.listProductSkus( ProductSkuConvert.INSTANCE.convert(queryReqDTO)); + if (CollectionUtils.isEmpty(productSkuBOs)) { + return Collections.emptyList(); + } + // return ProductSkuConvert.INSTANCE.convertList03(productSkuBOs); } diff --git a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/spu/ProductSpuManager.java b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/spu/ProductSpuManager.java index a4676f1e0..8b848420e 100644 --- a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/spu/ProductSpuManager.java +++ b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/manager/spu/ProductSpuManager.java @@ -131,7 +131,7 @@ public class ProductSpuManager { * @param productSpuIds 商品 SPU编号列表 * @return 商品 SPU列表 */ - public List listProductSpus(List productSpuIds) { + public List listProductSpus(Collection productSpuIds) { List productSpuBOs = productSpuService.listProductSpus(productSpuIds); return ProductSpuConvert.INSTANCE.convertList02(productSpuBOs); } diff --git a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpcImpl.java b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpcImpl.java index e9ae280a3..a00d969ae 100644 --- a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpcImpl.java +++ b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/rpc/spu/ProductSpuRpcImpl.java @@ -38,7 +38,7 @@ public class ProductSpuRpcImpl implements ProductSpuRpc { } @Override - public CommonResult> listProductSpus(List productSpuIds) { + public CommonResult> listProductSpus(Collection productSpuIds) { return success(productSpuManager.listProductSpus(productSpuIds)); } diff --git a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/service/spu/ProductSpuService.java b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/service/spu/ProductSpuService.java index bcd10b75d..a24df5acf 100644 --- a/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/service/spu/ProductSpuService.java +++ b/product-service-project/product-service-app/src/main/java/cn/iocoder/mall/productservice/service/spu/ProductSpuService.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; +import java.util.Collection; import java.util.List; import static cn.iocoder.mall.productservice.enums.ProductErrorCodeConstants.PRODUCT_SPU_NOT_EXISTS; @@ -76,7 +77,7 @@ public class ProductSpuService { * @param productSpuIds 商品 SPU编号列表 * @return 商品 SPU列表 */ - public List listProductSpus(List productSpuIds) { + public List listProductSpus(Collection productSpuIds) { List productSpuDOs = productSpuMapper.selectBatchIds(productSpuIds); return ProductSpuConvert.INSTANCE.convertList(productSpuDOs); } diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponCardBO.java b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/card/CouponCardRespDTO.java similarity index 90% rename from promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponCardBO.java rename to promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/card/CouponCardRespDTO.java index 48c2e755c..27e10f80b 100644 --- a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponCardBO.java +++ b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/card/CouponCardRespDTO.java @@ -1,4 +1,4 @@ -package cn.iocoder.mall.promotionservice.service.coupon.bo; +package cn.iocoder.mall.promotion.api.rpc.coupon.dto.card; import lombok.Data; import lombok.experimental.Accessors; @@ -7,11 +7,11 @@ import java.io.Serializable; import java.util.Date; /** - * 优惠劵 BO + * 优惠劵 Response DTO */ @Data @Accessors(chain = true) -public class CouponCardBO implements Serializable { +public class CouponCardRespDTO implements Serializable { // ========== 基本信息 BEGIN ========== /** @@ -100,8 +100,6 @@ public class CouponCardBO implements Serializable { */ private Date usedTime; - // TODO 芋艿,后续要加优惠劵的使用日志,因为下单后,可能会取消。 - // ========== 使用情况 END ========== /** diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponTemplateBO.java b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/template/CouponTemplateRespDTO.java similarity index 94% rename from promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponTemplateBO.java rename to promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/template/CouponTemplateRespDTO.java index c987c8784..359b6431a 100644 --- a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/bo/CouponTemplateBO.java +++ b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/coupon/dto/template/CouponTemplateRespDTO.java @@ -1,17 +1,18 @@ -package cn.iocoder.mall.promotionservice.service.coupon.bo; +package cn.iocoder.mall.promotion.api.rpc.coupon.dto.template; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.Date; +import java.util.List; /** * 优惠劵(码)模板 BO */ @Data @Accessors(chain = true) -public class CouponTemplateBO implements Serializable { +public class CouponTemplateRespDTO implements Serializable { // ========== 基本信息 BEGIN ========== /** @@ -85,7 +86,7 @@ public class CouponTemplateBO implements Serializable { /** * 指定商品 / 分类列表,使用逗号分隔商品编号 */ - private String rangeValues; + private List rangeValues; /** * 生效日期类型 * diff --git a/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/PriceRpc.java b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/PriceRpc.java index 1bcd9d055..991def4ba 100644 --- a/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/PriceRpc.java +++ b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/PriceRpc.java @@ -1,14 +1,14 @@ package cn.iocoder.mall.promotion.api.rpc.price; -import lombok.Data; -import lombok.experimental.Accessors; - -import javax.validation.constraints.NotNull; -import java.util.List; +import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcReqDTO; +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcRespDTO; /** * 价格 Rpc 接口,提供价格计算的功能 */ -public class PriceRpc { +public interface PriceRpc { + + CommonResult calcProductPrice(PriceProductCalcReqDTO calcReqDTO); } diff --git a/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcReqDTO.java b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcReqDTO.java index 1809ce568..88372df54 100644 --- a/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcReqDTO.java +++ b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcReqDTO.java @@ -11,7 +11,7 @@ import java.util.List; * 商品价格计算 Request DTO */ @Data -@Accessors +@Accessors(chain = true) public class PriceProductCalcReqDTO implements Serializable { /** diff --git a/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcRespDTO.java b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcRespDTO.java index 10e51d387..b748c17bd 100644 --- a/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcRespDTO.java +++ b/promotion-service-project/promotion-service-api/src/main/java/cn/iocoder/mall/promotion/api/rpc/price/dto/PriceProductCalcRespDTO.java @@ -1,5 +1,7 @@ package cn.iocoder.mall.promotion.api.rpc.price.dto; +import cn.iocoder.mall.promotion.api.enums.PromotionActivityTypeEnum; +import cn.iocoder.mall.promotion.api.rpc.activity.dto.PromotionActivityRespDTO; import lombok.Data; import lombok.experimental.Accessors; @@ -48,11 +50,14 @@ public class PriceProductCalcRespDTO implements Serializable { @Accessors(chain = true) public static class ItemGroup { -// /** -// * 优惠活动 -// */ -// // TODO 芋艿,目前只会有【满减送】的情况,未来有新的促销方式,可能需要改成数组 -// private PromotionActivityBO activity; + /** + * 优惠活动 + * + * 目前会有满减送 {@link PromotionActivityTypeEnum#FULL_PRIVILEGE} 类型的活动 + * + * // TODO 芋艿,目前只会有【满减送】的情况,未来有新的促销方式,可能需要改成数组 + */ + private PromotionActivityRespDTO activity; /** * 促销减少的金额 * @@ -77,14 +82,28 @@ public class PriceProductCalcRespDTO implements Serializable { @Accessors(chain = true) public static class Item { + /** + * 商品 SPU 编号 + */ + private Integer spuId; + /** + * 商品 SKU 编号 + */ + private Integer skuId; + /** + * 商品 Category 编号 + */ + private Integer cid; /** * 购买数量 */ private Integer buyQuantity; -// /** -// * 优惠活动 -// */ -// private PromotionActivityBO activity; + /** + * 优惠活动 + * + * 目前会有限时折扣 {@link PromotionActivityTypeEnum#TIME_LIMITED_DISCOUNT} 类型的活动 + */ + private PromotionActivityRespDTO activity; /** * 原始单价,单位:分。 */ diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/CouponCardConvert.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/CouponCardConvert.java index 562deb2cb..b6ce9f74d 100644 --- a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/CouponCardConvert.java +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/CouponCardConvert.java @@ -4,8 +4,6 @@ import cn.iocoder.mall.promotion.api.rpc.coupon.dto.CouponCardDetailRespDTO; import cn.iocoder.mall.promotion.api.rpc.coupon.dto.CouponCardReqDTO; import cn.iocoder.mall.promotionservice.dal.mysql.dataobject.coupon.CouponCardDO; import cn.iocoder.mall.promotionservice.service.coupon.bo.CouponCardAvailableBO; -import cn.iocoder.mall.promotionservice.service.coupon.bo.CouponCardBO; -import cn.iocoder.mall.promotionservice.service.coupon.bo.CouponCardDetailBO; import org.mapstruct.Mapper; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; @@ -19,17 +17,13 @@ public interface CouponCardConvert { // @Mappings({}) // CouponCardBO convertToBO(CouponCardDO banner); -// - @Mappings({}) - List convertToBO(List cardList); + + List convertToDTO(List cardList); CouponCardReqDTO convertToSingleDTO(CouponCardDO card); - @Mappings({}) - CouponCardBO convert(CouponCardDO card); - @Mappings({}) CouponCardDetailRespDTO convert2(CouponCardDO card); diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/card/CouponCardConvert.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/card/CouponCardConvert.java new file mode 100644 index 000000000..c4837576f --- /dev/null +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/card/CouponCardConvert.java @@ -0,0 +1,15 @@ +package cn.iocoder.mall.promotionservice.convert.coupon.card; + +import cn.iocoder.mall.promotion.api.rpc.coupon.dto.card.CouponCardRespDTO; +import cn.iocoder.mall.promotionservice.dal.mysql.dataobject.coupon.CouponCardDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CouponCardConvert { + + CouponCardConvert INSTANCE = Mappers.getMapper(CouponCardConvert.class); + + CouponCardRespDTO convert(CouponCardDO bean); + +} diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/CouponTemplateConvert.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/card/CouponTemplateConvert.java similarity index 68% rename from promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/CouponTemplateConvert.java rename to promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/card/CouponTemplateConvert.java index ad7843a32..9d9371111 100644 --- a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/CouponTemplateConvert.java +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/convert/coupon/card/CouponTemplateConvert.java @@ -1,12 +1,15 @@ -package cn.iocoder.mall.promotionservice.convert.coupon; +package cn.iocoder.mall.promotionservice.convert.coupon.card; +import cn.iocoder.common.framework.util.StringUtils; import cn.iocoder.mall.promotion.api.rpc.coupon.dto.*; +import cn.iocoder.mall.promotion.api.rpc.coupon.dto.template.CouponTemplateRespDTO; import cn.iocoder.mall.promotionservice.dal.mysql.dataobject.coupon.CouponTemplateDO; import cn.iocoder.mall.promotionservice.service.coupon.bo.CouponCardTemplateAddBO; import cn.iocoder.mall.promotionservice.service.coupon.bo.CouponCardTemplateUpdateBO; -import cn.iocoder.mall.promotionservice.service.coupon.bo.CouponTemplateBO; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.Mappings; +import org.mapstruct.Named; import org.mapstruct.factory.Mappers; import java.util.List; @@ -18,9 +21,7 @@ public interface CouponTemplateConvert { // @Mappings({}) // CouponTemplateBO convertToBO(CouponTemplateDO banner); -// - @Mappings({}) - List convertToBO(List templateList); + List convertToDTO(List templateList); @@ -42,7 +43,12 @@ public interface CouponTemplateConvert { @Mappings({}) CouponTemplateDO convert(CouponCardTemplateUpdateBO template); - @Mappings({}) - CouponTemplateBO convert(CouponTemplateDO template); + @Mapping(source = "rangeValues", target = "rangeValues", qualifiedByName = "translateStringToIntList") + CouponTemplateRespDTO convert(CouponTemplateDO bean); + + @Named("translateStringToIntList") + default List translateStringToIntList(String str) { + return StringUtils.splitToInt(str, ","); + } } diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/manager/price/PriceManager.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/manager/price/PriceManager.java index 198068f0b..9ca8206cf 100644 --- a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/manager/price/PriceManager.java +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/manager/price/PriceManager.java @@ -6,22 +6,27 @@ import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.mall.productservice.rpc.sku.ProductSkuRpc; import cn.iocoder.mall.productservice.rpc.sku.dto.ProductSkuListQueryReqDTO; import cn.iocoder.mall.productservice.rpc.sku.dto.ProductSkuRespDTO; -import cn.iocoder.mall.promotion.api.enums.PromotionActivityStatusEnum; +import cn.iocoder.mall.productservice.rpc.spu.ProductSpuRpc; +import cn.iocoder.mall.productservice.rpc.spu.dto.ProductSpuRespDTO; +import cn.iocoder.mall.promotion.api.enums.*; import cn.iocoder.mall.promotion.api.rpc.activity.dto.PromotionActivityRespDTO; +import cn.iocoder.mall.promotion.api.rpc.coupon.dto.card.CouponCardRespDTO; +import cn.iocoder.mall.promotion.api.rpc.coupon.dto.template.CouponTemplateRespDTO; import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcReqDTO; import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcRespDTO; import cn.iocoder.mall.promotionservice.service.activity.PromotionActivityService; +import cn.iocoder.mall.promotionservice.service.coupon.CouponCardService; +import cn.iocoder.mall.promotionservice.service.coupon.CouponTemplateService; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import org.springframework.validation.annotation.Validated; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; -import static cn.iocoder.mall.promotion.api.enums.PromotionErrorCodeConstants.PRICE_PRODUCT_SKU_NOT_EXISTS; +import static cn.iocoder.mall.promotion.api.enums.PromotionErrorCodeConstants.*; @Service @Validated @@ -29,9 +34,15 @@ public class PriceManager { @DubboReference(version = "${dubbo.consumer.ProductSkuRpc.version}") private ProductSkuRpc productSkuRpc; + @DubboReference(version = "${dubbo.consumer.ProductSpuRpc.version}") + private ProductSpuRpc productSpuRpc; @Autowired private PromotionActivityService promotionActivityService; + @Autowired + private CouponCardService couponCardService; + @Autowired + private CouponTemplateService couponTemplateService; public PriceProductCalcRespDTO calcProductPrice(PriceProductCalcReqDTO calcReqDTO) { // TODO 芋艿,补充一些表单校验。例如说,需要传入用户编号。 @@ -47,28 +58,54 @@ public class PriceManager { // TODO 库存相关 // 查询促销活动 List activityRespDTOs = promotionActivityService.listPromotionActivitiesBySpuIds( - calcProductItemDTOMap.keySet(), Collections.singleton(PromotionActivityStatusEnum.RUN.getValue())); + CollectionUtils.convertSet(listProductSkusResult.getData(), ProductSkuRespDTO::getSpuId), + Collections.singleton(PromotionActivityStatusEnum.RUN.getValue())); // 拼装结果(主要是计算价格) PriceProductCalcRespDTO calcRespDTO = new PriceProductCalcRespDTO(); // 1. 创建初始的每一项的数组 - List calcItemRespDTOs = initCalcOrderPriceItems( + List calcItemRespDTOs = this.initCalcOrderPriceItems( listProductSkusResult.getData(), calcProductItemDTOMap); // 2. 计算【限时折扣】促销 -// modifyPriceByTimeLimitDiscount(items, activityList); + this.modifyPriceByTimeLimitDiscount(calcItemRespDTOs, activityRespDTOs); // 3. 计算【满减送】促销 - // 4. 计算优惠劵 + List itemGroups = this.groupByFullPrivilege(calcItemRespDTOs, activityRespDTOs); + calcRespDTO.setItemGroups(itemGroups); + // 4. 计算优惠劵 TODO 芋艿:未详细测试; + if (calcReqDTO.getCouponCardId() != null) { + Integer result = this.modifyPriceByCouponCard(calcReqDTO.getUserId(), calcReqDTO.getCouponCardId(), itemGroups); + calcRespDTO.setCouponCardDiscountTotal(result); + } // 5. 计算最终的价格 - return null; + int buyTotal = 0; + int discountTotal = 0; + int presentTotal = 0; + for (PriceProductCalcRespDTO.ItemGroup itemGroup : calcRespDTO.getItemGroups()) { + buyTotal += itemGroup.getItems().stream().mapToInt(PriceProductCalcRespDTO.Item::getBuyTotal).sum(); + discountTotal += itemGroup.getItems().stream().mapToInt(PriceProductCalcRespDTO.Item::getDiscountTotal).sum(); + presentTotal += itemGroup.getItems().stream().mapToInt(PriceProductCalcRespDTO.Item::getPresentTotal).sum(); + } + Assert.isTrue(buyTotal - discountTotal == presentTotal, + String.format("价格合计( %d - %d == %d )不正确", buyTotal, discountTotal, presentTotal)); + calcRespDTO.setFee(new PriceProductCalcRespDTO.Fee(buyTotal, discountTotal, 0, presentTotal)); + return calcRespDTO; } private List initCalcOrderPriceItems(List skus, Map calcProductItemDTOMap) { + // 获得商品分类 Map + CommonResult> listProductSpusResult = productSpuRpc.listProductSpus(CollectionUtils.convertSet(skus, ProductSkuRespDTO::getSpuId)); + listProductSpusResult.checkError(); + Map spuIdCategoryIdMap = CollectionUtils.convertMap(listProductSpusResult.getData(), // SPU 编号与 Category 编号的映射 + ProductSpuRespDTO::getId, ProductSpuRespDTO::getCid); + // 生成商品列表 List items = new ArrayList<>(); for (ProductSkuRespDTO sku : skus) { PriceProductCalcRespDTO.Item item = new PriceProductCalcRespDTO.Item(); items.add(item); - // 将是否选中,购物数量,复制到 item 中 + // 将基本信息,复制到 item 中 PriceProductCalcReqDTO.Item calcOrderItem = calcProductItemDTOMap.get(sku.getId()); + item.setSpuId(sku.getSpuId()).setSkuId(sku.getId()); + item.setCid(spuIdCategoryIdMap.get(sku.getSpuId())); item.setBuyQuantity(calcOrderItem.getQuantity()); // 计算初始价格 item.setOriginPrice(sku.getPrice()); @@ -81,30 +118,256 @@ public class PriceManager { return items; } -// private void modifyPriceByTimeLimitDiscount(List items, List activityList) { -// for (CalcOrderPriceBO.Item item : items) { -// // 获得符合条件的限时折扣 -// PromotionActivityBO timeLimitedDiscount = activityList.stream() -// .filter(activity -> PromotionActivityTypeEnum.TIME_LIMITED_DISCOUNT.getValue().equals(activity.getActivityType()) -// && activity.getTimeLimitedDiscount().getItems().stream().anyMatch(item0 -> item0.getSpuId().equals(item.getSpu().getId()))) -// .findFirst().orElse(null); -// if (timeLimitedDiscount == null) { -// continue; -// } -// // 计算价格 -// ProductSkuBO sku = new ProductSkuBO().setId(item.getId()).setSpuId(item.getSpu().getId()).setPrice(item.getPrice()); -// Integer newPrice = calcSkuPriceByTimeLimitDiscount(sku, timeLimitedDiscount); -// if (newPrice.equals(item.getPrice())) { -// continue; -// } -// // 设置优惠 -// item.setActivity(timeLimitedDiscount); -// // 设置价格 -// item.setBuyPrice(newPrice); -// item.setBuyTotal(newPrice * item.getBuyQuantity()); -// item.setPresentTotal(item.getBuyTotal() - item.getDiscountTotal()); -// item.setPresentPrice(item.getPresentTotal() / item.getBuyQuantity()); -// } -// } + private void modifyPriceByTimeLimitDiscount(List items, List activityList) { + for (PriceProductCalcRespDTO.Item item : items) { + // 获得符合条件的限时折扣 + PromotionActivityRespDTO timeLimitedDiscount = activityList.stream() + .filter(activity -> PromotionActivityTypeEnum.TIME_LIMITED_DISCOUNT.getValue().equals(activity.getActivityType()) + && activity.getTimeLimitedDiscount().getItems().stream().anyMatch(item0 -> item0.getSpuId().equals(item.getSpuId()))) + .findFirst().orElse(null); + if (timeLimitedDiscount == null) { + continue; + } + // 计算价格 + Integer newBuyPrice = calcSkuPriceByTimeLimitDiscount(item, timeLimitedDiscount); + if (newBuyPrice.equals(item.getBuyPrice())) { // 未优惠 + continue; + } + // 设置优惠 + item.setActivity(timeLimitedDiscount); + // 设置价格 + item.setBuyPrice(newBuyPrice); + item.setBuyTotal(newBuyPrice * item.getBuyQuantity()); + item.setPresentTotal(item.getBuyTotal() - item.getDiscountTotal()); + item.setPresentPrice(item.getPresentTotal() / item.getBuyQuantity()); + } + } + + /** + * 计算指定 SKU 在限时折扣下的价格 + * + * @param sku SKU + * @param timeLimitedDiscount 限时折扣促销。 + * 传入的该活动,要保证该 SKU 在该促销下一定有优惠。 + * @return 计算后的价格 + */ + private Integer calcSkuPriceByTimeLimitDiscount(PriceProductCalcRespDTO.Item sku, PromotionActivityRespDTO timeLimitedDiscount) { + if (timeLimitedDiscount == null) { + return sku.getBuyPrice(); + } + // 获得对应的优惠项 + PromotionActivityRespDTO.TimeLimitedDiscount.Item item = timeLimitedDiscount.getTimeLimitedDiscount().getItems().stream() + .filter(item0 -> item0.getSpuId().equals(sku.getSpuId())) + .findFirst().orElse(null); + if (item == null) { + throw new IllegalArgumentException(String.format("折扣活动(%s) 不存在商品(%s) 的优惠配置", + timeLimitedDiscount.toString(), sku.toString())); + } + // 计算价格 + if (PreferentialTypeEnum.PRICE.getValue().equals(item.getPreferentialType())) { // 减价 + int presentPrice = sku.getBuyPrice() - item.getPreferentialValue(); + return presentPrice >= 0 ? presentPrice : sku.getBuyPrice(); // 如果计算优惠价格小于 0 ,则说明无法使用优惠。 + } + if (PreferentialTypeEnum.DISCOUNT.getValue().equals(item.getPreferentialType())) { // 打折 + return sku.getBuyPrice() * item.getPreferentialValue() / 100; + } + throw new IllegalArgumentException(String.format("折扣活动(%s) 的优惠类型不正确", timeLimitedDiscount.toString())); + } + + private List groupByFullPrivilege(List items, List activityList) { + List itemGroups = new ArrayList<>(); + // 获得所有满减送促销 + List fullPrivileges = activityList.stream() + .filter(activity -> PromotionActivityTypeEnum.FULL_PRIVILEGE.getValue().equals(activity.getActivityType())) + .collect(Collectors.toList()); + // 基于满减送促销,进行分组 + if (!fullPrivileges.isEmpty()) { + items = new ArrayList<>(items); // 因为下面会修改数组,进行浅拷贝,避免影响传入的 items 。 + for (PromotionActivityRespDTO fullPrivilege : fullPrivileges) { + // 创建 fullPrivilege 对应的分组 + PriceProductCalcRespDTO.ItemGroup itemGroup = new PriceProductCalcRespDTO.ItemGroup() + .setActivity(fullPrivilege) + .setItems(new ArrayList<>()); + // 筛选商品到分组中 + for (Iterator iterator = items.iterator(); iterator.hasNext(); ) { + PriceProductCalcRespDTO.Item item = iterator.next(); + if (!isSpuMatchFullPrivilege(item.getSpuId(), fullPrivilege)) { + continue; + } + itemGroup.getItems().add(item); + iterator.remove(); + } + // 如果匹配到,则添加到 itemGroups 中 + if (!itemGroup.getItems().isEmpty()) { + itemGroups.add(itemGroup); + } + } + } + // 处理未参加活动的商品,形成一个分组 + if (!items.isEmpty()) { + itemGroups.add(new PriceProductCalcRespDTO.ItemGroup().setItems(items)); + } + // 计算每个分组的价格 + for (PriceProductCalcRespDTO.ItemGroup itemGroup : itemGroups) { + itemGroup.setActivityDiscountTotal(calcSkuPriceByFullPrivilege(itemGroup)); + } + // 返回结果 + return itemGroups; + } + + private boolean isSpuMatchFullPrivilege(Integer spuId, PromotionActivityRespDTO activity) { + Assert.isTrue(PromotionActivityTypeEnum.FULL_PRIVILEGE.getValue().equals(activity.getActivityType()), + "传入的必须的促销活动必须是满减送"); + PromotionActivityRespDTO.FullPrivilege fullPrivilege = activity.getFullPrivilege(); + if (RangeTypeEnum.ALL.getValue().equals(fullPrivilege.getRangeType())) { + return true; + } else if (RangeTypeEnum.PRODUCT_INCLUDE_PART.getValue().equals(fullPrivilege.getRangeType())) { + return fullPrivilege.getRangeValues().contains(spuId); + } + throw new IllegalArgumentException(String.format("促销活动(%s) 可用范围的类型是不正确", activity.toString())); + } + + private Integer calcSkuPriceByFullPrivilege(PriceProductCalcRespDTO.ItemGroup itemGroup) { + if (itemGroup.getActivity() == null) { + return null; + } + PromotionActivityRespDTO activity = itemGroup.getActivity(); + Assert.isTrue(PromotionActivityTypeEnum.FULL_PRIVILEGE.getValue().equals(activity.getActivityType()), + "传入的必须的满减送活动必须是满减送"); + // 获得优惠信息 + List items = itemGroup.getItems(); + Integer itemCnt = items.stream().mapToInt(PriceProductCalcRespDTO.Item::getBuyQuantity).sum(); + Integer originalTotal = items.stream().mapToInt(PriceProductCalcRespDTO.Item::getPresentTotal).sum(); + List privileges = activity.getFullPrivilege().getPrivileges().stream() + .filter(privilege -> { + if (MeetTypeEnum.PRICE.getValue().equals(privilege.getMeetType())) { + return originalTotal >= privilege.getMeetValue(); + } + if (MeetTypeEnum.QUANTITY.getValue().equals(privilege.getMeetType())) { + return itemCnt >= privilege.getMeetValue(); + } + throw new IllegalArgumentException(String.format("满减送活动(%s) 的匹配(%s)不正确", itemGroup.getActivity().toString(), privilege.toString())); + }).collect(Collectors.toList()); + // 获得不到优惠信息,返回原始价格 + if (privileges.isEmpty()) { + return null; + } + // 获得到优惠信息,进行价格计算 + PromotionActivityRespDTO.FullPrivilege.Privilege privilege = privileges.get(privileges.size() - 1); + Integer presentTotal; + if (PreferentialTypeEnum.PRICE.getValue().equals(privilege.getPreferentialType())) { // 减价 + // 计算循环次数。这样,后续优惠的金额就是相乘了 + Integer cycleCount = 1; + if (activity.getFullPrivilege().getCycled()) { + if (MeetTypeEnum.PRICE.getValue().equals(privilege.getMeetType())) { + cycleCount = originalTotal / privilege.getMeetValue(); + } else if (MeetTypeEnum.QUANTITY.getValue().equals(privilege.getMeetType())) { + cycleCount = itemCnt / privilege.getMeetValue(); + } + } + presentTotal = originalTotal - cycleCount * privilege.getMeetValue(); + if (presentTotal < 0) { // 如果计算优惠价格小于 0 ,则说明无法使用优惠。 + presentTotal = originalTotal; + } + } else if (PreferentialTypeEnum.DISCOUNT.getValue().equals(privilege.getPreferentialType())) { // 打折 + presentTotal = originalTotal * privilege.getPreferentialValue() / 100; + } else { + throw new IllegalArgumentException(String.format("满减送促销(%s) 的优惠类型不正确", activity.toString())); + } + int discountTotal = originalTotal - presentTotal; + if (discountTotal == 0) { + return null; + } + // 按比例,拆分 presentTotal + splitDiscountPriceToItems(items, discountTotal, presentTotal); + // 返回优惠金额 + return originalTotal - presentTotal; + } + + private Integer modifyPriceByCouponCard(Integer userId, Integer couponCardId, List itemGroups) { + Assert.isTrue(couponCardId != null, "优惠劵编号不能为空"); + // 查询优惠劵 + CouponCardRespDTO couponCard = couponCardService.getCouponCard(userId, couponCardId); + if (couponCard == null) { + throw ServiceExceptionUtil.exception(COUPON_CARD_NOT_EXISTS); + } + CouponTemplateRespDTO couponTemplate = couponTemplateService.getCouponTemplate(couponCardId); + if (couponTemplate == null) { + throw ServiceExceptionUtil.exception(COUPON_TEMPLATE_NOT_EXISTS); + } + // 获得匹配的商品 + List items = new ArrayList<>(); + if (RangeTypeEnum.ALL.getValue().equals(couponTemplate.getRangeType())) { + itemGroups.forEach(itemGroup -> items.addAll(itemGroup.getItems())); + } else if (RangeTypeEnum.PRODUCT_INCLUDE_PART.getValue().equals(couponTemplate.getRangeType())) { + itemGroups.forEach(itemGroup -> items.forEach(item -> { + if (couponTemplate.getRangeValues().contains(item.getSpuId())) { + items.add(item); + } + })); + } else if (RangeTypeEnum.PRODUCT_EXCLUDE_PART.getValue().equals(couponTemplate.getRangeType())) { + itemGroups.forEach(itemGroup -> items.forEach(item -> { + if (!couponTemplate.getRangeValues().contains(item.getSpuId())) { + items.add(item); + } + })); + } else if (RangeTypeEnum.CATEGORY_INCLUDE_PART.getValue().equals(couponTemplate.getRangeType())) { + itemGroups.forEach(itemGroup -> items.forEach(item -> { + if (couponTemplate.getRangeValues().contains(item.getCid())) { + items.add(item); + } + })); + } else if (RangeTypeEnum.CATEGORY_EXCLUDE_PART.getValue().equals(couponTemplate.getRangeType())) { + itemGroups.forEach(itemGroup -> items.forEach(item -> { + if (!couponTemplate.getRangeValues().contains(item.getCid())) { + items.add(item); + } + })); + } + // 判断是否符合条件 + int originalTotal = items.stream().mapToInt(PriceProductCalcRespDTO.Item::getPresentTotal).sum(); // 此处,指的是以优惠劵视角的原价 + if (originalTotal == 0 || originalTotal < couponCard.getPriceAvailable()) { + throw ServiceExceptionUtil.exception(COUPON_CARD_NOT_MATCH); // TODO 芋艿,这种情况,会出现错误码的提示,无法格式化出来。另外,这块的最佳实践,找人讨论下。 + } + // 计算价格 + // 获得到优惠信息,进行价格计算 + int presentTotal; + if (PreferentialTypeEnum.PRICE.getValue().equals(couponCard.getPreferentialType())) { // 减价 + // 计算循环次数。这样,后续优惠的金额就是相乘了 + presentTotal = originalTotal - couponCard.getPriceOff(); + Assert.isTrue(presentTotal > 0, "计算后,价格为负数:" + presentTotal); + } else if (PreferentialTypeEnum.DISCOUNT.getValue().equals(couponCard.getPreferentialType())) { // 打折 + presentTotal = originalTotal * couponCard.getPercentOff() / 100; + if (couponCard.getDiscountPriceLimit() != null // 空,代表不限制优惠上限 + && originalTotal - presentTotal > couponCard.getDiscountPriceLimit()) { + presentTotal = originalTotal - couponCard.getDiscountPriceLimit(); + } + } else { + throw new IllegalArgumentException(String.format("优惠劵(%s) 的优惠类型不正确", couponCard.toString())); + } + int discountTotal = originalTotal - presentTotal; + Assert.isTrue(discountTotal > 0, "计算后,不产生优惠:" + discountTotal); + // 按比例,拆分 presentTotal + splitDiscountPriceToItems(items, discountTotal, presentTotal); + // 返回优惠金额 + return originalTotal - presentTotal; + } + + private void splitDiscountPriceToItems(List items, Integer discountTotal, Integer presentTotal) { + for (int i = 0; i < items.size(); i++) { + PriceProductCalcRespDTO.Item item = items.get(i); + Integer discountPart; + if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 + discountPart = (int) (discountTotal * (1.0D * item.getPresentTotal() / presentTotal)); + discountTotal -= discountPart; + } else { + discountPart = discountTotal; + } + Assert.isTrue(discountPart > 0, "优惠金额必须大于 0"); + item.setDiscountTotal(item.getDiscountTotal() + discountPart); + item.setPresentTotal(item.getBuyTotal() - item.getDiscountTotal()); + item.setPresentPrice(item.getPresentTotal() / item.getBuyQuantity()); + } + } } diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/package-info.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/package-info.java new file mode 100644 index 000000000..f7ddc97c2 --- /dev/null +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.mall.promotionservice.rpc; diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/price/PriceRpcImpl.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/price/PriceRpcImpl.java new file mode 100644 index 000000000..096d84b64 --- /dev/null +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/rpc/price/PriceRpcImpl.java @@ -0,0 +1,24 @@ +package cn.iocoder.mall.promotionservice.rpc.price; + +import cn.iocoder.common.framework.vo.CommonResult; +import cn.iocoder.mall.promotion.api.rpc.price.PriceRpc; +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcReqDTO; +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcRespDTO; +import cn.iocoder.mall.promotionservice.manager.price.PriceManager; +import org.apache.dubbo.config.annotation.DubboService; +import org.springframework.beans.factory.annotation.Autowired; + +import static cn.iocoder.common.framework.vo.CommonResult.success; + +@DubboService +public class PriceRpcImpl implements PriceRpc { + + @Autowired + private PriceManager priceManager; + + @Override + public CommonResult calcProductPrice(PriceProductCalcReqDTO calcReqDTO) { + return success(priceManager.calcProductPrice(calcReqDTO)); + } + +} diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponCardService.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponCardService.java new file mode 100644 index 000000000..51f35616b --- /dev/null +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponCardService.java @@ -0,0 +1,34 @@ +package cn.iocoder.mall.promotionservice.service.coupon; + +import cn.iocoder.mall.promotion.api.rpc.coupon.dto.card.CouponCardRespDTO; +import cn.iocoder.mall.promotionservice.convert.coupon.card.CouponCardConvert; +import cn.iocoder.mall.promotionservice.dal.mysql.dataobject.coupon.CouponCardDO; +import cn.iocoder.mall.promotionservice.dal.mysql.mapper.coupon.CouponCardMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Objects; + +/** + * 优惠劵 Service + */ +@Service +@Validated +public class CouponCardService { + + @Autowired + private CouponCardMapper couponCardMapper; + + public CouponCardRespDTO getCouponCard(Integer userId, Integer couponCardId) { + CouponCardDO card = couponCardMapper.selectById(couponCardId); + if (card == null) { + return null; + } + if (!Objects.equals(userId, card.getUserId())) { + return null; + } + return CouponCardConvert.INSTANCE.convert(card); + } + +} diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponService.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponService.java index 9754e19b4..d3a0a0f84 100644 --- a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponService.java +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponService.java @@ -5,7 +5,7 @@ import cn.iocoder.common.framework.util.DateUtil; import cn.iocoder.mall.promotion.api.enums.*; import cn.iocoder.mall.promotion.api.rpc.coupon.dto.*; import cn.iocoder.mall.promotionservice.convert.coupon.CouponCardConvert; -import cn.iocoder.mall.promotionservice.convert.coupon.CouponTemplateConvert; +import cn.iocoder.mall.promotionservice.convert.coupon.card.CouponTemplateConvert; import cn.iocoder.mall.promotionservice.dal.mysql.dataobject.coupon.CouponCardDO; import cn.iocoder.mall.promotionservice.dal.mysql.dataobject.coupon.CouponTemplateDO; import cn.iocoder.mall.promotionservice.dal.mysql.mapper.coupon.CouponCardMapper; @@ -31,11 +31,6 @@ public class CouponService { // ========== 优惠劵(码)模板 ========== - public cn.iocoder.mall.promotionservice.service.coupon.bo.CouponTemplateBO getCouponTemplate(Integer couponTemplateId) { - CouponTemplateDO template = couponTemplateMapper.selectById(couponTemplateId); - return CouponTemplateConvert.INSTANCE.convert(template); - } - public CouponTemplatePageRespDTO getCouponTemplatePage(CouponTemplatePageReqDTO couponTemplatePageDTO) { CouponTemplatePageRespDTO couponTemplatePageBO = new CouponTemplatePageRespDTO(); // 查询分页数据 @@ -51,7 +46,7 @@ public class CouponService { return couponTemplatePageBO; } - public cn.iocoder.mall.promotionservice.service.coupon.bo.CouponTemplateBO addCouponCardTemplate(CouponCardTemplateAddBO couponCardTemplateAddDTO) { + public Integer addCouponCardTemplate(CouponCardTemplateAddBO couponCardTemplateAddDTO) { // 校验生效日期相关 checkCouponTemplateDateType(couponCardTemplateAddDTO.getDateType(), couponCardTemplateAddDTO.getValidStartTime(), couponCardTemplateAddDTO.getValidEndTime(), @@ -68,7 +63,7 @@ public class CouponService { template.setCreateTime(new Date()); couponTemplateMapper.insert(template); // 返回成功 - return CouponTemplateConvert.INSTANCE.convert(template); + return template.getId(); } public Boolean updateCouponCodeTemplate(CouponCodeTemplateUpdateBO couponCodeTemplateUpdateDTO) { diff --git a/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponTemplateService.java b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponTemplateService.java new file mode 100644 index 000000000..d964c0d51 --- /dev/null +++ b/promotion-service-project/promotion-service-app/src/main/java/cn/iocoder/mall/promotionservice/service/coupon/CouponTemplateService.java @@ -0,0 +1,24 @@ +package cn.iocoder.mall.promotionservice.service.coupon; + +import cn.iocoder.mall.promotion.api.rpc.coupon.dto.template.CouponTemplateRespDTO; +import cn.iocoder.mall.promotionservice.convert.coupon.card.CouponTemplateConvert; +import cn.iocoder.mall.promotionservice.dal.mysql.mapper.coupon.CouponTemplateMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * 优惠劵模板 Service + */ +@Service +@Validated +public class CouponTemplateService { + + @Autowired + private CouponTemplateMapper couponTemplateMapper; + + public CouponTemplateRespDTO getCouponTemplate(Integer couponCardId) { + return CouponTemplateConvert.INSTANCE.convert(couponTemplateMapper.selectById(couponCardId)); + } + +} diff --git a/promotion-service-project/promotion-service-app/src/main/resources/application.yaml b/promotion-service-project/promotion-service-app/src/main/resources/application.yaml index 5fdb1ef92..542011fa4 100644 --- a/promotion-service-project/promotion-service-app/src/main/resources/application.yaml +++ b/promotion-service-project/promotion-service-app/src/main/resources/application.yaml @@ -41,7 +41,7 @@ dubbo: version: 1.0.0 ProductSkuRpc: version: 1.0.0 - ProductSpuService: + ProductSpuRpc: version: 1.0.0 # RocketMQ 配置项 diff --git a/promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/package-info.java b/promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/package-info.java new file mode 100644 index 000000000..d21a6c1f3 --- /dev/null +++ b/promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.mall.promotionservice.manager; diff --git a/promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/price/PriceManagerTest.java b/promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/price/PriceManagerTest.java new file mode 100644 index 000000000..565ff626c --- /dev/null +++ b/promotion-service-project/promotion-service-app/src/test/java/cn/iocoder/mall/promotionservice/manager/price/PriceManagerTest.java @@ -0,0 +1,30 @@ +package cn.iocoder.mall.promotionservice.manager.price; + +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcReqDTO; +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcRespDTO; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Arrays; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +public class PriceManagerTest { + + @Autowired + private PriceManager priceManager; + + @Test + public void testCalcProductPrice() { + PriceProductCalcReqDTO calcReqDTO = new PriceProductCalcReqDTO(); + PriceProductCalcReqDTO.Item item01 = new PriceProductCalcReqDTO.Item(33, 2); // 满足满减送的商品 + PriceProductCalcReqDTO.Item item02 = new PriceProductCalcReqDTO.Item(34, 2); // 满足限时折扣的商品 + calcReqDTO.setItems(Arrays.asList(item01, item02)); + PriceProductCalcRespDTO calcRespDTO = priceManager.calcProductPrice(calcReqDTO); + System.out.println(calcRespDTO); + } + +} diff --git a/shop-web-app/pom.xml b/shop-web-app/pom.xml index 749465e14..b91be9b76 100644 --- a/shop-web-app/pom.xml +++ b/shop-web-app/pom.xml @@ -72,6 +72,12 @@ order-service-api 1.0-SNAPSHOT + + + cn.iocoder.mall + promotion-service-api + 1.0-SNAPSHOT + cn.iocoder.mall diff --git a/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/CartController.java b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/CartController.java index e511ab0bf..39448331a 100644 --- a/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/CartController.java +++ b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/CartController.java @@ -2,6 +2,7 @@ package cn.iocoder.mall.shopweb.controller.order; import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.mall.security.user.core.context.UserSecurityContextHolder; +import cn.iocoder.mall.shopweb.controller.order.vo.cart.CartDetailVO; import cn.iocoder.mall.shopweb.manager.order.cart.CartManager; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; @@ -40,4 +41,10 @@ public class CartController { return success(cartManager.sumCartItemQuantity(UserSecurityContextHolder.getUserId())); } + @GetMapping("/get-detail") + @ApiOperation("查询用户的购物车的商品列表") + public CommonResult getCartDetail() { + return success(cartManager.getCartDetail()); + } + } diff --git a/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/vo/cart/CartDetailVO.java b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/vo/cart/CartDetailVO.java new file mode 100644 index 000000000..b1cad10b9 --- /dev/null +++ b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/controller/order/vo/cart/CartDetailVO.java @@ -0,0 +1,213 @@ +package cn.iocoder.mall.shopweb.controller.order.vo.cart; + +import cn.iocoder.mall.promotion.api.rpc.activity.dto.PromotionActivityRespDTO; +import cn.iocoder.mall.shopweb.controller.product.vo.attr.ProductAttrKeyValueRespVO; +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +@ApiModel(value = "用户的购物车明细 VO") +@Data +@Accessors(chain = true) +public class CartDetailVO { + + /** + * 商品分组数组 + */ + private List itemGroups; + /** + * 费用 + */ + private Fee fee; + + /** + * 商品分组 + * + * 多个商品,参加同一个活动,从而形成分组。 + */ + @Data + @Accessors(chain = true) + public static class ItemGroup { + + /** + * 优惠活动 + */ + private PromotionActivityRespDTO activity; // TODO 芋艿,偷懒 + /** + * 促销减少的金额 + * + * 1. 若未参与促销活动,或不满足促销条件,返回 null + * 2. 该金额,已经分摊到每个 Item 的 discountTotal ,需要注意。 + */ + private Integer activityDiscountTotal; + /** + * 商品数组 + */ + private List items; + + } + + @Data + @Accessors(chain = true) + public static class Sku { + + // SKU 自带信息 + /** + * sku 编号 + */ + private Integer id; + /** + * SPU 信息 + */ + private Spu spu; + /** + * 图片地址 + */ + private String picURL; + /** + * 规格值数组 + */ + private List attrs; // TODO 后面改下 + /** + * 价格,单位:分 + */ + private Integer price; + /** + * 库存数量 + */ + private Integer quantity; + + // 非 SKU 自带信息 + + /** + * 购买数量 + */ + private Integer buyQuantity; + /** + * 是否选中 + */ + private Boolean selected; + /** + * 优惠活动 + */ + private PromotionActivityRespDTO activity; // TODO 芋艿,偷懒 + /** + * 原始单价,单位:分。 + */ + private Integer originPrice; + /** + * 购买单价,单位:分 + */ + private Integer buyPrice; + /** + * 最终价格,单位:分。 + */ + private Integer presentPrice; + /** + * 购买总金额,单位:分 + * + * 用途类似 {@link #presentTotal} + */ + private Integer buyTotal; + /** + * 优惠总金额,单位:分。 + */ + private Integer discountTotal; + /** + * 最终总金额,单位:分。 + * + * 注意,presentPrice * quantity 不一定等于 presentTotal 。 + * 因为,存在无法整除的情况。 + * 举个例子,presentPrice = 8.33 ,quantity = 3 的情况,presentTotal 有可能是 24.99 ,也可能是 25 。 + * 所以,需要存储一个该字段。 + */ + private Integer presentTotal; + + } + + @Data + @Accessors(chain = true) + public static class Spu { + + /** + * SPU 编号 + */ + private Integer id; + + // ========== 基本信息 ========= + /** + * SPU 名字 + */ + private String name; + /** + * 分类编号 + */ + private Integer cid; + /** + * 商品主图地址 + * + * 数组,以逗号分隔 + * + * 建议尺寸:800*800像素,你可以拖拽图片调整顺序,最多上传15张 + */ + private List picUrls; + + } + + /** + * 费用(合计) + */ + @Data + @Accessors(chain = true) + public static class Fee { + + /** + * 购买总价 + */ + private Integer buyTotal; + /** + * 优惠总价 + * + * 注意,满多少元包邮,不算在优惠中。 + */ + private Integer discountTotal; + /** + * 邮费 + */ + private Integer postageTotal; + /** + * 最终价格 + * + * 计算公式 = 总价 - 优惠总价 + 邮费 + */ + private Integer presentTotal; + + public Fee() { + } + + public Fee(Integer buyTotal, Integer discountTotal, Integer postageTotal, Integer presentTotal) { + this.buyTotal = buyTotal; + this.discountTotal = discountTotal; + this.postageTotal = postageTotal; + this.presentTotal = presentTotal; + } + + } + + /** + * 邮费信息 TODO 芋艿,未完成 + */ + @Data + @Accessors(chain = true) + public static class Postage { + + /** + * 需要满足多少钱,可以包邮。单位:分 + */ + private Integer threshold; + + } + +} diff --git a/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/convert/order/CartConvert.java b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/convert/order/CartConvert.java new file mode 100644 index 000000000..7f99e5ec3 --- /dev/null +++ b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/convert/order/CartConvert.java @@ -0,0 +1,11 @@ +package cn.iocoder.mall.shopweb.convert.order; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CartConvert { + + CartConvert INSTANCE = Mappers.getMapper(CartConvert.class); + +} diff --git a/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/manager/order/cart/CartManager.java b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/manager/order/cart/CartManager.java index 2c627dbac..fca063c59 100644 --- a/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/manager/order/cart/CartManager.java +++ b/shop-web-app/src/main/java/cn/iocoder/mall/shopweb/manager/order/cart/CartManager.java @@ -1,11 +1,22 @@ package cn.iocoder.mall.shopweb.manager.order.cart; +import cn.iocoder.common.framework.util.CollectionUtils; import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.mall.orderservice.rpc.cart.CartRpc; import cn.iocoder.mall.orderservice.rpc.cart.dto.CartItemAddReqDTO; +import cn.iocoder.mall.orderservice.rpc.cart.dto.CartItemListReqDTO; +import cn.iocoder.mall.orderservice.rpc.cart.dto.CartItemRespDTO; +import cn.iocoder.mall.promotion.api.rpc.price.PriceRpc; +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcReqDTO; +import cn.iocoder.mall.promotion.api.rpc.price.dto.PriceProductCalcRespDTO; +import cn.iocoder.mall.shopweb.controller.order.vo.cart.CartDetailVO; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.stereotype.Service; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + /** * 购物车 Manager */ @@ -14,6 +25,8 @@ public class CartManager { @DubboReference(version = "${dubbo.consumer.ProductCategoryRpc.version}") private CartRpc cartRpc; + @DubboReference(version = "${dubbo.consumer.PriceRpc.version}") + private PriceRpc priceRpc; /** * 添加商品到购物车 @@ -40,4 +53,33 @@ public class CartManager { return sumCartItemQuantityResult.getData(); } + /** + * 查询用户的购物车的商品列表 + * + * @return 商品列表 + */ + public CartDetailVO getCartDetail(Integer userId) { + // 获得购物车中选中的 + CommonResult> listCartItemsResult = cartRpc.listCartItems(new CartItemListReqDTO().setUserId(userId)); + listCartItemsResult.checkError(); + // 购物车为空时,构造空的 UsersOrderConfirmCreateVO 返回 + if (CollectionUtils.isEmpty(listCartItemsResult.getData())) { + CartDetailVO result = new CartDetailVO(); + result.setItemGroups(Collections.emptyList()); + result.setFee(new CartDetailVO.Fee(0, 0, 0, 0)); + return result; + } + // 计算选中的商品价格 + CommonResult calcProductPriceResult = priceRpc.calcProductPrice(new PriceProductCalcReqDTO().setUserId(userId) + .setItems(listCartItemsResult.getData().stream() + .filter(CartItemRespDTO::getSelected) + .map(cartItem -> new PriceProductCalcReqDTO.Item(cartItem.getSkuId(), cartItem.getQuantity())) + .collect(Collectors.toList()))); + calcProductPriceResult.checkError(); + // 拼接结果 + + // 执行数据拼装 + return null; + } + } diff --git a/shop-web-app/src/main/resources/application.yml b/shop-web-app/src/main/resources/application.yml index a09b6edc4..884a02a0e 100644 --- a/shop-web-app/src/main/resources/application.yml +++ b/shop-web-app/src/main/resources/application.yml @@ -39,6 +39,8 @@ dubbo: version: 1.0.0 SearchProductRpc: version: 1.0.0 + PriceRpc: + version: 1.0.0 # Swagger 配置项 swagger: