feat: 优化优惠券发放与库存校验流程
This commit is contained in:
@@ -5,8 +5,8 @@ import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||
import com.starry.admin.modules.shop.module.vo.*;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||
import com.starry.common.annotation.Log;
|
||||
import com.starry.common.enums.BusinessType;
|
||||
@@ -38,9 +38,6 @@ public class PlayCouponInfoController {
|
||||
@Resource
|
||||
private IPlayCouponInfoService playCouponInfoService;
|
||||
|
||||
@Resource
|
||||
private IPlayCouponDetailsService playCouponDetailsService;
|
||||
|
||||
@Resource
|
||||
private IPlayCustomUserInfoService playCustomUserInfoService;
|
||||
|
||||
@@ -85,9 +82,9 @@ public class PlayCouponInfoController {
|
||||
@PostMapping("/sendCoupon")
|
||||
public R sendCoupon(@ApiParam(value = "优惠券发放信息", required = true) @Validated @RequestBody PlayCouponInfoSendVo vo) {
|
||||
PlayCustomUserInfoEntity customUserInfo = playCustomUserInfoService.selectById(vo.getCustomId());
|
||||
for (Integer i = 0; i < vo.getSendNumber(); i++) {
|
||||
playCouponDetailsService.create(customUserInfo.getId(), customUserInfo.getNickname(),
|
||||
customUserInfo.getLevelId(), vo.getId(), "2", "1");
|
||||
for (int i = 0; i < vo.getSendNumber(); i++) {
|
||||
playCouponInfoService.claimCouponForCustom(
|
||||
vo.getId(), customUserInfo, CouponObtainChannel.BACKEND_GRANT);
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
@@ -131,6 +128,31 @@ public class PlayCouponInfoController {
|
||||
if ("2".equals(vo.getValidityPeriodType()) && vo.getEffectiveDay() <= 0) {
|
||||
throw new CustomException("用券时间输入错误,有效天数不能为0");
|
||||
}
|
||||
|
||||
boolean unlimitedInventory = "1".equals(vo.getCouponQuantityType());
|
||||
if (unlimitedInventory) {
|
||||
entity.setCouponQuantity(null);
|
||||
entity.setRemainingQuantity(null);
|
||||
} else {
|
||||
Integer totalQuantity = vo.getCouponQuantity();
|
||||
if (totalQuantity == null || totalQuantity <= 0) {
|
||||
throw new CustomException("优惠券总数必须大于0");
|
||||
}
|
||||
entity.setCouponQuantity(totalQuantity);
|
||||
entity.setRemainingQuantity(totalQuantity);
|
||||
}
|
||||
|
||||
boolean unlimitedPerUser = "1".equals(vo.getClerkObtainedMaxQuantityType());
|
||||
if (unlimitedPerUser) {
|
||||
entity.setClerkObtainedMaxQuantity(null);
|
||||
} else {
|
||||
Integer perUserLimit = vo.getClerkObtainedMaxQuantity();
|
||||
if (perUserLimit == null || perUserLimit <= 0) {
|
||||
throw new CustomException("每人限领数量必须大于0");
|
||||
}
|
||||
entity.setClerkObtainedMaxQuantity(perUserLimit);
|
||||
}
|
||||
|
||||
String discountContent = "";
|
||||
if (BigDecimal.ZERO.compareTo(vo.getUseMinAmount()) == 0) {
|
||||
discountContent = "无门槛";
|
||||
|
||||
@@ -2,13 +2,33 @@ package com.starry.admin.modules.shop.mapper;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* 优惠券信息Mapper接口
|
||||
*
|
||||
* @author admin
|
||||
* @since 2024-07-04
|
||||
*/
|
||||
@Mapper
|
||||
public interface PlayCouponInfoMapper extends MPJBaseMapper<PlayCouponInfoEntity> {
|
||||
|
||||
@Update({
|
||||
"UPDATE play_coupon_info",
|
||||
"SET issued_quantity = IFNULL(issued_quantity, 0) + 1,",
|
||||
" remaining_quantity = CASE",
|
||||
" WHEN remaining_quantity IS NULL THEN NULL",
|
||||
" WHEN remaining_quantity < 0 THEN remaining_quantity",
|
||||
" ELSE remaining_quantity - 1",
|
||||
" END",
|
||||
"WHERE id = #{id}",
|
||||
" AND (remaining_quantity IS NULL OR remaining_quantity <> 0)"
|
||||
})
|
||||
int increaseIssuedAndConsumeRemaining(String id);
|
||||
|
||||
@Select({
|
||||
"SELECT * FROM play_coupon_info",
|
||||
"WHERE id = #{id}",
|
||||
"FOR UPDATE"
|
||||
})
|
||||
PlayCouponInfoEntity selectByIdForUpdate(String id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.starry.admin.modules.shop.module.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 优惠券领取渠道
|
||||
*/
|
||||
@Getter
|
||||
public enum CouponObtainChannel {
|
||||
SELF_SERVICE("1"),
|
||||
BACKEND_GRANT("2"),
|
||||
REFUND_RETURN("3"),
|
||||
SHARE("4"),
|
||||
LOTTERY("5");
|
||||
|
||||
private final String code;
|
||||
|
||||
CouponObtainChannel(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static Optional<CouponObtainChannel> fromCode(String code) {
|
||||
return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.starry.admin.modules.shop.module.support;
|
||||
|
||||
/**
|
||||
* Encapsulates how coupon inventory should behave (finite vs. unlimited).
|
||||
*/
|
||||
public enum CouponInventoryPolicy {
|
||||
UNLIMITED,
|
||||
FINITE;
|
||||
|
||||
public boolean isUnlimited() {
|
||||
return this == UNLIMITED;
|
||||
}
|
||||
|
||||
public boolean consumesStock() {
|
||||
return this == FINITE;
|
||||
}
|
||||
|
||||
public boolean hasStock(Integer remainingQuantity) {
|
||||
if (isUnlimited()) {
|
||||
return true;
|
||||
}
|
||||
return remainingQuantity == null || remainingQuantity > 0;
|
||||
}
|
||||
|
||||
public static CouponInventoryPolicy resolve(Integer totalQuantity, Integer remainingQuantity) {
|
||||
if (isUnlimitedSentinel(totalQuantity) || isUnlimitedSentinel(remainingQuantity)) {
|
||||
return UNLIMITED;
|
||||
}
|
||||
if (totalQuantity == null && remainingQuantity == null) {
|
||||
return UNLIMITED;
|
||||
}
|
||||
return FINITE;
|
||||
}
|
||||
|
||||
private static boolean isUnlimitedSentinel(Integer value) {
|
||||
return value != null && value < 0;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,11 @@ public interface IPlayCouponDetailsService extends IService<PlayCouponDetailsEnt
|
||||
*/
|
||||
boolean create(PlayCouponDetailsEntity playCouponDetails);
|
||||
|
||||
/**
|
||||
* 统计指定用户的有效优惠券数量(排除已回收)。
|
||||
*/
|
||||
long countActiveByCustomAndCoupon(String couponId, String customId);
|
||||
|
||||
/**
|
||||
* 修改优惠券详情
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo;
|
||||
import java.math.BigDecimal;
|
||||
@@ -53,6 +54,11 @@ public interface IPlayCouponInfoService extends IService<PlayCouponInfoEntity> {
|
||||
String useState,
|
||||
LocalDateTime obtainingTime);
|
||||
|
||||
/**
|
||||
* 顾客领取优惠券,校验库存与限领并生成明细。
|
||||
*/
|
||||
void claimCouponForCustom(String couponId, PlayCustomUserInfoEntity customUserInfo, CouponObtainChannel obtainChannel);
|
||||
|
||||
/**
|
||||
* 获取优惠券不可领取的原因
|
||||
*
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
import com.starry.admin.modules.shop.mapper.PlayCouponDetailsMapper;
|
||||
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsQueryVo;
|
||||
@@ -123,6 +124,15 @@ public class PlayCouponDetailsServiceImpl extends ServiceImpl<PlayCouponDetailsM
|
||||
return save(playCouponDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countActiveByCustomAndCoupon(String couponId, String customId) {
|
||||
return lambdaQuery()
|
||||
.eq(PlayCouponDetailsEntity::getCouponId, couponId)
|
||||
.eq(PlayCouponDetailsEntity::getCustomId, customId)
|
||||
.eq(PlayCouponDetailsEntity::getUseState, CouponUseState.UNUSED.getCode())
|
||||
.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlayCouponDetailsReturnVo> selectByCustomId(String customId) {
|
||||
MPJLambdaWrapper<PlayCouponDetailsEntity> lambdaWrapper = new MPJLambdaWrapper<>();
|
||||
|
||||
@@ -12,10 +12,13 @@ import com.starry.admin.modules.shop.mapper.PlayCouponInfoMapper;
|
||||
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||
import com.starry.admin.modules.shop.module.support.CouponInventoryPolicy;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import java.math.BigDecimal;
|
||||
@@ -25,6 +28,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 优惠券信息Service业务层处理
|
||||
@@ -39,6 +43,9 @@ public class PlayCouponInfoServiceImpl extends ServiceImpl<PlayCouponInfoMapper,
|
||||
@Resource
|
||||
private PlayCouponInfoMapper playCouponInfoMapper;
|
||||
|
||||
@Resource
|
||||
private IPlayCouponDetailsService playCouponDetailsService;
|
||||
|
||||
/**
|
||||
* 获取优惠券不可用的原因
|
||||
*
|
||||
@@ -130,6 +137,54 @@ public class PlayCouponInfoServiceImpl extends ServiceImpl<PlayCouponInfoMapper,
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void claimCouponForCustom(
|
||||
String couponId,
|
||||
PlayCustomUserInfoEntity customUserInfo,
|
||||
CouponObtainChannel obtainChannel) {
|
||||
if (customUserInfo == null) {
|
||||
throw new CustomException("顾客信息不存在");
|
||||
}
|
||||
CouponObtainChannel channel = obtainChannel != null ? obtainChannel : CouponObtainChannel.SELF_SERVICE;
|
||||
|
||||
PlayCouponInfoEntity couponInfo = playCouponInfoMapper.selectByIdForUpdate(couponId);
|
||||
if (couponInfo == null) {
|
||||
throw new CustomException("优惠券不存在");
|
||||
}
|
||||
Optional<CouponOnlineState> onlineState = CouponOnlineState.fromCode(couponInfo.getCouponOnLineState());
|
||||
if (onlineState.isPresent() && onlineState.get() != CouponOnlineState.ONLINE) {
|
||||
throw new CustomException("优惠券已下架");
|
||||
}
|
||||
|
||||
CouponInventoryPolicy inventoryPolicy = CouponInventoryPolicy
|
||||
.resolve(couponInfo.getCouponQuantity(), couponInfo.getRemainingQuantity());
|
||||
if (!inventoryPolicy.hasStock(couponInfo.getRemainingQuantity())) {
|
||||
throw new CustomException("优惠券已领完");
|
||||
}
|
||||
|
||||
Integer maxQuantityPerUser = couponInfo.getClerkObtainedMaxQuantity();
|
||||
if (maxQuantityPerUser != null && maxQuantityPerUser > 0) {
|
||||
long claimed = playCouponDetailsService.countActiveByCustomAndCoupon(couponId, customUserInfo.getId());
|
||||
if (claimed >= maxQuantityPerUser) {
|
||||
throw new CustomException("优惠券已达到领取上限");
|
||||
}
|
||||
}
|
||||
|
||||
int affected = playCouponInfoMapper.increaseIssuedAndConsumeRemaining(couponId);
|
||||
if (inventoryPolicy.consumesStock() && affected == 0) {
|
||||
throw new CustomException("优惠券已领完");
|
||||
}
|
||||
|
||||
playCouponDetailsService.create(
|
||||
customUserInfo.getId(),
|
||||
customUserInfo.getNickname(),
|
||||
customUserInfo.getLevelId(),
|
||||
couponId,
|
||||
channel.getCode(),
|
||||
CouponUseState.UNUSED.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取优惠券不可领取的原因
|
||||
*
|
||||
@@ -144,6 +199,22 @@ public class PlayCouponInfoServiceImpl extends ServiceImpl<PlayCouponInfoMapper,
|
||||
@Override
|
||||
public String getReasonForNotObtainingCoupons(PlayCouponInfoEntity entity,
|
||||
PlayCustomUserInfoEntity customUserInfo) {
|
||||
CouponInventoryPolicy inventoryPolicy = CouponInventoryPolicy
|
||||
.resolve(entity.getCouponQuantity(), entity.getRemainingQuantity());
|
||||
if (!inventoryPolicy.hasStock(entity.getRemainingQuantity())) {
|
||||
return "优惠券已领完";
|
||||
}
|
||||
if (customUserInfo != null && playCouponDetailsService != null) {
|
||||
Integer perUserLimit = entity.getClerkObtainedMaxQuantity();
|
||||
if (perUserLimit != null && perUserLimit > 0) {
|
||||
long claimed = playCouponDetailsService.countActiveByCustomAndCoupon(
|
||||
entity.getId(),
|
||||
customUserInfo.getId());
|
||||
if (claimed >= perUserLimit) {
|
||||
return "优惠券已达到领取上限";
|
||||
}
|
||||
}
|
||||
}
|
||||
CouponClaimConditionType type = CouponClaimConditionType.of(entity.getClaimConditionType());
|
||||
switch (type) {
|
||||
case ALL:
|
||||
|
||||
@@ -10,8 +10,10 @@ import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||
import com.starry.admin.modules.shop.module.support.CouponInventoryPolicy;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||
@@ -75,10 +77,13 @@ public class WxCouponController {
|
||||
PlayCouponInfoEntity entity = couponInfoService.selectPlayCouponInfoById(id);
|
||||
String msg = couponInfoService.getReasonForNotObtainingCoupons(entity, customUserInfo);
|
||||
boolean success = StrUtil.isBlank(msg);
|
||||
// 优惠券领取验证通过,发放优惠券
|
||||
if (success) {
|
||||
couponDetailsService.create(customUserInfo.getId(), customUserInfo.getNickname(),
|
||||
customUserInfo.getLevelId(), entity.getId(), "1", "1");
|
||||
try {
|
||||
couponInfoService.claimCouponForCustom(entity.getId(), customUserInfo, CouponObtainChannel.SELF_SERVICE);
|
||||
} catch (CustomException ex) {
|
||||
success = false;
|
||||
msg = ex.getMessage();
|
||||
}
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", success);
|
||||
@@ -112,7 +117,9 @@ public class WxCouponController {
|
||||
if (!online) {
|
||||
continue;
|
||||
}
|
||||
if (couponInfoEntity.getRemainingQuantity() != null && couponInfoEntity.getRemainingQuantity() <= 0) {
|
||||
CouponInventoryPolicy inventoryPolicy = CouponInventoryPolicy
|
||||
.resolve(couponInfoEntity.getCouponQuantity(), couponInfoEntity.getRemainingQuantity());
|
||||
if (!inventoryPolicy.hasStock(couponInfoEntity.getRemainingQuantity())) {
|
||||
continue;
|
||||
}
|
||||
CouponValidityPeriodType validityType = CouponValidityPeriodType
|
||||
|
||||
Reference in New Issue
Block a user