diff --git a/doc/20251211.sql b/doc/20251211.sql index abc7c63f..485d4f1c 100644 --- a/doc/20251211.sql +++ b/doc/20251211.sql @@ -8,14 +8,12 @@ CREATE TABLE `cai_prize_info` `guarantee_draws` int NOT NULL DEFAULT 0 COMMENT '保底抽数(0表示无保底,谢谢惠顾奖无效)', `min_win_draws` int NOT NULL DEFAULT 0 COMMENT '最低中奖抽数(0表示无限制,谢谢惠顾奖无效)', `stock` int NOT NULL DEFAULT 0 COMMENT '奖品库存(谢谢惠顾奖填0,不校验)', - `prize_type` tinyint not null comment '奖品类型', + `prize_type` tinyint not null comment '奖品类型 1-谢谢惠顾 2-普通奖 3-大奖', `prize_price` bigint(20) not null default 0 comment '奖品价值估算', `auto_give` tinyint not null default 0 comment '是否自动兑奖', - `is_thank` tinyint NOT NULL DEFAULT 0 COMMENT '是否为谢谢惠顾奖:0-否,1-是(全局仅一个)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_is_thank` (`is_thank`) COMMENT '谢谢惠顾奖索引' + PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='奖品基础表'; @@ -32,13 +30,29 @@ CREATE TABLE `cai_prize_online` `guarantee_draws` int NOT NULL DEFAULT 0 COMMENT '保底抽数(0表示无保底,谢谢惠顾奖无效)', `min_win_draws` int NOT NULL DEFAULT 0 COMMENT '最低中奖抽数(0表示无限制,谢谢惠顾奖无效)', `stock` int NOT NULL DEFAULT 0 COMMENT '奖品库存(谢谢惠顾奖填0,不校验)', - `prize_type` tinyint not null comment '奖品类型', + `prize_type` tinyint not null comment '奖品类型 1-谢谢惠顾 2-普通奖 3-大奖', `prize_price` bigint(20) not null default 0 comment '奖品价值估算', `auto_give` tinyint not null default 0 comment '是否自动兑奖', - `is_thank` tinyint NOT NULL DEFAULT 0 COMMENT '是否为谢谢惠顾奖:0-否,1-是(全局仅一个)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_is_thank` (`is_thank`) COMMENT '谢谢惠顾奖索引' + PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='已发布奖品表'; + + +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values(2003344862891581442, '抽奖奖品', '1738072642014617602', '1', 'prizeInfo', 'cai/prizeInfo/index', 1, 0, 'C', '0', '0', 'cai:prizeInfo:list', '#', 'admin', sysdate(), '', null, '抽奖奖品菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values(2003344862891581443, '抽奖奖品查询', 2003344862891581442, '1', '#', '', 1, 0, 'F', '0', '0', 'cai:prizeInfo:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values(2003344862891581444, '抽奖奖品新增', 2003344862891581442, '2', '#', '', 1, 0, 'F', '0', '0', 'cai:prizeInfo:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values(2003344862891581445, '抽奖奖品修改', 2003344862891581442, '3', '#', '', 1, 0, 'F', '0', '0', 'cai:prizeInfo:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values(2003344862891581446, '抽奖奖品删除', 2003344862891581442, '4', '#', '', 1, 0, 'F', '0', '0', 'cai:prizeInfo:remove', '#', 'admin', sysdate(), '', null, ''); diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeInfo.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeInfo.java index dff22788..4e227ebc 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeInfo.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeInfo.java @@ -45,19 +45,19 @@ public class PrizeInfo implements Serializable { */ private BigDecimal winProbability; /** - * 保底抽数(0表示无保底,谢谢惠顾奖无效) + * 保底抽数(0表示无保底) */ private Long guaranteeDraws; /** - * 最低中奖抽数(0表示无限制,谢谢惠顾奖无效) + * 最低中奖抽数(0表示无限制) */ private Long minWinDraws; /** - * 奖品库存(谢谢惠顾奖填0,不校验) + * 奖品库存(谢谢惠顾奖填0) */ private Long stock; /** - * 奖品类型 + * 奖品类型 1-谢谢惠顾 2-普通奖 3-大奖 */ private Long prizeType; /** @@ -68,10 +68,6 @@ public class PrizeInfo implements Serializable { * 是否自动兑奖 */ private Boolean autoGive; - /** - * 是否为谢谢惠顾奖:0-否,1-是(全局仅一个) - */ - private Boolean thank; private LocalDateTime createTime; private LocalDateTime updateTime; diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeOnline.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeOnline.java index 9d30f897..8ef68c1b 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeOnline.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/domain/PrizeOnline.java @@ -55,19 +55,19 @@ public class PrizeOnline implements Serializable { /** * 保底抽数(0表示无保底,谢谢惠顾奖无效) */ - private Long guaranteeDraws; + private Integer guaranteeDraws; /** * 最低中奖抽数(0表示无限制,谢谢惠顾奖无效) */ - private Long minWinDraws; + private Integer minWinDraws; /** * 奖品库存(谢谢惠顾奖填0,不校验) */ private Long stock; /** - * 奖品类型 + * 奖品类型 1-谢谢惠顾 2-普通奖 3-大奖 */ - private Long prizeType; + private Integer prizeType; /** * 奖品价值估算 */ @@ -76,10 +76,6 @@ public class PrizeOnline implements Serializable { * 是否自动兑奖 */ private Boolean autoGive; - /** - * 是否为谢谢惠顾奖:0-否,1-是(全局仅一个) - */ - private Boolean thank; private LocalDateTime createTime; private LocalDateTime updateTime; diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/GenderEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/GenderEnum.java index d274052c..2f89b17e 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/GenderEnum.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/GenderEnum.java @@ -19,6 +19,16 @@ public enum GenderEnum { this.defaultAvatar = defaultAvatar; } + public static boolean isSelect(Integer code){ + if(GenderEnum.WOMEN.getCode().equals(code)){ + return true; + } + if(GenderEnum.MAN.getCode().equals(code)){ + return true; + } + return false; + } + public static GenderEnum getByCode(Integer code){ GenderEnum[] values = GenderEnum.values(); for (GenderEnum value : values) { diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigEnum.java index 0a026ef7..26dbb677 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigEnum.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigEnum.java @@ -62,6 +62,12 @@ public enum SystemConfigEnum { V12_XIAOCHENGXU_ORG_ID("gh_62790d4f9c57", "V12德商小程序原始id",SystemConfigGroupEnum.PAY), V12_XIAOCHENGXU_PATH("pages/zf/index?", "V12德商小程序页面路径",SystemConfigGroupEnum.PAY), V12_WX_APP_ID("wxae39c7eed3221d26", "微信开放平台ID",SystemConfigGroupEnum.PAY), + /** + * 抽奖和积分 + */ + OPEN_DRAW("1","是否开启积分抽奖",SystemConfigGroupEnum.DRAW,new BooleanSystemConfigCheck()), + WOMEN_DRAW_POINT("100","女用户抽奖分数",SystemConfigGroupEnum.DRAW,new NumberSystemConfigCheck()), + MEN_DRAW_POINT("100","男用户抽奖分数",SystemConfigGroupEnum.DRAW,new NumberSystemConfigCheck()), /** * 域名配置 */ diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigGroupEnum.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigGroupEnum.java index c6e339e0..f2aca578 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigGroupEnum.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/enums/SystemConfigGroupEnum.java @@ -5,6 +5,7 @@ public enum SystemConfigGroupEnum { BUSINESS, SECURITY, PAY, - DOMAIN + DOMAIN, + DRAW ; } diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/lottery/LotteryService.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/lottery/LotteryService.java index 7e7d447d..9ae41675 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/lottery/LotteryService.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/lottery/LotteryService.java @@ -1,167 +1,284 @@ package com.ruoyi.cai.lottery; -import com.lottery.entity.Prize; -import com.lottery.entity.UserDrawRecord; -import com.lottery.mapper.UserDrawRecordMapper; -import com.ruoyi.cai.domain.PrizeInfo; +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.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Random; + +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 = 1L; + // 固定配置 + private static final Long THANKS_PRIZE_ID = 0L; private static final String USER_DRAW_COUNT_KEY = "user:draw:count:%s"; - // 每个用户的锁,防止并发抽奖(单机版用ReentrantLock) - private final ReentrantLock lock = new ReentrantLock(); + 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 RedisTemplate redisTemplate; + private RedissonClient redissonClient; + @Autowired + private UserService userService; + @Autowired + private AccountService accountService; + @Autowired + private SystemConfigManager systemConfigManager; + + private Integer getDrawPoint(Integer gender){ + if(GenderEnum.WOMEN.getCode().equals(gender)){ + Integer womenDrawPoint = systemConfigManager.getSystemConfigOfInt(SystemConfigEnum.WOMEN_DRAW_POINT); + return womenDrawPoint; + } + return systemConfigManager.getSystemConfigOfInt(SystemConfigEnum.MEN_DRAW_POINT); + } /** - * 用户抽奖 + * 用户抽奖(核心方法,优化后) + * @param userId 用户ID + * @return 中奖奖品 */ - public PrizeInfo draw(Long userId) { - // 加锁防止并发抽奖 - lock.lock(); + public PrizeOnline draw(Long userId) { + boolean openDraw = systemConfigManager.getSystemConfigOfBool(SystemConfigEnum.OPEN_DRAW); + if(!openDraw){ + throw new ServiceException("暂未开启积分抽奖,请等待活动通知"); + } + User user = userService.getById(userId); + if(user == null){ + throw new ServiceException("用户不存在"); + } + boolean select = GenderEnum.isSelect(user.getGender()); + if(select){ + throw new ServiceException("请选择性别后在抽奖"); + } + Account account = accountService.getByUserId(user.getId()); + Integer drawPoint = getDrawPoint(user.getGender()); + if(account.getPoints() < drawPoint){ + throw new ServiceException("积分不足"); + } + ReentrantLock userLock = userLockMap.computeIfAbsent(userId, k -> new ReentrantLock()); try { - // 1. 获取用户累计抽数 - int continuousDraws = getContinuousDraws(userId); - int newContinuousDraws = continuousDraws + 1; - - // 2. 获取启用的奖品(排除谢谢惠顾) - List prizes = prizeService.getEnablePrizesExcludeThanks(); - if (prizes.isEmpty()) { - // 只有谢谢惠顾 - saveDrawRecord(userId, THANKS_PRIZE_ID, newContinuousDraws); - return prizeService.getPrizeFromCache(THANKS_PRIZE_ID); + boolean lockAcquired = userLock.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS); + if (!lockAcquired) { + log.warn("用户{}抽奖请求太频繁,获取锁失败", userId); + throw new ServiceException("您的请求太频繁,请稍后再试"); } - - // 3. 处理抽奖规则(保底、最低中奖抽数、中奖率) - Prize winPrize = null; - boolean isGuarantee = false; - - // 3.1 检查保底规则 - for (Prize prize : prizes) { - if (prize.getGuaranteeDraws() > 0 && newContinuousDraws >= prize.getGuaranteeDraws()) { - // 触发保底,直接中该奖品 - if (prizeService.deductStock(prize.getId())) { - winPrize = prize; - isGuarantee = true; - break; - } + 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); } - - // 3.2 未触发保底,按中奖率和最低中奖抽数规则抽奖 - if (winPrize == null) { - winPrize = randomDraw(prizes, newContinuousDraws); - } - - // 4. 处理中奖结果 - Long prizeId = winPrize != null ? winPrize.getId() : THANKS_PRIZE_ID; - int finalContinuousDraws = winPrize != null ? 0 : newContinuousDraws; // 中奖后重置累计抽数 - - // 5. 保存抽奖记录 - saveDrawRecord(userId, prizeId, finalContinuousDraws); - - // 6. 更新用户累计抽数缓存 - updateContinuousDrawsCache(userId, finalContinuousDraws); - - return prizeService.getPrizeFromCache(prizeId); - } finally { - lock.unlock(); + } catch (InterruptedException e) { + log.error("用户{}抽奖获取锁被中断", userId, e); + Thread.currentThread().interrupt(); // 恢复中断状态 + throw new ServiceException("抽奖请求处理中,请稍后再试"); + } catch (Exception e) { + log.error("用户{}抽奖失败", userId, e); + throw new ServiceException("抽奖失败,请重新刷新页面后在尝试"); } } /** - * 随机抽奖(处理中奖率和最低中奖抽数) + * 实际抽奖逻辑(抽离出来,便于维护) */ - private Prize randomDraw(List prizes, int continuousDraws) { - // 计算总中奖率 - double totalProbability = 0.0; - for (Prize prize : prizes) { - // 检查最低中奖抽数规则:未达到则该奖品中奖率为0 - if (continuousDraws < prize.getMinWinDraws()) { - continue; - } - totalProbability += prize.getWinProbability(); + 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("无有效奖品,请刷新页面后再次抽奖"); } - - if (totalProbability <= 0) { - // 所有奖品都未达到最低中奖抽数,返回null(谢谢惠顾) - return null; + // 步骤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; + } - // 随机数(0-总中奖率) - double random = new Random().nextDouble() * totalProbability; - double current = 0.0; - - for (Prize prize : prizes) { - if (continuousDraws < prize.getMinWinDraws()) { - continue; - } - current += prize.getWinProbability(); - if (random <= current) { - // 抽中该奖品,检查库存 - if (prizeService.deductStock(prize.getId())) { - return prize; - } else { - // 库存不足,重新抽奖(递归) - return randomDraw(prizes, continuousDraws); - } + /** + * 保底规则判断 + * @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 key = String.format(USER_DRAW_COUNT_KEY, userId); - // 先从缓存获取 - Integer count = (Integer) redisTemplate.opsForValue().get(key); - if (count != null) { - return count; + 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; } - // 从数据库查询 - count = userDrawRecordMapper.selectLastContinuousDraws(userId); - count = count == null ? 0 : count; - // 存入缓存 - redisTemplate.opsForValue().set(key, count); - return count; + // 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; } /** - * 更新用户累计抽数缓存 - */ - private void updateContinuousDrawsCache(Long userId, int count) { - String key = String.format(USER_DRAW_COUNT_KEY, userId); - redisTemplate.opsForValue().set(key, count); - } - - /** - * 保存抽奖记录 + * 保存抽奖记录(事务控制) */ @Transactional(rollbackFor = Exception.class) - private void saveDrawRecord(Long userId, Long prizeId, int continuousDraws) { - UserDrawRecord record = new UserDrawRecord(); - record.setUserId(userId); - record.setPrizeId(prizeId); - record.setDrawTime(LocalDateTime.now()); - record.setContinuousDraws(continuousDraws); - userDrawRecordMapper.insert(record); + public void winPrizeAfter(PrizeOnline prizeOnline, User user,Integer drawPoint, int continuousDraws) { + // 扣减积分 + String traceId = IdManager.nextIdStr(); + PointChangeLog pointChangeLog = accountService.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); + } } } diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/AccountService.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/AccountService.java index fce516a6..60591e33 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/AccountService.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/AccountService.java @@ -10,6 +10,7 @@ import com.ruoyi.cai.enums.ConsumeLogType; import com.ruoyi.cai.enums.account.AccountChangeCodeEnum; import com.ruoyi.cai.ws.bean.Room; import com.ruoyi.common.core.domain.PageQuery; +import org.springframework.transaction.annotation.Transactional; /** * 用户账户Service接口 @@ -31,6 +32,9 @@ public interface AccountService extends IService { void withdrawFail(Long userId, Long incomeCoin, Long traceId); + @Transactional(rollbackFor = Exception.class) + PointChangeLog drawPoint(PrizeOnline prizeOnline, User user, Integer drawPoint, String traceId); + PointRecordLog rechargePoint(RechargeOrder rechargeOrder, User user); void recharge(ConsumeLog consumeLog); diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PointChangeLogService.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PointChangeLogService.java index 4e581560..dc4daca8 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PointChangeLogService.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PointChangeLogService.java @@ -2,6 +2,8 @@ package com.ruoyi.cai.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.cai.domain.PointChangeLog; +import com.ruoyi.cai.domain.PrizeOnline; +import com.ruoyi.cai.domain.User; /** * 积分记录Service接口 @@ -18,4 +20,6 @@ public interface PointChangeLogService extends IService { void adminChange(Long userId, Long givePoint); void adminInvite(Long userId, Long givePoint, Long inviteUserId, String traceId); + + PointChangeLog drawPoint(PrizeOnline prizeOnline, User user, Integer drawPoint, String traceId); } diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PrizeOnlineService.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PrizeOnlineService.java index 7cbb2248..9f9afd64 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PrizeOnlineService.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/PrizeOnlineService.java @@ -3,6 +3,8 @@ package com.ruoyi.cai.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.cai.domain.PrizeOnline; +import java.util.List; + /** * 已发布奖品Service接口 * @@ -11,4 +13,5 @@ import com.ruoyi.cai.domain.PrizeOnline; */ public interface PrizeOnlineService extends IService { + List selectPrizeOnlineList(Integer gender); } diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/AccountServiceImpl.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/AccountServiceImpl.java index aa19454e..8b47cd12 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/AccountServiceImpl.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/AccountServiceImpl.java @@ -222,6 +222,17 @@ public class AccountServiceImpl extends ServiceImpl impl @Autowired private PointChangeLogService pointChangeLogService; + @Transactional(rollbackFor = Exception.class) + @Override + public PointChangeLog drawPoint(PrizeOnline prizeOnline, User user, Integer drawPoint, String traceId){ + boolean bb = baseMapper.decrPoint(user.getId(), Long.valueOf(drawPoint)); + if(!bb){ + throw new ServiceException("积分不足"); + } + PointChangeLog pointChangeLog = pointChangeLogService.drawPoint(prizeOnline, user, drawPoint, traceId); + return pointChangeLog; + } + @Transactional(rollbackFor = Exception.class) @Override public PointRecordLog rechargePoint(RechargeOrder rechargeOrder, User user){ @@ -229,6 +240,7 @@ public class AccountServiceImpl extends ServiceImpl impl return null; } log.info("开始赠送积分 orderNo={}", rechargeOrder.getOrderNo()); + baseMapper.incrPoint(rechargeOrder.getUserId(), rechargeOrder.getGivePoint()); pointChangeLogService.rechargeOrderChange(rechargeOrder.getOrderNo(), rechargeOrder.getUserId(),rechargeOrder.getGivePoint()); PointRecordLog pointRecordLog = pointRecordLogService.initOrder(rechargeOrder, user); return pointRecordLog; diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/PointChangeLogServiceImpl.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/PointChangeLogServiceImpl.java index b494b3a1..68734fa0 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/PointChangeLogServiceImpl.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/PointChangeLogServiceImpl.java @@ -2,6 +2,7 @@ package com.ruoyi.cai.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.cai.domain.PointChangeLog; +import com.ruoyi.cai.domain.PrizeOnline; import com.ruoyi.cai.domain.User; import com.ruoyi.cai.enums.point.PointChangeLogActionTypeEnum; import com.ruoyi.cai.enums.point.PointChangeTraceTypeEnum; @@ -9,6 +10,7 @@ import com.ruoyi.cai.mapper.AccountMapper; import com.ruoyi.cai.mapper.PointChangeLogMapper; import com.ruoyi.cai.service.PointChangeLogService; import com.ruoyi.cai.service.UserService; +import com.ruoyi.common.utils.ServletUtils; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -105,4 +107,21 @@ public class PointChangeLogServiceImpl extends ServiceImpl implements PrizeOnlineService { + @Override + public List selectPrizeOnlineList(Integer gender){ + return new ArrayList<>(); + } + } diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/RechargeOrderServiceImpl.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/RechargeOrderServiceImpl.java index b9a8c52b..8d61f10e 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/RechargeOrderServiceImpl.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/RechargeOrderServiceImpl.java @@ -127,7 +127,7 @@ public class RechargeOrderServiceImpl extends ServiceImpl - - - - - - - - - - - - - - - - - diff --git a/ruoyi-cai/src/main/resources/mapper/cai/PrizeOnlineMapper.xml b/ruoyi-cai/src/main/resources/mapper/cai/PrizeOnlineMapper.xml index 5f5f3958..1b107430 100644 --- a/ruoyi-cai/src/main/resources/mapper/cai/PrizeOnlineMapper.xml +++ b/ruoyi-cai/src/main/resources/mapper/cai/PrizeOnlineMapper.xml @@ -4,24 +4,5 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - - - - - - - - - - - - - - - - - - -