Browse Source

Merge branch 'develop' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17

YunaiV 9 months ago
parent
commit
e6496af9ae
26 changed files with 496 additions and 19 deletions
  1. 42 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/point/PointActivityApi.java
  2. 24 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/point/dto/PointValidateJoinRespDTO.java
  3. 4 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
  4. 36 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/point/PointActivityApiImpl.java
  5. 5 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/AppPointActivityController.java
  6. 8 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityDetailRespVO.java
  7. 31 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointActivityMapper.java
  8. 34 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointProductMapper.java
  9. 31 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityService.java
  10. 62 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java
  11. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
  12. 6 5
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  13. 5 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderTypeEnum.java
  14. 6 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java
  15. 2 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
  16. 7 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
  17. 2 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryService.java
  18. 4 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  19. 71 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradePointOrderHandler.java
  20. 2 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java
  21. 8 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateReqBO.java
  22. 94 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePointActivityPriceCalculator.java
  23. 6 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePointUsePriceCalculator.java
  24. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculator.java
  25. 3 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
  26. 1 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeSeckillActivityPriceCalculator.java

+ 42 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/point/PointActivityApi.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.promotion.api.point;
+
+import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
+
+/**
+ * 积分商城活动 API 接口
+ *
+ * @author HUIHUI
+ */
+public interface PointActivityApi {
+
+    /**
+     * 【下单前】校验是否参与积分商城活动
+     *
+     * 如果校验失败,则抛出业务异常
+     *
+     * @param activityId 活动编号
+     * @param skuId      SKU 编号
+     * @param count      数量
+     * @return 积分商城商品信息
+     */
+    PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count);
+
+    /**
+     * 更新积分商城商品库存(减少)
+     *
+     * @param id    活动编号
+     * @param skuId sku 编号
+     * @param count 数量(正数)
+     */
+    void updatePointStockDecr(Long id, Long skuId, Integer count);
+
+    /**
+     * 更新积分商城商品库存(增加)
+     *
+     * @param id    活动编号
+     * @param skuId sku 编号
+     * @param count 数量(正数)
+     */
+    void updatePointStockIncr(Long id, Long skuId, Integer count);
+
+}

+ 24 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/point/dto/PointValidateJoinRespDTO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.promotion.api.point.dto;
+
+import lombok.Data;
+
+/**
+ * 校验参与积分商城 Response DTO
+ */
+@Data
+public class PointValidateJoinRespDTO {
+
+    /**
+     * 可兑换次数
+     */
+    private Integer count;
+    /**
+     * 所需兑换积分
+     */
+    private Integer point;
+    /**
+     * 所需兑换金额,单位:分
+     */
+    private Integer price;
+
+}

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

@@ -50,6 +50,10 @@ public interface ErrorCodeConstants {
     ErrorCode POINT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_002, "积分商城活动已关闭,不能修改");
     ErrorCode POINT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1_013_007_003, "积分商城活动未关闭或未结束,不能删除");
     ErrorCode POINT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_004, "积分商城活动已关闭,不能重复关闭");
+    ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_STATUS_CLOSED = new ErrorCode(1_013_007_005, "积分商品兑换失败,原因:积分商城活动已关闭");
+    ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED = new ErrorCode(1_013_007_006, "积分商品兑换失败,原因:单次限购超出");
+    ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS = new ErrorCode(1_013_007_007, "积分商品兑换失败,原因:商品不存在");
+    ErrorCode POINT_ACTIVITY_UPDATE_STOCK_FAIL = new ErrorCode(1_013_007_008, "积分商品兑换失败,原因:积分商品库存不足");
 
     // ========== 秒杀活动 1-013-008-000 ==========
     ErrorCode SECKILL_ACTIVITY_NOT_EXISTS = new ErrorCode(1_013_008_000, "秒杀活动不存在");

+ 36 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/point/PointActivityApiImpl.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.promotion.api.point;
+
+import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
+import cn.iocoder.yudao.module.promotion.service.point.PointActivityService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * 积分商城活动 Api 接口实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+@Validated
+public class PointActivityApiImpl implements PointActivityApi {
+
+    @Resource
+    private PointActivityService pointActivityService;
+
+    @Override
+    public PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count) {
+        return pointActivityService.validateJoinPointActivity(activityId, skuId, count);
+    }
+
+    @Override
+    public void updatePointStockDecr(Long id, Long skuId, Integer count) {
+        pointActivityService.updatePointStockDecr(id, skuId, count);
+    }
+
+    @Override
+    public void updatePointStockIncr(Long id, Long skuId, Integer count) {
+        pointActivityService.updatePointStockIncr(id, skuId, count);
+    }
+
+}

+ 5 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/AppPointActivityController.java

@@ -76,8 +76,11 @@ public class AppPointActivityController {
 
         // 2. 拼接数据
         List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(Collections.singletonList(id));
-        AppPointActivityDetailRespVO respVO = BeanUtils.toBean(activity, AppPointActivityDetailRespVO.class);
-        respVO.setProducts(BeanUtils.toBean(products, AppPointActivityDetailRespVO.Product.class));
+        PointProductDO minProduct = getMinObject(products, PointProductDO::getPoint);
+        assert minProduct != null;
+        AppPointActivityDetailRespVO respVO = BeanUtils.toBean(activity, AppPointActivityDetailRespVO.class)
+                .setProducts(BeanUtils.toBean(products, AppPointActivityDetailRespVO.Product.class))
+                .setPoint(minProduct.getPoint()).setPrice(minProduct.getPrice());
         return success(respVO);
     }
 

+ 8 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityDetailRespVO.java

@@ -30,6 +30,14 @@ public class AppPointActivityDetailRespVO {
     @Schema(description = "商品信息数组", requiredMode = Schema.RequiredMode.REQUIRED)
     private List<Product> products;
 
+    //======================= 显示所需兑换积分最少的 SKU 信息 =======================
+
+    @Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Integer point;
+
+    @Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
+    private Integer price;
+
     @Schema(description = "商品信息")
     @Data
     public static class Product {

+ 31 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointActivityMapper.java

@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.point;
 
+import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 /**
@@ -25,4 +27,33 @@ public interface PointActivityMapper extends BaseMapperX<PointActivityDO> {
                 .orderByDesc(PointActivityDO::getId));
     }
 
+    /**
+     * 更新活动库存(减少)
+     *
+     * @param id    活动编号
+     * @param count 扣减的库存数量(正数)
+     * @return 影响的行数
+     */
+    default int updateStockDecr(Long id, int count) {
+        Assert.isTrue(count > 0);
+        return update(null, new LambdaUpdateWrapper<PointActivityDO>()
+                .eq(PointActivityDO::getId, id)
+                .ge(PointActivityDO::getStock, count)
+                .setSql("stock = stock - " + count));
+    }
+
+    /**
+     * 更新活动库存(增加)
+     *
+     * @param id    活动编号
+     * @param count 增加的库存数量(正数)
+     * @return 影响的行数
+     */
+    default int updateStockIncr(Long id, int count) {
+        Assert.isTrue(count > 0);
+        return update(null, new LambdaUpdateWrapper<PointActivityDO>()
+                .eq(PointActivityDO::getId, id)
+                .setSql("stock = stock + " + count));
+    }
+
 }

+ 34 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointProductMapper.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.point;
 
+import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -29,4 +30,37 @@ public interface PointProductMapper extends BaseMapperX<PointProductDO> {
                 .eq(PointProductDO::getActivityId, pointProductDO.getActivityId()));
     }
 
+    default PointProductDO selectListByActivityIdAndSkuId(Long activityId, Long skuId) {
+        return selectOne(PointProductDO::getActivityId, activityId,
+                PointProductDO::getSkuId, skuId);
+    }
+
+    /**
+     * 更新活动库存(减少)
+     *
+     * @param id    活动编号
+     * @param count 扣减的库存数量(减少库存)
+     * @return 影响的行数
+     */
+    default int updateStockDecr(Long id, int count) {
+        Assert.isTrue(count > 0);
+        return update(null, new LambdaUpdateWrapper<PointProductDO>()
+                .eq(PointProductDO::getId, id)
+                .ge(PointProductDO::getStock, count)
+                .setSql("stock = stock - " + count));
+    }
+
+    /**
+     * 更新活动库存(增加)
+     *
+     * @param id    活动编号
+     * @param count 需要增加的库存(增加库存)
+     * @return 影响的行数
+     */
+    default int updateStockIncr(Long id, int count) {
+        Assert.isTrue(count > 0);
+        return update(null, new LambdaUpdateWrapper<PointProductDO>()
+                .eq(PointProductDO::getId, id)
+                .setSql("stock = stock + " + count));
+    }
 }

+ 31 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityService.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.promotion.service.point;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
@@ -32,6 +33,24 @@ public interface PointActivityService {
      */
     void updatePointActivity(@Valid PointActivitySaveReqVO updateReqVO);
 
+    /**
+     * 更新积分商城商品库存(减少)
+     *
+     * @param id    活动编号
+     * @param skuId sku 编号
+     * @param count 数量(正数)
+     */
+    void updatePointStockDecr(Long id, Long skuId, Integer count);
+
+    /**
+     * 更新积分商城商品库存(增加)
+     *
+     * @param id    活动编号
+     * @param skuId sku 编号
+     * @param count 数量(正数)
+     */
+    void updatePointStockIncr(Long id, Long skuId, Integer count);
+
     /**
      * 关闭积分商城活动
      *
@@ -78,4 +97,16 @@ public interface PointActivityService {
      */
     List<PointProductDO> getPointProductListByActivityIds(Collection<Long> activityIds);
 
+    /**
+     * 【下单前】校验是否参与积分商城活动
+     *
+     * 如果校验失败,则抛出业务异常
+     *
+     * @param activityId 活动编号
+     * @param skuId      SKU 编号
+     * @param count      数量
+     * @return 积分商城商品信息
+     */
+    PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count);
+
 }

+ 62 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
+import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductSaveReqVO;
@@ -102,6 +103,43 @@ public class PointActivityServiceImpl implements PointActivityService {
         updateSeckillProduct(updateObj, updateReqVO.getProducts());
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePointStockDecr(Long id, Long skuId, Integer count) {
+        // 1.1 校验活动库存是否充足
+        PointActivityDO activity = validatePointActivityExists(id);
+        if (count > activity.getStock()) {
+            throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
+        }
+        // 1.2 校验商品库存是否充足
+        PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(id, skuId);
+        if (product == null || count > product.getStock()) {
+            throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
+        }
+
+        // 2.1 更新活动商品库存
+        int updateCount = pointProductMapper.updateStockDecr(product.getId(), count);
+        if (updateCount == 0) {
+            throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
+        }
+
+        // 2.2 更新活动库存
+        updateCount = pointActivityMapper.updateStockDecr(id, count);
+        if (updateCount == 0) {
+            throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePointStockIncr(Long id, Long skuId, Integer count) {
+        PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(id, skuId);
+        // 更新活动商品库存
+        pointProductMapper.updateStockIncr(product.getId(), count);
+        // 更新活动库存
+        pointActivityMapper.updateStockIncr(id, count);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void closePointActivity(Long id) {
@@ -244,4 +282,28 @@ public class PointActivityServiceImpl implements PointActivityService {
         return pointProductMapper.selectListByActivityId(activityIds);
     }
 
+    @Override
+    public PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count) {
+        // 1. 校验积分商城活动是否存在
+        PointActivityDO activity = validatePointActivityExists(activityId);
+        if (CommonStatusEnum.isDisable(activity.getStatus())) {
+            throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_STATUS_CLOSED);
+        }
+
+        // 2.1 校验积分商城商品是否存在
+        PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(activityId, skuId);
+        if (product == null) {
+            throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS);
+        }
+        // 2.2 超过单次购买限制
+        if (count > product.getCount()) {
+            throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED);
+        }
+        // 2.2 校验库存是否充足
+        if (count > product.getStock()) {
+            throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
+        }
+        return BeanUtils.toBean(product, PointValidateJoinRespDTO.class);
+    }
+
 }

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java

@@ -160,7 +160,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
     public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
         // 1.1 校验活动库存是否充足
         SeckillActivityDO seckillActivity = validateSeckillActivityExists(id);
-        if (count > seckillActivity.getTotalStock()) {
+        if (count > seckillActivity.getStock()) {
             throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
         }
         // 1.2 校验商品库存是否充足

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

@@ -58,11 +58,12 @@ public interface ErrorCodeConstants {
 
     // ========== Price 相关 1-011-003-000 ============
     ErrorCode PRICE_CALCULATE_PAY_PRICE_ILLEGAL = new ErrorCode(1_011_003_000, "支付价格计算异常,原因:价格小于等于 0");
-    ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
-    ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
-    ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
-    ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
-    ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:{}」");
+    ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_001, "计算快递运费异常,找不到对应的运费模板");
+    ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_002, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
+    ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_003, "参与秒杀的商品,超过了秒杀总限购数量");
+    ErrorCode PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_004, "参与积分活动的商品,超过了积分活动商品总限购数量");
+    ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_005, "计算快递运费异常,配送方式不匹配");
+    ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_006, "该优惠劵无法使用,原因:{}」");
 
     // ========== 物流 Express 模块 1-011-004-000 ==========
     ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");

+ 5 - 0
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderTypeEnum.java

@@ -20,6 +20,7 @@ public enum TradeOrderTypeEnum implements IntArrayValuable {
     SECKILL(1, "秒杀订单"),
     BARGAIN(2, "砍价订单"),
     COMBINATION(3, "拼团订单"),
+    POINT(4, "积分商城"),
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderTypeEnum::getType).toArray();
@@ -54,4 +55,8 @@ public enum TradeOrderTypeEnum implements IntArrayValuable {
         return ObjectUtil.equal(type, COMBINATION.getType());
     }
 
+    public static boolean isPoint(Integer type) {
+        return ObjectUtil.equal(type, POINT.getType());
+    }
+
 }

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

@@ -6,13 +6,13 @@ import cn.iocoder.yudao.framework.common.validation.Mobile;
 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.AssertTrue;
 import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
 import java.util.List;
 
 @Schema(description = "用户 App - 交易订单结算 Request VO")
@@ -62,6 +62,10 @@ public class AppTradeOrderSettlementReqVO {
     @Schema(description = "砍价记录编号", example = "123")
     private Long bargainRecordId;
 
+    // ========== 积分商城活动相关字段 ==========
+    @Schema(description = "积分商城活动编号", example = "123")
+    private Long pointActivityId;
+
     @AssertTrue(message = "活动商品每次只能购买一种规格")
     @JsonIgnore
     public boolean isValidActivityItems() {

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

@@ -219,7 +219,8 @@ public interface TradeOrderConvert {
                 .setSeckillActivityId(settlementReqVO.getSeckillActivityId())
                 .setBargainRecordId(settlementReqVO.getBargainRecordId())
                 .setCombinationActivityId(settlementReqVO.getCombinationActivityId())
-                .setCombinationHeadId(settlementReqVO.getCombinationHeadId());
+                .setCombinationHeadId(settlementReqVO.getCombinationHeadId())
+                .setPointActivityId(settlementReqVO.getPointActivityId());
         // 商品项的构建
         Map<Long, CartDO> cartMap = convertMap(cartList, CartDO::getId);
         for (AppTradeOrderSettlementReqVO.Item item : settlementReqVO.getItems()) {

+ 7 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java

@@ -353,4 +353,11 @@ public class TradeOrderDO extends BaseDO {
      */
     private Long combinationRecordId;
 
+    /**
+     * 积分商城活动的编号
+     *
+     * 关联 PointActivityDO 的 id 字段
+     */
+    private Long pointActivityId;
+
 }

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

@@ -42,9 +42,9 @@ public interface TradeOrderQueryService {
     /**
      * 获得指定用户,指定活动,指定状态的交易订单
      *
-     * @param userId     用户编号
+     * @param userId                用户编号
      * @param combinationActivityId 活动编号
-     * @param status     订单状态
+     * @param status                订单状态
      * @return 交易订单
      */
     TradeOrderDO getOrderByUserIdAndStatusAndCombination(Long userId, Long combinationActivityId, Integer status);

+ 4 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -245,7 +245,10 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         }
 
         // 3. 生成预支付
-        createPayOrder(order, orderItems);
+        // 特殊情况:积分兑换时,可能支付金额为零
+        if (order.getPayPrice() > 0) {
+            createPayOrder(order, orderItems);
+        }
 
         // 4. 插入订单日志
         TradeOrderLogUtils.setOrderInfo(order.getId(), null, order.getStatus());

+ 71 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradePointOrderHandler.java

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.trade.service.order.handler;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
+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.enums.order.TradeOrderStatusEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 积分商城活动订单的 {@link TradeOrderHandler} 实现类
+ *
+ * @author HUIHUI
+ */
+@Component
+public class TradePointOrderHandler implements TradeOrderHandler {
+
+    @Resource
+    private PointActivityApi pointActivityApi;
+
+    @Override
+    public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        if (!TradeOrderTypeEnum.isPoint(order.getType())) {
+            return;
+        }
+        // 明确校验一下
+        Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
+
+        // 扣减积分商城活动的库存
+        pointActivityApi.updatePointStockDecr(order.getPointActivityId(),
+                orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
+
+        // 如果支付金额为 0,则直接设置为已支付
+        if (Objects.equals(order.getPayPrice(), 0)) {
+            order.setPayStatus(true).setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus());
+        }
+    }
+
+    @Override
+    public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        if (!TradeOrderTypeEnum.isPoint(order.getType())) {
+            return;
+        }
+        // 明确校验一下
+        Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
+
+        // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
+        orderItems = filterOrderItemListByNoneAfterSale(orderItems);
+        if (CollUtil.isEmpty(orderItems)) {
+            return;
+        }
+        afterCancelOrderItem(order, orderItems.get(0));
+    }
+
+    @Override
+    public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
+        if (!TradeOrderTypeEnum.isPoint(order.getType())) {
+            return;
+        }
+        // 恢复积分商城活动的库存
+        pointActivityApi.updatePointStockIncr(order.getPointActivityId(),
+                orderItem.getSkuId(), orderItem.getCount());
+    }
+
+}

+ 2 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java

@@ -69,7 +69,8 @@ public class TradePriceServiceImpl implements TradePriceService {
                 .buildCalculateResp(calculateReqBO, spuList, skuList);
         priceCalculators.forEach(calculator -> calculator.calculate(calculateReqBO, calculateRespBO));
         // 2.2  如果最终支付金额小于等于 0,则抛出业务异常
-        if (calculateRespBO.getPrice().getPayPrice() <= 0) {
+        if (calculateReqBO.getPointActivityId() == null // 积分订单,允许支付金额为 0
+                && calculateRespBO.getPrice().getPayPrice() <= 0) {
             log.error("[calculatePrice][价格计算不正确,请求 calculateReqDTO({}),结果 priceCalculate({})]",
                     calculateReqBO, calculateRespBO);
             throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);

+ 8 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateReqBO.java

@@ -1,11 +1,11 @@
 package cn.iocoder.yudao.module.trade.service.price.bo;
 
 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
-import lombok.Data;
-
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
 import java.util.List;
 
 /**
@@ -84,6 +84,12 @@ public class TradePriceCalculateReqBO {
      */
     private Long bargainRecordId;
 
+    // ========== 积分商城活动相关字段 ==========
+    /**
+     * 积分商城活动编号
+     */
+    private Long pointActivityId;
+
     /**
      * 商品 SKU
      */

+ 94 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePointActivityPriceCalculator.java

@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.trade.service.price.calculator;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
+import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
+import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
+import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT;
+
+/**
+ * 积分商城的 {@link TradePriceCalculator} 实现类
+ *
+ * @author owen
+ */
+@Component
+@Order(TradePriceCalculator.ORDER_POINT_ACTIVITY)
+@Slf4j
+public class TradePointActivityPriceCalculator implements TradePriceCalculator {
+
+    @Resource
+    private PointActivityApi pointActivityApi;
+    @Resource
+    private MemberUserApi memberUserApi;
+
+    @Resource
+    private TradeOrderQueryService tradeOrderQueryService;
+
+    @Override
+    public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
+        // 1.1 判断订单类型是否为积分商城活动
+        if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
+            return;
+        }
+        // 1.2 初始化积分
+        MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
+        result.setTotalPoint(user.getPoint()).setUsePoint(0);
+
+        // 1.3 校验用户积分余额
+        if (user.getPoint() == null || user.getPoint() <= 0) {
+            return;
+        }
+
+        Assert.isTrue(param.getItems().size() == 1, "积分商城兑换商品时,只允许选择一个商品");
+        // 2. 校验是否可以参与积分商城活动
+        TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0);
+        PointValidateJoinRespDTO activity = validateJoinSeckill(
+                param.getUserId(), param.getPointActivityId(),
+                orderItem.getSkuId(), orderItem.getCount());
+
+        // 3.1 记录优惠明细
+        int discountPrice = orderItem.getPayPrice(); // 情况一:单使用积分兑换
+        Assert.isTrue(activity.getPoint() >= 1, "积分商城商品兑换积分必须大于 1");
+        result.setUsePoint(activity.getPoint() * orderItem.getCount());
+        orderItem.setUsePoint(activity.getPoint() * orderItem.getCount());
+        if (activity.getPrice() != null && activity.getPrice() > 0) { // 情况二:积分 + 金额
+            discountPrice = orderItem.getPayPrice() - activity.getPrice() * orderItem.getCount();
+        }
+        // 3.2 记录优惠明细
+        TradePriceCalculatorHelper.addPromotion(result, orderItem,
+                param.getPointActivityId(), "积分商城活动", PromotionTypeEnum.POINT.getType(),
+                StrUtil.format("积分商城活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)),
+                discountPrice);
+
+        // 3.3 更新 SKU 优惠金额
+        orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice);
+        TradePriceCalculatorHelper.recountPayPrice(orderItem);
+        TradePriceCalculatorHelper.recountAllPrice(result);
+    }
+
+    private PointValidateJoinRespDTO validateJoinSeckill(Long userId, Long activityId, Long skuId, Integer count) {
+        // 1. 校验是否可以参与积分商城活动
+        PointValidateJoinRespDTO pointValidateJoinRespDTO = pointActivityApi.validateJoinPointActivity(activityId, skuId, count);
+        // 2. 校验总限购数量,目前只有 trade 有具体下单的数据,需要交给 trade 价格计算使用
+        int activityProductCount = tradeOrderQueryService.getSeckillProductCount(userId, activityId);
+        if (activityProductCount + count > pointValidateJoinRespDTO.getCount()) {
+            throw exception(PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT);
+        }
+        return pointValidateJoinRespDTO;
+    }
+
+}

+ 6 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePointUsePriceCalculator.java

@@ -1,12 +1,14 @@
 package cn.iocoder.yudao.module.trade.service.price.calculator;
 
 import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.module.member.api.config.MemberConfigApi;
 import cn.iocoder.yudao.module.member.api.config.dto.MemberConfigRespDTO;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
 import jakarta.annotation.Resource;
@@ -37,6 +39,10 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
 
     @Override
     public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
+        // 判断订单类型是否不为积分商城活动
+        if (ObjectUtil.equal(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
+            return;
+        }
         // 0. 初始化积分
         MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
         result.setTotalPoint(user.getPoint()).setUsePoint(0);

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculator.java

@@ -16,6 +16,7 @@ public interface TradePriceCalculator {
     int ORDER_SECKILL_ACTIVITY = 8;
     int ORDER_BARGAIN_ACTIVITY = 8;
     int ORDER_COMBINATION_ACTIVITY = 8;
+    int ORDER_POINT_ACTIVITY = 8;
 
     int ORDER_DISCOUNT_ACTIVITY = 10;
     int ORDER_REWARD_ACTIVITY = 20;

+ 3 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java

@@ -90,6 +90,9 @@ public class TradePriceCalculatorHelper {
         if (param.getBargainRecordId() != null) {
             return TradeOrderTypeEnum.BARGAIN.getType();
         }
+        if (param.getPointActivityId() != null) {
+            return TradeOrderTypeEnum.POINT.getType();
+        }
         return TradeOrderTypeEnum.NORMAL.getType();
     }
 

+ 1 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeSeckillActivityPriceCalculator.java

@@ -8,11 +8,10 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import jakarta.annotation.Resource;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
-import jakarta.annotation.Resource;
-
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT;