Bladeren bron

mall:价格计算的 API 简单实现

YunaiV 2 jaren geleden
bovenliggende
commit
15798cea10
39 gewijzigde bestanden met toevoegingen van 1017 en 375 verwijderingen
  1. 43 32
      yudao-module-mall/yudao-module-market-api/src/main/java/cn/iocoder/yudao/module/market/api/price/dto/PriceCalculateRespDTO.java
  2. 5 0
      yudao-module-mall/yudao-module-market-biz/pom.xml
  3. 1 0
      yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/api/discount/package-info.java
  4. 9 7
      yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/api/price/PriceApiImpl.java
  5. 40 0
      yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/convert/price/PriceConvert.java
  6. 21 0
      yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceService.java
  7. 64 0
      yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceServiceImpl.java
  8. 15 5
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java
  9. 3 1
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/ProductSkuRespDTO.java
  10. 7 6
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java
  11. 5 5
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/SpuInfoRespDTO.java
  12. 1 0
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/enums/ErrorCodeConstants.java
  13. 21 4
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApiImpl.java
  14. 5 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApiImpl.java
  15. 1 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/sku/vo/ProductSkuBaseVO.java
  16. 5 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/sku/ProductSkuConvert.java
  17. 7 7
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  18. 4 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/package-info.java
  19. 23 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java
  20. 34 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java
  21. 25 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java
  22. 0 71
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/CartController.java
  23. 47 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/TradeCartController.http
  24. 84 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/TradeCartController.java
  25. 118 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartDetailRespVO.java
  26. 23 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartItemAddCountReqVO.java
  27. 23 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartItemUpdateCountReqVO.java
  28. 22 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartItemUpdateSelectedReqVO.java
  29. 0 211
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/CartDetailVO.java
  30. 2 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java
  31. 45 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/cart/TradeCartConvert.java
  32. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderItemConvert.java
  33. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/sku/ProductSkuConvert.java
  34. 4 8
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/cart/TradeCartItemDO.java
  35. 47 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/cart/TradeCartItemMapper.java
  36. 66 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/cart/TradeCartService.java
  37. 184 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/cart/TradeCartServiceImpl.java
  38. 10 10
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderServiceImpl.java
  39. 1 0
      yudao-server/src/main/resources/application-local.yaml

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

@@ -19,15 +19,10 @@ public class PriceCalculateRespDTO {
      */
     private Order order;
 
-    /**
-     * 商品 SKU 数组
-     */
-    private List<Item> items;
-
     /**
      * 营销活动数组
      *
-     * 只对应 {@link #items} 商品匹配的活动
+     * 只对应 {@link Order#items} 商品匹配的活动
      */
     private List<Promotion> promotions;
 
@@ -40,13 +35,13 @@ public class PriceCalculateRespDTO {
         /**
          * 商品原价(总),单位:分
          *
-         * 基于 {@link Item#getTotalOriginalPrice()} 求和
+         * 基于 {@link OrderItem#getTotalOriginalPrice()} 求和
          */
         private Integer skuOriginalPrice;
         /**
          * 商品优惠(总),单位:分
          *
-         * 基于 {@link Item#getTotalPromotionPrice()} 求和
+         * 基于 {@link OrderItem#getTotalPromotionPrice()} 求和
          */
         private Integer skuPromotionPrice;
         /**
@@ -69,6 +64,10 @@ public class PriceCalculateRespDTO {
          */
         //         * - {@link #couponPrice}  // TODO 芋艿:靠营销表记录
         private Integer payPrice;
+        /**
+         * 商品 SKU 数组
+         */
+        private List<OrderItem> items;
 
         // ========== 营销基本信息 ==========
         /**
@@ -85,10 +84,15 @@ public class PriceCalculateRespDTO {
     }
 
     /**
-     * 商品 SKU
+     * 订单商品 SKU
      */
     @Data
-    public static class Item extends PriceCalculateReqDTO.Item {
+    public static class OrderItem extends PriceCalculateReqDTO.Item {
+
+        /**
+         * 购买数量
+         */
+        private Integer count;
 
         /**
          * 商品原价(单),单位:分
@@ -140,6 +144,10 @@ public class PriceCalculateRespDTO {
          * 例如说:营销活动的编号、优惠劵的编号
          */
         private Long id;
+        /**
+         * 营销名字
+         */
+        private String name;
         /**
          * 营销类型
          *
@@ -152,10 +160,6 @@ public class PriceCalculateRespDTO {
          * 枚举 {@link PromotionLevelEnum}
          */
         private Integer level;
-        /**
-         * 匹配的商品 SKU 数组
-         */
-        private List<Item> items;
         /**
          * 计算时的原价(总),单位:分
          */
@@ -164,6 +168,13 @@ public class PriceCalculateRespDTO {
          * 计算时的优惠(总),单位:分
          */
         private Integer totalPromotionPrice;
+        /**
+         * 匹配的商品 SKU 数组
+         */
+        private List<PromotionItem> items;
+
+        // ========== 匹配情况 ==========
+
         /**
          * 是否满足优惠条件
          */
@@ -176,26 +187,26 @@ public class PriceCalculateRespDTO {
          */
         private String meetTip;
 
-        /**
-         * 匹配的商品 SKU
-         */
-        @Data
-        public static class Item {
+    }
 
-            /**
-             * 商品 SKU 编号
-             */
-            private Long skuId;
-            /**
-             * 计算时的原价(总),单位:分
-             */
-            private Integer totalOriginalPrice;
-            /**
-             * 计算时的优惠(总),单位:分
-             */
-            private Integer totalPromotionPrice;
+    /**
+     * 营销匹配的商品 SKU
+     */
+    @Data
+    public static class PromotionItem {
 
-        }
+        /**
+         * 商品 SKU 编号
+         */
+        private Long skuId;
+        /**
+         * 计算时的原价(总),单位:分
+         */
+        private Integer totalOriginalPrice;
+        /**
+         * 计算时的优惠(总),单位:分
+         */
+        private Integer totalPromotionPrice;
 
     }
 

+ 5 - 0
yudao-module-mall/yudao-module-market-biz/pom.xml

@@ -24,6 +24,11 @@
             <artifactId>yudao-module-market-api</artifactId>
             <version>${revision}</version>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-product-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- 业务组件 -->
         <dependency>

+ 1 - 0
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/api/discount/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.market.api.discount;

+ 9 - 7
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceApiImpl.java → yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/api/price/PriceApiImpl.java

@@ -1,24 +1,26 @@
-package cn.iocoder.yudao.module.market.service.price;
+package cn.iocoder.yudao.module.market.api.price;
 
-import cn.iocoder.yudao.module.market.api.price.PriceApi;
 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.service.price.PriceService;
 import org.springframework.stereotype.Service;
 
+import javax.annotation.Resource;
+
 /**
  * 价格 API 实现类
  *
- * TODO 完善注释
- *
- * @author TODO
+ * @author 芋道源码
  */
 @Service
 public class PriceApiImpl implements PriceApi {
 
+    @Resource
+    private PriceService priceService;
+
     @Override
     public PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO) {
-        // TODO fixme:实现逻辑
-        return new PriceCalculateRespDTO();
+        return priceService.calculatePrice(calculateReqDTO);
     }
 
 }

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

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.market.convert.price;
+
+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.product.api.sku.dto.ProductSkuRespDTO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface PriceConvert {
+
+    PriceConvert INSTANCE = Mappers.getMapper(PriceConvert.class);
+
+    default PriceCalculateRespDTO convert(PriceCalculateReqDTO calculateReqDTO, List<ProductSkuRespDTO> skuList) {
+        // 创建 PriceCalculateRespDTO 对象
+        PriceCalculateRespDTO priceCalculate = new PriceCalculateRespDTO();
+        priceCalculate.setOrder(new PriceCalculateRespDTO.Order().setSkuOriginalPrice(0).setSkuPromotionPrice(0)
+                .setOrderPromotionPrice(0).setDeliveryPrice(0).setPayPrice(0).setItems(new ArrayList<>())
+                .setCouponId(calculateReqDTO.getCouponId()));
+        priceCalculate.setPromotions(new ArrayList<>());
+        // 创建它的 OrderItem 属性
+        Map<Long, Integer> 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)
+                    .setOriginalPrice(sku.getPrice()).setTotalOriginalPrice(sku.getPrice() * count).setTotalPromotionPrice(0);
+            orderItem.setTotalPresentPrice(orderItem.getTotalPresentPrice()).setPresentPrice(orderItem.getOriginalPrice())
+                    .setTotalPayPrice(orderItem.getTotalPayPrice());
+            priceCalculate.getOrder().getItems().add(orderItem);
+        });
+        return priceCalculate;
+    }
+
+}

+ 21 - 0
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/service/price/PriceService.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.market.service.price;
+
+import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO;
+import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
+
+/**
+ * 价格计算 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface PriceService {
+
+    /**
+     * 计算商品的价格
+     *
+     * @param calculateReqDTO 价格请求
+     * @return 价格相应
+     */
+    PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO);
+
+}

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

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.market.service.price;
+
+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.product.api.sku.ProductSkuApi;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH;
+
+/**
+ * 价格计算 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class PriceServiceImpl implements PriceService {
+
+    @Resource
+    private ProductSkuApi productSkuApi;
+
+    @Override
+    public PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO) {
+        // 获得商品 SKU 数组
+        List<ProductSkuRespDTO> skuList = checkProductSkus(calculateReqDTO);
+        // 初始化 PriceCalculateRespDTO 对象
+        PriceCalculateRespDTO priceCalculate = PriceConvert.INSTANCE.convert(calculateReqDTO, skuList);
+
+        // 计算【限时折扣】促销 TODO 待实现
+        // 计算【满减送】促销 TODO 待实现
+        // 计算【优惠劵】促销 TODO 待实现
+        return priceCalculate;
+    }
+
+    private List<ProductSkuRespDTO> checkProductSkus(PriceCalculateReqDTO calculateReqDTO) {
+        // 获得商品 SKU 数组
+        Map<Long, Integer> skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(),
+                PriceCalculateReqDTO.Item::getSkuId, PriceCalculateReqDTO.Item::getCount);
+        List<ProductSkuRespDTO> skus = productSkuApi.getSkuList(skuIdCountMap.keySet());
+
+        // 校验商品 SKU
+        skus.forEach(sku -> {
+            Integer count = skuIdCountMap.get(sku.getId());
+            if (count == null) {
+                throw exception(SKU_NOT_EXISTS);
+            }
+            if (count > sku.getStock()) {
+                throw exception(SKU_STOCK_NOT_ENOUGH);
+            }
+        });
+        return skus;
+    }
+
+}

+ 15 - 5
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java

@@ -1,25 +1,34 @@
 package cn.iocoder.yudao.module.product.api.sku;
 
 import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
-import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 
 import java.util.Collection;
 import java.util.List;
 
 /**
+ * 商品 SKU API 接口
+ *
  * @author LeeYan9
  * @since 2022-08-26
  */
 public interface ProductSkuApi {
 
+    /**
+     * 查询 SKU 信息
+     *
+     * @param id SKU 编号
+     * @return SKU 信息
+     */
+    ProductSkuRespDTO getSku(Long id);
 
     /**
-     * 根据skuId列表 查询sku信息
+     * 批量查询 SKU 数组
      *
-     * @param skuIds sku ID列表
-     * @return sku信息列表
+     * @param ids SKU 编号列表
+     * @return SKU 数组
      */
-    List<SkuInfoRespDTO> getSkusByIds(Collection<Long> skuIds);
+    List<ProductSkuRespDTO> getSkuList(Collection<Long> ids);
 
     /**
      * 批量扣减sku库存
@@ -27,4 +36,5 @@ public interface ProductSkuApi {
      * @param batchReqDTO sku库存信息列表
      */
     void decrementStockBatch(SkuDecrementStockBatchReqDTO batchReqDTO);
+
 }

+ 3 - 1
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuInfoRespDTO.java → yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/ProductSkuRespDTO.java

@@ -6,11 +6,13 @@ import lombok.Data;
 import java.util.List;
 
 /**
+ * 商品 SKU 信息 Response DTO
+ *
  * @author LeeYan9
  * @since 2022-08-26
  */
 @Data
-public class SkuInfoRespDTO {
+public class ProductSkuRespDTO {
 
     /**
      * 商品 SKU 编号,自增

+ 7 - 6
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java

@@ -1,23 +1,24 @@
 package cn.iocoder.yudao.module.product.api.spu;
 
-import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO;
 import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO;
 
 import java.util.Collection;
 import java.util.List;
 
 /**
+ * 商品 SPU API 接口
+ *
  * @author LeeYan9
  * @since 2022-08-26
  */
 public interface ProductSpuApi {
 
-
     /**
-     * 根据spuId列表 查询spu信息
+     * 批量查询 SPU 数组
      *
-     * @param spuIds spu ID列表
-     * @return spu信息列表
+     * @param ids SPU 编号列表
+     * @return SPU 数组
      */
-    List<SpuInfoRespDTO> getSpusByIds(Collection<Long> spuIds);
+    List<SpuInfoRespDTO> getSpuList(Collection<Long> ids);
+
 }

+ 5 - 5
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/SpuInfoRespDTO.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.product.api.spu.dto;
 
-import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuSpecTypeEnum;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
 import lombok.Data;
@@ -80,25 +80,25 @@ public class SpuInfoRespDTO {
     /**
      * 最小价格,单位使用:分
      * <p>
-     * 基于其对应的 {@link SkuInfoRespDTO#getPrice()} 最小值
+     * 基于其对应的 {@link ProductSkuRespDTO#getPrice()} 最小值
      */
     private Integer minPrice;
     /**
      * 最大价格,单位使用:分
      * <p>
-     * 基于其对应的 {@link SkuInfoRespDTO#getPrice()} 最大值
+     * 基于其对应的 {@link ProductSkuRespDTO#getPrice()} 最大值
      */
     private Integer maxPrice;
     /**
      * 市场价,单位使用:分
      * <p>
-     * 基于其对应的 {@link SkuInfoRespDTO#getMarketPrice()} 最大值
+     * 基于其对应的 {@link ProductSkuRespDTO#getMarketPrice()} 最大值
      */
     private Integer marketPrice;
     /**
      * 总库存
      * <p>
-     * 基于其对应的 {@link SkuInfoRespDTO#getStock()} 求和
+     * 基于其对应的 {@link ProductSkuRespDTO#getStock()} 求和
      */
     private Integer totalStock;
     /**

+ 1 - 0
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/enums/ErrorCodeConstants.java

@@ -36,5 +36,6 @@ public interface ErrorCodeConstants {
     ErrorCode SKU_PROPERTIES_DUPLICATED = new ErrorCode(1008006001, "商品 SKU 的属性组合存在重复");
     ErrorCode SPU_ATTR_NUMBERS_MUST_BE_EQUALS = new ErrorCode(1008006002, "一个 SPU 下的每个 SKU,其规格数必须一致");
     ErrorCode SPU_SKU_NOT_DUPLICATE = new ErrorCode(1008006003, "一个 SPU 下的每个 SKU,必须不重复");
+    ErrorCode SKU_STOCK_NOT_ENOUGH = new ErrorCode(1008006004, "商品 SKU 库存不足");
 
 }

+ 21 - 4
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApiImpl.java

@@ -1,21 +1,38 @@
 package cn.iocoder.yudao.module.product.api.sku;
 
 import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
-import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import cn.iocoder.yudao.module.product.convert.sku.ProductSkuConvert;
+import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
+import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
 import org.springframework.stereotype.Service;
 
+import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
 
 /**
- * todo 注释
+ * 商品 SKU API 实现类
+ *
+ * @author 芋道源码
+ * @since 2022-08-26
  */
 @Service
 public class ProductSkuApiImpl implements ProductSkuApi {
 
+    @Resource
+    private ProductSkuService productSkuService;
+
+    @Override
+    public ProductSkuRespDTO getSku(Long id) {
+        ProductSkuDO skuDO = productSkuService.getSku(id);
+        return ProductSkuConvert.INSTANCE.convert02(skuDO);
+    }
+
     @Override
-    public List<SkuInfoRespDTO> getSkusByIds(Collection<Long> skuIds) {
-        return null;
+    public List<ProductSkuRespDTO> getSkuList(Collection<Long> ids) {
+        List<ProductSkuDO> list = productSkuService.getSkuList(ids);
+        return ProductSkuConvert.INSTANCE.convertList02(list);
     }
 
     @Override

+ 5 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApiImpl.java

@@ -7,13 +7,16 @@ import java.util.Collection;
 import java.util.List;
 
 /**
- * todo 注释
+ * 商品 SPU API 实现类
+ *
+ * @author LeeYan9
+ * @since 2022-08-26
  */
 @Service
 public class ProductSpuApiImpl implements ProductSpuApi {
 
     @Override
-    public List<SpuInfoRespDTO> getSpusByIds(Collection<Long> spuIds) {
+    public List<SpuInfoRespDTO> getSpuList(Collection<Long> spuIds) {
         return null;
     }
 

+ 1 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/sku/vo/ProductSkuBaseVO.java

@@ -33,7 +33,7 @@ public class ProductSkuBaseVO {
     @ApiModelProperty(value = "条形码", example = "haha")
     private String barCode;
 
-    @ApiModelProperty(value = "图片地址")
+    @ApiModelProperty(value = "图片地址", required = true, example = "https://www.iocoder.cn/xx.png")
     @NotNull(message = "图片地址不能为空")
     private String picUrl;
 

+ 5 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/sku/ProductSkuConvert.java

@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.product.convert.sku;
 
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuRespVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
@@ -21,11 +21,14 @@ public interface ProductSkuConvert {
 
     ProductSkuDO convert(ProductSkuCreateOrUpdateReqVO bean);
 
-    @Mapping(source = "properties", target = "properties")
     ProductSkuRespVO convert(ProductSkuDO bean);
 
     List<ProductSkuRespVO> convertList(List<ProductSkuDO> list);
 
     List<ProductSkuDO> convertSkuDOList(List<ProductSkuCreateOrUpdateReqVO> list);
 
+    ProductSkuRespDTO convert02(ProductSkuDO bean);
+
+    List<ProductSkuRespDTO> convertList02(List<ProductSkuDO> list);
+
 }

+ 7 - 7
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/enums/ErrorCodeConstants.java → yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java

@@ -1,23 +1,23 @@
-package cn.iocoder.yudao.module.trade.enums.enums;
+package cn.iocoder.yudao.module.trade.enums;
 
 import cn.iocoder.yudao.framework.common.exception.ErrorCode;
 
 /**
- * Trade-Order 错误码 Core 枚举类
- * <p>
- * Trade-Order 系统,使用 1-011-000-000 段
+ * 交易 错误码枚举类
+ * 交易系统,使用 1-011-000-000 段
  *
  * @author LeeYan9
  * @since 2022-08-26
  */
 public interface ErrorCodeConstants {
 
-    /**
-     * ========== Order 模块 1-011-000-000 ==========
-     */
+    // ==========  Order 模块 1-011-000-000 ==========
     ErrorCode ORDER_SKU_NOT_FOUND = new ErrorCode(1011000001, "商品不存在");
     ErrorCode ORDER_SPU_NOT_SALE = new ErrorCode(1011000002, "商品不可售卖");
     ErrorCode ORDER_SKU_NOT_SALE = new ErrorCode(1011000003, "商品Sku不可售卖");
     ErrorCode ORDER_SKU_STOCK_NOT_ENOUGH = new ErrorCode(1011000004, "商品库存不足");
 
+    // ==========  Cart 模块 1-011-001-000 ==========
+    ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1001001000, "购物车项不存在");
+
 }

+ 4 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 基础包,放一些通用的 VO 类
+ */
+package cn.iocoder.yudao.module.trade.controller.app.base;

+ 23 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.trade.controller.app.base.property;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@ApiModel("用户 App - 规格 + 规格值 Response VO")
+@Data
+public class AppProductPropertyValueDetailRespVO {
+
+    @ApiModelProperty(value = "属性的编号", required = true, example = "1")
+    private Long propertyId;
+
+    @ApiModelProperty(value = "属性的名称", required = true, example = "颜色")
+    private String propertyName;
+
+    @ApiModelProperty(value = "属性值的编号", required = true, example = "1024")
+    private Long valueId;
+
+    @ApiModelProperty(value = "属性值的名称", required = true, example = "红色")
+    private String valueName;
+
+}

+ 34 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.trade.controller.app.base.sku;
+
+import cn.iocoder.yudao.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 商品 SKU 基础 Response VO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class AppProductSkuBaseRespVO {
+
+    @ApiModelProperty(value = "主键", required = true, example = "1024")
+    private Long id;
+
+    @ApiModelProperty(value = "商品 SKU 名字", required = true, example = "芋道")
+    private String name;
+
+    @ApiModelProperty(value = "图片地址", example = "https://www.iocoder.cn/xx.png")
+    private String picUrl;
+
+    @ApiModelProperty(value = "库存", required = true, example = "1")
+    private Integer stock;
+
+    /**
+     * 规格数组
+     */
+    private List<AppProductPropertyValueDetailRespVO> properties;
+
+}

+ 25 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.trade.controller.app.base.spu;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 商品 SPU 基础 Response VO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class AppProductSpuBaseRespVO {
+
+    @ApiModelProperty(value = "主键", required = true, example = "1024")
+    private Long id;
+
+    @ApiModelProperty(value = "商品 SPU 名字", required = true, example = "芋道")
+    private String name;
+
+    @ApiModelProperty(value = "商品主图地址", example = "https://www.iocoder.cn/xx.png")
+    private List<String> picUrls;
+
+}

+ 0 - 71
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/CartController.java

@@ -1,71 +0,0 @@
-package cn.iocoder.yudao.module.trade.controller.app.cart;
-
-import io.swagger.annotations.Api;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@Api(tags = "购物车 API")
-@RestController
-@RequestMapping("/cart")
-@Validated
-public class CartController {
-
-//    @Autowired
-//    private CartManager cartManager;
-//
-//    @PostMapping("add")
-//    @ApiOperation("添加商品到购物车")
-//    @ApiImplicitParams({
-//            @ApiImplicitParam(name = "skuId", value = "商品 SKU 编号", required = true, example = "1"),
-//            @ApiImplicitParam(name = "quantity", value = "增加数量", required = true, example = "1024")
-//    })
-//    @RequiresAuthenticate
-//    public CommonResult<Boolean> addCartItem(@RequestParam("skuId") Integer skuId,
-//                                             @RequestParam("quantity") Integer quantity) {
-//        cartManager.addCartItem(UserSecurityContextHolder.getUserId(), skuId, quantity);
-//        return success(true);
-//    }
-//
-//    @GetMapping("sum-quantity")
-//    @ApiOperation("查询用户在购物车中的商品数量")
-//    @RequiresAuthenticate
-//    public CommonResult<Integer> sumCartItemQuantity() {
-//        return success(cartManager.sumCartItemQuantity(UserSecurityContextHolder.getUserId()));
-//    }
-//
-//    @GetMapping("/get-detail")
-//    @ApiOperation("查询用户的购物车的商品列表")
-//    @RequiresAuthenticate
-//    public CommonResult<CartDetailVO> getCartDetail() {
-//        return success(cartManager.getCartDetail(UserSecurityContextHolder.getUserId()));
-//    }
-//
-//    @PostMapping("update-quantity")
-//    @ApiOperation("更新购物车商品数量")
-//    @ApiImplicitParams({
-//            @ApiImplicitParam(name = "skuId", value = "商品 SKU 编号", required = true, example = "1"),
-//            @ApiImplicitParam(name = "quantity", value = "增加数量", required = true, example = "1024")
-//    })
-//    @RequiresAuthenticate
-//    public CommonResult<Boolean> updateCartItemQuantity(@RequestParam("skuId") Integer skuId,
-//                                                        @RequestParam("quantity") Integer quantity) {
-//        cartManager.updateCartItemQuantity(UserSecurityContextHolder.getUserId(), skuId, quantity);
-//        return success(true);
-//    }
-//
-//    @PostMapping("update-selected")
-//    @ApiOperation("更新购物车商品是否选中")
-//    @ApiImplicitParams({
-//            @ApiImplicitParam(name = "skuIds", value = "商品 SKU 编号数组", required = true, example = "1,3"),
-//            @ApiImplicitParam(name = "selected", value = "是否选中", required = true, example = "true")
-//    })
-//    @RequiresAuthenticate
-//    public CommonResult<Boolean> updateCartItemSelected(@RequestParam("skuIds") Set<Integer> skuIds,
-//                                                        @RequestParam("selected") Boolean selected) {
-//        cartManager.updateCartItemSelected(UserSecurityContextHolder.getUserId(), skuIds, selected);
-//        // 获得目前购物车明细
-//        return success(true);
-//    }
-
-}

+ 47 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/TradeCartController.http

@@ -0,0 +1,47 @@
+### 请求 /trade/cart/add-count 接口 => 成功
+POST {{appApi}}/trade/cart/add-count
+tenant-id: {{appTenentId}}
+Authorization: Bearer {{appToken}}
+Content-Type: application/json
+
+{
+  "skuId": 1,
+  "count": 1
+}
+
+### 请求 /trade/cart/update-count 接口 => 成功
+PUT {{appApi}}/trade/cart/update-count
+tenant-id: {{appTenentId}}
+Authorization: Bearer {{appToken}}
+Content-Type: application/json
+
+{
+  "skuId": 1,
+  "count": 5
+}
+
+### 请求 /trade/cart/update-selected 接口 => 成功
+PUT {{appApi}}/trade/cart/update-selected
+tenant-id: {{appTenentId}}
+Authorization: Bearer {{appToken}}
+Content-Type: application/json
+
+{
+  "skuIds": [1],
+  "selected": false
+}
+
+### 请求 /trade/cart/delete 接口 => 成功
+DELETE {{appApi}}/trade/cart/delete?skuIds=1
+tenant-id: {{appTenentId}}
+Authorization: Bearer {{appToken}}
+
+### 请求 /trade/cart/get-count 接口 => 成功
+GET {{appApi}}/trade/cart/get-count
+tenant-id: {{appTenentId}}
+Authorization: Bearer {{appToken}}
+
+### 请求 /trade/cart/get-detail 接口 => 成功
+GET {{appApi}}/trade/cart/get-detail
+tenant-id: {{appTenentId}}
+Authorization: Bearer {{appToken}}

+ 84 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/TradeCartController.java

@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.trade.controller.app.cart;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartDetailRespVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemAddCountReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemUpdateCountReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemUpdateSelectedReqVO;
+import cn.iocoder.yudao.module.trade.service.cart.TradeCartService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Api(tags = "用户 App - 购物车")
+@RestController
+@RequestMapping("/trade/cart")
+@RequiredArgsConstructor
+@Validated
+@Slf4j
+public class TradeCartController {
+
+    @Resource
+    private TradeCartService cartService;
+
+    @PostMapping("/add-count")
+    @ApiOperation("添加商品到购物车")
+    @PreAuthenticated
+    public CommonResult<Boolean> addCartItemCount(@Valid @RequestBody AppTradeCartItemAddCountReqVO addCountReqVO) {
+        cartService.addCartItemCount(getLoginUserId(), addCountReqVO);
+        return success(true);
+    }
+
+    @PutMapping("update-count")
+    @ApiOperation("更新购物车商品数量")
+    @PreAuthenticated
+    public CommonResult<Boolean> updateCartItemQuantity(@Valid @RequestBody AppTradeCartItemUpdateCountReqVO updateCountReqVO) {
+        cartService.updateCartItemCount(getLoginUserId(), updateCountReqVO);
+        return success(true);
+    }
+
+    @PutMapping("update-selected")
+    @ApiOperation("更新购物车商品是否选中")
+    @PreAuthenticated
+    public CommonResult<Boolean> updateCartItemSelected(@Valid @RequestBody AppTradeCartItemUpdateSelectedReqVO updateSelectedReqVO) {
+        cartService.updateCartItemSelected(getLoginUserId(), updateSelectedReqVO);
+        // 获得目前购物车明细
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @ApiOperation("删除购物车商品")
+    @ApiImplicitParam(name = "skuId", value = "商品 SKU 编号的数组", required = true, example = "1024,2048", dataTypeClass = List.class)
+    @PreAuthenticated
+    public CommonResult<Boolean> deleteCartItem(@RequestParam("skuIds") List<Long> skuIds) {
+        cartService.deleteCartItems(getLoginUserId(), skuIds);
+        return success(true);
+    }
+
+    @GetMapping("get-count")
+    @ApiOperation("查询用户在购物车中的商品数量")
+    @PreAuthenticated
+    public CommonResult<Integer> getCartCount() {
+        return success(cartService.getCartCount(getLoginUserId()));
+    }
+
+    @GetMapping("/get-detail")
+    @ApiOperation("查询用户的购物车的详情")
+    @PreAuthenticated
+    public CommonResult<AppTradeCartDetailRespVO> getCartDetail() {
+        return success(cartService.getCartDetail(getLoginUserId()));
+    }
+
+}

+ 118 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartDetailRespVO.java

@@ -0,0 +1,118 @@
+package cn.iocoder.yudao.module.trade.controller.app.cart.vo;
+
+import cn.iocoder.yudao.module.trade.controller.app.base.sku.AppProductSkuBaseRespVO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@ApiModel(value = "用户 App - 用户的购物车明细 Response VO")
+@Data
+public class AppTradeCartDetailRespVO {
+
+    /**
+     * 商品分组数组
+     */
+    private List<ItemGroup> itemGroups;
+
+    /**
+     * 费用
+     */
+    private Order order;
+
+    @ApiModel(value = "商品分组", description = "多个商品,参加同一个活动,从而形成分组")
+    @Data
+    public static class ItemGroup {
+
+        /**
+         * 商品数组
+         */
+        private List<Sku> items;
+        /**
+         * 营销活动,订单级别
+         */
+        private Promotion promotion;
+
+    }
+
+    @ApiModel(value = "商品 SKU")
+    @Data
+    public static class Sku extends AppProductSkuBaseRespVO {
+
+        /**
+         * SPU 信息
+         */
+        private AppProductSkuBaseRespVO spu;
+
+        // ========== 购物车相关的字段 ==========
+
+        @ApiModelProperty(value = "商品数量", required = true, example = "1")
+        private Integer count;
+        @ApiModelProperty(value = "是否选中", required = true, example = "true")
+        private Boolean selected;
+
+        // ========== 价格相关的字段,对应 PriceCalculateRespDTO.OrderItem 的属性 ==========
+
+        // TODO 芋艿:后续可以去除一些无用的字段
+
+        @ApiModelProperty(value = "商品原价(单)", required = true, example = "100")
+        private Integer originalPrice;
+        @ApiModelProperty(value = "商品原价(总)", required = true, example = "200")
+        private Integer totalOriginalPrice;
+        @ApiModelProperty(value = "商品级优惠(总)", required = true, example = "300")
+        private Integer totalPromotionPrice;
+        @ApiModelProperty(value = "最终购买金额(总)", required = true, example = "400")
+        private Integer totalPresentPrice;
+        @ApiModelProperty(value = "最终购买金额(单)", required = true, example = "500")
+        private Integer presentPrice;
+        @ApiModelProperty(value = "应付金额(总)", required = true, example = "600")
+        private Integer totalPayPrice;
+
+        // ========== 营销相关的字段 ==========
+        /**
+         * 营销活动,商品级别
+         */
+        private Promotion promotion;
+
+    }
+
+    @ApiModel(value = "订单", description = "对应 PriceCalculateRespDTO.Order 类,用于费用(合计)")
+    @Data
+    public static class Order {
+
+        // TODO 芋艿:后续可以去除一些无用的字段
+
+        @ApiModelProperty(value = "商品原价(总)", required = true, example = "100")
+        private Integer skuOriginalPrice;
+        @ApiModelProperty(value = "商品优惠(总)", required = true, example = "200")
+        private Integer skuPromotionPrice;
+        @ApiModelProperty(value = "订单优惠(总)", required = true, example = "300")
+        private Integer orderPromotionPrice;
+        @ApiModelProperty(value = "运费金额", required = true, example = "400")
+        private Integer deliveryPrice;
+        @ApiModelProperty(value = "应付金额(总)", required = true, example = "500")
+        private Integer payPrice;
+
+    }
+
+    @ApiModel(value = "营销活动", description = "对应 PriceCalculateRespDTO.Promotion 类的属性")
+    @Data
+    public static class Promotion {
+
+        @ApiModelProperty(value = "营销编号", required = true, example = "1024", notes = "营销活动的编号、优惠劵的编号")
+        private Long id;
+        @ApiModelProperty(value = "营销名字", required = true, example = "xx 活动")
+        private String name;
+        @ApiModelProperty(value = "营销类型", required = true, example = "1", notes = "参见 PromotionTypeEnum 枚举类")
+        private Integer type;
+
+        // ========== 匹配情况 ==========
+        @ApiModelProperty(value = "是否满足优惠条件", required = true, example = "true")
+        private Boolean meet;
+        @ApiModelProperty(value = "满足条件的提示", required = true, example = "圣诞价:省 150.00 元")
+        private String meetTip;
+
+    }
+
+}

+ 23 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartItemAddCountReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.trade.controller.app.cart.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+@ApiModel(value = "用户 App - 购物车添加购物项 Request VO")
+@Data
+public class AppTradeCartItemAddCountReqVO {
+
+    @ApiModelProperty(value = "商品 SKU 编号", required = true,example = "1024")
+    @NotNull(message = "商品 SKU 编号不能为空")
+    private Long skuId;
+
+    @ApiModelProperty(value = "商品数量", required = true, example = "1", notes = "注意,这是新增数量")
+    @NotNull(message = "数量不能为空")
+    @Min(message = "数量必须大于 0", value = 1L)
+    private Integer count;
+
+}

+ 23 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartItemUpdateCountReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.trade.controller.app.cart.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+@ApiModel(value = "用户 App - 购物车更新数量 Request VO")
+@Data
+public class AppTradeCartItemUpdateCountReqVO {
+
+    @ApiModelProperty(value = "商品 SKU 编号", required = true, example = "1024")
+    @NotNull(message = "商品 SKU 编号不能为空")
+    private Long skuId;
+
+    @ApiModelProperty(value = "商品数量", required = true, example = "1")
+    @NotNull(message = "数量不能为空")
+    @Min(message = "数量必须大于 0", value = 1L)
+    private Integer count;
+
+}

+ 22 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/AppTradeCartItemUpdateSelectedReqVO.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.trade.controller.app.cart.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.Collection;
+
+@ApiModel(value = "用户 App - 购物车更新是否选中 Request VO")
+@Data
+public class AppTradeCartItemUpdateSelectedReqVO {
+
+    @ApiModelProperty(value = "商品 SKU 编号列表", required = true, example = "1024,2048")
+    @NotNull(message = "商品 SKU 编号列表不能为空")
+    private Collection<Long> skuIds;
+
+    @ApiModelProperty(value = "是否选中", required = true, example = "true")
+    @NotNull(message = "是否选中不能为空")
+    private Boolean selected;
+
+}

+ 0 - 211
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/vo/CartDetailVO.java

@@ -1,211 +0,0 @@
-package cn.iocoder.yudao.module.trade.controller.app.cart.vo;
-
-import io.swagger.annotations.ApiModel;
-import lombok.Data;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-@ApiModel(value = "用户的购物车明细 Response VO") // TODO 芋艿:swagger 文档完善
-@Data
-@Accessors(chain = true)
-public class CartDetailVO {
-
-    /**
-     * 商品分组数组
-     */
-    private List<ItemGroup> 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<Sku> items;
-
-    }
-
-    @Data
-    @Accessors(chain = true)
-    public static class Sku {
-
-        // SKU 自带信息
-        /**
-         * sku 编号
-         */
-        private Integer id;
-        /**
-         * SPU 信息
-         */
-        private Spu spu;
-        /**
-         * 图片地址
-         */
-        private String picURL;
-//        /**
-//         * 规格值数组
-//         */
-//        private List<ProductAttrKeyValueRespVO> 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<String> 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;
-
-    }
-
-}

+ 2 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java

@@ -60,8 +60,8 @@ public class AppTradeOrderController {
 
     @GetMapping("/get")
     @ApiOperation("获得交易订单")
-    @ApiImplicitParam(name = "tradeOrderId", value = "交易订单编号", required = true)
-    public CommonResult<TradeOrderRespVO> getTradeOrder(@RequestParam("tradeOrderId") Integer tradeOrderId) {
+    @ApiImplicitParam(name = "id", value = "交易订单编号", required = true, dataTypeClass = Long.class)
+    public CommonResult<TradeOrderRespVO> getTradeOrder(@RequestParam("id") Integer id) {
 //        return success(tradeOrderService.getTradeOrder(tradeOrderId));
         return null;
     }

+ 45 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/cart/TradeCartConvert.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.trade.convert.cart;
+
+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.trade.controller.app.cart.vo.AppTradeCartDetailRespVO;
+import cn.iocoder.yudao.module.trade.dal.dataobject.cart.TradeCartItemDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+@Mapper
+public interface TradeCartConvert {
+
+    TradeCartConvert INSTANCE = Mappers.getMapper(TradeCartConvert.class);
+
+    default AppTradeCartDetailRespVO buildEmptyAppTradeCartDetailRespVO() {
+        return new AppTradeCartDetailRespVO().setItemGroups(Collections.emptyList())
+                .setOrder(new AppTradeCartDetailRespVO.Order().setSkuOriginalPrice(0).setSkuPromotionPrice(0)
+                        .setOrderPromotionPrice(0).setDeliveryPrice(0).setPayPrice(0));
+    }
+
+    default PriceCalculateReqDTO convert(Long userId, List<TradeCartItemDO> cartItems) {
+        return new PriceCalculateReqDTO().setUserId(userId)
+                .setItems(convertList(cartItems, cartItem -> new PriceCalculateReqDTO.Item().setSkuId(cartItem.getSkuId())
+                        .setCount(cartItem.getSelected() ? cartItem.getCount() : 0)));
+    }
+
+    // ========== AppTradeCartDetailRespVO 相关 ==========
+
+    AppTradeCartDetailRespVO.Promotion convert(PriceCalculateRespDTO.Promotion bean);
+
+    @Mappings({
+            @Mapping(source = "cartItem.count", target = "count")
+    })
+    AppTradeCartDetailRespVO.Sku convert(PriceCalculateRespDTO.OrderItem orderItem, TradeCartItemDO cartItem);
+
+    AppTradeCartDetailRespVO.Order convert(PriceCalculateRespDTO.Order bean);
+
+}

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderItemConvert.java

@@ -29,7 +29,7 @@ public interface TradeOrderItemConvert {
             @Mapping(source = "tradeOrder.userId", target = "userId"),
             @Mapping(source = "tradeOrder.orderId", target = "orderId")
     })
-    default List<TradeOrderItemDO> convertList(TradeOrderDO tradeOrder, List<PriceCalculateRespDTO.Item> items) {
+    default List<TradeOrderItemDO> convertList(TradeOrderDO tradeOrder, List<PriceCalculateRespDTO.OrderItem> items) {
         // TODO @Com: Mapstruct 生成会报错
         throw new UnsupportedOperationException("无法实现");
     }

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/sku/ProductSkuConvert.java

@@ -7,6 +7,7 @@ import org.mapstruct.factory.Mappers;
 
 import java.util.List;
 
+// TODO @LeeYan9:挪到 OrderConvert 那
 /**
  * @author LeeYan9
  * @since 2022-08-26

+ 4 - 8
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/cart/CartItemDO.java → yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/cart/TradeCartItemDO.java

@@ -6,16 +6,16 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
 
-import java.util.Date;
-
 /**
  * 购物车的商品信息 DO
+ *
+ * @author 芋道源码
  */
 @TableName("trade_cart_item")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @Accessors(chain = true)
-public class CartItemDO extends BaseDO {
+public class TradeCartItemDO extends BaseDO {
 
     // ========= 基础字段 BEGIN =========
 
@@ -27,10 +27,6 @@ public class CartItemDO extends BaseDO {
      * 是否选中
      */
     private Boolean selected;
-    /**
-     * 购物时间
-     */
-    private Date buyTime; // basket_date
 
     // ========= 基础字段 END =========
 
@@ -62,7 +58,7 @@ public class CartItemDO extends BaseDO {
     /**
      * 商品购买数量
      */
-    private Integer stock;
+    private Integer count;
 
     // ========= 商品信息 END =========
 

+ 47 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/cart/TradeCartItemMapper.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.trade.dal.mysql.cart;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.trade.dal.dataobject.cart.TradeCartItemDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface TradeCartItemMapper extends BaseMapperX<TradeCartItemDO> {
+
+    default TradeCartItemDO selectByUserIdAndSkuId(Long userId, Long skuId) {
+        return selectOne(TradeCartItemDO::getUserId, userId,
+                TradeCartItemDO::getSkuId, skuId);
+    }
+
+    default List<TradeCartItemDO> selectListByUserIdAndSkuIds(Long userId, Collection<Long> skuIds) {
+        return selectList(new LambdaQueryWrapper<TradeCartItemDO>().eq(TradeCartItemDO::getUserId, userId)
+                .in(TradeCartItemDO::getSkuId, skuIds));
+    }
+
+   default void updateByIds(Collection<Long> ids, TradeCartItemDO updateObject) {
+       update(updateObject, new LambdaQueryWrapper<TradeCartItemDO>().in(TradeCartItemDO::getId, ids));
+   }
+
+    default Integer selectSumByUserId(Long userId) {
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<TradeCartItemDO>()
+                .select("SUM(count) AS sumCount")
+                .eq("user_id", userId));
+        // 获得数量
+        return CollUtil.isNotEmpty(result) ? MapUtil.getInt(result.get(0), "sumCount") : 0;
+    }
+
+    default List<TradeCartItemDO> selectListByUserId(Long userId, Boolean selected) {
+        return selectList(new LambdaQueryWrapperX<TradeCartItemDO>().eq(TradeCartItemDO::getUserId, userId)
+                .eqIfPresent(TradeCartItemDO::getSelected, selected));
+    }
+
+}

+ 66 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/cart/TradeCartService.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.trade.service.cart;
+
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartDetailRespVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemAddCountReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemUpdateCountReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemUpdateSelectedReqVO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+
+/**
+ * 购物车 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface TradeCartService {
+
+    /**
+     * 添加商品到购物车
+     *
+     * @param userId 用户编号
+     * @param addCountReqVO 添加信息
+     */
+    void addCartItemCount(Long userId, @Valid AppTradeCartItemAddCountReqVO addCountReqVO);
+
+    /**
+     * 更新购物车商品数量
+     *
+     * @param userId 用户编号
+     * @param updateCountReqVO 更新信息
+     */
+    void updateCartItemCount(Long userId, AppTradeCartItemUpdateCountReqVO updateCountReqVO);
+
+    /**
+     * 更新购物车商品是否选中
+     *
+     * @param userId 用户编号
+     * @param updateSelectedReqVO 更新信息
+     */
+    void updateCartItemSelected(Long userId, AppTradeCartItemUpdateSelectedReqVO updateSelectedReqVO);
+
+    /**
+     * 删除购物车商品
+     *
+     * @param userId 用户编号
+     * @param skuIds SKU 编号的数组
+     */
+    void deleteCartItems(Long userId, Collection<Long> skuIds);
+
+    /**
+     * 查询用户在购物车中的商品数量
+     *
+     * @param userId 用户编号
+     * @return 商品数量
+     */
+    Integer getCartCount(Long userId);
+
+    /**
+     * 查询用户的购物车详情
+     *
+     * @param userId 用户编号
+     * @return 购物车详情
+     */
+    AppTradeCartDetailRespVO getCartDetail(Long userId);
+
+}

+ 184 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/cart/TradeCartServiceImpl.java

@@ -0,0 +1,184 @@
+package cn.iocoder.yudao.module.trade.service.cart;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.market.api.price.PriceApi;
+import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
+import cn.iocoder.yudao.module.market.enums.common.PromotionLevelEnum;
+import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartDetailRespVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemAddCountReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemUpdateCountReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.cart.vo.AppTradeCartItemUpdateSelectedReqVO;
+import cn.iocoder.yudao.module.trade.convert.cart.TradeCartConvert;
+import cn.iocoder.yudao.module.trade.dal.dataobject.cart.TradeCartItemDO;
+import cn.iocoder.yudao.module.trade.dal.mysql.cart.TradeCartItemMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH;
+import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.CARD_ITEM_NOT_FOUND;
+
+/**
+ * 购物车 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class TradeCartServiceImpl implements TradeCartService {
+
+    @Resource
+    private TradeCartItemMapper cartItemMapper;
+
+    @Resource
+    private ProductSkuApi productSkuApi;
+    @Resource
+    private PriceApi priceApi;
+
+    @Override
+    public void addCartItemCount(Long userId, AppTradeCartItemAddCountReqVO addCountReqVO) {
+        Long skuId = addCountReqVO.getSkuId();
+        Integer count = addCountReqVO.getCount();
+        // 查询 CartItemDO
+        TradeCartItemDO tradeItem = cartItemMapper.selectByUserIdAndSkuId(userId, addCountReqVO.getSkuId());
+
+        // 存在,则进行数量更新
+        if (tradeItem != null) {
+            checkProductSku(skuId, tradeItem.getCount() + count);
+            cartItemMapper.updateById(new TradeCartItemDO().setId(tradeItem.getId())
+                    .setSelected(true).setCount(tradeItem.getCount() + count));
+            return;
+        }
+
+        // 不存在,则进行插入
+        ProductSkuRespDTO sku = checkProductSku(skuId, count);
+        cartItemMapper.insert(new TradeCartItemDO().setUserId(userId).setSpuId(sku.getSpuId()).setSkuId(sku.getId())
+                .setSelected(true).setCount(count));
+    }
+
+    @Override
+    public void updateCartItemCount(Long userId, AppTradeCartItemUpdateCountReqVO updateCountReqVO) {
+        // 校验 TradeCartItemDO 存在
+        TradeCartItemDO tradeItem = cartItemMapper.selectByUserIdAndSkuId(userId, updateCountReqVO.getSkuId());
+        if (tradeItem == null) {
+            throw exception(CARD_ITEM_NOT_FOUND);
+        }
+        // 校验商品 SKU
+        checkProductSku(updateCountReqVO.getSkuId(), updateCountReqVO.getCount());
+
+        // 更新数量
+        cartItemMapper.updateById(new TradeCartItemDO().setId(tradeItem.getId()).setCount(updateCountReqVO.getCount()));
+    }
+
+    @Override
+    public void updateCartItemSelected(Long userId, AppTradeCartItemUpdateSelectedReqVO updateSelectedReqVO) {
+        // 查询 CartItemDO 列表
+        List<TradeCartItemDO> cartItems = cartItemMapper.selectListByUserIdAndSkuIds(userId, updateSelectedReqVO.getSkuIds());
+        if (CollUtil.isEmpty(cartItems)) {
+            return;
+        }
+
+        // 更新选中
+        cartItemMapper.updateByIds(CollectionUtils.convertList(cartItems, TradeCartItemDO::getId),
+                new TradeCartItemDO().setSelected(updateSelectedReqVO.getSelected()));
+    }
+
+    /**
+     * 购物车删除商品
+     *
+     * @param userId 用户编号
+     * @param skuIds 商品 SKU 编号的数组
+     */
+    @Override
+    public void deleteCartItems(Long userId, Collection<Long> skuIds) {
+        // 查询 CartItemDO 列表
+        List<TradeCartItemDO> cartItems = cartItemMapper.selectListByUserIdAndSkuIds(userId, skuIds);
+        if (CollUtil.isEmpty(cartItems)) {
+            return;
+        }
+
+        // 批量标记删除
+        cartItemMapper.deleteBatchIds(CollectionUtils.convertSet(cartItems, TradeCartItemDO::getId));
+    }
+
+    @Override
+    public Integer getCartCount(Long userId) {
+        return cartItemMapper.selectSumByUserId(userId);
+    }
+
+    @Override
+    public AppTradeCartDetailRespVO getCartDetail(Long userId) {
+        // 获得购物车的商品
+        List<TradeCartItemDO> cartItems = cartItemMapper.selectListByUserId(userId, null);
+        // 如果未空,则返回空结果
+        if (CollUtil.isEmpty(cartItems)) {
+            return TradeCartConvert.INSTANCE.buildEmptyAppTradeCartDetailRespVO();
+        }
+
+        // 调用价格服务,计算价格
+        PriceCalculateRespDTO priceCalculate = priceApi.calculatePrice(TradeCartConvert.INSTANCE.convert(userId, cartItems));
+
+        // 转换返回
+        Map<Long, TradeCartItemDO> cartItemMap = convertMap(cartItems, TradeCartItemDO::getSkuId);
+        Map<Long, PriceCalculateRespDTO.OrderItem> orderItemMap = convertMap(priceCalculate.getOrder().getItems(),
+                PriceCalculateRespDTO.OrderItem::getSkuId);
+        List<AppTradeCartDetailRespVO.ItemGroup> itemGroups = new ArrayList<>(cartItems.size());
+        // ① 场景一,营销活动,订单级别 TODO 芋艿:待测试
+        priceCalculate.getPromotions().stream().filter(promotion -> PromotionLevelEnum.ORDER.getLevel().equals(promotion.getLevel()))
+                .forEach(promotion -> {
+                    AppTradeCartDetailRespVO.ItemGroup itemGroup = new AppTradeCartDetailRespVO.ItemGroup().setItems(new ArrayList<>())
+                            .setPromotion(TradeCartConvert.INSTANCE.convert(promotion));
+                    itemGroups.add(itemGroup);
+                    promotion.getItems().forEach(promotionItem -> {
+                        PriceCalculateRespDTO.OrderItem orderItem = orderItemMap.remove(promotionItem.getSkuId());
+                        Assert.notNull(orderItem, "商品 SKU({}) 对应的订单项不能为空", promotionItem.getSkuId());
+                        TradeCartItemDO cartItem = cartItemMap.get(orderItem.getSkuId());
+                        itemGroup.getItems().add(TradeCartConvert.INSTANCE.convert(orderItem, cartItem)); // TODO spu
+                    });
+                });
+        // ② 场景二,营销活动,商品级别
+        orderItemMap.values().forEach(orderItem -> {
+            AppTradeCartDetailRespVO.ItemGroup itemGroup = new AppTradeCartDetailRespVO.ItemGroup().setItems(new ArrayList<>(1)).setPromotion(null);
+            itemGroups.add(itemGroup);
+            TradeCartItemDO cartItem = cartItemMap.get(orderItem.getSkuId());
+            itemGroup.getItems().add(TradeCartConvert.INSTANCE.convert(orderItem, cartItem)); // TODO spu
+        });
+        return new AppTradeCartDetailRespVO().setItemGroups(itemGroups)
+                .setOrder(TradeCartConvert.INSTANCE.convert(priceCalculate.getOrder()));
+    }
+
+    /**
+     * 校验商品 SKU 是否合法
+     * 1. 是否存在
+     * 2. 是否下架
+     * 3. 库存不足
+     *
+     * @param skuId 商品 SKU 编号
+     * @param count 商品数量
+     * @return 商品 SKU
+     */
+    private ProductSkuRespDTO checkProductSku(Long skuId, Integer count) {
+        ProductSkuRespDTO sku = productSkuApi.getSku(skuId);
+        if (sku == null || CommonStatusEnum.DISABLE.getStatus().equals(sku.getStatus())) {
+            throw exception(SKU_NOT_EXISTS);
+        }
+        if (count > sku.getStock()) {
+            throw exception(SKU_STOCK_NOT_ENOUGH);
+        }
+        return sku;
+    }
+
+}

+ 10 - 10
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderServiceImpl.java

@@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
 import cn.iocoder.yudao.module.pay.api.order.PayOrderDataCreateReqDTO;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
-import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
 import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
@@ -26,7 +26,7 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
 import cn.iocoder.yudao.module.trade.dal.mysql.orderitem.TradeOrderItemMapper;
-import cn.iocoder.yudao.module.trade.enums.enums.ErrorCodeConstants;
+import cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -62,12 +62,12 @@ public class TradeOrderServiceImpl implements TradeOrderService {
 
         List<Item> items = createReqVO.getItems();
         //  商品SKU检查 sku可售状态,库存
-        List<SkuInfoRespDTO> skuInfos = productSkuApi.getSkusByIds(CollectionUtils.convertSet(items, Item::getSkuId));
-        Map<Long, SkuInfoRespDTO> skuInfoMap = CollectionUtils.convertMap(skuInfos, SkuInfoRespDTO::getId);
+        List<ProductSkuRespDTO> skuInfos = productSkuApi.getSkuList(CollectionUtils.convertSet(items, Item::getSkuId));
+        Map<Long, ProductSkuRespDTO> skuInfoMap = CollectionUtils.convertMap(skuInfos, ProductSkuRespDTO::getId);
         checkSaleableAndStockFromSpu(skuInfoMap, items);
 
         //  商品SPU检查 sku可售状态,库存
-        List<SpuInfoRespDTO> spuInfos = productSpuApi.getSpusByIds(CollectionUtils.convertSet(skuInfos, SkuInfoRespDTO::getSpuId));
+        List<SpuInfoRespDTO> spuInfos = productSpuApi.getSpuList(CollectionUtils.convertSet(skuInfos, ProductSkuRespDTO::getSpuId));
         checkSaleableFromSpu(spuInfos);
 
         // 价格计算
@@ -79,7 +79,7 @@ public class TradeOrderServiceImpl implements TradeOrderService {
         tradeOrderMapper.insert(tradeOrderDO);
 
         // 订单项信息记录
-        List<TradeOrderItemDO> tradeOrderItems = TradeOrderItemConvert.INSTANCE.convertList(tradeOrderDO, priceResp.getItems());
+        List<TradeOrderItemDO> tradeOrderItems = TradeOrderItemConvert.INSTANCE.convertList(tradeOrderDO, priceResp.getOrder().getItems());
         //-填充订单项-SKU信息
         fillItemsInfoFromSku(tradeOrderItems, skuInfoMap);
         tradeOrderItemMapper.insertBatch(tradeOrderItems);
@@ -94,10 +94,10 @@ public class TradeOrderServiceImpl implements TradeOrderService {
     }
 
     private void fillItemsInfoFromSku(List<TradeOrderItemDO> tradeOrderItems,
-                                      Map<Long, SkuInfoRespDTO> spuInfos) {
+                                      Map<Long, ProductSkuRespDTO> spuInfos) {
         for (TradeOrderItemDO tradeOrderItem : tradeOrderItems) {
             // 填充SKU信息
-            SkuInfoRespDTO skuInfoRespDTO = spuInfos.get(tradeOrderItem.getSkuId());
+            ProductSkuRespDTO skuInfoRespDTO = spuInfos.get(tradeOrderItem.getSkuId());
             tradeOrderItem.setSpuId(skuInfoRespDTO.getSpuId());
             tradeOrderItem.setPicUrl(skuInfoRespDTO.getPicUrl());
             tradeOrderItem.setName(skuInfoRespDTO.getName());
@@ -116,14 +116,14 @@ public class TradeOrderServiceImpl implements TradeOrderService {
         }
     }
 
-    private void checkSaleableAndStockFromSpu(Map<Long, SkuInfoRespDTO> skuInfoMap,
+    private void checkSaleableAndStockFromSpu(Map<Long, ProductSkuRespDTO> skuInfoMap,
                                               List<Item> items) {
         // sku 不存在
         if (items.size() != skuInfoMap.size()) {
             throw ServiceExceptionUtil.exception(ErrorCodeConstants.ORDER_SKU_NOT_FOUND);
         }
         for (Item item : items) {
-            SkuInfoRespDTO skuInfoDTO = skuInfoMap.get(item.getSkuId());
+            ProductSkuRespDTO skuInfoDTO = skuInfoMap.get(item.getSkuId());
             // sku禁用
             if (!Objects.equals(CommonStatusEnum.ENABLE.getStatus(), skuInfoDTO.getStatus())) {
                 throw ServiceExceptionUtil.exception(ErrorCodeConstants.ORDER_SKU_NOT_SALE);

+ 1 - 0
yudao-server/src/main/resources/application-local.yaml

@@ -173,6 +173,7 @@ logging:
     cn.iocoder.yudao.module.system.dal.mysql: debug
     cn.iocoder.yudao.module.tool.dal.mysql: debug
     cn.iocoder.yudao.module.member.dal.mysql: debug
+    cn.iocoder.yudao.module.trade.dal.mysql: debug
 
 --- #################### 微信公众号、小程序相关配置 ####################
 wx: