Prechádzať zdrojové kódy

promotion:增加获得优惠劵匹配结果的列表

YunaiV 2 rokov pred
rodič
commit
edc5d0f1d0

+ 15 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java

@@ -135,4 +135,19 @@ public class DateUtils {
         return DateUtil.isSameDay(date, new Date());
     }
 
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 是否
+     */
+    public static boolean isBetween(Date startTime, Date endTime) {
+        if (startTime == null || endTime == null) {
+            return false;
+        }
+        return startTime.getTime() <= System.currentTimeMillis()
+                && endTime.getTime() >= System.currentTimeMillis();
+    }
+
 }

+ 35 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/price/dto/CouponMeetRespDTO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.promotion.api.price.dto;
+
+import lombok.Data;
+
+/**
+ * 优惠劵的匹配信息 Response DTO
+ *
+ * why 放在 price 包下?主要获取的时候,需要涉及到较多的价格计算逻辑,放在 price 可以更好的复用逻辑
+ *
+ * @author 芋道源码
+ */
+@Data
+public class CouponMeetRespDTO {
+
+    /**
+     * 优惠劵编号
+     */
+    private Long id;
+
+    // ========== 非优惠劵的基本信息字段 ==========
+    /**
+     * 是否匹配
+     */
+    private Boolean meet;
+    /**
+     * 不匹配的提示,即 {@link #meet} = true 才有值
+     *
+     * 例如说:
+     * 1. 所结算商品没有符合条件的商品
+     * 2. 差 XXX 元可用优惠劵
+     * 3. 优惠劵未到使用时间
+     */
+    private String meetTip;
+
+}

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

@@ -31,6 +31,8 @@ public interface ErrorCodeConstants {
     // ========== 优惠劵模板 1003005000 ==========
     ErrorCode COUPON_NOT_EXISTS = new ErrorCode(1003005000, "优惠劵不存在");
     ErrorCode COUPON_DELETE_FAIL_USED = new ErrorCode(1003005001, "回收优惠劵失败,优惠劵已被使用");
+    ErrorCode COUPON_STATUS_NOT_UNUSED = new ErrorCode(1006003003, "优惠劵不处于待使用状态");
+    ErrorCode COUPON_VALID_TIME_NOT_NOW = new ErrorCode(1006003004, "优惠劵不在有效期内");
 
     // ========== 满减送活动 1003006000 ==========
     ErrorCode REWARD_ACTIVITY_NOT_EXISTS = new ErrorCode(1003006000, "满减送活动不存在");

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

@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.promotion.convert.price;
 
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.promotion.api.price.dto.CouponMeetRespDTO;
 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
@@ -42,4 +44,6 @@ public interface PriceConvert {
         return priceCalculate;
     }
 
+    CouponMeetRespDTO convert(CouponDO coupon);
+
 }

+ 16 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * 优惠劵 Mapper
@@ -27,10 +28,25 @@ public interface CouponMapper extends BaseMapperX<CouponDO> {
                 .orderByDesc(CouponDO::getId));
     }
 
+    default List<CouponDO> selectListByUserIdAndStatus(Long userId, Integer status) {
+        return selectList(new LambdaQueryWrapperX<CouponDO>()
+                .eq(CouponDO::getUserId, userId).eq(CouponDO::getStatus, status));
+    }
+
+    default CouponDO selectByIdAndUserId(Long id, Long userId) {
+        return selectOne(new LambdaQueryWrapperX<CouponDO>()
+                .eq(CouponDO::getId, id).eq(CouponDO::getUserId, userId));
+    }
+
     default int delete(Long id, Collection<Integer> whereStatuses) {
         return update(null, new LambdaUpdateWrapper<CouponDO>()
                 .eq(CouponDO::getId, id).in(CouponDO::getStatus, whereStatuses)
                 .set(CouponDO::getDeleted, 1));
     }
 
+    default int updateByIdAndStatus(Long id, Integer status, CouponDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<CouponDO>()
+                .eq(CouponDO::getId, id).eq(CouponDO::getStatus, status));
+    }
+
 }

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

@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 
+import java.util.List;
+
 /**
  * 优惠劵 Service 接口
  *
@@ -23,6 +25,15 @@ public interface CouponService {
      */
     CouponDO validCoupon(Long id, Long userId);
 
+    /**
+     * 校验优惠劵,包括状态、有限期
+     *
+     * @see #validCoupon(Long, Long) 逻辑相同,只是入参不同
+     *
+     * @param coupon 优惠劵
+     */
+    void validCoupon(CouponDO coupon);
+
     /**
      * 获得优惠劵分页
      *
@@ -31,6 +42,14 @@ public interface CouponService {
      */
     PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
 
+    /**
+     * 使用优惠劵
+     *
+     * @param id 优惠劵编号
+     * @param userId 用户编号
+     */
+    void useCoupon(Long id, Long userId);
+
     /**
      * 回收优惠劵
      *
@@ -38,4 +57,13 @@ public interface CouponService {
      */
     void deleteCoupon(Long id);
 
+    /**
+     * 获得用户的优惠劵列表
+     *
+     * @param userId 用户编号
+     * @param status 优惠劵状态
+     * @return 优惠劵列表
+     */
+    List<CouponDO> getCouponList(Long userId, Integer status);
+
 }

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

@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.promotion.service.coupon;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.UserRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
@@ -15,11 +17,12 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
 import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_DELETE_FAIL_USED;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NOT_EXISTS;
+import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 import static java.util.Arrays.asList;
 
 /**
@@ -40,10 +43,26 @@ public class CouponServiceImpl implements CouponService {
     @Resource
     private MemberUserApi memberUserApi;
 
-    // TODO 芋艿:待实现
     @Override
     public CouponDO validCoupon(Long id, Long userId) {
-        return null;
+        CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId);
+        if (coupon == null) {
+            throw exception(COUPON_NOT_EXISTS);
+        }
+        validCoupon(coupon);
+        return coupon;
+    }
+
+    @Override
+    public void validCoupon(CouponDO coupon) {
+        // 校验状态
+        if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) {
+            throw exception(COUPON_STATUS_NOT_UNUSED);
+        }
+        // 校验有效期;为避免定时器没跑,实际优惠劵已经过期
+        if (DateUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) {
+            throw exception(COUPON_VALID_TIME_NOT_NOW);
+        }
     }
 
     @Override
@@ -61,6 +80,18 @@ public class CouponServiceImpl implements CouponService {
         return couponMapper.selectPage(pageReqVO, userIds);
     }
 
+    @Override
+    public void useCoupon(Long id, Long userId) {
+        // 校验优惠劵
+        validCoupon(id, userId);
+        // 更新状态
+        int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
+                new CouponDO().setStatus(CouponStatusEnum.USED.getStatus()).setUseTime(new Date()));
+        if (updateCount == 0) {
+            throw exception(COUPON_STATUS_NOT_UNUSED);
+        }
+    }
+
     @Override
     @Transactional
     public void deleteCoupon(Long id) {
@@ -77,6 +108,11 @@ public class CouponServiceImpl implements CouponService {
         couponTemplateService.updateCouponTemplateTakeCount(id, -1);
     }
 
+    @Override
+    public List<CouponDO> getCouponList(Long userId, Integer status) {
+        return couponMapper.selectListByUserIdAndStatus(userId, status);
+    }
+
     private void validateCouponExists(Long id) {
         if (couponMapper.selectById(id) == null) {
             throw exception(COUPON_NOT_EXISTS);

+ 12 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceService.java

@@ -1,8 +1,11 @@
 package cn.iocoder.yudao.module.promotion.service.price;
 
+import cn.iocoder.yudao.module.promotion.api.price.dto.CouponMeetRespDTO;
 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
 
+import java.util.List;
+
 /**
  * 价格计算 Service 接口
  *
@@ -14,8 +17,16 @@ public interface PriceService {
      * 计算商品的价格
      *
      * @param calculateReqDTO 价格请求
-     * @return 价格
+     * @return 价格
      */
     PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO);
 
+    /**
+     * 获得优惠劵的匹配信息列表
+     *
+     * @param calculateReqDTO 价格请求
+     * @return 价格响应
+     */
+    List<CouponMeetRespDTO> getMeetCouponList(PriceCalculateReqDTO calculateReqDTO);
+
 }

+ 50 - 4
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceServiceImpl.java

@@ -3,15 +3,18 @@ package cn.iocoder.yudao.module.promotion.service.price;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
+import cn.iocoder.yudao.module.promotion.api.price.dto.CouponMeetRespDTO;
 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
 import cn.iocoder.yudao.module.promotion.convert.price.PriceConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.enums.common.*;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
 import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
 import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityService;
 import cn.iocoder.yudao.module.promotion.service.discount.bo.DiscountProductDetailBO;
@@ -22,10 +25,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Supplier;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -87,6 +87,52 @@ public class PriceServiceImpl implements PriceService {
         return priceCalculate;
     }
 
+    @Override
+    public List<CouponMeetRespDTO> getMeetCouponList(PriceCalculateReqDTO calculateReqDTO) {
+        // 先计算一轮价格
+        PriceCalculateRespDTO priceCalculate = calculatePrice(calculateReqDTO);
+
+        // 获得用户的待使用优惠劵
+        List<CouponDO> couponList = couponService.getCouponList(calculateReqDTO.getUserId(), CouponStatusEnum.UNUSED.getStatus());
+        if (CollUtil.isEmpty(couponList)) {
+            return Collections.emptyList();
+        }
+
+        // 获得优惠劵的匹配信息
+        return CollectionUtils.convertList(couponList, coupon -> {
+            CouponMeetRespDTO couponMeetRespDTO = PriceConvert.INSTANCE.convert(coupon);
+            try {
+                // 校验优惠劵
+                couponService.validCoupon(coupon);
+
+                // 获得匹配的商品 SKU 数组
+                List<PriceCalculateRespDTO.OrderItem> orderItems = getMatchCouponOrderItems(priceCalculate, coupon);
+                if (CollUtil.isEmpty(orderItems)) {
+                    return couponMeetRespDTO.setMeet(false).setMeetTip("所结算商品没有符合条件的商品");
+                }
+
+                // 计算是否满足优惠劵的使用金额
+                Integer originPrice = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
+                assert originPrice != null;
+                if (originPrice < coupon.getUsePrice()) {
+                    return couponMeetRespDTO.setMeet(false)
+                            .setMeetTip(String.format("差 %s 元可用优惠劵", formatPrice(coupon.getUsePrice() - originPrice)));
+                }
+            } catch (ServiceException serviceException) {
+                couponMeetRespDTO.setMeet(false);
+                if (serviceException.getCode().equals(COUPON_VALID_TIME_NOT_NOW.getCode())) {
+                    couponMeetRespDTO.setMeetTip("优惠劵未到使用时间");
+                } else {
+                    log.error("[getMeetCouponList][calculateReqDTO({}) 获得优惠劵匹配信息异常]", calculateReqDTO, serviceException);
+                    couponMeetRespDTO.setMeetTip("优惠劵不满足使用条件");
+                }
+                return couponMeetRespDTO;
+            }
+            // 满足
+            return couponMeetRespDTO.setMeet(true);
+        });
+    }
+
     private List<ProductSkuRespDTO> checkSkus(PriceCalculateReqDTO calculateReqDTO) {
         // 获得商品 SKU 数组
         Map<Long, Integer> skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(),