diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java index 97fe67f61..5053984ab 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java +++ b/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(); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/price/dto/CouponMeetRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/price/dto/CouponMeetRespDTO.java new file mode 100644 index 000000000..310959e2c --- /dev/null +++ b/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; + +} diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java index e405a0c59..b9c4eee45 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java +++ b/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, "满减送活动不存在"); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/price/PriceConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/price/PriceConvert.java index cafe81526..8e4b24666 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/price/PriceConvert.java +++ b/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); + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java index d60e75cdb..e5ae5f79d 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java +++ b/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 { .orderByDesc(CouponDO::getId)); } + default List selectListByUserIdAndStatus(Long userId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(CouponDO::getUserId, userId).eq(CouponDO::getStatus, status)); + } + + default CouponDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(CouponDO::getId, id).eq(CouponDO::getUserId, userId)); + } + default int delete(Long id, Collection whereStatuses) { return update(null, new LambdaUpdateWrapper() .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() + .eq(CouponDO::getId, id).eq(CouponDO::getStatus, status)); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java index 22d5d5cd0..169feeca0 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java +++ b/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 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 getCouponList(Long userId, Integer status); + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index 3cd3c22c8..124e0b157 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/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 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); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceService.java index c7e518dca..a7420e119 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceService.java +++ b/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 getMeetCouponList(PriceCalculateReqDTO calculateReqDTO); + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceServiceImpl.java index 062ec2f89..8d68819e0 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/price/PriceServiceImpl.java +++ b/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 getMeetCouponList(PriceCalculateReqDTO calculateReqDTO) { + // 先计算一轮价格 + PriceCalculateRespDTO priceCalculate = calculatePrice(calculateReqDTO); + + // 获得用户的待使用优惠劵 + List 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 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 checkSkus(PriceCalculateReqDTO calculateReqDTO) { // 获得商品 SKU 数组 Map skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(),