package com.zsElectric.boot.business.service.impl; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.zsElectric.boot.business.model.form.applet.AppGainCouponForm; 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.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; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.zsElectric.boot.business.mapper.CouponMapper; import com.zsElectric.boot.business.service.CouponService; import com.zsElectric.boot.business.model.entity.Coupon; 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; import org.springframework.transaction.annotation.Transactional; /** * 优惠劵服务实现类 * * @author zsElectric * @since 2025-12-19 09:58 */ @Service @Slf4j @RequiredArgsConstructor public class CouponServiceImpl extends ServiceImpl implements CouponService { private final CouponConverter couponConverter; private final CouponTemplateService couponTemplateService; /** * 获取优惠劵分页列表 * * @param queryParams 查询参数 * @return {@link IPage} 优惠劵分页列表 */ @Override public IPage getCouponPage(CouponQuery queryParams) { Page pageVO = this.baseMapper.getCouponPage( new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), queryParams ); return pageVO; } /** * 获取优惠劵表单数据 * * @param id 优惠劵ID * @return 优惠劵表单数据 */ @Override public CouponForm getCouponFormData(Long id) { Coupon entity = this.getById(id); return couponConverter.toForm(entity); } /** * 新增优惠劵 * * @param formData 优惠劵表单对象 * @return 是否新增成功 */ @Override public boolean saveCoupon(CouponForm formData) { Coupon entity = couponConverter.toEntity(formData); return this.save(entity); } /** * 更新优惠劵 * * @param id 优惠劵ID * @param formData 优惠劵表单对象 * @return 是否修改成功 */ @Override public boolean updateCoupon(Long id, CouponForm formData) { Coupon entity = couponConverter.toEntity(formData); return this.updateById(entity); } /** * 删除优惠劵 * * @param ids 优惠劵ID,多个以英文逗号(,)分割 * @return 是否删除成功 */ @Override public boolean deleteCoupons(String ids) { Assert.isTrue(StrUtil.isNotBlank(ids), "删除的优惠劵数据为空"); // 逻辑删除 List idList = Arrays.stream(ids.split(",")) .map(Long::parseLong) .toList(); return this.removeByIds(idList); } /** * 领取优惠券 * * @param templateId 优惠券模板ID * @param userId 用户ID * @param takeType 领取类型(1-用户领取,2-后台发放) * @return 优惠券ID */ @Transactional(rollbackFor = Exception.class) public Long takeCoupon(Long templateId, Long userId, Integer takeType) { // 获取优惠券模板 CouponTemplate template = couponTemplateService.getById(templateId); if (template == null) { throw new CouponException("优惠券模板不存在"); } // 检查模板状态(1-上线,2-下线) if (template.getStatus() != 1) { throw new CouponException("优惠券模板已下线"); } // 检查发放数量限制 if (template.getTotalCount() != -1) { // -1表示不限制 if (template.getTotalCountAll() != null && template.getTotalCountAll() >= template.getTotalCount()) { throw new CouponException("优惠券已领完"); } } // 创建优惠券 Coupon coupon = new Coupon(); coupon.setTemplateId(templateId); coupon.setName(template.getName()); coupon.setUserId(userId); coupon.setTakeType(takeType); coupon.setTakeTime(LocalDateTime.now()); coupon.setStatus(1); // 未使用 // 设置过期时间 if (template.getFailureTime() != null && template.getFailureTime() > 0) { LocalDateTime expireTime = LocalDateTime.now().plusDays(template.getFailureTime()); coupon.setExpireTime(expireTime); } // 保存优惠券 this.save(coupon); // 更新模板已发放数量 template.setTotalCountAll(template.getTotalCountAll() != null ? template.getTotalCountAll() + 1 : 1); couponTemplateService.updateById(template); return coupon.getId(); } /** * 计算优惠金额 * * @param coupon 优惠券 * @param orderAmount 订单金额 * @return 优惠金额 */ public BigDecimal calculateDiscount(Coupon coupon, BigDecimal orderAmount) { // 检查优惠券状态 if (!isValidCoupon(coupon.getId())) { return BigDecimal.ZERO; } // 获取优惠券模板 CouponTemplate template = couponTemplateService.getById(coupon.getTemplateId()); if (template == null) { return BigDecimal.ZERO; } // 根据优惠券类型计算优惠金额 switch (template.getType()) { case 1: // 折扣券 return calculateDiscountCoupon(template, orderAmount); case 2: // 满减券 return calculateReductionCoupon(template, orderAmount); case 3: // 无门槛券 return calculateNoThresholdCoupon(template); default: return BigDecimal.ZERO; } } /** * 计算折扣券优惠金额 * * @param template 优惠券模板 * @param orderAmount 订单金额 * @return 优惠金额 */ private BigDecimal calculateDiscountCoupon(CouponTemplate template, BigDecimal orderAmount) { // 检查是否满足使用条件 if (template.getUsePrice() != null && orderAmount.compareTo(template.getUsePrice()) < 0) { return BigDecimal.ZERO; } // 计算折扣金额 = 订单金额 × 折扣率 if (template.getDiscountPrice() != null) { return orderAmount.multiply(template.getDiscountPrice()).divide(BigDecimal.valueOf(100)); } return BigDecimal.ZERO; } /** * 计算满减券优惠金额 * * @param template 优惠券模板 * @param orderAmount 订单金额 * @return 优惠金额 */ private BigDecimal calculateReductionCoupon(CouponTemplate template, BigDecimal orderAmount) { // 检查是否满足使用条件 if (template.getUsePrice() != null && orderAmount.compareTo(template.getUsePrice()) < 0) { return BigDecimal.ZERO; } // 返回减免金额 return template.getDiscountPrice() != null ? template.getDiscountPrice() : BigDecimal.ZERO; } /** * 计算无门槛券优惠金额 * * @param template 优惠券模板 * @return 优惠金额 */ private BigDecimal calculateNoThresholdCoupon(CouponTemplate template) { // 直接返回减免金额 return template.getDiscountPrice() != null ? template.getDiscountPrice() : BigDecimal.ZERO; } /** * 检查优惠券是否有效 * * @param couponId 优惠券ID * @return 是否有效 */ @Override public boolean isValidCoupon(Long couponId) { Coupon coupon = this.getById(couponId); if (coupon == null) { return false; } // 检查优惠券状态(1-未使用,2-已使用,3-已过期) if (coupon.getStatus() != 1) { return false; } // 检查是否过期 if (coupon.getExpireTime().isBefore(LocalDateTime.now())) { return false; } return true; } /** * 检查优惠券是否可以使用 * * @param couponId 优惠券ID * @param orderAmount 订单金额 * @return 是否可以使用 */ public boolean canUseCoupon(Long couponId, BigDecimal orderAmount) { // 获取优惠券 Coupon coupon = this.getById(couponId); if (coupon == null) { return false; } // 检查优惠券状态 if (coupon.getStatus() != 1) { return false; } // 检查是否过期 if (coupon.getExpireTime().isBefore(LocalDateTime.now())) { return false; } // 获取优惠券模板 CouponTemplate template = couponTemplateService.getById(coupon.getTemplateId()); if (template == null) { return false; } // 根据优惠券类型检查使用条件 switch (template.getType()) { case 1: // 折扣券 // 检查是否满足使用条件 if (template.getUsePrice() != null && orderAmount.compareTo(template.getUsePrice()) < 0) { return false; } return true; case 2: // 满减券 // 检查是否满足使用条件 if (template.getUsePrice() != null && orderAmount.compareTo(template.getUsePrice()) < 0) { return false; } return true; case 3: // 无门槛券 return true; default: return false; } } /** * 使用优惠券 * * @param couponId 优惠券ID * @param orderId 订单ID * @param orderAmount 订单金额 * @return 实际优惠金额 */ @Transactional(rollbackFor = Exception.class) public BigDecimal useCoupon(Long couponId, Long orderId, BigDecimal orderAmount) { // 获取优惠券 Coupon coupon = this.getById(couponId); if (coupon == null) { throw new CouponException("优惠券不存在"); } // 检查优惠券状态 if (coupon.getStatus() != 1) { throw new CouponException("优惠券状态不正确"); } // 检查是否过期 if (coupon.getExpireTime().isBefore(LocalDateTime.now())) { throw new CouponException("优惠券已过期"); } // 计算优惠金额 BigDecimal discountAmount = calculateDiscount(coupon, orderAmount); // 更新优惠券状态为已使用 coupon.setStatus(2); // 已使用 coupon.setUseOrderId(orderId); coupon.setUseTime(LocalDateTime.now()); this.updateById(coupon); return discountAmount; } /** * 过期优惠券处理 * * @param couponId 优惠券ID * @return 是否处理成功 */ @Override public boolean expireCoupon(Long couponId) { Coupon coupon = this.getById(couponId); if (coupon == null) { return false; } // 检查优惠券状态 if (coupon.getStatus() != 1) { return false; } // 检查是否过期 if (coupon.getExpireTime().isAfter(LocalDateTime.now())) { return false; } // 更新优惠券状态为已过期 coupon.setStatus(3); // 已过期 return this.updateById(coupon); } /** * 获取用户指定状态的优惠券列表 * * @param userId 用户ID * @param status 优惠券状态 * @return 优惠券列表 */ @Override public List getUserCouponsByStatus(Long userId, Integer status) { return this.list(Wrappers.lambdaQuery(Coupon.class) .eq(Coupon::getUserId, userId) .eq(Coupon::getStatus, status)); } /** * 根据模板ID和用户ID获取用户持有的优惠券 * * @param templateId 模板ID * @param userId 用户ID * @return 优惠券列表 */ @Override public List getCouponsByTemplateAndUser(Long templateId, Long userId) { return this.list(Wrappers.lambdaQuery(Coupon.class) .eq(Coupon::getTemplateId, templateId) .eq(Coupon::getUserId, userId)); } @Override public IPage getUserCouponPage(AppCouponQuery queryParams) { queryParams.setUserId(SecurityUtils.getUserId()); Page pageVO = this.baseMapper.getUserCouponPage( new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), queryParams ); return pageVO; } @Override public AppCouponStatusNumVO getCouponStatusNum(Long userId) { return this.baseMapper.getCouponStatusNum(userId); } @Resource private RedissonClient redissonClient; // 获取分布式锁 private RLock getLock(String couponCode) { return redissonClient.getLock("lock:coupon:" + couponCode); } @Autowired private RedisTemplate redisTemplate; @Transactional(rollbackFor = Exception.class) @Override public Boolean gainCoupon(AppGainCouponForm formData) { 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(); } } } /** * 初始化优惠券库存到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); } } /** * 更新MySQL中的库存数量 * @param templateId 模板ID * @return 是否更新成功 */ private boolean updateMysqlStock(Long templateId) { // 使用乐观锁更新已发放数量 CouponTemplate template = couponTemplateService.getById(templateId); if (template == null) { return false; } // 检查是否有限制发放数量且已达到上限 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.lambdaUpdate() .setSql("total_count_all = total_count_all + 1") .eq(CouponTemplate::getId, templateId) .eq(CouponTemplate::getVersion, template.getVersion()) ); } }