|
|
@@ -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);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|