Quellcode durchsuchen

好评赠券和收藏赠券冲突 处理

lutong vor 1 Monat
Ursprung
Commit
12a285920a

+ 84 - 23
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponServiceImpl.java

@@ -4,6 +4,7 @@ import com.alibaba.nacos.common.utils.CollectionUtils;
 import com.aliyun.tea.utils.StringUtils;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -18,10 +19,12 @@ import shop.alien.entity.store.vo.LifeDiscountCouponStoreFriendVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponUserWebVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponVo;
 import shop.alien.mapper.*;
+import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.LifeDiscountCouponQuantumRulesService;
 import shop.alien.store.service.LifeDiscountCouponService;
 import shop.alien.store.service.LifeDiscountCouponUserService;
 import shop.alien.util.common.constant.DiscountCouponEnum;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
@@ -72,6 +75,8 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
 
     private final LifeCollectMapper lifeCollectMapper;
 
+    private final BaseRedisService baseRedisService;
+
     @Override
     public boolean addDiscountCoupon(LifeDiscountCouponDto lifeDiscountCouponDto) {
         try {
@@ -1855,21 +1860,23 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
      * @return 发放的优惠券数量
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int issueCouponsForStoreCollect(Integer userId, String storeId, String collectId) {
         if (userId == null || StringUtils.isEmpty(storeId)) {
             return 0;
         }
 
-        // 如果提供了收藏记录ID,检查是否已发放过
-        if (!StringUtils.isEmpty(collectId)) {
-            LifeCollect lifeCollect = lifeCollectMapper.selectById(collectId);
-            if (lifeCollect != null && lifeCollect.getCouponIssuedFlag() != null && lifeCollect.getCouponIssuedFlag() == 1) {
-                // 该收藏记录已发放过优惠券,不再重复发放
+        // 分布式锁:防止并发重复发放
+        String lockKey = "coupon:collect:" + userId + ":" + storeId;
+        String identifier = null;
+        try {
+            // 获取分布式锁,锁超时时间30秒,获取锁超时时间5秒
+            identifier = baseRedisService.lock(lockKey, 30000, 5000);
+            if (StringUtils.isEmpty(identifier)) {
+                // 获取锁失败,可能正在处理中,直接返回0避免重复发放
                 return 0;
             }
-        }
 
-        try {
             LocalDate now = LocalDate.now();
             
             // 查询该店铺所有开启领取的优惠券(有效期内、有库存、未删除、收藏可领)
@@ -1903,27 +1910,50 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                 return 0;
             }
 
-            // 检查用户已领取的优惠券ID列表(避免重复发放)
+            // 查询用户已通过收藏店铺领取的优惠券(只检查 issueSource=2,不检查其他来源)
+            // 业务规则:好评赠券和收藏领券不冲突,可以分别领取
+            // 因此只检查是否通过收藏领取过,不检查好评等其他方式
+            // 获取所有可发放的优惠券ID列表
+            List<Integer> availableCouponIds = couponByType.values().stream()
+                    .map(LifeDiscountCoupon::getId)
+                    .collect(Collectors.toList());
+            
             LambdaQueryWrapper<LifeDiscountCouponUser> userCouponWrapper = new LambdaQueryWrapper<>();
             userCouponWrapper.eq(LifeDiscountCouponUser::getUserId, userId)
                     .eq(LifeDiscountCouponUser::getDeleteFlag, 0)
-                    .in(LifeDiscountCouponUser::getCouponId, 
-                        couponByType.values().stream().map(LifeDiscountCoupon::getId).collect(Collectors.toList()));
+                    .eq(LifeDiscountCouponUser::getIssueSource, 2) // 只检查收藏店铺领取的(issueSource=2)
+                    .in(LifeDiscountCouponUser::getCouponId, availableCouponIds);
             List<LifeDiscountCouponUser> existingCoupons = lifeDiscountCouponUserMapper.selectList(userCouponWrapper);
             Set<Integer> existingCouponIds = existingCoupons.stream()
                     .map(LifeDiscountCouponUser::getCouponId)
                     .filter(Objects::nonNull)
                     .collect(Collectors.toSet());
+            
+            // 通过已领取的优惠券ID查询对应的优惠券类型,建立类型到已领取优惠券的映射
+            final Set<Integer> existingCouponTypes;
+            if (!existingCouponIds.isEmpty()) {
+                LambdaQueryWrapper<LifeDiscountCoupon> existingCouponQuery = new LambdaQueryWrapper<>();
+                existingCouponQuery.in(LifeDiscountCoupon::getId, existingCouponIds)
+                        .isNotNull(LifeDiscountCoupon::getCouponType);
+                List<LifeDiscountCoupon> existingCouponList = lifeDiscountCouponMapper.selectList(existingCouponQuery);
+                existingCouponTypes = existingCouponList.stream()
+                        .map(LifeDiscountCoupon::getCouponType)
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toSet());
+            } else {
+                existingCouponTypes = new HashSet<>();
+            }
+            
+            // 过滤掉用户已通过收藏领取过的优惠券类型
+            // 业务规则:收藏时每种类型只发放一张,但好评等其他方式可以另外领取
+            final Set<Integer> finalExistingCouponTypes = existingCouponTypes;
+            couponByType.entrySet().removeIf(entry -> finalExistingCouponTypes.contains(entry.getKey()));
 
             // 发放每种类型的优惠券(每种一张)
+            // 注意:此时 couponByType 已经过滤掉了用户已领取过的类型,所以直接发放即可
             int issuedCount = 0;
             for (Map.Entry<Integer, LifeDiscountCoupon> entry : couponByType.entrySet()) {
                 LifeDiscountCoupon coupon = entry.getValue();
-                
-                // 如果用户已领取过该优惠券,跳过
-                if (existingCouponIds.contains(coupon.getId())) {
-                    continue;
-                }
 
                 // 再次检查库存(防止并发问题)
                 if (coupon.getSingleQty() == null || coupon.getSingleQty() <= 0) {
@@ -1931,16 +1961,32 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                 }
 
                 try {
+                    // 重新查询优惠券信息,确保获取最新的库存(防止并发问题)
+                    LifeDiscountCoupon latestCoupon = lifeDiscountCouponMapper.selectById(coupon.getId());
+                    if (latestCoupon == null || latestCoupon.getSingleQty() == null || latestCoupon.getSingleQty() <= 0) {
+                        continue;
+                    }
+                    
+                    // 检查用户是否已经领取过该优惠券(双重检查,防止并发)
+                    LambdaQueryWrapper<LifeDiscountCouponUser> duplicateCheck = new LambdaQueryWrapper<>();
+                    duplicateCheck.eq(LifeDiscountCouponUser::getUserId, userId)
+                            .eq(LifeDiscountCouponUser::getCouponId, latestCoupon.getId())
+                            .eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+                    long duplicateCount = lifeDiscountCouponUserMapper.selectCount(duplicateCheck);
+                    if (duplicateCount > 0) {
+                        continue;
+                    }
+                    
                     // 创建用户优惠券记录
                     LifeDiscountCouponUser lifeDiscountCouponUser = new LifeDiscountCouponUser();
-                    lifeDiscountCouponUser.setCouponId(coupon.getId());
+                    lifeDiscountCouponUser.setCouponId(latestCoupon.getId());
                     lifeDiscountCouponUser.setUserId(userId);
                     lifeDiscountCouponUser.setReceiveTime(new Date());
                     
                     // 设置过期时间:优先使用validDate,如果为null则使用endDate
-                    LocalDate expirationTime = coupon.getValidDate();
-                    if (expirationTime == null && coupon.getEndDate() != null) {
-                        expirationTime = coupon.getEndDate();
+                    LocalDate expirationTime = latestCoupon.getValidDate();
+                    if (expirationTime == null && latestCoupon.getEndDate() != null) {
+                        expirationTime = latestCoupon.getEndDate();
                     }
                     lifeDiscountCouponUser.setExpirationTime(expirationTime);
                     lifeDiscountCouponUser.setStatus(Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
@@ -1950,11 +1996,21 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                     // 插入用户优惠券记录
                     lifeDiscountCouponUserMapper.insert(lifeDiscountCouponUser);
                     
-                    // 扣减库存
-                    coupon.setSingleQty(coupon.getSingleQty() - 1);
-                    lifeDiscountCouponMapper.updateById(coupon);
+                    // 使用乐观锁扣减库存:通过 WHERE single_qty > 0 确保库存充足
+                    LambdaUpdateWrapper<LifeDiscountCoupon> updateWrapper = new LambdaUpdateWrapper<>();
+                    updateWrapper.eq(LifeDiscountCoupon::getId, latestCoupon.getId())
+                            .gt(LifeDiscountCoupon::getSingleQty, 0) // 确保库存大于0
+                            .setSql("single_qty = single_qty - 1");
+                    int updateResult = lifeDiscountCouponMapper.update(null, updateWrapper);
                     
-                    issuedCount++;
+                    if (updateResult > 0) {
+                        // 库存扣减成功
+                        issuedCount++;
+                    } else {
+                        // 库存扣减失败(可能库存已被其他请求扣减),回滚用户优惠券记录
+                        lifeDiscountCouponUser.setDeleteFlag(1);
+                        lifeDiscountCouponUserMapper.updateById(lifeDiscountCouponUser);
+                    }
                 } catch (Exception e) {
                     // 单个优惠券发放失败不影响其他优惠券的发放
                     // 静默处理,不记录日志,避免日志过多
@@ -1977,6 +2033,11 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
         } catch (Exception e) {
             // 异常时静默处理,不影响收藏功能
             return 0;
+        } finally {
+            // 释放分布式锁
+            if (!StringUtils.isEmpty(identifier)) {
+                baseRedisService.unlock(lockKey, identifier);
+            }
         }
     }
 }