Commit ce367d99a9981fda33bef3f1e336dfc50b7cee80

Authored by 芋道源码
Committed by Gitee
2 parents f6a6a1ff c74d85d9

!1097 【功能完善】商城: 积分商城

Merge pull request !1097 from puhui999/develop
Showing 26 changed files with 487 additions and 20 deletions
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/point/PointActivityApi.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.api.point;
  2 +
  3 +import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
  4 +
  5 +/**
  6 + * 积分商城活动 API 接口
  7 + *
  8 + * @author HUIHUI
  9 + */
  10 +public interface PointActivityApi {
  11 +
  12 + /**
  13 + * 【下单前】校验是否参与积分商城活动
  14 + *
  15 + * 如果校验失败,则抛出业务异常
  16 + *
  17 + * @param activityId 活动编号
  18 + * @param skuId SKU 编号
  19 + * @param count 数量
  20 + * @return 积分商城商品信息
  21 + */
  22 + PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count);
  23 +
  24 + /**
  25 + * 更新积分商城商品库存(减少)
  26 + *
  27 + * @param id 活动编号
  28 + * @param skuId sku 编号
  29 + * @param count 数量(正数)
  30 + */
  31 + void updatePointStockDecr(Long id, Long skuId, Integer count);
  32 +
  33 + /**
  34 + * 更新积分商城商品库存(增加)
  35 + *
  36 + * @param id 活动编号
  37 + * @param skuId sku 编号
  38 + * @param count 数量(正数)
  39 + */
  40 + void updatePointStockIncr(Long id, Long skuId, Integer count);
  41 +
  42 +}
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/point/dto/PointValidateJoinRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.api.point.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 校验参与积分商城 Response DTO
  7 + */
  8 +@Data
  9 +public class PointValidateJoinRespDTO {
  10 +
  11 + /**
  12 + * 可兑换次数
  13 + */
  14 + private Integer count;
  15 + /**
  16 + * 所需兑换积分
  17 + */
  18 + private Integer point;
  19 + /**
  20 + * 所需兑换金额,单位:分
  21 + */
  22 + private Integer price;
  23 +
  24 +}
... ...
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 {
50 50 ErrorCode POINT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_002, "积分商城活动已关闭,不能修改");
51 51 ErrorCode POINT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1_013_007_003, "积分商城活动未关闭或未结束,不能删除");
52 52 ErrorCode POINT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_004, "积分商城活动已关闭,不能重复关闭");
  53 + ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_STATUS_CLOSED = new ErrorCode(1_013_007_005, "积分商品兑换失败,原因:积分商城活动已关闭");
  54 + ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED = new ErrorCode(1_013_007_006, "积分商品兑换失败,原因:单次限购超出");
  55 + ErrorCode POINT_ACTIVITY_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS = new ErrorCode(1_013_007_007, "积分商品兑换失败,原因:商品不存在");
  56 + ErrorCode POINT_ACTIVITY_UPDATE_STOCK_FAIL = new ErrorCode(1_013_007_008, "积分商品兑换失败,原因:积分商品库存不足");
53 57  
54 58 // ========== 秒杀活动 1-013-008-000 ==========
55 59 ErrorCode SECKILL_ACTIVITY_NOT_EXISTS = new ErrorCode(1_013_008_000, "秒杀活动不存在");
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/point/PointActivityApiImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.api.point;
  2 +
  3 +import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
  4 +import cn.iocoder.yudao.module.promotion.service.point.PointActivityService;
  5 +import jakarta.annotation.Resource;
  6 +import org.springframework.stereotype.Service;
  7 +import org.springframework.validation.annotation.Validated;
  8 +
  9 +/**
  10 + * 积分商城活动 Api 接口实现类
  11 + *
  12 + * @author HUIHUI
  13 + */
  14 +@Service
  15 +@Validated
  16 +public class PointActivityApiImpl implements PointActivityApi {
  17 +
  18 + @Resource
  19 + private PointActivityService pointActivityService;
  20 +
  21 + @Override
  22 + public PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count) {
  23 + return pointActivityService.validateJoinPointActivity(activityId, skuId, count);
  24 + }
  25 +
  26 + @Override
  27 + public void updatePointStockDecr(Long id, Long skuId, Integer count) {
  28 + pointActivityService.updatePointStockDecr(id, skuId, count);
  29 + }
  30 +
  31 + @Override
  32 + public void updatePointStockIncr(Long id, Long skuId, Integer count) {
  33 + pointActivityService.updatePointStockIncr(id, skuId, count);
  34 + }
  35 +
  36 +}
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/AppPointActivityController.java
... ... @@ -74,7 +74,11 @@ public class AppPointActivityController {
74 74 // 2. 拼接数据
75 75 List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(Collections.singletonList(id));
76 76 AppPointActivityDetailRespVO respVO = BeanUtils.toBean(activity, AppPointActivityDetailRespVO.class);
  77 + // 设置 product 信息
77 78 respVO.setProducts(BeanUtils.toBean(products, AppPointActivityDetailRespVO.Product.class));
  79 + PointProductDO minProduct = getMinPropertyObj(products, PointProductDO::getPoint);
  80 + assert minProduct != null;
  81 + respVO.setPoint(minProduct.getPoint()).setPrice(minProduct.getPrice());
78 82 return success(respVO);
79 83 }
80 84  
... ...
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 {
30 30 @Schema(description = "商品信息数组", requiredMode = Schema.RequiredMode.REQUIRED)
31 31 private List<Product> products;
32 32  
  33 + //======================= 显示所需兑换积分最少的 sku 信息 =======================
  34 +
  35 + @Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
  36 + private Integer point;
  37 +
  38 + @Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
  39 + private Integer price;
  40 +
33 41 @Schema(description = "商品信息")
34 42 @Data
35 43 public static class Product {
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointActivityMapper.java
1 1 package cn.iocoder.yudao.module.promotion.dal.mysql.point;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 7 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
7 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
  9 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
8 10 import org.apache.ibatis.annotations.Mapper;
9 11  
10 12 /**
... ... @@ -25,4 +27,33 @@ public interface PointActivityMapper extends BaseMapperX&lt;PointActivityDO&gt; {
25 27 .orderByDesc(PointActivityDO::getId));
26 28 }
27 29  
  30 + /**
  31 + * 更新活动库存(减少)
  32 + *
  33 + * @param id 活动编号
  34 + * @param count 扣减的库存数量(正数)
  35 + * @return 影响的行数
  36 + */
  37 + default int updateStockDecr(Long id, int count) {
  38 + Assert.isTrue(count > 0);
  39 + return update(null, new LambdaUpdateWrapper<PointActivityDO>()
  40 + .eq(PointActivityDO::getId, id)
  41 + .ge(PointActivityDO::getStock, count)
  42 + .setSql("stock = stock - " + count));
  43 + }
  44 +
  45 + /**
  46 + * 更新活动库存(增加)
  47 + *
  48 + * @param id 活动编号
  49 + * @param count 增加的库存数量(正数)
  50 + * @return 影响的行数
  51 + */
  52 + default int updateStockIncr(Long id, int count) {
  53 + Assert.isTrue(count > 0);
  54 + return update(null, new LambdaUpdateWrapper<PointActivityDO>()
  55 + .eq(PointActivityDO::getId, id)
  56 + .setSql("stock = stock + " + count));
  57 + }
  58 +
28 59 }
29 60 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointProductMapper.java
1 1 package cn.iocoder.yudao.module.promotion.dal.mysql.point;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 5 import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
5 6 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
... ... @@ -29,4 +30,37 @@ public interface PointProductMapper extends BaseMapperX&lt;PointProductDO&gt; {
29 30 .eq(PointProductDO::getActivityId, pointProductDO.getActivityId()));
30 31 }
31 32  
  33 + default PointProductDO selectListByActivityIdAndSkuId(Long activityId, Long skuId) {
  34 + return selectOne(PointProductDO::getActivityId, activityId,
  35 + PointProductDO::getSkuId, skuId);
  36 + }
  37 +
  38 + /**
  39 + * 更新活动库存(减少)
  40 + *
  41 + * @param id 活动编号
  42 + * @param count 扣减的库存数量(减少库存)
  43 + * @return 影响的行数
  44 + */
  45 + default int updateStockDecr(Long id, int count) {
  46 + Assert.isTrue(count > 0);
  47 + return update(null, new LambdaUpdateWrapper<PointProductDO>()
  48 + .eq(PointProductDO::getId, id)
  49 + .ge(PointProductDO::getStock, count)
  50 + .setSql("stock = stock - " + count));
  51 + }
  52 +
  53 + /**
  54 + * 更新活动库存(增加)
  55 + *
  56 + * @param id 活动编号
  57 + * @param count 需要增加的库存(增加库存)
  58 + * @return 影响的行数
  59 + */
  60 + default int updateStockIncr(Long id, int count) {
  61 + Assert.isTrue(count > 0);
  62 + return update(null, new LambdaUpdateWrapper<PointProductDO>()
  63 + .eq(PointProductDO::getId, id)
  64 + .setSql("stock = stock + " + count));
  65 + }
32 66 }
33 67 \ No newline at end of file
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityService.java
1 1 package cn.iocoder.yudao.module.promotion.service.point;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
4 5 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
5 6 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
6 7 import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
... ... @@ -33,6 +34,24 @@ public interface PointActivityService {
33 34 void updatePointActivity(@Valid PointActivitySaveReqVO updateReqVO);
34 35  
35 36 /**
  37 + * 更新积分商城商品库存(减少)
  38 + *
  39 + * @param id 活动编号
  40 + * @param skuId sku 编号
  41 + * @param count 数量(正数)
  42 + */
  43 + void updatePointStockDecr(Long id, Long skuId, Integer count);
  44 +
  45 + /**
  46 + * 更新积分商城商品库存(增加)
  47 + *
  48 + * @param id 活动编号
  49 + * @param skuId sku 编号
  50 + * @param count 数量(正数)
  51 + */
  52 + void updatePointStockIncr(Long id, Long skuId, Integer count);
  53 +
  54 + /**
36 55 * 关闭积分商城活动
37 56 *
38 57 * @param id 编号
... ... @@ -78,4 +97,16 @@ public interface PointActivityService {
78 97 */
79 98 List<PointProductDO> getPointProductListByActivityIds(Collection<Long> activityIds);
80 99  
  100 + /**
  101 + * 【下单前】校验是否参与积分商城活动
  102 + *
  103 + * 如果校验失败,则抛出业务异常
  104 + *
  105 + * @param activityId 活动编号
  106 + * @param skuId SKU 编号
  107 + * @param count 数量
  108 + * @return 积分商城商品信息
  109 + */
  110 + PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count);
  111 +
81 112 }
82 113 \ No newline at end of file
... ...
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;
8 8 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
9 9 import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
10 10 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
  11 +import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
11 12 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
12 13 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
13 14 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductSaveReqVO;
... ... @@ -104,6 +105,43 @@ public class PointActivityServiceImpl implements PointActivityService {
104 105  
105 106 @Override
106 107 @Transactional(rollbackFor = Exception.class)
  108 + public void updatePointStockDecr(Long id, Long skuId, Integer count) {
  109 + // 1.1 校验活动库存是否充足
  110 + PointActivityDO activity = validatePointActivityExists(id);
  111 + if (count > activity.getStock()) {
  112 + throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
  113 + }
  114 + // 1.2 校验商品库存是否充足
  115 + PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(id, skuId);
  116 + if (product == null || count > product.getStock()) {
  117 + throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
  118 + }
  119 +
  120 + // 2.1 更新活动商品库存
  121 + int updateCount = pointProductMapper.updateStockDecr(product.getId(), count);
  122 + if (updateCount == 0) {
  123 + throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
  124 + }
  125 +
  126 + // 2.2 更新活动库存
  127 + updateCount = pointActivityMapper.updateStockDecr(id, count);
  128 + if (updateCount == 0) {
  129 + throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
  130 + }
  131 + }
  132 +
  133 + @Override
  134 + @Transactional(rollbackFor = Exception.class)
  135 + public void updatePointStockIncr(Long id, Long skuId, Integer count) {
  136 + PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(id, skuId);
  137 + // 更新活动商品库存
  138 + pointProductMapper.updateStockIncr(product.getId(), count);
  139 + // 更新活动库存
  140 + pointActivityMapper.updateStockIncr(id, count);
  141 + }
  142 +
  143 + @Override
  144 + @Transactional(rollbackFor = Exception.class)
107 145 public void closePointActivity(Long id) {
108 146 // 校验存在
109 147 PointActivityDO pointActivity = validatePointActivityExists(id);
... ... @@ -244,4 +282,28 @@ public class PointActivityServiceImpl implements PointActivityService {
244 282 return pointProductMapper.selectListByActivityId(activityIds);
245 283 }
246 284  
  285 + @Override
  286 + public PointValidateJoinRespDTO validateJoinPointActivity(Long activityId, Long skuId, Integer count) {
  287 + // 1. 校验积分商城活动是否存在
  288 + PointActivityDO activity = validatePointActivityExists(activityId);
  289 + if (CommonStatusEnum.isDisable(activity.getStatus())) {
  290 + throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_STATUS_CLOSED);
  291 + }
  292 +
  293 + // 2.1 校验积分商城商品是否存在
  294 + PointProductDO product = pointProductMapper.selectListByActivityIdAndSkuId(activityId, skuId);
  295 + if (product == null) {
  296 + throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS);
  297 + }
  298 + // 2.2 超过单次购买限制
  299 + if (count > product.getCount()) {
  300 + throw exception(POINT_ACTIVITY_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED);
  301 + }
  302 + // 2.2 校验库存是否充足
  303 + if (count > product.getStock()) {
  304 + throw exception(POINT_ACTIVITY_UPDATE_STOCK_FAIL);
  305 + }
  306 + return BeanUtils.toBean(product, PointValidateJoinRespDTO.class);
  307 + }
  308 +
247 309 }
248 310 \ No newline at end of file
... ...
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 {
160 160 public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
161 161 // 1.1 校验活动库存是否充足
162 162 SeckillActivityDO seckillActivity = validateSeckillActivityExists(id);
163   - if (count > seckillActivity.getTotalStock()) {
  163 + if (count > seckillActivity.getStock()) {
164 164 throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
165 165 }
166 166 // 1.2 校验商品库存是否充足
... ...
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 {
58 58  
59 59 // ========== Price 相关 1-011-003-000 ============
60 60 ErrorCode PRICE_CALCULATE_PAY_PRICE_ILLEGAL = new ErrorCode(1_011_003_000, "支付价格计算异常,原因:价格小于等于 0");
61   - ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
62   - ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
63   - ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
64   - ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
65   - ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:{}」");
  61 + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_001, "计算快递运费异常,找不到对应的运费模板");
  62 + ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_002, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
  63 + ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_003, "参与秒杀的商品,超过了秒杀总限购数量");
  64 + ErrorCode PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_004, "参与积分活动的商品,超过了积分活动商品总限购数量");
  65 + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_005, "计算快递运费异常,配送方式不匹配");
  66 + ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_006, "该优惠劵无法使用,原因:{}」");
66 67  
67 68 // ========== 物流 Express 模块 1-011-004-000 ==========
68 69 ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");
... ...
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 {
20 20 SECKILL(1, "秒杀订单"),
21 21 BARGAIN(2, "砍价订单"),
22 22 COMBINATION(3, "拼团订单"),
  23 + POINT(4, "积分商城"),
23 24 ;
24 25  
25 26 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderTypeEnum::getType).toArray();
... ... @@ -54,4 +55,8 @@ public enum TradeOrderTypeEnum implements IntArrayValuable {
54 55 return ObjectUtil.equal(type, COMBINATION.getType());
55 56 }
56 57  
  58 + public static boolean isPoint(Integer type) {
  59 + return ObjectUtil.equal(type, POINT.getType());
  60 + }
  61 +
57 62 }
... ...
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;
6 6 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
7 7 import com.fasterxml.jackson.annotation.JsonIgnore;
8 8 import io.swagger.v3.oas.annotations.media.Schema;
9   -import lombok.Data;
10   -
11 9 import jakarta.validation.Valid;
12 10 import jakarta.validation.constraints.AssertTrue;
13 11 import jakarta.validation.constraints.Min;
14 12 import jakarta.validation.constraints.NotEmpty;
15 13 import jakarta.validation.constraints.NotNull;
  14 +import lombok.Data;
  15 +
16 16 import java.util.List;
17 17  
18 18 @Schema(description = "用户 App - 交易订单结算 Request VO")
... ... @@ -62,6 +62,10 @@ public class AppTradeOrderSettlementReqVO {
62 62 @Schema(description = "砍价记录编号", example = "123")
63 63 private Long bargainRecordId;
64 64  
  65 + // ========== 积分商城活动相关字段 ==========
  66 + @Schema(description = "积分商城活动编号", example = "123")
  67 + private Long pointActivityId;
  68 +
65 69 @AssertTrue(message = "活动商品每次只能购买一种规格")
66 70 @JsonIgnore
67 71 public boolean isValidActivityItems() {
... ...
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 {
219 219 .setSeckillActivityId(settlementReqVO.getSeckillActivityId())
220 220 .setBargainRecordId(settlementReqVO.getBargainRecordId())
221 221 .setCombinationActivityId(settlementReqVO.getCombinationActivityId())
222   - .setCombinationHeadId(settlementReqVO.getCombinationHeadId());
  222 + .setCombinationHeadId(settlementReqVO.getCombinationHeadId())
  223 + .setPointActivityId(settlementReqVO.getPointActivityId());
223 224 // 商品项的构建
224 225 Map<Long, CartDO> cartMap = convertMap(cartList, CartDO::getId);
225 226 for (AppTradeOrderSettlementReqVO.Item item : settlementReqVO.getItems()) {
... ...
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 {
353 353 */
354 354 private Long combinationRecordId;
355 355  
  356 + /**
  357 + * 积分商城活动的编号
  358 + *
  359 + * 关联 PointActivityDO 的 id 字段
  360 + */
  361 + private Long pointActivityId;
  362 +
356 363 }
... ...
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 {
42 42 /**
43 43 * 获得指定用户,指定活动,指定状态的交易订单
44 44 *
45   - * @param userId 用户编号
  45 + * @param userId 用户编号
46 46 * @param combinationActivityId 活动编号
47   - * @param status 订单状态
  47 + * @param status 订单状态
48 48 * @return 交易订单
49 49 */
50 50 TradeOrderDO getOrderByUserIdAndStatusAndCombination(Long userId, Long combinationActivityId, Integer status);
... ... @@ -116,7 +116,7 @@ public interface TradeOrderQueryService {
116 116 * @param activityId 活动编号
117 117 * @return 秒杀商品数量
118 118 */
119   - int getSeckillProductCount(Long userId, Long activityId);
  119 + int getActivityProductCount(Long userId, Long activityId);
120 120  
121 121 // =================== Order Item ===================
122 122  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java
... ... @@ -23,10 +23,10 @@ import cn.iocoder.yudao.module.trade.framework.delivery.core.client.ExpressClien
23 23 import cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO;
24 24 import cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO;
25 25 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
  26 +import jakarta.annotation.Resource;
26 27 import org.springframework.cache.annotation.Cacheable;
27 28 import org.springframework.stereotype.Service;
28 29  
29   -import jakarta.annotation.Resource;
30 30 import java.util.*;
31 31  
32 32 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -174,7 +174,7 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
174 174 }
175 175  
176 176 @Override
177   - public int getSeckillProductCount(Long userId, Long activityId) {
  177 + public int getActivityProductCount(Long userId, Long activityId) {
178 178 // 获得订单列表
179 179 List<TradeOrderDO> orders = tradeOrderMapper.selectListByUserIdAndSeckillActivityId(userId, activityId);
180 180 orders.removeIf(order -> TradeOrderStatusEnum.isCanceled(order.getStatus())); // 过滤掉【已取消】的订单
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradePointOrderHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.trade.service.order.handler;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
  6 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
  7 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  8 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
  9 +import jakarta.annotation.Resource;
  10 +import org.springframework.stereotype.Component;
  11 +
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * 积分商城活动订单的 {@link TradeOrderHandler} 实现类
  16 + *
  17 + * @author HUIHUI
  18 + */
  19 +@Component
  20 +public class TradePointOrderHandler implements TradeOrderHandler {
  21 +
  22 + @Resource
  23 + private PointActivityApi pointActivityApi;
  24 +
  25 + @Override
  26 + public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  27 + if (!TradeOrderTypeEnum.isPoint(order.getType())) {
  28 + return;
  29 + }
  30 + // 明确校验一下
  31 + Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
  32 +
  33 + // 扣减积分商城活动的库存
  34 + pointActivityApi.updatePointStockDecr(order.getPointActivityId(),
  35 + orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
  36 + }
  37 +
  38 + @Override
  39 + public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  40 + if (!TradeOrderTypeEnum.isPoint(order.getType())) {
  41 + return;
  42 + }
  43 + // 明确校验一下
  44 + Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
  45 +
  46 + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
  47 + orderItems = filterOrderItemListByNoneAfterSale(orderItems);
  48 + if (CollUtil.isEmpty(orderItems)) {
  49 + return;
  50 + }
  51 + afterCancelOrderItem(order, orderItems.get(0));
  52 + }
  53 +
  54 + @Override
  55 + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
  56 + if (!TradeOrderTypeEnum.isPoint(order.getType())) {
  57 + return;
  58 + }
  59 + // 恢复积分商城活动的库存
  60 + pointActivityApi.updatePointStockIncr(order.getPointActivityId(),
  61 + orderItem.getSkuId(), orderItem.getCount());
  62 + }
  63 +
  64 +}
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java
... ... @@ -69,7 +69,7 @@ public class TradePriceServiceImpl implements TradePriceService {
69 69 .buildCalculateResp(calculateReqBO, spuList, skuList);
70 70 priceCalculators.forEach(calculator -> calculator.calculate(calculateReqBO, calculateRespBO));
71 71 // 2.2 如果最终支付金额小于等于 0,则抛出业务异常
72   - if (calculateRespBO.getPrice().getPayPrice() <= 0) {
  72 + if (calculateReqBO.getPointActivityId() == null && calculateRespBO.getPrice().getPayPrice() <= 0) {
73 73 log.error("[calculatePrice][价格计算不正确,请求 calculateReqDTO({}),结果 priceCalculate({})]",
74 74 calculateReqBO, calculateRespBO);
75 75 throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateReqBO.java
1 1 package cn.iocoder.yudao.module.trade.service.price.bo;
2 2  
3 3 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
4   -import lombok.Data;
5   -
6 4 import jakarta.validation.Valid;
7 5 import jakarta.validation.constraints.Min;
8 6 import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
9 9 import java.util.List;
10 10  
11 11 /**
... ... @@ -84,6 +84,12 @@ public class TradePriceCalculateReqBO {
84 84 */
85 85 private Long bargainRecordId;
86 86  
  87 + // ========== 积分商城活动相关字段 ==========
  88 + /**
  89 + * 积分商城活动编号
  90 + */
  91 + private Long pointActivityId;
  92 +
87 93 /**
88 94 * 商品 SKU
89 95 */
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePointActivityPriceCalculator.java 0 → 100644
  1 +package cn.iocoder.yudao.module.trade.service.price.calculator;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ObjectUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
  7 +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
  8 +import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
  9 +import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
  10 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
  11 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
  12 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
  13 +import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
  14 +import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
  15 +import jakarta.annotation.Resource;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +import org.springframework.core.annotation.Order;
  18 +import org.springframework.stereotype.Component;
  19 +
  20 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  21 +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT;
  22 +
  23 +/**
  24 + * 积分商城的 {@link TradePriceCalculator} 实现类
  25 + *
  26 + * @author owen
  27 + */
  28 +@Component
  29 +@Order(TradePriceCalculator.ORDER_POINT_ACTIVITY)
  30 +@Slf4j
  31 +public class TradePointActivityPriceCalculator implements TradePriceCalculator {
  32 +
  33 + @Resource
  34 + private PointActivityApi pointActivityApi;
  35 + @Resource
  36 + private MemberUserApi memberUserApi;
  37 +
  38 + @Resource
  39 + private TradeOrderQueryService tradeOrderQueryService;
  40 +
  41 + @Override
  42 + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
  43 + // 1.1 判断订单类型是否为积分商城活动
  44 + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
  45 + return;
  46 + }
  47 + // 1.2 初始化积分
  48 + MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
  49 + result.setTotalPoint(user.getPoint()).setUsePoint(0);
  50 +
  51 + // 1.3 校验用户积分余额
  52 + if (user.getPoint() == null || user.getPoint() <= 0) {
  53 + return;
  54 + }
  55 +
  56 + Assert.isTrue(param.getItems().size() == 1, "积分商城兑换商品时,只允许选择一个商品");
  57 + // 2. 校验是否可以参与积分商城活动
  58 + TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0);
  59 + PointValidateJoinRespDTO activity = validateJoinSeckill(
  60 + param.getUserId(), param.getPointActivityId(),
  61 + orderItem.getSkuId(), orderItem.getCount());
  62 +
  63 + // 3.1 记录优惠明细
  64 + int discountPrice = orderItem.getPayPrice(); // 情况一:单使用积分兑换
  65 + Assert.isTrue(activity.getPoint() >= 1, "积分商城商品兑换积分必须大于 1");
  66 + result.setUsePoint(activity.getPoint() * orderItem.getCount());
  67 + orderItem.setUsePoint(activity.getPoint() * orderItem.getCount());
  68 + if (activity.getPrice() != null && activity.getPrice() > 0) { // 情况二:积分 + 金额
  69 + discountPrice = orderItem.getPayPrice() - activity.getPrice() * orderItem.getCount();
  70 + }
  71 + // 3.2 记录优惠明细
  72 + TradePriceCalculatorHelper.addPromotion(result, orderItem,
  73 + param.getPointActivityId(), "积分商城活动", PromotionTypeEnum.POINT.getType(),
  74 + StrUtil.format("积分商城活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)),
  75 + discountPrice);
  76 +
  77 + // 3.3 更新 SKU 优惠金额
  78 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice);
  79 + TradePriceCalculatorHelper.recountPayPrice(orderItem);
  80 + TradePriceCalculatorHelper.recountAllPrice(result);
  81 + }
  82 +
  83 + private PointValidateJoinRespDTO validateJoinSeckill(Long userId, Long activityId, Long skuId, Integer count) {
  84 + // 1. 校验是否可以参与积分商城活动
  85 + PointValidateJoinRespDTO pointValidateJoinRespDTO = pointActivityApi.validateJoinPointActivity(activityId, skuId, count);
  86 + // 2. 校验总限购数量,目前只有 trade 有具体下单的数据,需要交给 trade 价格计算使用
  87 + int activityProductCount = tradeOrderQueryService.getActivityProductCount(userId, activityId);
  88 + if (activityProductCount + count > pointValidateJoinRespDTO.getCount()) {
  89 + throw exception(PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT);
  90 + }
  91 + return pointValidateJoinRespDTO;
  92 + }
  93 +
  94 +}
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePointUsePriceCalculator.java
1 1 package cn.iocoder.yudao.module.trade.service.price.calculator;
2 2  
3 3 import cn.hutool.core.util.BooleanUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
4 5 import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.yudao.module.member.api.config.MemberConfigApi;
6 7 import cn.iocoder.yudao.module.member.api.config.dto.MemberConfigRespDTO;
7 8 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
8 9 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
9 10 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
  11 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
10 12 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
11 13 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
12 14 import jakarta.annotation.Resource;
... ... @@ -37,6 +39,10 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
37 39  
38 40 @Override
39 41 public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
  42 + // 判断订单类型是否不为积分商城活动
  43 + if (ObjectUtil.equal(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
  44 + return;
  45 + }
40 46 // 0. 初始化积分
41 47 MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
42 48 result.setTotalPoint(user.getPoint()).setUsePoint(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 {
16 16 int ORDER_SECKILL_ACTIVITY = 8;
17 17 int ORDER_BARGAIN_ACTIVITY = 8;
18 18 int ORDER_COMBINATION_ACTIVITY = 8;
  19 + int ORDER_POINT_ACTIVITY = 8;
19 20  
20 21 int ORDER_DISCOUNT_ACTIVITY = 10;
21 22 int ORDER_REWARD_ACTIVITY = 20;
... ...
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 {
90 90 if (param.getBargainRecordId() != null) {
91 91 return TradeOrderTypeEnum.BARGAIN.getType();
92 92 }
  93 + if (param.getPointActivityId() != null) {
  94 + return TradeOrderTypeEnum.POINT.getType();
  95 + }
93 96 return TradeOrderTypeEnum.NORMAL.getType();
94 97 }
95 98  
... ...
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;
8 8 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
9 9 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
10 10 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
  11 +import jakarta.annotation.Resource;
11 12 import org.springframework.core.annotation.Order;
12 13 import org.springframework.stereotype.Component;
13 14  
14   -import jakarta.annotation.Resource;
15   -
16 15 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
17 16 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT;
18 17  
... ... @@ -61,7 +60,7 @@ public class TradeSeckillActivityPriceCalculator implements TradePriceCalculator
61 60 // 1. 校验是否可以参与秒杀
62 61 SeckillValidateJoinRespDTO seckillActivity = seckillActivityApi.validateJoinSeckill(activityId, skuId, count);
63 62 // 2. 校验总限购数量,目前只有 trade 有具体下单的数据,需要交给 trade 价格计算使用
64   - int seckillProductCount = tradeOrderQueryService.getSeckillProductCount(userId, activityId);
  63 + int seckillProductCount = tradeOrderQueryService.getActivityProductCount(userId, activityId);
65 64 if (seckillProductCount + count > seckillActivity.getTotalLimitCount()) {
66 65 throw exception(PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT);
67 66 }
... ...