package com.ruoyi.cai.lottery; import com.ruoyi.cai.domain.Account; import com.ruoyi.cai.domain.PointChangeLog; import com.ruoyi.cai.domain.PrizeOnline; import com.ruoyi.cai.domain.User; import com.ruoyi.cai.enums.GenderEnum; import com.ruoyi.cai.enums.SystemConfigEnum; import com.ruoyi.cai.manager.IdManager; import com.ruoyi.cai.manager.SystemConfigManager; import com.ruoyi.cai.service.AccountService; import com.ruoyi.cai.service.PrizeOnlineService; import com.ruoyi.cai.service.UserService; import com.ruoyi.common.exception.ServiceException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBucket; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * 抽奖核心服务(优化版) */ @Service @RequiredArgsConstructor @Slf4j public class LotteryService { // 固定配置 private static final Long THANKS_PRIZE_ID = 0L; private static final String USER_DRAW_COUNT_KEY = "user:draw:count:%s"; private static final long USER_DRAW_COUNT_EXPIRE = 7 * 24 * 60 * 60; // 用户累计抽数缓存过期时间:7天 private static final double RANDOM_MAX = 10000; // 概率放大倍数,提升随机数精度 private static final long LOCK_TIMEOUT = 500; // 锁超时时间500ms(非阻塞获取) // 每个用户的独立锁:key=userId,value=ReentrantLock private final ConcurrentHashMap userLockMap = new ConcurrentHashMap<>(); @Autowired private PrizeOnlineService prizeOnlineService; @Autowired private RedissonClient redissonClient; @Autowired private UserService userService; @Autowired private AccountService accountService; @Autowired private SystemConfigManager systemConfigManager; @Autowired private PointManager pointManager; @Autowired private DrawService drawService; /** * 用户抽奖(核心方法,优化后) * @param userId 用户ID * @return 中奖奖品 */ public PrizeOnline draw(Long userId) { User user = userService.getById(userId); if(user == null){ throw new ServiceException("用户不存在"); } boolean openDraw = drawService.getOpenDraw(user.getGender()); if(!openDraw){ throw new ServiceException("暂未开启积分抽奖,请等待活动通知"); } boolean select = GenderEnum.isSelect(user.getGender()); if(select){ throw new ServiceException("请选择性别后在抽奖"); } Account account = accountService.getByUserId(user.getId()); Integer drawPoint = drawService.getDrawPoint(user.getGender()); if(account.getPoints() < drawPoint){ throw new ServiceException("积分不足"); } ReentrantLock userLock = userLockMap.computeIfAbsent(userId, k -> new ReentrantLock()); try { boolean lockAcquired = userLock.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS); if (!lockAcquired) { log.warn("用户{}抽奖请求太频繁,获取锁失败", userId); throw new ServiceException("您的请求太频繁,请稍后再试"); } try { Account accountNew = accountService.getByUserId(user.getId()); if(accountNew.getPoints() < drawPoint){ throw new ServiceException("积分不足"); } PrizeOnline winPrize = doDrawLogic(user,drawPoint); return winPrize; } finally { userLock.unlock(); // 6. 清理未使用的锁(防止内存溢出) cleanUnusedLock(userId); } } catch (InterruptedException e) { log.error("用户{}抽奖获取锁被中断", userId, e); Thread.currentThread().interrupt(); // 恢复中断状态 throw new ServiceException("抽奖请求处理中,请稍后再试"); } catch (Exception e) { log.error("用户{}抽奖失败", userId, e); throw new ServiceException("抽奖失败,请重新刷新页面后在尝试"); } } /** * 实际抽奖逻辑(抽离出来,便于维护) */ private PrizeOnline doDrawLogic(User user,Integer drawPoint) { Long userId = user.getId(); // 步骤1:获取用户当前累计抽数(缓存+数据库兜底) int currentContinuousDraws = getContinuousDraws(userId); int newContinuousDraws = currentContinuousDraws + 1; log.info("用户{}当前累计抽数:{},本次抽数:{}", userId, currentContinuousDraws, newContinuousDraws); // 步骤2:获取有效奖品列表(启用状态,排除谢谢惠顾) List validPrizes = prizeOnlineService.selectPrizeOnlineList(user.getGender()); if (validPrizes.isEmpty()) { throw new ServiceException("无有效奖品,请刷新页面后再次抽奖"); } // 步骤3:执行抽奖规则(保底→最低抽数过滤→概率抽奖) PrizeOnline winPrize = null; // 3.1 保底规则判断(优先触发) winPrize = checkGuaranteeRule(validPrizes, newContinuousDraws); // 3.2 未触发保底,执行概率抽奖(含最低中奖抽数过滤) if (winPrize == null) { winPrize = executeProbabilityDraw(validPrizes, newContinuousDraws); } // 步骤4:处理中奖结果,确定最终奖品ID和累计抽数重置 if(winPrize == null){ winPrize = new PrizeOnline(); // TODO 谢谢惠顾 } // 步骤5:持久化抽奖记录+更新缓存 winPrizeAfter(winPrize, user, drawPoint, newContinuousDraws); // 步骤6:返回中奖奖品 return winPrize; } /** * 保底规则判断 * @param validPrizes 有效奖品列表 * @param currentDraws 当前累计抽数 * @return 保底中奖的奖品(无则返回null) */ private PrizeOnline checkGuaranteeRule(List validPrizes, int currentDraws) { // 按保底抽数升序排序,优先触发保底抽数小的奖品 validPrizes.sort(Comparator.comparingInt(PrizeOnline::getGuaranteeDraws)); for (PrizeOnline prize : validPrizes) { int guaranteeDraws = prize.getGuaranteeDraws(); if (guaranteeDraws > 0 && currentDraws >= guaranteeDraws) { log.info("触发保底规则,用户抽中奖品:{}", prize.getPrizeName()); return prize; } } return null; } /** * 执行概率抽奖(含最低中奖抽数过滤) * @param validPrizes 有效奖品列表 * @param currentDraws 当前累计抽数 * @return 中奖奖品(无则返回null) */ private PrizeOnline executeProbabilityDraw(List validPrizes, int currentDraws) { // 步骤1:过滤出满足最低中奖抽数的奖品 List filterPrizes = filterByMinWinDraws(validPrizes, currentDraws); if (filterPrizes.isEmpty()) { log.info("无满足最低中奖抽数的奖品,返回谢谢惠顾"); return null; } // 步骤2:计算奖品的概率总和(放大为整数,提升精度) double totalProbability = 0.0; Map prizeProbMap = new LinkedHashMap<>(); // 保留顺序 for (PrizeOnline prize : filterPrizes) { double prob = prize.getWinProbability().doubleValue(); if (prob < 0 || prob > 1) { log.warn("奖品{}中奖率{}非法,默认设为0", prize.getPrizeName(), prob); prob = 0.0; } prizeProbMap.put(prize, prob); totalProbability += prob; } // 步骤3:若总概率为0,直接返回null if (totalProbability <= 0) { log.info("满足条件的奖品总中奖率为0,返回谢谢惠顾"); return null; } // 步骤4:生成随机数(放大10000倍,转为整数计算,减少浮点误差) int randomNum = new Random().nextInt((int) (totalProbability * RANDOM_MAX)); int currentNum = 0; // 步骤5:匹配中奖奖品 for (Map.Entry entry : prizeProbMap.entrySet()) { PrizeOnline prize = entry.getKey(); double prob = entry.getValue(); int probInt = (int) (prob * RANDOM_MAX); currentNum += probInt; if (randomNum < currentNum) { log.info("概率抽奖抽中奖品:{}", prize.getPrizeName()); return prize; } } return null; } /** * 过滤出满足最低中奖抽数的奖品 */ private List filterByMinWinDraws(List prizes, int currentDraws) { List filterList = new ArrayList<>(); for (PrizeOnline prize : prizes) { if (currentDraws >= prize.getMinWinDraws()) { filterList.add(prize); } } return filterList; } /** * 获取用户累计抽数(缓存优先,数据库兜底) */ private int getContinuousDraws(Long userId) { String cacheKey = String.format(USER_DRAW_COUNT_KEY, userId); // 1. 从Redis缓存获取 RBucket bucket = redissonClient.getBucket(cacheKey); Integer cacheCount = bucket.get(); if (cacheCount != null) { return cacheCount; } // 2. 缓存未命中,从数据库查询最后一次累计抽数 // Integer dbCount = userDrawRecordMapper.selectLastContinuousDraws(userId); Integer dbCount = 0; int finalCount = dbCount == null ? 0 : dbCount; // 3. 存入缓存(设置过期时间) bucket.set(finalCount, USER_DRAW_COUNT_EXPIRE, java.util.concurrent.TimeUnit.SECONDS); return finalCount; } /** * 保存抽奖记录(事务控制) */ @Transactional(rollbackFor = Exception.class) public void winPrizeAfter(PrizeOnline prizeOnline, User user,Integer drawPoint, int continuousDraws) { // 扣减积分 String traceId = IdManager.nextIdStr(); PointChangeLog pointChangeLog = pointManager.drawPoint(prizeOnline, user, drawPoint, traceId); // 记录用户抽奖记录 // UserDrawRecord record = new UserDrawRecord(); // record.setUserId(userId); // record.setPrizeId(prizeId); // record.setDrawTime(LocalDateTime.now()); // record.setContinuousDraws(continuousDraws); // userDrawRecordMapper.insert(record); // 更新缓存 String cacheKey = String.format(USER_DRAW_COUNT_KEY, user.getId()); RBucket bucket = redissonClient.getBucket(cacheKey); bucket.set(continuousDraws, USER_DRAW_COUNT_EXPIRE, java.util.concurrent.TimeUnit.SECONDS); } /** * 清理长时间未使用的用户锁(可选,防止内存溢出) * 这里简单实现:若锁未被持有,则移除(可根据业务增加时间判断) */ private void cleanUnusedLock(Long userId) { ReentrantLock lock = userLockMap.get(userId); if (lock != null && !lock.isLocked()) { userLockMap.remove(userId); log.debug("清理用户{}的锁", userId); } } }