Browse Source

trade:增加优惠劵使用、商品库存的扣减

YunaiV 2 years ago
parent
commit
16f5d0f5a4
21 changed files with 454 additions and 159 deletions
  1. 8 1
      yudao-module-mall/yudao-module-product-api/pom.xml
  2. 4 5
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java
  3. 47 0
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/ProductSkuUpdateStockReqDTO.java
  4. 0 48
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuDecrementStockBatchReqDTO.java
  5. 10 13
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApiImpl.java
  6. 27 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/sku/ProductSkuConvert.java
  7. 30 14
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/sku/ProductSkuMapper.java
  8. 14 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java
  9. 12 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuService.java
  10. 31 12
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceImpl.java
  11. 7 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java
  12. 9 4
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java
  13. 98 0
      yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceTest.java
  14. 1 0
      yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/SkuServiceImplTest.java
  15. 21 9
      yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImplTest.java
  16. 47 47
      yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/create_tables.sql
  17. 21 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
  18. 33 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponUseReqDTO.java
  19. 27 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
  20. 4 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
  21. 3 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java

+ 8 - 1
yudao-module-mall/yudao-module-product-api/pom.xml

@@ -22,6 +22,13 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-common</artifactId>
         </dependency>
+
+        <!-- 参数校验 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+            <optional>true</optional>
+        </dependency>
     </dependencies>
 
-</project>
+</project>

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

@@ -1,6 +1,6 @@
 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.ProductSkuUpdateStockReqDTO;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 
 import java.util.Collection;
@@ -31,11 +31,10 @@ public interface ProductSkuApi {
     List<ProductSkuRespDTO> getSkuList(Collection<Long> ids);
 
     /**
-     * 批量扣减 SKU 库存
+     * 更新 SKU 库存
      *
-     * @param batchReqDTO sku库存信息列表
+     * @param updateStockReqDTO 更新请求
      */
-    // TODO @LeeYan9: decrementSkuStockBatch? 啊哈, 动名词;
-    void decrementStockBatch(SkuDecrementStockBatchReqDTO batchReqDTO);
+    void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO);
 
 }

+ 47 - 0
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/ProductSkuUpdateStockReqDTO.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.product.api.sku.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 商品 SKU 更新库存 Request DTO
+ *
+ * @author LeeYan9
+ * @since 2022-08-26
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductSkuUpdateStockReqDTO {
+
+    /**
+     * 商品 SKU
+     */
+    @NotNull(message = "商品 SKU 不能为空")
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        /**
+         * 商品 SKU 编号
+         */
+        @NotNull(message = "商品 SKU 编号不能为空")
+        private Long id;
+
+        /**
+         * 库存变化数量
+         *
+         * 正数:增加库存
+         * 负数:扣减库存
+         */
+        @NotNull(message = "库存变化数量不能为空")
+        private Integer incCount;
+
+    }
+
+}

+ 0 - 48
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/SkuDecrementStockBatchReqDTO.java

@@ -1,48 +0,0 @@
-package cn.iocoder.yudao.module.product.api.sku.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.util.List;
-
-/**
- * TODO @LeeYan9: 1) 类注释; 2) Product 开头哈;
- * @author LeeYan9
- * @since 2022-08-26
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class SkuDecrementStockBatchReqDTO {
-
-    // TODO @LeeYan9: 参数校验
-    private List<Item> items;
-
-    @Data
-    public static class Item {
-
-        /**
-         * 商品 SPU 编号,自增
-         */
-        // TODO @LeeYan9: 是不是不用传递哈
-        private Long productId;
-
-        /**
-         * 商品 SKU 编号,自增
-         */
-        private Long skuId;
-
-        /**
-         * 数量
-         */
-        private Integer count;
-
-    }
-
-    // TODO @LeeYan9: 构造方法, 是不是可以满足啦
-    public static SkuDecrementStockBatchReqDTO of(List<Item> items) {
-        return new SkuDecrementStockBatchReqDTO(items);
-    }
-
-}

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

@@ -1,13 +1,12 @@
 package cn.iocoder.yudao.module.product.api.sku;
 
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
-import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
 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.dal.mysql.sku.ProductSkuMapper;
+import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
@@ -25,7 +24,7 @@ import java.util.List;
 public class ProductSkuApiImpl implements ProductSkuApi {
 
     @Resource
-    private ProductSkuMapper productSkuMapper;
+    private ProductSkuService productSkuService;
 
     @Override
     public ProductSkuRespDTO getSku(Long id) {
@@ -35,18 +34,16 @@ public class ProductSkuApiImpl implements ProductSkuApi {
 
     @Override
     public List<ProductSkuRespDTO> getSkuList(Collection<Long> ids) {
-        // TODO TODO LeeYan9: AllEmpty?
-        if (CollectionUtils.isAnyEmpty(ids)) {
+        if (CollUtil.isEmpty(ids)) {
             return Collections.emptyList();
         }
-        List<ProductSkuDO> productSkuDOList = productSkuMapper.selectBatchIds(ids);
-        return ProductSkuConvert.INSTANCE.convertList04(productSkuDOList);
+        List<ProductSkuDO> skus = productSkuService.getSkuList(ids);
+        return ProductSkuConvert.INSTANCE.convertList04(skus);
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void decrementStockBatch(SkuDecrementStockBatchReqDTO batchReqDTO) {
-        // TODO @LeeYan9: 最好 Service 去 for 循环;
-        productSkuMapper.decrementStockBatch(batchReqDTO.getItems());
+    public void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO) {
+        productSkuService.updateSkuStock(updateStockReqDTO);
     }
+
 }

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

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.product.convert.sku;
 
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuOptionRespVO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuRespVO;
@@ -9,7 +10,11 @@ import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
 /**
  * 商品 SKU Convert
@@ -39,4 +44,26 @@ public interface ProductSkuConvert {
 
     List<ProductSkuOptionRespVO> convertList05(List<ProductSkuDO> skus);
 
+    /**
+     * 获得 SPU 的库存变化 Map
+     *
+     * @param items SKU 库存变化
+     * @param skus SKU 列表
+     * @return SPU 的库存变化 Map
+     */
+    default Map<Long, Integer> convertSpuStockMap(List<ProductSkuUpdateStockReqDTO.Item> items,
+                                                  List<ProductSkuDO> skus) {
+        Map<Long, Long> skuIdAndSpuIdMap = convertMap(skus, ProductSkuDO::getId, ProductSkuDO::getSpuId); // SKU 与 SKU 编号的 Map 关系
+        Map<Long, Integer> spuIdAndStockMap = new HashMap<>(); // SPU 的库存变化 Map 关系
+        items.forEach(item -> {
+            Long spuId = skuIdAndSpuIdMap.get(item.getId());
+            if (spuId == null) {
+                return;
+            }
+            Integer stock = spuIdAndStockMap.getOrDefault(spuId, 0) + item.getIncCount();
+            spuIdAndStockMap.put(spuId, stock);
+        });
+        return spuIdAndStockMap;
+    }
+
 }

+ 30 - 14
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/sku/ProductSkuMapper.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.product.dal.mysql.sku;
 
+import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -23,21 +23,37 @@ public interface ProductSkuMapper extends BaseMapperX<ProductSkuDO> {
     }
 
     default void deleteBySpuId(Long spuId) {
-        delete(new LambdaQueryWrapperX<ProductSkuDO>()
-                .eqIfPresent(ProductSkuDO::getSpuId, spuId));
+        delete(new LambdaQueryWrapperX<ProductSkuDO>().eq(ProductSkuDO::getSpuId, spuId));
     }
 
-    default void decrementStockBatch(List<SkuDecrementStockBatchReqDTO.Item> items) {
-        for (SkuDecrementStockBatchReqDTO.Item item : items) {
-            // 扣减库存 cas 逻辑
-            LambdaUpdateWrapper<ProductSkuDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<ProductSkuDO>()
-                    .setSql(" stock = stock-" + item.getCount())
-                    .eq(ProductSkuDO::getSpuId, item.getProductId())
-                    .eq(ProductSkuDO::getId, item.getSkuId())
-                    .ge(ProductSkuDO::getStock, item.getCount());
-            // 执行
-            this.update(null, lambdaUpdateWrapper);
-        }
+    /**
+     * 更新 SKU 库存(增加)
+     *
+     * @param id 编号
+     * @param incrCount 增加库存(正数)
+     */
+    default void updateStockIncr(Long id, Integer incrCount) {
+        Assert.isTrue(incrCount > 0);
+        LambdaUpdateWrapper<ProductSkuDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<ProductSkuDO>()
+                .setSql(" stock = stock + " + incrCount)
+                .eq(ProductSkuDO::getId, id);
+        update(null, lambdaUpdateWrapper);
+    }
+
+    /**
+     * 更新 SKU 库存(减少)
+     *
+     * @param id 编号
+     * @param incrCount 减少库存(负数)
+     * @return 更新条数
+     */
+    default int updateStockDecr(Long id, Integer incrCount) {
+        Assert.isTrue(incrCount < 0);
+        LambdaUpdateWrapper<ProductSkuDO> updateWrapper = new LambdaUpdateWrapper<ProductSkuDO>()
+                .setSql(" stock = stock + " + incrCount) // 负数,所以使用 + 号
+                .eq(ProductSkuDO::getId, id)
+                .ge(ProductSkuDO::getStock, -incrCount); // cas 逻辑
+        return update(null, updateWrapper);
     }
 
     default List<ProductSkuDO> selectListByAlarmStock(){

+ 14 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.Set;
@@ -43,4 +44,17 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
                 .orderByDesc(ProductSpuDO::getSort));
     }
 
+    /**
+     * 更新商品 SPU 库存
+     *
+     * @param id 商品 SPU 编号
+     * @param incrCount 增加的库存数量
+     */
+    default void updateStock(Long id, Integer incrCount) {
+        LambdaUpdateWrapper<ProductSpuDO> updateWrapper = new LambdaUpdateWrapper<ProductSpuDO>()
+                .setSql(" total_stock = total_stock +" + incrCount) // 负数,所以使用 + 号
+                .eq(ProductSpuDO::getId, id);
+        update(null, updateWrapper);
+    }
+
 }

+ 12 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuService.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.product.service.sku;
 
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 
@@ -64,7 +65,16 @@ public interface ProductSkuService {
      * @param spuId SPU 编码
      * @param skus SKU 的集合
      */
-    void updateProductSkus(Long spuId, List<ProductSkuCreateOrUpdateReqVO> skus);
+    void updateSkus(Long spuId, List<ProductSkuCreateOrUpdateReqVO> skus);
+
+    /**
+     * 更新 SKU 库存(增量)
+     *
+     * 如果更新的库存不足,会抛出异常
+     *
+     * @param updateStockReqDTO 更行请求
+     */
+    void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO);
 
     /**
      * 获得商品 sku 集合
@@ -96,4 +106,5 @@ public interface ProductSkuService {
      */
     List<ProductSkuDO> getSkusByAlarmStock();
 
+
 }

+ 31 - 12
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceImpl.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.product.service.sku;
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
 import cn.iocoder.yudao.module.product.controller.admin.property.vo.property.ProductPropertyRespVO;
 import cn.iocoder.yudao.module.product.controller.admin.property.vo.value.ProductPropertyValueRespVO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuBaseVO;
@@ -13,6 +14,8 @@ import cn.iocoder.yudao.module.product.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuSpecTypeEnum;
 import cn.iocoder.yudao.module.product.service.property.ProductPropertyService;
 import cn.iocoder.yudao.module.product.service.property.ProductPropertyValueService;
+import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -22,6 +25,7 @@ import java.util.*;
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.*;
 
 /**
@@ -36,16 +40,18 @@ public class ProductSkuServiceImpl implements ProductSkuService {
     @Resource
     private ProductSkuMapper productSkuMapper;
 
+    @Resource
+    @Lazy // 循环依赖,避免报错
+    private ProductSpuService productSpuService;
     @Resource
     private ProductPropertyService productPropertyService;
-
     @Resource
     private ProductPropertyValueService productPropertyValueService;
 
     @Override
     public void deleteSku(Long id) {
         // 校验存在
-        this.validateSkuExists(id);
+        validateSkuExists(id);
         // 删除
         productSkuMapper.deleteById(id);
     }
@@ -89,7 +95,7 @@ public class ProductSkuServiceImpl implements ProductSkuService {
         // 2. 校验,一个 SKU 下,没有重复的规格。校验方式是,遍历每个 SKU ,看看是否有重复的规格 propertyId
         Map<Long, ProductPropertyValueRespVO> propertyValueMap = CollectionUtils.convertMap(productPropertyValueService.getPropertyValueListByPropertyId(new ArrayList<>(propertyIds)), ProductPropertyValueRespVO::getId);
         skus.forEach(sku -> {
-            Set<Long> skuPropertyIds = CollectionUtils.convertSet(sku.getProperties(), propertyItem -> propertyValueMap.get(propertyItem.getValueId()).getPropertyId());
+            Set<Long> skuPropertyIds = convertSet(sku.getProperties(), propertyItem -> propertyValueMap.get(propertyItem.getValueId()).getPropertyId());
             if (skuPropertyIds.size() != sku.getProperties().size()) {
                 throw exception(SKU_PROPERTIES_DUPLICATED);
             }
@@ -106,7 +112,7 @@ public class ProductSkuServiceImpl implements ProductSkuService {
         // 4. 最后校验,每个 Sku 之间不是重复的
         Set<Set<Long>> skuAttrValues = new HashSet<>(); // 每个元素,都是一个 Sku 的 attrValueId 集合。这样,通过最外层的 Set ,判断是否有重复的.
         for (ProductSkuCreateOrUpdateReqVO sku : skus) {
-            if (!skuAttrValues.add(CollectionUtils.convertSet(sku.getProperties(), ProductSkuBaseVO.Property::getValueId))) { // 添加失败,说明重复
+            if (!skuAttrValues.add(convertSet(sku.getProperties(), ProductSkuBaseVO.Property::getValueId))) { // 添加失败,说明重复
                 throw exception(ErrorCodeConstants.SPU_SKU_NOT_DUPLICATE);
             }
         }
@@ -142,7 +148,7 @@ public class ProductSkuServiceImpl implements ProductSkuService {
 
     @Override
     @Transactional
-    public void updateProductSkus(Long spuId, List<ProductSkuCreateOrUpdateReqVO> skus) {
+    public void updateSkus(Long spuId, List<ProductSkuCreateOrUpdateReqVO> skus) {
         // 查询 SPU 下已经存在的 SKU 的集合
         List<ProductSkuDO> existsSkus = productSkuMapper.selectListBySpuId(spuId);
         // 构建规格与 SKU 的映射关系;
@@ -192,14 +198,27 @@ public class ProductSkuServiceImpl implements ProductSkuService {
         }
     }
 
-    public static void main(String[] args) {
-        List<Integer> ids = new ArrayList<>();
-        ids.add(1);
-
-        List<Integer> ids2 = new ArrayList<>();
-        ids2.add(2);
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO) {
+        // 更新 SKU 库存
+        updateStockReqDTO.getItems().forEach(item -> {
+            if (item.getIncCount() > 0) {
+                productSkuMapper.updateStockIncr(item.getId(), item.getIncCount());
+            } else if (item.getIncCount() < 0) {
+                int updateStockIncr = productSkuMapper.updateStockDecr(item.getId(), item.getIncCount());
+                if (updateStockIncr == 0) {
+                    throw exception(SKU_STOCK_NOT_ENOUGH);
+                }
+            }
+        });
 
-        System.out.println(ids.equals(ids2));
+        // 更新 SPU 库存
+        List<ProductSkuDO> skus = productSkuMapper.selectBatchIds(
+                convertSet(updateStockReqDTO.getItems(), ProductSkuUpdateStockReqDTO.Item::getId));
+        Map<Long, Integer> spuStockIncrCounts = ProductSkuConvert.INSTANCE.convertSpuStockMap(
+                updateStockReqDTO.getItems(), skus);
+        productSpuService.updateSpuStock(spuStockIncrCounts);
     }
 
 }

+ 7 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java

@@ -99,4 +99,11 @@ public interface ProductSpuService {
      */
     PageResult<AppSpuPageRespVO> getSpuPage(AppSpuPageReqVO pageReqVO);
 
+    /**
+     * 更新商品 SPU 库存(增量)
+     *
+     * @param stockIncrCounts SPU 编号与库存变化(增量)的映射
+     */
+    void updateSpuStock(Map<Long, Integer> stockIncrCounts);
+
 }

+ 9 - 4
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java

@@ -24,6 +24,7 @@ import cn.iocoder.yudao.module.product.service.category.ProductCategoryService;
 import cn.iocoder.yudao.module.product.service.property.ProductPropertyService;
 import cn.iocoder.yudao.module.product.service.property.ProductPropertyValueService;
 import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -51,14 +52,12 @@ public class ProductSpuServiceImpl implements ProductSpuService {
     private ProductCategoryService categoryService;
 
     @Resource
+    @Lazy // 循环依赖,避免报错
     private ProductSkuService productSkuService;
-
     @Resource
     private ProductPropertyService productPropertyService;
-
     @Resource
     private ProductPropertyValueService productPropertyValueService;
-
     @Resource
     private ProductBrandService brandService;
 
@@ -106,7 +105,7 @@ public class ProductSpuServiceImpl implements ProductSpuService {
         updateObj.setTotalStock(CollectionUtils.getSumValue(skuCreateReqList, ProductSkuCreateOrUpdateReqVO::getStock, Integer::sum));
         productSpuMapper.updateById(updateObj);
         // 批量更新 SKU
-        productSkuService.updateProductSkus(updateObj.getId(), updateReqVO.getSkus());
+        productSkuService.updateSkus(updateObj.getId(), updateReqVO.getSkus());
     }
 
     @Override
@@ -210,4 +209,10 @@ public class ProductSpuServiceImpl implements ProductSpuService {
         return pageResult;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSpuStock(Map<Long, Integer> stockIncrCounts) {
+        stockIncrCounts.forEach((id, incCount) -> productSpuMapper.updateStock(id, incCount));
+    }
+
 }

+ 98 - 0
yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceTest.java

@@ -0,0 +1,98 @@
+package cn.iocoder.yudao.module.product.service.sku;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
+import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
+import cn.iocoder.yudao.module.product.dal.mysql.sku.ProductSkuMapper;
+import cn.iocoder.yudao.module.product.service.property.ProductPropertyService;
+import cn.iocoder.yudao.module.product.service.property.ProductPropertyValueService;
+import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH;
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.verify;
+
+/**
+ * {@link ProductSkuServiceImpl} 的单元测试
+ *
+ * @author 芋道源码
+ */
+@Import(ProductSkuServiceImpl.class)
+public class ProductSkuServiceTest extends BaseDbUnitTest {
+
+    @Resource
+    private ProductSkuService productSkuService;
+
+    @Resource
+    private ProductSkuMapper productSkuMapper;
+
+    @MockBean
+    private ProductSpuService productSpuService;
+    @MockBean
+    private ProductPropertyService productPropertyService;
+    @MockBean
+    private ProductPropertyValueService productPropertyValueService;
+
+    @Test
+    public void testUpdateSkuStock_incrSuccess() {
+        // 准备参数
+        ProductSkuUpdateStockReqDTO updateStockReqDTO = new ProductSkuUpdateStockReqDTO()
+                .setItems(singletonList(new ProductSkuUpdateStockReqDTO.Item().setId(1L).setIncCount(10)));
+        // mock 数据
+        productSkuMapper.insert(randomPojo(ProductSkuDO.class, o -> o.setId(1L).setSpuId(10L).setStock(20)));
+
+        // 调用
+        productSkuService.updateSkuStock(updateStockReqDTO);
+        // 断言
+        ProductSkuDO sku = productSkuMapper.selectById(1L);
+        assertEquals(sku.getStock(), 30);
+        verify(productSpuService).updateSpuStock(argThat(spuStockIncrCounts -> {
+            assertEquals(spuStockIncrCounts.size(), 1);
+            assertEquals(spuStockIncrCounts.get(10L), 10);
+            return true;
+        }));
+    }
+
+    @Test
+    public void testUpdateSkuStock_decrSuccess() {
+        // 准备参数
+        ProductSkuUpdateStockReqDTO updateStockReqDTO = new ProductSkuUpdateStockReqDTO()
+                .setItems(singletonList(new ProductSkuUpdateStockReqDTO.Item().setId(1L).setIncCount(-10)));
+        // mock 数据
+        productSkuMapper.insert(randomPojo(ProductSkuDO.class, o -> o.setId(1L).setSpuId(10L).setStock(20)));
+
+        // 调用
+        productSkuService.updateSkuStock(updateStockReqDTO);
+        // 断言
+        ProductSkuDO sku = productSkuMapper.selectById(1L);
+        assertEquals(sku.getStock(), 10);
+        verify(productSpuService).updateSpuStock(argThat(spuStockIncrCounts -> {
+            assertEquals(spuStockIncrCounts.size(), 1);
+            assertEquals(spuStockIncrCounts.get(10L), -10);
+            return true;
+        }));
+    }
+
+    @Test
+    public void testUpdateSkuStock_decrFail() {
+        // 准备参数
+        ProductSkuUpdateStockReqDTO updateStockReqDTO = new ProductSkuUpdateStockReqDTO()
+                .setItems(singletonList(new ProductSkuUpdateStockReqDTO.Item().setId(1L).setIncCount(-30)));
+        // mock 数据
+        productSkuMapper.insert(randomPojo(ProductSkuDO.class, o -> o.setId(1L).setSpuId(10L).setStock(20)));
+
+        // 调用并断言
+        AssertUtils.assertServiceException(() -> productSkuService.updateSkuStock(updateStockReqDTO),
+                SKU_STOCK_NOT_ENOUGH);
+    }
+
+}

+ 1 - 0
yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/SkuServiceImplTest.java

@@ -13,6 +13,7 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static org.junit.jupiter.api.Assertions.assertNull;
 
+// TODO 芋艿:整合到 {@link ProductSkuServiceTest} 中
 /**
 * {@link ProductSkuServiceImpl} 的单元测试类
 *

+ 21 - 9
yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImplTest.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.product.service.spu;
 
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -39,6 +40,7 @@ import java.util.stream.Stream;
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 // TODO @芋艿:review 下单元测试
 
@@ -56,19 +58,14 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
     @Resource
     private ProductSpuMapper productSpuMapper;
 
-
     @MockBean
     private ProductSkuServiceImpl productSkuService;
-
     @MockBean
     private ProductCategoryServiceImpl categoryService;
-
     @MockBean
     private ProductBrandServiceImpl brandService;
-
     @MockBean
     private ProductPropertyService productPropertyService;
-
     @MockBean
     private ProductPropertyValueService productPropertyValueService;
 
@@ -228,7 +225,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
 
         PageResult<Object> result = PageResult.empty();
         Assertions.assertIterableEquals(result.getList(), spuPage.getList());
-        Assertions.assertEquals(spuPage.getTotal(), result.getTotal());
+        assertEquals(spuPage.getTotal(), result.getTotal());
     }
 
     @Test
@@ -277,7 +274,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
 
         PageResult<ProductSpuRespVO> result = ProductSpuConvert.INSTANCE.convertPage(productSpuMapper.selectPage(productSpuPageReqVO, alarmStockSpuIds));
         Assertions.assertIterableEquals(result.getList(), spuPage.getList());
-        Assertions.assertEquals(spuPage.getTotal(), result.getTotal());
+        assertEquals(spuPage.getTotal(), result.getTotal());
     }
 
     @Test
@@ -328,7 +325,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
         PageResult<ProductSpuRespVO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
 
         PageResult<ProductSpuRespVO> result = ProductSpuConvert.INSTANCE.convertPage(productSpuMapper.selectPage(productSpuPageReqVO, (Set<Long>) null));
-        Assertions.assertEquals(result, spuPage);
+        assertEquals(result, spuPage);
     }
 
     @Test
@@ -354,7 +351,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
                 .collect(Collectors.toList());
 
         Assertions.assertIterableEquals(collect, spuPage.getList());
-        Assertions.assertEquals(spuPage.getTotal(), result.getTotal());
+        assertEquals(spuPage.getTotal(), result.getTotal());
     }
 
 
@@ -389,4 +386,19 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
         return res;
     }
 
+    @Test
+    public void testUpdateSpuStock() {
+        // 准备参数
+        Map<Long, Integer> stockIncrCounts = MapUtil.builder(1L, 10).put(2L, -20).build();
+        // mock 方法(数据)
+        productSpuMapper.insert(randomPojo(ProductSpuDO.class, o -> o.setId(1L).setTotalStock(20)));
+        productSpuMapper.insert(randomPojo(ProductSpuDO.class, o -> o.setId(2L).setTotalStock(30)));
+
+        // 调用
+        productSpuService.updateSpuStock(stockIncrCounts);
+        // 断言
+        assertEquals(productSpuService.getSpu(1L).getTotalStock(), 30);
+        assertEquals(productSpuService.getSpu(2L).getTotalStock(), 10);
+    }
+
 }

+ 47 - 47
yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/create_tables.sql

@@ -1,54 +1,54 @@
 CREATE TABLE IF NOT EXISTS `product_sku` (
-`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
-`spu_id` bigint NOT NULL COMMENT 'spu编号',
-`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
-`name` varchar(128) DEFAULT NULL COMMENT '商品 SKU 名字',
-`properties` varchar(128) DEFAULT NULL COMMENT '规格值数组-json格式, [{propertId: , valueId: }, {propertId: , valueId: }]',
-`price` int NOT NULL DEFAULT '-1' COMMENT '销售价格,单位:分',
-`market_price` int DEFAULT NULL COMMENT '市场价',
-`cost_price` int NOT NULL DEFAULT '-1' COMMENT '成本价,单位: 分',
-`pic_url` varchar(128) NOT NULL COMMENT '图片地址',
-`stock` int DEFAULT NULL COMMENT '库存',
-`warn_stock` int DEFAULT NULL COMMENT '预警库存',
-`volume` double DEFAULT NULL COMMENT '商品体积',
-`weight` double DEFAULT NULL COMMENT '商品重量',
-`bar_code` varchar(64) DEFAULT NULL COMMENT '条形码',
-`status` tinyint DEFAULT NULL COMMENT '状态: 0-正常 1-禁用',
-`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-`creator` varchar(64) DEFAULT NULL COMMENT '创建人',
-`updater` double DEFAULT NULL COMMENT '更新人',
-`deleted` bit(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
-PRIMARY KEY (`id`)
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `spu_id` bigint NOT NULL COMMENT 'spu编号',
+    `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
+    `name` varchar DEFAULT NULL COMMENT '商品 SKU 名字',
+    `properties` varchar DEFAULT NULL COMMENT '规格值数组-json格式, [{propertId: , valueId: }, {propertId: , valueId: }]',
+    `price` int NOT NULL DEFAULT '-1' COMMENT '销售价格,单位:分',
+    `market_price` int DEFAULT NULL COMMENT '市场价',
+    `cost_price` int NOT NULL DEFAULT '-1' COMMENT '成本价,单位: 分',
+    `pic_url` varchar NOT NULL COMMENT '图片地址',
+    `stock` int DEFAULT NULL COMMENT '库存',
+    `warn_stock` int DEFAULT NULL COMMENT '预警库存',
+    `volume` double DEFAULT NULL COMMENT '商品体积',
+    `weight` double DEFAULT NULL COMMENT '商品重量',
+    `bar_code` varchar DEFAULT NULL COMMENT '条形码',
+    `status` tinyint DEFAULT NULL COMMENT '状态: 0-正常 1-禁用',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `creator` varchar DEFAULT NULL COMMENT '创建人',
+    `updater` varchar DEFAULT NULL COMMENT '更新人',
+    `deleted` bit(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
+    PRIMARY KEY (`id`)
 ) COMMENT '商品sku';
 
 
 CREATE TABLE IF NOT EXISTS `product_spu` (
-`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
-`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
-`brand_id` bigint DEFAULT NULL COMMENT '商品品牌编号',
-`category_id` bigint NOT NULL COMMENT '分类id',
-`spec_type` int NOT NULL COMMENT '规格类型:0 单规格 1 多规格',
-`code` varchar(128) DEFAULT NULL COMMENT '商品编码',
-`name` varchar(128) NOT NULL COMMENT '商品名称',
-`sell_point` varchar(128) DEFAULT NULL COMMENT '卖点',
-`description` text COMMENT '描述',
-`pic_urls` varchar(1024) DEFAULT '' COMMENT '商品轮播图地址数组,以逗号分隔最多上传15张',
-`video_url` varchar(128) DEFAULT NULL COMMENT '商品视频',
-`market_price` int DEFAULT NULL COMMENT '市场价,单位使用:分',
-`min_price` int DEFAULT NULL COMMENT '最小价格,单位使用:分',
-`max_price` int DEFAULT NULL COMMENT '最大价格,单位使用:分',
-`total_stock` int NOT NULL DEFAULT '0' COMMENT '总库存',
-`show_stock` int DEFAULT '0' COMMENT '是否展示库存',
-`sales_count` int DEFAULT '0' COMMENT '商品销量',
-`virtual_sales_count` int DEFAULT '0' COMMENT '虚拟销量',
-`click_count` int DEFAULT '0' COMMENT '商品点击量',
-`status` bit(1) DEFAULT NULL COMMENT '上下架状态: 0 上架(开启) 1 下架(禁用)-1 回收',
-`sort` int NOT NULL DEFAULT '0' COMMENT '排序字段',
-`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-`creator` varchar(64) DEFAULT NULL COMMENT '创建人',
-`updater` varchar(64) DEFAULT NULL COMMENT '更新人',
-`deleted` bit(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
+    `brand_id` bigint DEFAULT NULL COMMENT '商品品牌编号',
+    `category_id` bigint NOT NULL COMMENT '分类id',
+    `spec_type` int NOT NULL COMMENT '规格类型:0 单规格 1 多规格',
+    `code` varchar(128) DEFAULT NULL COMMENT '商品编码',
+    `name` varchar(128) NOT NULL COMMENT '商品名称',
+    `sell_point` varchar(128) DEFAULT NULL COMMENT '卖点',
+    `description` text COMMENT '描述',
+    `pic_urls` varchar(1024) DEFAULT '' COMMENT '商品轮播图地址数组,以逗号分隔最多上传15张',
+    `video_url` varchar(128) DEFAULT NULL COMMENT '商品视频',
+    `market_price` int DEFAULT NULL COMMENT '市场价,单位使用:分',
+    `min_price` int DEFAULT NULL COMMENT '最小价格,单位使用:分',
+    `max_price` int DEFAULT NULL COMMENT '最大价格,单位使用:分',
+    `total_stock` int NOT NULL DEFAULT '0' COMMENT '总库存',
+    `show_stock` int DEFAULT '0' COMMENT '是否展示库存',
+    `sales_count` int DEFAULT '0' COMMENT '商品销量',
+    `virtual_sales_count` int DEFAULT '0' COMMENT '虚拟销量',
+    `click_count` int DEFAULT '0' COMMENT '商品点击量',
+    `status` bit(1) DEFAULT NULL COMMENT '上下架状态: 0 上架(开启) 1 下架(禁用)-1 回收',
+    `sort` int NOT NULL DEFAULT '0' COMMENT '排序字段',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `creator` varchar DEFAULT NULL COMMENT '创建人',
+    `updater` varchar DEFAULT NULL COMMENT '更新人',
+    `deleted` bit(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
 PRIMARY KEY (`id`)
 ) COMMENT '商品spu';

+ 21 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.promotion.api.coupon;
+
+import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
+
+import javax.validation.Valid;
+
+/**
+ * 优惠劵 API 接口
+ *
+ * @author 芋道源码
+ */
+public interface CouponApi {
+
+    /**
+     * 使用优惠劵
+     *
+     * @param useReqDTO 使用请求
+     */
+    void useCoupon(@Valid CouponUseReqDTO useReqDTO);
+
+}

+ 33 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponUseReqDTO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.promotion.api.coupon.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 优惠劵使用 Request DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class CouponUseReqDTO {
+
+    /**
+     * 优惠劵编号
+     */
+    @NotNull(message = "优惠劵编号不能为空")
+    private Long id;
+
+    /**
+     * 用户编号
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    /**
+     * 订单编号
+     */
+    @NotNull(message = "订单编号不能为空")
+    private Long orderId;
+
+}

+ 27 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.promotion.api.coupon;
+
+
+import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
+import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+/**
+ * 优惠劵 API 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+public class CouponApiImpl implements CouponApi {
+
+    @Resource
+    private CouponService couponService;
+
+    @Override
+    public void useCoupon(CouponUseReqDTO useReqDTO) {
+        couponService.useCoupon(useReqDTO.getId(), useReqDTO.getUserId(),
+                useReqDTO.getOrderId());
+    }
+
+}

+ 4 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java

@@ -45,10 +45,11 @@ public interface CouponService {
     /**
      * 使用优惠劵
      *
-     * @param id 优惠劵编号
-     * @param userId 用户编号
+     * @param id      优惠劵编号
+     * @param userId  用户编号
+     * @param orderId 订单编号
      */
-    void useCoupon(Long id, Long userId);
+    void useCoupon(Long id, Long userId, Long orderId);
 
     /**
      * 回收优惠劵

+ 3 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java

@@ -81,12 +81,13 @@ public class CouponServiceImpl implements CouponService {
     }
 
     @Override
-    public void useCoupon(Long id, Long userId) {
+    public void useCoupon(Long id, Long userId, Long orderId) {
         // 校验优惠劵
         validCoupon(id, userId);
         // 更新状态
         int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
-                new CouponDO().setStatus(CouponStatusEnum.USED.getStatus()).setUseTime(new Date()));
+                new CouponDO().setStatus(CouponStatusEnum.USED.getStatus())
+                        .setUseOrderId(orderId).setUseTime(new Date()));
         if (updateCount == 0) {
             throw exception(COUPON_STATUS_NOT_UNUSED);
         }