diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/controller/PlayCouponInfoController.java b/play-admin/src/main/java/com/starry/admin/modules/shop/controller/PlayCouponInfoController.java index 4cc61f4..63dddd2 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/shop/controller/PlayCouponInfoController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/controller/PlayCouponInfoController.java @@ -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 = "无门槛"; diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/mapper/PlayCouponInfoMapper.java b/play-admin/src/main/java/com/starry/admin/modules/shop/mapper/PlayCouponInfoMapper.java index 667786f..aad89db 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/shop/mapper/PlayCouponInfoMapper.java +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/mapper/PlayCouponInfoMapper.java @@ -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 { + @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); } diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponObtainChannel.java b/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponObtainChannel.java new file mode 100644 index 0000000..1cb1b07 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponObtainChannel.java @@ -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 fromCode(String code) { + return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst(); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/module/support/CouponInventoryPolicy.java b/play-admin/src/main/java/com/starry/admin/modules/shop/module/support/CouponInventoryPolicy.java new file mode 100644 index 0000000..87bbaf0 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/module/support/CouponInventoryPolicy.java @@ -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; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponDetailsService.java b/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponDetailsService.java index e0dbcc0..07bed05 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponDetailsService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponDetailsService.java @@ -82,6 +82,11 @@ public interface IPlayCouponDetailsService extends IService { String useState, LocalDateTime obtainingTime); + /** + * 顾客领取优惠券,校验库存与限领并生成明细。 + */ + void claimCouponForCustom(String couponId, PlayCustomUserInfoEntity customUserInfo, CouponObtainChannel obtainChannel); + /** * 获取优惠券不可领取的原因 * diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponDetailsServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponDetailsServiceImpl.java index a6c02c5..ca1e726 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponDetailsServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponDetailsServiceImpl.java @@ -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 selectByCustomId(String customId) { MPJLambdaWrapper lambdaWrapper = new MPJLambdaWrapper<>(); diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java index 40c9ccb..a68fb9f 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java @@ -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 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 0) { + long claimed = playCouponDetailsService.countActiveByCustomAndCoupon( + entity.getId(), + customUserInfo.getId()); + if (claimed >= perUserLimit) { + return "优惠券已达到领取上限"; + } + } + } CouponClaimConditionType type = CouponClaimConditionType.of(entity.getClaimConditionType()); switch (type) { case ALL: diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java index e0cc73e..f4621f6 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java @@ -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 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