From 7c906d5529ffee31318a4a9ad6f26fa3f914ec78 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 17 Nov 2025 00:53:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(pricing):=20=E4=BC=98=E5=8C=96=E4=BC=98?= =?UTF-8?q?=E6=83=A0=E5=88=B8=E9=A2=86=E5=8F=96=E9=80=BB=E8=BE=91=E4=B8=8E?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 CouponInvalidException 添加错误码支持 - 在 countUserCouponClaims 查询中添加 FOR UPDATE 锁 - 新增 incrementClaimedQuantityIfAvailable 方法用于原子性增加已领取数量 - 移除重复的用户优惠券领取检查逻辑 - 调整领取流程步骤编号并优化事务回滚处理 - 增加对优惠券库存耗尽情况的业务异常处理 - 使用 --- .../exception/CouponInvalidException.java | 25 ++++++++++--- .../mapper/PriceCouponClaimRecordMapper.java | 4 +- .../mapper/PriceCouponConfigMapper.java | 10 ++++- .../service/impl/CouponServiceImpl.java | 37 ++++++++++++------- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java index 73a6fa40..e4be45da 100644 --- a/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java +++ b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java @@ -4,12 +4,27 @@ package com.ycwl.basic.pricing.exception; * 优惠券无效异常 */ public class CouponInvalidException extends RuntimeException { - + + private final String errorCode; + public CouponInvalidException(String message) { - super(message); + this(null, message, null); } - + public CouponInvalidException(String message, Throwable cause) { - super(message, cause); + this(null, message, cause); } -} \ No newline at end of file + + public CouponInvalidException(String errorCode, String message) { + this(errorCode, message, null); + } + + public CouponInvalidException(String errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + public String getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java index f08241ec..107ba60b 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -40,7 +40,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper selectScenicCouponUsageStats(@Param("scenicId") String scenicId); -} \ No newline at end of file +} diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java index 077e470d..71496c2e 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java @@ -38,6 +38,14 @@ public interface PriceCouponConfigMapper extends BaseMapper { @Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " + "update_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity") int incrementUsedQuantity(Long couponId); + + /** + * 原子性增加已领取数量(仅对有限库存的优惠券生效) + */ + @Update("UPDATE price_coupon_config SET claimed_quantity = COALESCE(claimed_quantity, 0) + 1, " + + "update_time = NOW() WHERE id = #{couponId} AND total_quantity IS NOT NULL AND total_quantity > 0 " + + "AND COALESCE(claimed_quantity, 0) < total_quantity") + int incrementClaimedQuantityIfAvailable(@Param("couponId") Long couponId); /** * 插入优惠券配置 @@ -133,4 +141,4 @@ public interface PriceCouponConfigMapper extends BaseMapper { "SUM(used_quantity) as used_quantity " + "FROM price_coupon_config WHERE scenic_id = #{scenicId}") java.util.Map selectScenicCouponConfigStats(@Param("scenicId") String scenicId); -} \ No newline at end of file +} diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java index 904e1b10..e9d0e15f 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -18,6 +18,7 @@ import java.util.Date; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.math.BigDecimal; import java.math.RoundingMode; @@ -243,6 +244,7 @@ public class CouponServiceImpl implements ICouponService { if (coupon.getUserClaimLimit() != null && coupon.getUserClaimLimit() > 0) { int userClaimCount = couponClaimRecordMapper.countUserCouponClaims( request.getUserId(), request.getCouponId()); + // countUserCouponClaims 使用 FOR UPDATE + 复合索引,确保并发下的计数准确 if (userClaimCount >= coupon.getUserClaimLimit()) { return CouponClaimResult.failure( CouponClaimResult.ERROR_CLAIM_LIMIT_REACHED, @@ -250,14 +252,7 @@ public class CouponServiceImpl implements ICouponService { } } - // 7. 检查用户是否已经领取过该优惠券(兼容旧逻辑,双重保护) - PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord( - request.getUserId(), request.getCouponId()); - if (existingRecord != null) { - return CouponClaimResult.failure(CouponClaimResult.ERROR_ALREADY_CLAIMED, "您已经领取过该优惠券"); - } - - // 8. 创建领取记录 + // 7. 创建领取记录 Date claimTime = new Date(); PriceCouponClaimRecord claimRecord = new PriceCouponClaimRecord(); claimRecord.setCouponId(request.getCouponId()); @@ -269,7 +264,7 @@ public class CouponServiceImpl implements ICouponService { claimRecord.setUpdateTime(claimTime); claimRecord.setDeleted(0); - // 9. 插入领取记录 + // 8. 插入领取记录 int insertResult = couponClaimRecordMapper.insert(claimRecord); if (insertResult <= 0) { log.error("插入优惠券领取记录失败: userId={}, couponId={}", @@ -277,24 +272,38 @@ public class CouponServiceImpl implements ICouponService { return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "领取失败,请稍后重试"); } - // 10. 更新优惠券已领取数量(区分于已使用数量) + // 9. 更新优惠券已领取数量(区分于已使用数量) // 仅在有总量限制时才更新claimedQuantity(totalQuantity为正整数) if (coupon.getTotalQuantity() != null && coupon.getTotalQuantity() > 0) { + int affected = couponConfigMapper.incrementClaimedQuantityIfAvailable(coupon.getId()); + if (affected == 0) { + throw new CouponInvalidException( + CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK, + "优惠券已被领取完,请稍后重试"); + } int updatedClaimedQuantity = (coupon.getClaimedQuantity() == null ? 0 : coupon.getClaimedQuantity()) + 1; coupon.setClaimedQuantity(updatedClaimedQuantity); - couponConfigMapper.updateById(coupon); } log.info("优惠券领取成功: userId={}, couponId={}, claimRecordId={}", request.getUserId(), request.getCouponId(), claimRecord.getId()); - // 11. 返回成功结果 + // 10. 返回成功结果 return CouponClaimResult.success(claimRecord, coupon); + } catch (CouponInvalidException e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + log.warn("领取优惠券失败(业务校验不通过): userId={}, couponId={}, reason={}", + request.getUserId(), request.getCouponId(), e.getMessage()); + String errorCode = e.getErrorCode() == null + ? CouponClaimResult.ERROR_SYSTEM_ERROR + : e.getErrorCode(); + return CouponClaimResult.failure(errorCode, e.getMessage()); } catch (Exception e) { - log.error("领取优惠券失败: userId={}, couponId={}", + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + log.error("领取优惠券失败: userId={}, couponId={}", request.getUserId(), request.getCouponId(), e); return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "系统错误,领取失败:" + e.getMessage()); } } -} \ No newline at end of file +}