|
@@ -24,9 +24,11 @@ import com.kym.service.admin.CouponService;
|
|
|
import com.kym.service.miniapp.UserCouponService;
|
|
import com.kym.service.miniapp.UserCouponService;
|
|
|
import com.kym.service.miniapp.UserService;
|
|
import com.kym.service.miniapp.UserService;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
import org.springframework.beans.BeanUtils;
|
|
|
|
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
+import java.time.Duration;
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.LocalTime;
|
|
import java.time.LocalTime;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
@@ -68,7 +70,7 @@ public PageBean<UserCouponVo> listUserCoupons(CouponQueryParam params) {
|
|
|
|
|
|
|
|
// 构造基础查询条件
|
|
// 构造基础查询条件
|
|
|
var queryWrapper = lambdaQuery()
|
|
var queryWrapper = lambdaQuery()
|
|
|
- .eq(CommUtil.isNotEmptyAndNull(params.getActivityId()), UserCoupon::getId, params.getCouponId())
|
|
|
|
|
|
|
+ .eq(CommUtil.isNotEmptyAndNull(params.getActivityId()), UserCoupon::getActivityId, params.getActivityId())
|
|
|
.like(CommUtil.isNotEmptyAndNull(params.getActivityName()), UserCoupon::getActivityName, params.getActivityName())
|
|
.like(CommUtil.isNotEmptyAndNull(params.getActivityName()), UserCoupon::getActivityName, params.getActivityName())
|
|
|
.eq(CommUtil.isNotEmptyAndNull(params.getCouponId()), UserCoupon::getId, params.getCouponId())
|
|
.eq(CommUtil.isNotEmptyAndNull(params.getCouponId()), UserCoupon::getId, params.getCouponId())
|
|
|
.like(CommUtil.isNotEmptyAndNull(params.getCouponName()), UserCoupon::getCouponName, params.getCouponName())
|
|
.like(CommUtil.isNotEmptyAndNull(params.getCouponName()), UserCoupon::getCouponName, params.getCouponName())
|
|
@@ -128,69 +130,91 @@ public PageBean<UserCouponVo> listUserCoupons(CouponQueryParam params) {
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public void collectCoupon(Long couponId) {
|
|
public void collectCoupon(Long couponId) {
|
|
|
- // 手动切换数据源
|
|
|
|
|
- DynamicDataSourceContextHolder.push("db-admin");
|
|
|
|
|
- // 优惠券状态校验
|
|
|
|
|
- var coupon = couponService.lambdaQuery()
|
|
|
|
|
- .eq(Coupon::getId, couponId)
|
|
|
|
|
- .in(Coupon::getStatus, Coupon.STATUS_未开始, Coupon.STATUS_进行中)
|
|
|
|
|
- .one();
|
|
|
|
|
-
|
|
|
|
|
- if (coupon == null) {
|
|
|
|
|
- DynamicDataSourceContextHolder.poll();
|
|
|
|
|
- throw new BusinessException("优惠券不存在或已失效!");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (coupon.getClaimedQuantity() >= coupon.getQuantity() && coupon.getQuantity() > 0) {
|
|
|
|
|
- DynamicDataSourceContextHolder.poll();
|
|
|
|
|
- throw new BusinessException("优惠券已领完!");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 重复领取校验
|
|
|
|
|
|
|
+ // 同一用户并发领同一张券互斥,防止重复领取与超发
|
|
|
var userId = StpUtil.getLoginIdAsLong();
|
|
var userId = StpUtil.getLoginIdAsLong();
|
|
|
- var exist = redisTemplate.opsForSet().isMember(RedisKeys.COUPON_ID_TO_USERS, couponId + ":" + userId);
|
|
|
|
|
- if (Boolean.TRUE.equals(exist)) {
|
|
|
|
|
- DynamicDataSourceContextHolder.poll();
|
|
|
|
|
- throw new BusinessException("优惠券已领取,请勿重复领取!");
|
|
|
|
|
|
|
+ String lockKey = RedisKeys.COUPON_LOCK + couponId + ":" + userId;
|
|
|
|
|
+ Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(30));
|
|
|
|
|
+ if (Boolean.FALSE.equals(locked)) {
|
|
|
|
|
+ throw new BusinessException("操作过于频繁,请稍后再试!");
|
|
|
}
|
|
}
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 重复领取校验(Redis快路径)
|
|
|
|
|
+ var exist = redisTemplate.opsForSet().isMember(RedisKeys.COUPON_ID_TO_USERS, couponId + ":" + userId);
|
|
|
|
|
+ if (Boolean.TRUE.equals(exist)) {
|
|
|
|
|
+ throw new BusinessException("优惠券已领取,请勿重复领取!");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 活动
|
|
|
|
|
- var activity = activityService.getById(coupon.getActivityId());
|
|
|
|
|
-
|
|
|
|
|
- // 更新优惠券已领取数量
|
|
|
|
|
- coupon.setClaimedQuantity(coupon.getClaimedQuantity() + 1);
|
|
|
|
|
|
|
+ // 优惠券状态校验与库存扣减(db-admin)
|
|
|
|
|
+ Coupon coupon;
|
|
|
|
|
+ try {
|
|
|
|
|
+ DynamicDataSourceContextHolder.push("db-admin");
|
|
|
|
|
+ coupon = couponService.lambdaQuery()
|
|
|
|
|
+ .eq(Coupon::getId, couponId)
|
|
|
|
|
+ .in(Coupon::getStatus, Coupon.STATUS_未开始, Coupon.STATUS_进行中)
|
|
|
|
|
+ .one();
|
|
|
|
|
+ if (coupon == null) {
|
|
|
|
|
+ throw new BusinessException("优惠券不存在或已失效!");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 条件自增已领取数量(原子操作防超发),失败说明已领完
|
|
|
|
|
+ var claimed = couponService.lambdaUpdate()
|
|
|
|
|
+ .setSql("claimed_quantity = claimed_quantity + 1")
|
|
|
|
|
+ .eq(Coupon::getId, couponId)
|
|
|
|
|
+ .apply(coupon.getQuantity() > 0, "claimed_quantity < quantity")
|
|
|
|
|
+ .update();
|
|
|
|
|
+ if (!claimed) {
|
|
|
|
|
+ throw new BusinessException("优惠券已领完!");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 已领完则结束活动
|
|
|
|
|
+ if (coupon.getQuantity() > 0) {
|
|
|
|
|
+ couponService.lambdaUpdate()
|
|
|
|
|
+ .set(Coupon::getStatus, Coupon.STATUS_已结束)
|
|
|
|
|
+ .eq(Coupon::getId, couponId)
|
|
|
|
|
+ .apply("claimed_quantity >= quantity")
|
|
|
|
|
+ .update();
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ DynamicDataSourceContextHolder.poll();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 校验优惠券数量是否已领完
|
|
|
|
|
- if (coupon.getClaimedQuantity() >= coupon.getQuantity() && coupon.getQuantity() > 0) {
|
|
|
|
|
- coupon.setStatus(Coupon.STATUS_已结束);
|
|
|
|
|
|
|
+ // 活动
|
|
|
|
|
+ var activity = activityService.getById(coupon.getActivityId());
|
|
|
|
|
+
|
|
|
|
|
+ // 先落库(靠(coupon_id,user_id)唯一索引兜底防重),成功后再写Redis,保证两者最终一致
|
|
|
|
|
+ var userCoupon = new UserCoupon();
|
|
|
|
|
+ userCoupon.setActivityId(coupon.getActivityId());
|
|
|
|
|
+ userCoupon.setActivityName(activity.getName());
|
|
|
|
|
+ userCoupon.setCouponId(coupon.getId());
|
|
|
|
|
+ userCoupon.setCouponName(coupon.getName());
|
|
|
|
|
+ userCoupon.setUserId(userId);
|
|
|
|
|
+ userCoupon.setStartTime(LocalDateTime.now());
|
|
|
|
|
+ userCoupon.setEndTime(LocalDateTime.now().with(LocalTime.MAX).plusDays(coupon.getValidity() - 1));
|
|
|
|
|
+ userCoupon.setValidity(coupon.getValidity());
|
|
|
|
|
+ userCoupon.setCouponType(coupon.getCouponType());
|
|
|
|
|
+ userCoupon.setDiscount(coupon.getDiscount());
|
|
|
|
|
+ userCoupon.setMinServiceMoney(coupon.getMinServiceMoney());
|
|
|
|
|
+ userCoupon.setAllowStacke(coupon.getAllowStacke());
|
|
|
|
|
+ userCoupon.setStatus(UserCoupon.STATUS_有效);
|
|
|
|
|
+ userCoupon.setRemark(coupon.getRemark());
|
|
|
|
|
+ try {
|
|
|
|
|
+ save(userCoupon);
|
|
|
|
|
+ } catch (DuplicateKeyException e) {
|
|
|
|
|
+ // 落库失败(重复领取),回退库存自增
|
|
|
|
|
+ try {
|
|
|
|
|
+ DynamicDataSourceContextHolder.push("db-admin");
|
|
|
|
|
+ couponService.lambdaUpdate()
|
|
|
|
|
+ .setSql("claimed_quantity = claimed_quantity - 1")
|
|
|
|
|
+ .eq(Coupon::getId, couponId)
|
|
|
|
|
+ .apply("claimed_quantity > 0")
|
|
|
|
|
+ .update();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ DynamicDataSourceContextHolder.poll();
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new BusinessException("优惠券已领取,请勿重复领取!");
|
|
|
|
|
+ }
|
|
|
|
|
+ redisTemplate.opsForSet().add(RedisKeys.COUPON_ID_TO_USERS, couponId + ":" + userId);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ redisTemplate.delete(lockKey);
|
|
|
}
|
|
}
|
|
|
- couponService.updateById(coupon);
|
|
|
|
|
-
|
|
|
|
|
- DynamicDataSourceContextHolder.poll();
|
|
|
|
|
-
|
|
|
|
|
- // 存入缓存和数据库
|
|
|
|
|
- redisTemplate.opsForSet().add(RedisKeys.COUPON_ID_TO_USERS, couponId + ":" + userId);
|
|
|
|
|
-
|
|
|
|
|
- var userCoupon = new UserCoupon();
|
|
|
|
|
- userCoupon.setActivityId(coupon.getActivityId());
|
|
|
|
|
- userCoupon.setActivityName(activity.getName());
|
|
|
|
|
- userCoupon.setCouponId(coupon.getId());
|
|
|
|
|
- userCoupon.setCouponName(coupon.getName());
|
|
|
|
|
- userCoupon.setUserId(userId);
|
|
|
|
|
- userCoupon.setStartTime(LocalDateTime.now());
|
|
|
|
|
- userCoupon.setEndTime(LocalDateTime.now().with(LocalTime.MAX).plusDays(coupon.getValidity() - 1));
|
|
|
|
|
- userCoupon.setValidity(coupon.getValidity());
|
|
|
|
|
- userCoupon.setCouponType(coupon.getCouponType());
|
|
|
|
|
- userCoupon.setDiscount(coupon.getDiscount());
|
|
|
|
|
- userCoupon.setMinServiceMoney(coupon.getMinServiceMoney());
|
|
|
|
|
- userCoupon.setAllowStacke(coupon.getAllowStacke());
|
|
|
|
|
- userCoupon.setStatus(UserCoupon.STATUS_有效);
|
|
|
|
|
- userCoupon.setRemark(coupon.getRemark());
|
|
|
|
|
-// userCouponSender.sendMessage(userCoupon);
|
|
|
|
|
- save(userCoupon);
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- // todo 优惠券活动终止时处理redis数据,优惠券失效或者使用之后该做什么操作?
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|