287 lines
12 KiB
Java
287 lines
12 KiB
Java
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.PrizeWinningRecordService;
|
||
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 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<Long, ReentrantLock> 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 select = GenderEnum.isSelect(user.getGender());
|
||
if(select){
|
||
throw new ServiceException("请选择性别后在抽奖");
|
||
}
|
||
boolean openDraw = drawService.getOpenDraw(user.getGender());
|
||
if(!openDraw){
|
||
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<PrizeOnline> validPrizes = prizeOnlineService.selectPrizeOnlineList(user.getGender());
|
||
if (validPrizes.isEmpty()) {
|
||
throw new ServiceException("无有效奖品,请刷新后再次抽奖");
|
||
}
|
||
// 找出系统内置谢谢惠顾
|
||
PrizeOnline thankPrize = null;
|
||
for (int i = 0; i < validPrizes.size(); i++) {
|
||
PrizeOnline validPrize = validPrizes.get(i);
|
||
if(validPrize.getPrizeId() == 1){
|
||
thankPrize = validPrize;
|
||
validPrizes.remove(i);
|
||
break;
|
||
}
|
||
}
|
||
if(thankPrize == null){
|
||
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<PrizeOnline> 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<PrizeOnline> validPrizes, int currentDraws) {
|
||
// 步骤1:过滤出满足最低中奖抽数的奖品
|
||
List<PrizeOnline> filterPrizes = filterByMinWinDraws(validPrizes, currentDraws);
|
||
if (filterPrizes.isEmpty()) {
|
||
log.info("无满足最低中奖抽数的奖品,返回谢谢惠顾");
|
||
return null;
|
||
}
|
||
// 步骤2:计算奖品的概率总和(放大为整数,提升精度)
|
||
double totalProbability = 0.0;
|
||
Map<PrizeOnline, Double> 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<PrizeOnline, Double> 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<PrizeOnline> filterByMinWinDraws(List<PrizeOnline> prizes, int currentDraws) {
|
||
List<PrizeOnline> 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<Integer> bucket = redissonClient.getBucket(cacheKey);
|
||
Integer cacheCount = bucket.get();
|
||
if (cacheCount != null) {
|
||
return cacheCount;
|
||
}
|
||
// 3. 存入缓存
|
||
// bucket.set(0, USER_DRAW_COUNT_EXPIRE, java.util.concurrent.TimeUnit.SECONDS);
|
||
bucket.set(0);
|
||
return 0;
|
||
}
|
||
|
||
@Autowired
|
||
private PrizeWinningRecordService prizeWinningRecordService;
|
||
|
||
/**
|
||
* 保存抽奖记录(事务控制)
|
||
*/
|
||
@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);
|
||
// 记录用户抽奖记录
|
||
prizeWinningRecordService.winningRecord(pointChangeLog, prizeOnline, user, drawPoint);
|
||
// 更新缓存
|
||
String cacheKey = String.format(USER_DRAW_COUNT_KEY, user.getId());
|
||
RBucket<Integer> 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);
|
||
}
|
||
}
|
||
}
|