| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- 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<CouponMapper, Coupon> implements CouponService {
- private final CouponConverter couponConverter;
- private final CouponTemplateService couponTemplateService;
- /**
- * 获取优惠劵分页列表
- *
- * @param queryParams 查询参数
- * @return {@link IPage<CouponVO>} 优惠劵分页列表
- */
- @Override
- public IPage<CouponVO> getCouponPage(CouponQuery queryParams) {
- Page<CouponVO> 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<Long> 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<Coupon> 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<Coupon> getCouponsByTemplateAndUser(Long templateId, Long userId) {
- return this.list(Wrappers.lambdaQuery(Coupon.class)
- .eq(Coupon::getTemplateId, templateId)
- .eq(Coupon::getUserId, userId));
- }
- @Override
- public IPage<CouponVO> getUserCouponPage(AppCouponQuery queryParams) {
- queryParams.setUserId(SecurityUtils.getUserId());
- Page<CouponVO> 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<String, Object> 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.<CouponTemplate>lambdaUpdate()
- .setSql("total_count_all = total_count_all + 1")
- .eq(CouponTemplate::getId, templateId)
- .eq(CouponTemplate::getVersion, template.getVersion())
- );
- }
- }
|