|
|
@@ -6,11 +6,17 @@ import com.zsElectric.boot.business.model.form.applet.AppletGainCouponForm;
|
|
|
import com.zsElectric.boot.business.model.query.applet.AppCouponQuery;
|
|
|
import com.zsElectric.boot.business.model.vo.applet.AppCouponStatusNumVO;
|
|
|
import com.zsElectric.boot.business.service.CouponTemplateService;
|
|
|
+import com.zsElectric.boot.core.exception.BusinessException;
|
|
|
import com.zsElectric.boot.core.exception.CouponException;
|
|
|
import com.zsElectric.boot.business.mapper.CouponTemplateMapper;
|
|
|
import com.zsElectric.boot.business.model.entity.CouponTemplate;
|
|
|
import com.zsElectric.boot.security.util.SecurityUtils;
|
|
|
+import jakarta.annotation.Resource;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.redisson.api.RLock;
|
|
|
+import org.redisson.api.RedissonClient;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
@@ -22,11 +28,13 @@ import com.zsElectric.boot.business.model.form.CouponForm;
|
|
|
import com.zsElectric.boot.business.model.query.CouponQuery;
|
|
|
import com.zsElectric.boot.business.model.vo.CouponVO;
|
|
|
import com.zsElectric.boot.business.converter.CouponConverter;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.List;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
import cn.hutool.core.lang.Assert;
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
@@ -39,6 +47,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
* @since 2025-12-19 09:58
|
|
|
*/
|
|
|
@Service
|
|
|
+@Slf4j
|
|
|
@RequiredArgsConstructor
|
|
|
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements CouponService {
|
|
|
|
|
|
@@ -47,11 +56,11 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
private final CouponTemplateService couponTemplateService;
|
|
|
|
|
|
/**
|
|
|
- * 获取优惠劵分页列表
|
|
|
- *
|
|
|
- * @param queryParams 查询参数
|
|
|
- * @return {@link IPage<CouponVO>} 优惠劵分页列表
|
|
|
- */
|
|
|
+ * 获取优惠劵分页列表
|
|
|
+ *
|
|
|
+ * @param queryParams 查询参数
|
|
|
+ * @return {@link IPage<CouponVO>} 优惠劵分页列表
|
|
|
+ */
|
|
|
@Override
|
|
|
public IPage<CouponVO> getCouponPage(CouponQuery queryParams) {
|
|
|
Page<CouponVO> pageVO = this.baseMapper.getCouponPage(
|
|
|
@@ -60,7 +69,7 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
);
|
|
|
return pageVO;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 获取优惠劵表单数据
|
|
|
*
|
|
|
@@ -72,7 +81,7 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
Coupon entity = this.getById(id);
|
|
|
return couponConverter.toForm(entity);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 新增优惠劵
|
|
|
*
|
|
|
@@ -84,20 +93,20 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
Coupon entity = couponConverter.toEntity(formData);
|
|
|
return this.save(entity);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 更新优惠劵
|
|
|
*
|
|
|
- * @param id 优惠劵ID
|
|
|
+ * @param id 优惠劵ID
|
|
|
* @param formData 优惠劵表单对象
|
|
|
* @return 是否修改成功
|
|
|
*/
|
|
|
@Override
|
|
|
- public boolean updateCoupon(Long id,CouponForm formData) {
|
|
|
+ public boolean updateCoupon(Long id, CouponForm formData) {
|
|
|
Coupon entity = couponConverter.toEntity(formData);
|
|
|
return this.updateById(entity);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 删除优惠劵
|
|
|
*
|
|
|
@@ -118,8 +127,8 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
* 领取优惠券
|
|
|
*
|
|
|
* @param templateId 优惠券模板ID
|
|
|
- * @param userId 用户ID
|
|
|
- * @param takeType 领取类型(1-用户领取,2-后台发放)
|
|
|
+ * @param userId 用户ID
|
|
|
+ * @param takeType 领取类型(1-用户领取,2-后台发放)
|
|
|
* @return 优惠券ID
|
|
|
*/
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
@@ -170,7 +179,7 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
/**
|
|
|
* 计算优惠金额
|
|
|
*
|
|
|
- * @param coupon 优惠券
|
|
|
+ * @param coupon 优惠券
|
|
|
* @param orderAmount 订单金额
|
|
|
* @return 优惠金额
|
|
|
*/
|
|
|
@@ -202,7 +211,7 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
/**
|
|
|
* 计算折扣券优惠金额
|
|
|
*
|
|
|
- * @param template 优惠券模板
|
|
|
+ * @param template 优惠券模板
|
|
|
* @param orderAmount 订单金额
|
|
|
* @return 优惠金额
|
|
|
*/
|
|
|
@@ -223,7 +232,7 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
/**
|
|
|
* 计算满减券优惠金额
|
|
|
*
|
|
|
- * @param template 优惠券模板
|
|
|
+ * @param template 优惠券模板
|
|
|
* @param orderAmount 订单金额
|
|
|
* @return 优惠金额
|
|
|
*/
|
|
|
@@ -277,7 +286,7 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
/**
|
|
|
* 检查优惠券是否可以使用
|
|
|
*
|
|
|
- * @param couponId 优惠券ID
|
|
|
+ * @param couponId 优惠券ID
|
|
|
* @param orderAmount 订单金额
|
|
|
* @return 是否可以使用
|
|
|
*/
|
|
|
@@ -328,8 +337,8 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
/**
|
|
|
* 使用优惠券
|
|
|
*
|
|
|
- * @param couponId 优惠券ID
|
|
|
- * @param orderId 订单ID
|
|
|
+ * @param couponId 优惠券ID
|
|
|
+ * @param orderId 订单ID
|
|
|
* @param orderAmount 订单金额
|
|
|
* @return 实际优惠金额
|
|
|
*/
|
|
|
@@ -410,7 +419,7 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
* 根据模板ID和用户ID获取用户持有的优惠券
|
|
|
*
|
|
|
* @param templateId 模板ID
|
|
|
- * @param userId 用户ID
|
|
|
+ * @param userId 用户ID
|
|
|
* @return 优惠券列表
|
|
|
*/
|
|
|
@Override
|
|
|
@@ -435,40 +444,156 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> impleme
|
|
|
return this.baseMapper.getCouponStatusNum(userId);
|
|
|
}
|
|
|
|
|
|
+ @Resource
|
|
|
+ private RedissonClient redissonClient;
|
|
|
+
|
|
|
+ // 获取分布式锁
|
|
|
+ private RLock getLock(String couponCode) {
|
|
|
+ return redissonClient.getLock("lock:coupon:" + couponCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RedisTemplate<String, Object> redisTemplate;
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
@Override
|
|
|
public Boolean gainCoupon(AppletGainCouponForm formData) {
|
|
|
- formData.setUserId(SecurityUtils.getUserId());
|
|
|
- //查询优惠券模板是否存在且可用
|
|
|
- CouponTemplate template = couponTemplateService.getOne(Wrappers.lambdaQuery(CouponTemplate.class)
|
|
|
- .eq(CouponTemplate::getCode, formData.getCouponCode()));
|
|
|
- if (ObjectUtil.isEmpty(template)) {
|
|
|
- throw new CouponException("优惠券不存在");
|
|
|
+ RLock lock = getLock(formData.getCouponCode());
|
|
|
+ try {
|
|
|
+ // 尝试获取锁(等待3秒,自动续期10秒)
|
|
|
+ boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
|
|
|
+ if (!locked) {
|
|
|
+ throw new BusinessException("系统繁忙,请稍后重试!");
|
|
|
+ }
|
|
|
+
|
|
|
+ formData.setUserId(SecurityUtils.getUserId());
|
|
|
+
|
|
|
+ // 查询优惠券模板是否存在且可用
|
|
|
+ CouponTemplate template = couponTemplateService.getOne(Wrappers.lambdaQuery(CouponTemplate.class)
|
|
|
+ .eq(CouponTemplate::getCode, formData.getCouponCode()));
|
|
|
+ if (ObjectUtil.isEmpty(template)) {
|
|
|
+ throw new CouponException("优惠券不存在");
|
|
|
+ }
|
|
|
+ if (!couponTemplateService.isValidTemplate(template.getId())) {
|
|
|
+ throw new CouponException("优惠券已失效");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断当前用户是否已经领取过该优惠券
|
|
|
+ Coupon existCoupon = this.getOne(Wrappers.lambdaQuery(Coupon.class)
|
|
|
+ .eq(Coupon::getUserId, formData.getUserId())
|
|
|
+ .eq(Coupon::getCouponCode, formData.getCouponCode())
|
|
|
+ );
|
|
|
+ if (ObjectUtil.isNotEmpty(existCoupon)) {
|
|
|
+ throw new CouponException("当前用户已经领取过该优惠券");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Redis和MySQL库存扣减
|
|
|
+ String stockKey = "coupon:stock:" + template.getId();
|
|
|
+
|
|
|
+ // 初始化库存到Redis(如果不存在)
|
|
|
+ initStockInRedis(stockKey, template);
|
|
|
+
|
|
|
+ // 使用Redis的decr命令进行原子性扣减库存操作
|
|
|
+ Long stock = redisTemplate.opsForValue().decrement(stockKey);
|
|
|
+ if (stock == null || stock < 0) {
|
|
|
+ // 库存不足,回滚操作
|
|
|
+ if (stock != null && stock < 0) {
|
|
|
+ redisTemplate.opsForValue().increment(stockKey);
|
|
|
+ }
|
|
|
+ throw new CouponException("优惠券库存不足");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 同步更新MySQL中的库存(使用乐观锁)
|
|
|
+ boolean mysqlUpdated = updateMysqlStock(template.getId());
|
|
|
+ if (!mysqlUpdated) {
|
|
|
+ // MySQL库存更新失败,回滚Redis库存
|
|
|
+ redisTemplate.opsForValue().increment(stockKey);
|
|
|
+ throw new CouponException("优惠券库存不足");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建用户优惠券
|
|
|
+ Coupon userCoupon = new Coupon();
|
|
|
+ userCoupon.setTemplateId(template.getId());
|
|
|
+ userCoupon.setName(template.getName());
|
|
|
+ userCoupon.setCouponCode(template.getCode());
|
|
|
+ userCoupon.setStatus(1);
|
|
|
+ userCoupon.setDescription(template.getDescription());
|
|
|
+ userCoupon.setUserId(formData.getUserId());
|
|
|
+ userCoupon.setTakeType(1);
|
|
|
+
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ userCoupon.setTakeTime(now);
|
|
|
+ userCoupon.setExpireTime(now.plusDays(template.getFailureTime()));
|
|
|
+
|
|
|
+ return this.save(userCoupon);
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ throw new BusinessException("系统繁忙,请稍后重试!");
|
|
|
+ } catch (CouponException e) {
|
|
|
+ // 优惠券相关异常直接抛出
|
|
|
+ throw e;
|
|
|
+ } catch (BusinessException e) {
|
|
|
+ // 业务异常直接抛出
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 其他异常记录日志后抛出
|
|
|
+ log.error("优惠券领取异常,用户ID:{}, 优惠券码:{}", formData.getUserId(), formData.getCouponCode(), e);
|
|
|
+ throw new BusinessException("优惠券领取失败,请稍后重试!");
|
|
|
+ } finally {
|
|
|
+ if (lock.isHeldByCurrentThread()) {
|
|
|
+ lock.unlock();
|
|
|
+ }
|
|
|
}
|
|
|
- if (!couponTemplateService.isValidTemplate(template.getId())) {
|
|
|
- throw new CouponException("优惠券已失效");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化优惠券库存到Redis
|
|
|
+ * @param stockKey 库存键
|
|
|
+ * @param template 优惠券模板
|
|
|
+ */
|
|
|
+ private void initStockInRedis(String stockKey, CouponTemplate template) {
|
|
|
+ // 使用Redis的SETNX命令,只有当key不存在时才设置,保证原子性
|
|
|
+ Boolean success = redisTemplate.opsForValue().setIfAbsent(stockKey, 0);
|
|
|
+ if (Boolean.TRUE.equals(success)) {
|
|
|
+ // 首次初始化,计算剩余库存
|
|
|
+ int totalCount = template.getTotalCount();
|
|
|
+ int usedCount = template.getTotalCountAll() != null ? template.getTotalCountAll() : 0;
|
|
|
+
|
|
|
+ // 如果是无限库存(-1)则设置一个大数值
|
|
|
+ int stock = totalCount == -1 ? 999999 : Math.max(0, totalCount - usedCount);
|
|
|
+ redisTemplate.opsForValue().set(stockKey, stock);
|
|
|
+
|
|
|
+ // 设置过期时间,防止Redis数据永久存在(可选,根据业务需求调整)
|
|
|
+ redisTemplate.expire(stockKey, 7, TimeUnit.DAYS);
|
|
|
}
|
|
|
- //判断当前用户是否已经领取过该优惠券
|
|
|
- Coupon coupon = this.getOne(Wrappers.lambdaQuery(Coupon.class)
|
|
|
- .eq(Coupon::getUserId, formData.getUserId())
|
|
|
- .eq(Coupon::getCouponCode, formData.getCouponCode())
|
|
|
- );
|
|
|
- if (ObjectUtil.isNotEmpty(coupon)) {
|
|
|
- throw new CouponException("当前用户已经领取过该优惠券");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新MySQL中的库存数量
|
|
|
+ * @param templateId 模板ID
|
|
|
+ * @return 是否更新成功
|
|
|
+ */
|
|
|
+ private boolean updateMysqlStock(Long templateId) {
|
|
|
+ // 使用乐观锁更新已发放数量
|
|
|
+ CouponTemplate template = couponTemplateService.getById(templateId);
|
|
|
+ if (template == null) {
|
|
|
+ return false;
|
|
|
}
|
|
|
- Coupon userCoupon = new Coupon();
|
|
|
- userCoupon.setTemplateId(template.getId());
|
|
|
- userCoupon.setName(template.getName());
|
|
|
- userCoupon.setCouponCode(template.getCode());
|
|
|
- userCoupon.setStatus(1);
|
|
|
- userCoupon.setDescription(template.getDescription());
|
|
|
- userCoupon.setUserId(formData.getUserId());
|
|
|
- userCoupon.setTakeType(1);
|
|
|
-
|
|
|
- LocalDateTime now = LocalDateTime.now();
|
|
|
-
|
|
|
- userCoupon.setTakeTime(now);
|
|
|
- userCoupon.setExpireTime(now.plusDays(template.getFailureTime()));
|
|
|
- return this.save(userCoupon);
|
|
|
+
|
|
|
+ // 检查是否有限制发放数量且已达到上限
|
|
|
+ if (template.getTotalCount() != -1) { // -1表示不限制
|
|
|
+ int currentCount = template.getTotalCountAll() != null ? template.getTotalCountAll() : 0;
|
|
|
+ if (currentCount >= template.getTotalCount()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用乐观锁更新已发放数量(version字段由MyBatis-Plus自动处理)
|
|
|
+ return couponTemplateService.update(
|
|
|
+ Wrappers.<CouponTemplate>lambdaUpdate()
|
|
|
+ .setSql("total_count_all = total_count_all + 1")
|
|
|
+ .eq(CouponTemplate::getId, templateId)
|
|
|
+ .eq(CouponTemplate::getVersion, template.getVersion())
|
|
|
+ );
|
|
|
}
|
|
|
-
|
|
|
}
|