You've already forked FrameTour-BE
Merge branch 'coupon_user_limit'
This commit is contained in:
@@ -122,8 +122,68 @@ public enum CouponStatus { CLAIMED("claimed", ...), USED("used", ...), EXPIRED("
|
|||||||
- 商品类型限制:通过 JSON 字段(结合 `ProductTypeListTypeHandler`)控制适用商品
|
- 商品类型限制:通过 JSON 字段(结合 `ProductTypeListTypeHandler`)控制适用商品
|
||||||
- 消费限制:支持最小消费金额、最大折扣限制
|
- 消费限制:支持最小消费金额、最大折扣限制
|
||||||
- 时效性:基于时间的有效期控制
|
- 时效性:基于时间的有效期控制
|
||||||
|
- **用户领取数量限制**:通过 `userClaimLimit` 字段控制单个用户可领取优惠券的最大数量(v1.0.0新增)
|
||||||
|
- **库存精细化管理**:区分 `claimedQuantity`(已领取数量) 和 `usedQuantity`(已使用数量)
|
||||||
- 统计分析:完整的使用统计与分析能力
|
- 统计分析:完整的使用统计与分析能力
|
||||||
|
|
||||||
|
#### 优惠券数量管理机制 (v1.0.0更新)
|
||||||
|
```java
|
||||||
|
// PriceCouponConfig 实体字段
|
||||||
|
private Integer totalQuantity; // 发行总量(NULL或0表示不限制)
|
||||||
|
private Integer userClaimLimit; // 每个用户可领取数量限制(NULL或0表示不限制)
|
||||||
|
private Integer claimedQuantity; // 已领取数量(区分于已使用数量)
|
||||||
|
private Integer usedQuantity; // 已使用数量(实际消耗时更新)
|
||||||
|
|
||||||
|
// 字段语义
|
||||||
|
totalQuantity: NULL/0=不限总量, >0=限制总量
|
||||||
|
userClaimLimit: NULL/0=不限用户, >0=限制单用户
|
||||||
|
claimedQuantity: 仅在totalQuantity>0时更新
|
||||||
|
usedQuantity: 实际使用时更新
|
||||||
|
|
||||||
|
// 领取流程检查顺序
|
||||||
|
1. 检查优惠券是否存在且启用
|
||||||
|
2. 检查有效期
|
||||||
|
3. 检查总量库存: totalQuantity>0 时,检查 claimedQuantity < totalQuantity
|
||||||
|
4. 检查用户领取限制: userClaimLimit>0 时,检查 countUserClaims(userId, couponId) < userClaimLimit
|
||||||
|
5. 创建领取记录并更新 claimedQuantity (仅当totalQuantity>0时)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 用户领取限制配置示例
|
||||||
|
```java
|
||||||
|
// 场景1: 新人专享券,每人限领1张,总量1000张
|
||||||
|
{
|
||||||
|
"couponName": "新人专享券",
|
||||||
|
"userClaimLimit": 1,
|
||||||
|
"totalQuantity": 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景2: 活动券,每人限领3张,总量5000张
|
||||||
|
{
|
||||||
|
"couponName": "618促销券",
|
||||||
|
"userClaimLimit": 3,
|
||||||
|
"totalQuantity": 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景3: 不限制领取次数,不限制总量
|
||||||
|
{
|
||||||
|
"couponName": "会员专享券",
|
||||||
|
"userClaimLimit": null, // 或 0
|
||||||
|
"totalQuantity": null // 或 0,表示无限量
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景4: 不限用户次数,但限制总量
|
||||||
|
{
|
||||||
|
"couponName": "限量抢购券",
|
||||||
|
"userClaimLimit": null, // 不限单用户
|
||||||
|
"totalQuantity": 500 // 总共500张
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 错误码扩展
|
||||||
|
- `CLAIM_LIMIT_REACHED`: 用户已达到该优惠券的领取上限
|
||||||
|
- `COUPON_OUT_OF_STOCK`: 优惠券库存不足(基于claimedQuantity检查)
|
||||||
|
- `ALREADY_CLAIMED`: 用户已领取过(兼容旧逻辑)
|
||||||
|
|
||||||
### 3. 商品配置管理
|
### 3. 商品配置管理
|
||||||
|
|
||||||
#### API端点(摘)
|
#### API端点(摘)
|
||||||
@@ -550,9 +610,28 @@ public class PriceCalculationResult {
|
|||||||
- `price_product_config`: 商品价格基础配置(包含 `can_use_coupon`、`can_use_voucher`、`can_use_one_price` 优惠控制字段)
|
- `price_product_config`: 商品价格基础配置(包含 `can_use_coupon`、`can_use_voucher`、`can_use_one_price` 优惠控制字段)
|
||||||
- `price_tier_config`: 分层定价配置
|
- `price_tier_config`: 分层定价配置
|
||||||
- `price_bundle_config`: 套餐配置
|
- `price_bundle_config`: 套餐配置
|
||||||
- `price_coupon_config`: 优惠券配置
|
- `price_coupon_config`: 优惠券配置(**v1.0.0新增**: `user_claim_limit` 用户领取限制, `claimed_quantity` 已领取数量)
|
||||||
- `price_coupon_claim_record`: 优惠券领取记录
|
- `price_coupon_claim_record`: 优惠券领取记录
|
||||||
|
|
||||||
|
### price_coupon_config 表字段说明 (v1.0.0更新)
|
||||||
|
```sql
|
||||||
|
-- 核心数量管理字段
|
||||||
|
total_quantity INT -- 发行总量(NULL或0表示不限制,>0表示限量)
|
||||||
|
claimed_quantity INT -- 已领取数量(v1.0.0新增,领取时+1,仅在total_quantity>0时更新)
|
||||||
|
used_quantity INT -- 已使用数量(实际使用时+1)
|
||||||
|
user_claim_limit INT -- 每个用户可领取数量限制(v1.0.0新增,NULL或0表示不限制,>0表示限制)
|
||||||
|
|
||||||
|
-- 字段关系
|
||||||
|
-- claimed_quantity >= used_quantity (已领取 >= 已使用)
|
||||||
|
-- claimed_quantity <= total_quantity (已领取 <= 总量,仅当total_quantity>0时适用)
|
||||||
|
|
||||||
|
-- 典型配置组合
|
||||||
|
-- 1. 无限量发行: total_quantity=NULL/0, user_claim_limit=NULL/0
|
||||||
|
-- 2. 限量限人: total_quantity=1000, user_claim_limit=1
|
||||||
|
-- 3. 限量不限人: total_quantity=500, user_claim_limit=NULL/0
|
||||||
|
-- 4. 不限量但限人: total_quantity=NULL/0, user_claim_limit=3
|
||||||
|
```
|
||||||
|
|
||||||
### 新增表结构
|
### 新增表结构
|
||||||
- `price_voucher_batch_config`: 券码批次配置表
|
- `price_voucher_batch_config`: 券码批次配置表
|
||||||
- `price_voucher_code`: 券码表
|
- `price_voucher_code`: 券码表
|
||||||
@@ -562,11 +641,15 @@ public class PriceCalculationResult {
|
|||||||
|
|
||||||
### 索引优化(示例)
|
### 索引优化(示例)
|
||||||
```sql
|
```sql
|
||||||
|
-- 优惠券领取记录查询优化 (v1.0.0新增)
|
||||||
|
CREATE INDEX idx_user_coupon ON price_coupon_claim_record(user_id, coupon_id);
|
||||||
|
CREATE INDEX idx_coupon_status ON price_coupon_claim_record(coupon_id, status);
|
||||||
|
|
||||||
-- 券码查询优化
|
-- 券码查询优化
|
||||||
CREATE INDEX idx_voucher_code ON price_voucher_code(code);
|
CREATE INDEX idx_voucher_code ON price_voucher_code(code);
|
||||||
CREATE INDEX idx_face_scenic ON price_voucher_code(face_id, scenic_id);
|
CREATE INDEX idx_face_scenic ON price_voucher_code(face_id, scenic_id);
|
||||||
|
|
||||||
-- 批次查询优化
|
-- 批次查询优化
|
||||||
CREATE INDEX idx_scenic_broker ON price_voucher_batch_config(scenic_id, broker_id);
|
CREATE INDEX idx_scenic_broker ON price_voucher_batch_config(scenic_id, broker_id);
|
||||||
|
|
||||||
-- 使用记录与打印记录查询优化(示例)
|
-- 使用记录与打印记录查询优化(示例)
|
||||||
@@ -579,10 +662,37 @@ CREATE INDEX idx_print_face_scenic ON voucher_print_record(face_id, scenic_id);
|
|||||||
- 券码表可能数据量较大,考虑按景区维度分表或归档
|
- 券码表可能数据量较大,考虑按景区维度分表或归档
|
||||||
- 定期清理已删除的过期数据
|
- 定期清理已删除的过期数据
|
||||||
- 使用数据完整性检查 SQL 验证统计数据准确性
|
- 使用数据完整性检查 SQL 验证统计数据准确性
|
||||||
|
- **优惠券领取记录表查询优化** (v1.0.0): 为 `(user_id, coupon_id)` 添加复合索引以加速用户领取次数统计
|
||||||
|
|
||||||
## 兼容性与注意事项
|
## 兼容性与注意事项
|
||||||
|
|
||||||
- 本模块使用 PageHelper(优惠券相关)与 MyBatis‑Plus(券码/一口价等)并存,请根据对应 Service/Mapper 选择分页与查询方式。
|
- 本模块使用 PageHelper(优惠券相关)与 MyBatis‑Plus(券码/一口价等)并存,请根据对应 Service/Mapper 选择分页与查询方式。
|
||||||
- 优惠优先级及叠加规则以各 Provider 与业务配置为准,避免在外层重复实现优先级判断逻辑。
|
- 优惠优先级及叠加规则以各 Provider 与业务配置为准,避免在外层重复实现优先级判断逻辑。
|
||||||
- 若扩展新的优惠类型,务必实现 `IDiscountProvider` 并在 `IDiscountDetectionService` 中完成注册(当前实现通过组件扫描自动注册并排序)。
|
- 若扩展新的优惠类型,务必实现 `IDiscountProvider` 并在 `IDiscountDetectionService` 中完成注册(当前实现通过组件扫描自动注册并排序)。
|
||||||
|
- **优惠券数量管理** (v1.0.0): 现有代码已调整为领取时更新 `claimedQuantity`,使用时更新 `usedQuantity`。如业务需求不同,请调整 `CouponServiceImpl.claimCoupon()` 和 `CouponServiceImpl.useCoupon()` 逻辑。
|
||||||
|
|
||||||
|
## 版本更新记录
|
||||||
|
|
||||||
|
### v1.0.0 (2025-11-16)
|
||||||
|
**优惠券用户领取数量限制功能**
|
||||||
|
|
||||||
|
新增特性:
|
||||||
|
- ✅ 支持为每个优惠券配置用户领取数量限制 (`userClaimLimit`)
|
||||||
|
- ✅ 区分已领取数量 (`claimedQuantity`) 和已使用数量 (`usedQuantity`)
|
||||||
|
- ✅ 新增错误码 `CLAIM_LIMIT_REACHED` 处理超限场景
|
||||||
|
- ✅ 新增统计方法 `PriceCouponClaimRecordMapper.countUserCouponClaims()`
|
||||||
|
|
||||||
|
数据库变更:
|
||||||
|
- 表 `price_coupon_config` 新增字段 `user_claim_limit INT`
|
||||||
|
- 表 `price_coupon_config` 新增字段 `claimed_quantity INT`
|
||||||
|
- 建议索引 `idx_user_coupon ON price_coupon_claim_record(user_id, coupon_id)`
|
||||||
|
|
||||||
|
受影响文件:
|
||||||
|
- `entity/PriceCouponConfig.java`
|
||||||
|
- `mapper/PriceCouponClaimRecordMapper.java`
|
||||||
|
- `service/impl/CouponServiceImpl.java`
|
||||||
|
- `dto/CouponClaimResult.java`
|
||||||
|
- `dto/CouponInfo.java`
|
||||||
|
|
||||||
|
迁移指南: 详见 `docs/coupon-user-claim-limit-guide.md`
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ public class CouponClaimResult {
|
|||||||
public static final String ERROR_COUPON_INACTIVE = "COUPON_INACTIVE";
|
public static final String ERROR_COUPON_INACTIVE = "COUPON_INACTIVE";
|
||||||
public static final String ERROR_COUPON_OUT_OF_STOCK = "COUPON_OUT_OF_STOCK";
|
public static final String ERROR_COUPON_OUT_OF_STOCK = "COUPON_OUT_OF_STOCK";
|
||||||
public static final String ERROR_ALREADY_CLAIMED = "ALREADY_CLAIMED";
|
public static final String ERROR_ALREADY_CLAIMED = "ALREADY_CLAIMED";
|
||||||
|
public static final String ERROR_CLAIM_LIMIT_REACHED = "CLAIM_LIMIT_REACHED";
|
||||||
public static final String ERROR_INVALID_PARAMS = "INVALID_PARAMS";
|
public static final String ERROR_INVALID_PARAMS = "INVALID_PARAMS";
|
||||||
public static final String ERROR_SYSTEM_ERROR = "SYSTEM_ERROR";
|
public static final String ERROR_SYSTEM_ERROR = "SYSTEM_ERROR";
|
||||||
}
|
}
|
||||||
@@ -30,9 +30,14 @@ public class CouponInfo {
|
|||||||
* 优惠值
|
* 优惠值
|
||||||
*/
|
*/
|
||||||
private BigDecimal discountValue;
|
private BigDecimal discountValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实际优惠金额
|
* 实际优惠金额
|
||||||
*/
|
*/
|
||||||
private BigDecimal actualDiscountAmount;
|
private BigDecimal actualDiscountAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个用户可领取数量限制(NULL表示不限制)
|
||||||
|
*/
|
||||||
|
private Integer userClaimLimit;
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,17 @@ public class PriceCouponConfig {
|
|||||||
* 已使用数量
|
* 已使用数量
|
||||||
*/
|
*/
|
||||||
private Integer usedQuantity;
|
private Integer usedQuantity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已领取数量(区分于已使用数量)
|
||||||
|
*/
|
||||||
|
private Integer claimedQuantity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个用户可领取数量限制(NULL表示不限制)
|
||||||
|
*/
|
||||||
|
private Integer userClaimLimit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生效时间
|
* 生效时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,12 +4,27 @@ package com.ycwl.basic.pricing.exception;
|
|||||||
* 优惠券无效异常
|
* 优惠券无效异常
|
||||||
*/
|
*/
|
||||||
public class CouponInvalidException extends RuntimeException {
|
public class CouponInvalidException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String errorCode;
|
||||||
|
|
||||||
public CouponInvalidException(String message) {
|
public CouponInvalidException(String message) {
|
||||||
super(message);
|
this(null, message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CouponInvalidException(String message, Throwable cause) {
|
public CouponInvalidException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
this(null, message, cause);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,9 +33,16 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper<PriceCouponClai
|
|||||||
*/
|
*/
|
||||||
@Select("SELECT * FROM price_coupon_claim_record " +
|
@Select("SELECT * FROM price_coupon_claim_record " +
|
||||||
"WHERE user_id = #{userId} AND coupon_id = #{couponId}")
|
"WHERE user_id = #{userId} AND coupon_id = #{couponId}")
|
||||||
PriceCouponClaimRecord selectUserCouponRecord(@Param("userId") Long userId,
|
PriceCouponClaimRecord selectUserCouponRecord(@Param("userId") Long userId,
|
||||||
@Param("couponId") Long couponId);
|
@Param("couponId") Long couponId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户领取某优惠券的次数(所有状态)
|
||||||
|
*/
|
||||||
|
@Select("SELECT COUNT(*) FROM price_coupon_claim_record " +
|
||||||
|
"WHERE user_id = #{userId} AND coupon_id = #{couponId} AND deleted = 0 FOR UPDATE")
|
||||||
|
int countUserCouponClaims(@Param("userId") Long userId, @Param("couponId") Long couponId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新优惠券使用状态
|
* 更新优惠券使用状态
|
||||||
*/
|
*/
|
||||||
@@ -206,4 +213,4 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper<PriceCouponClai
|
|||||||
"COUNT(DISTINCT user_id) as total_users " +
|
"COUNT(DISTINCT user_id) as total_users " +
|
||||||
"FROM price_coupon_claim_record WHERE scenic_id = #{scenicId}")
|
"FROM price_coupon_claim_record WHERE scenic_id = #{scenicId}")
|
||||||
java.util.Map<String, Object> selectScenicCouponUsageStats(@Param("scenicId") String scenicId);
|
java.util.Map<String, Object> selectScenicCouponUsageStats(@Param("scenicId") String scenicId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
|
|||||||
@Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " +
|
@Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " +
|
||||||
"update_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity")
|
"update_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity")
|
||||||
int incrementUsedQuantity(Long couponId);
|
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<PriceCouponConfig> {
|
|||||||
"SUM(used_quantity) as used_quantity " +
|
"SUM(used_quantity) as used_quantity " +
|
||||||
"FROM price_coupon_config WHERE scenic_id = #{scenicId}")
|
"FROM price_coupon_config WHERE scenic_id = #{scenicId}")
|
||||||
java.util.Map<String, Object> selectScenicCouponConfigStats(@Param("scenicId") String scenicId);
|
java.util.Map<String, Object> selectScenicCouponConfigStats(@Param("scenicId") String scenicId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.NoTransactionException;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
@@ -194,6 +196,7 @@ public class CouponServiceImpl implements ICouponService {
|
|||||||
info.setDiscountType(coupon.getCouponType());
|
info.setDiscountType(coupon.getCouponType());
|
||||||
info.setDiscountValue(coupon.getDiscountValue());
|
info.setDiscountValue(coupon.getDiscountValue());
|
||||||
info.setActualDiscountAmount(actualDiscountAmount);
|
info.setActualDiscountAmount(actualDiscountAmount);
|
||||||
|
info.setUserClaimLimit(coupon.getUserClaimLimit());
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,19 +233,26 @@ public class CouponServiceImpl implements ICouponService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. 检查库存(如果有总量限制)
|
// 5. 检查库存(如果有总量限制)
|
||||||
if (coupon.getTotalQuantity() != null && coupon.getUsedQuantity() != null) {
|
// totalQuantity为NULL或0表示不限制总量
|
||||||
if (coupon.getUsedQuantity() >= coupon.getTotalQuantity()) {
|
if (coupon.getTotalQuantity() != null && coupon.getTotalQuantity() > 0) {
|
||||||
|
int currentClaimed = (coupon.getClaimedQuantity() == null ? 0 : coupon.getClaimedQuantity());
|
||||||
|
if (currentClaimed >= coupon.getTotalQuantity()) {
|
||||||
return CouponClaimResult.failure(CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK, "优惠券已领完");
|
return CouponClaimResult.failure(CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK, "优惠券已领完");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 检查用户是否已经领取过该优惠券
|
// 6. 检查用户领取数量限制
|
||||||
PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord(
|
if (coupon.getUserClaimLimit() != null && coupon.getUserClaimLimit() > 0) {
|
||||||
request.getUserId(), request.getCouponId());
|
int userClaimCount = couponClaimRecordMapper.countUserCouponClaims(
|
||||||
if (existingRecord != null) {
|
request.getUserId(), request.getCouponId());
|
||||||
return CouponClaimResult.failure(CouponClaimResult.ERROR_ALREADY_CLAIMED, "您已经领取过该优惠券");
|
// countUserCouponClaims 使用 FOR UPDATE + 复合索引,确保并发下的计数准确
|
||||||
|
if (userClaimCount >= coupon.getUserClaimLimit()) {
|
||||||
|
return CouponClaimResult.failure(
|
||||||
|
CouponClaimResult.ERROR_CLAIM_LIMIT_REACHED,
|
||||||
|
"您已达到该优惠券的领取上限(" + coupon.getUserClaimLimit() + "张)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 创建领取记录
|
// 7. 创建领取记录
|
||||||
Date claimTime = new Date();
|
Date claimTime = new Date();
|
||||||
PriceCouponClaimRecord claimRecord = new PriceCouponClaimRecord();
|
PriceCouponClaimRecord claimRecord = new PriceCouponClaimRecord();
|
||||||
@@ -254,32 +264,55 @@ public class CouponServiceImpl implements ICouponService {
|
|||||||
claimRecord.setCreateTime(claimTime);
|
claimRecord.setCreateTime(claimTime);
|
||||||
claimRecord.setUpdateTime(claimTime);
|
claimRecord.setUpdateTime(claimTime);
|
||||||
claimRecord.setDeleted(0);
|
claimRecord.setDeleted(0);
|
||||||
|
|
||||||
// 8. 插入领取记录
|
// 8. 插入领取记录
|
||||||
int insertResult = couponClaimRecordMapper.insert(claimRecord);
|
int insertResult = couponClaimRecordMapper.insert(claimRecord);
|
||||||
if (insertResult <= 0) {
|
if (insertResult <= 0) {
|
||||||
log.error("插入优惠券领取记录失败: userId={}, couponId={}",
|
log.error("插入优惠券领取记录失败: userId={}, couponId={}",
|
||||||
request.getUserId(), request.getCouponId());
|
request.getUserId(), request.getCouponId());
|
||||||
return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "领取失败,请稍后重试");
|
return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "领取失败,请稍后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 更新优惠券已使用数量(如果有总量限制)
|
// 9. 更新优惠券已领取数量(区分于已使用数量)
|
||||||
if (coupon.getTotalQuantity() != null) {
|
// 仅在有总量限制时才更新claimedQuantity(totalQuantity为正整数)
|
||||||
int updatedUsedQuantity = (coupon.getUsedQuantity() == null ? 0 : coupon.getUsedQuantity()) + 1;
|
if (coupon.getTotalQuantity() != null && coupon.getTotalQuantity() > 0) {
|
||||||
coupon.setUsedQuantity(updatedUsedQuantity);
|
int affected = couponConfigMapper.incrementClaimedQuantityIfAvailable(coupon.getId());
|
||||||
couponConfigMapper.updateById(coupon);
|
if (affected == 0) {
|
||||||
|
throw new CouponInvalidException(
|
||||||
|
CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK,
|
||||||
|
"优惠券已被领取完,请稍后重试");
|
||||||
|
}
|
||||||
|
int updatedClaimedQuantity = (coupon.getClaimedQuantity() == null ? 0 : coupon.getClaimedQuantity()) + 1;
|
||||||
|
coupon.setClaimedQuantity(updatedClaimedQuantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("优惠券领取成功: userId={}, couponId={}, claimRecordId={}",
|
log.info("优惠券领取成功: userId={}, couponId={}, claimRecordId={}",
|
||||||
request.getUserId(), request.getCouponId(), claimRecord.getId());
|
request.getUserId(), request.getCouponId(), claimRecord.getId());
|
||||||
|
|
||||||
// 10. 返回成功结果
|
// 10. 返回成功结果
|
||||||
return CouponClaimResult.success(claimRecord, coupon);
|
return CouponClaimResult.success(claimRecord, coupon);
|
||||||
|
|
||||||
|
} catch (CouponInvalidException e) {
|
||||||
|
markRollbackOnly();
|
||||||
|
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) {
|
} catch (Exception e) {
|
||||||
log.error("领取优惠券失败: userId={}, couponId={}",
|
markRollbackOnly();
|
||||||
|
log.error("领取优惠券失败: userId={}, couponId={}",
|
||||||
request.getUserId(), request.getCouponId(), e);
|
request.getUserId(), request.getCouponId(), e);
|
||||||
return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "系统错误,领取失败:" + e.getMessage());
|
return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "系统错误,领取失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void markRollbackOnly() {
|
||||||
|
try {
|
||||||
|
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||||
|
} catch (NoTransactionException ex) {
|
||||||
|
log.debug("未检测到Spring事务上下文,跳过回滚标记");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user