登录改造

This commit is contained in:
777
2025-12-26 12:16:29 +08:00
parent f0d4b595f4
commit 9cd9841f09
17 changed files with 335 additions and 181 deletions

View File

@@ -8,14 +8,12 @@ CREATE TABLE `cai_prize_info`
`guarantee_draws` int NOT NULL DEFAULT 0 COMMENT '保底抽数0表示无保底谢谢惠顾奖无效', `guarantee_draws` int NOT NULL DEFAULT 0 COMMENT '保底抽数0表示无保底谢谢惠顾奖无效',
`min_win_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不校验', `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 '奖品价值估算', `prize_price` bigint(20) not null default 0 comment '奖品价值估算',
`auto_give` tinyint 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 '创建时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`)
KEY `idx_is_thank` (`is_thank`) COMMENT '谢谢惠顾奖索引'
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='奖品基础表'; DEFAULT CHARSET = utf8mb4 COMMENT ='奖品基础表';
@@ -32,13 +30,29 @@ CREATE TABLE `cai_prize_online`
`guarantee_draws` int NOT NULL DEFAULT 0 COMMENT '保底抽数0表示无保底谢谢惠顾奖无效', `guarantee_draws` int NOT NULL DEFAULT 0 COMMENT '保底抽数0表示无保底谢谢惠顾奖无效',
`min_win_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不校验', `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 '奖品价值估算', `prize_price` bigint(20) not null default 0 comment '奖品价值估算',
`auto_give` tinyint 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 '创建时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`)
KEY `idx_is_thank` (`is_thank`) COMMENT '谢谢惠顾奖索引'
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='已发布奖品表'; 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, '');

View File

@@ -45,19 +45,19 @@ public class PrizeInfo implements Serializable {
*/ */
private BigDecimal winProbability; private BigDecimal winProbability;
/** /**
* 保底抽数0表示无保底,谢谢惠顾奖无效 * 保底抽数0表示无保底
*/ */
private Long guaranteeDraws; private Long guaranteeDraws;
/** /**
* 最低中奖抽数0表示无限制,谢谢惠顾奖无效 * 最低中奖抽数0表示无限制
*/ */
private Long minWinDraws; private Long minWinDraws;
/** /**
* 奖品库存谢谢惠顾奖填0,不校验 * 奖品库存谢谢惠顾奖填0
*/ */
private Long stock; private Long stock;
/** /**
* 奖品类型 * 奖品类型 1-谢谢惠顾 2-普通奖 3-大奖
*/ */
private Long prizeType; private Long prizeType;
/** /**
@@ -68,10 +68,6 @@ public class PrizeInfo implements Serializable {
* 是否自动兑奖 * 是否自动兑奖
*/ */
private Boolean autoGive; private Boolean autoGive;
/**
* 是否为谢谢惠顾奖0-否1-是(全局仅一个)
*/
private Boolean thank;
private LocalDateTime createTime; private LocalDateTime createTime;
private LocalDateTime updateTime; private LocalDateTime updateTime;

View File

@@ -55,19 +55,19 @@ public class PrizeOnline implements Serializable {
/** /**
* 保底抽数0表示无保底谢谢惠顾奖无效 * 保底抽数0表示无保底谢谢惠顾奖无效
*/ */
private Long guaranteeDraws; private Integer guaranteeDraws;
/** /**
* 最低中奖抽数0表示无限制谢谢惠顾奖无效 * 最低中奖抽数0表示无限制谢谢惠顾奖无效
*/ */
private Long minWinDraws; private Integer minWinDraws;
/** /**
* 奖品库存谢谢惠顾奖填0不校验 * 奖品库存谢谢惠顾奖填0不校验
*/ */
private Long stock; private Long stock;
/** /**
* 奖品类型 * 奖品类型 1-谢谢惠顾 2-普通奖 3-大奖
*/ */
private Long prizeType; private Integer prizeType;
/** /**
* 奖品价值估算 * 奖品价值估算
*/ */
@@ -76,10 +76,6 @@ public class PrizeOnline implements Serializable {
* 是否自动兑奖 * 是否自动兑奖
*/ */
private Boolean autoGive; private Boolean autoGive;
/**
* 是否为谢谢惠顾奖0-否1-是(全局仅一个)
*/
private Boolean thank;
private LocalDateTime createTime; private LocalDateTime createTime;
private LocalDateTime updateTime; private LocalDateTime updateTime;

View File

@@ -19,6 +19,16 @@ public enum GenderEnum {
this.defaultAvatar = defaultAvatar; 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){ public static GenderEnum getByCode(Integer code){
GenderEnum[] values = GenderEnum.values(); GenderEnum[] values = GenderEnum.values();
for (GenderEnum value : values) { for (GenderEnum value : values) {

View File

@@ -62,6 +62,12 @@ public enum SystemConfigEnum {
V12_XIAOCHENGXU_ORG_ID("gh_62790d4f9c57", "V12德商小程序原始id",SystemConfigGroupEnum.PAY), V12_XIAOCHENGXU_ORG_ID("gh_62790d4f9c57", "V12德商小程序原始id",SystemConfigGroupEnum.PAY),
V12_XIAOCHENGXU_PATH("pages/zf/index?", "V12德商小程序页面路径",SystemConfigGroupEnum.PAY), V12_XIAOCHENGXU_PATH("pages/zf/index?", "V12德商小程序页面路径",SystemConfigGroupEnum.PAY),
V12_WX_APP_ID("wxae39c7eed3221d26", "微信开放平台ID",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()),
/** /**
* 域名配置 * 域名配置
*/ */

View File

@@ -5,6 +5,7 @@ public enum SystemConfigGroupEnum {
BUSINESS, BUSINESS,
SECURITY, SECURITY,
PAY, PAY,
DOMAIN DOMAIN,
DRAW
; ;
} }

View File

@@ -1,167 +1,284 @@
package com.ruoyi.cai.lottery; package com.ruoyi.cai.lottery;
import com.lottery.entity.Prize; import com.ruoyi.cai.domain.Account;
import com.lottery.entity.UserDrawRecord; import com.ruoyi.cai.domain.PointChangeLog;
import com.lottery.mapper.UserDrawRecordMapper; import com.ruoyi.cai.domain.PrizeOnline;
import com.ruoyi.cai.domain.PrizeInfo; 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.PrizeOnlineService;
import com.ruoyi.cai.service.UserService;
import com.ruoyi.common.exception.ServiceException;
import lombok.RequiredArgsConstructor; 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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List; import java.util.*;
import java.util.Random; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
/**
* 抽奖核心服务(优化版)
*/
@Service @Service
@RequiredArgsConstructor
@Slf4j
public class LotteryService { 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"; private static final String USER_DRAW_COUNT_KEY = "user:draw:count:%s";
// 每个用户的锁防止并发抽奖单机版用ReentrantLock private static final long USER_DRAW_COUNT_EXPIRE = 7 * 24 * 60 * 60; // 用户累计抽数缓存过期时间7天
private final ReentrantLock lock = new ReentrantLock(); private static final double RANDOM_MAX = 10000; // 概率放大倍数,提升随机数精度
private static final long LOCK_TIMEOUT = 500; // 锁超时时间500ms非阻塞获取
// 每个用户的独立锁key=userIdvalue=ReentrantLock
private final ConcurrentHashMap<Long, ReentrantLock> userLockMap = new ConcurrentHashMap<>();
@Autowired @Autowired
private PrizeOnlineService prizeOnlineService; private PrizeOnlineService prizeOnlineService;
@Autowired @Autowired
private RedisTemplate<String, Object> 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) { public PrizeOnline draw(Long userId) {
// 加锁防止并发抽奖 boolean openDraw = systemConfigManager.getSystemConfigOfBool(SystemConfigEnum.OPEN_DRAW);
lock.lock(); 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 { try {
// 1. 获取用户累计抽数 boolean lockAcquired = userLock.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS);
int continuousDraws = getContinuousDraws(userId); if (!lockAcquired) {
int newContinuousDraws = continuousDraws + 1; log.warn("用户{}抽奖请求太频繁,获取锁失败", userId);
throw new ServiceException("您的请求太频繁,请稍后再试");
// 2. 获取启用的奖品(排除谢谢惠顾)
List<Prize> prizes = prizeService.getEnablePrizesExcludeThanks();
if (prizes.isEmpty()) {
// 只有谢谢惠顾
saveDrawRecord(userId, THANKS_PRIZE_ID, newContinuousDraws);
return prizeService.getPrizeFromCache(THANKS_PRIZE_ID);
} }
try {
// 3. 处理抽奖规则(保底、最低中奖抽数、中奖率) Account accountNew = accountService.getByUserId(user.getId());
Prize winPrize = null; if(accountNew.getPoints() < drawPoint){
boolean isGuarantee = false; throw new ServiceException("积分不足");
// 3.1 检查保底规则
for (Prize prize : prizes) {
if (prize.getGuaranteeDraws() > 0 && newContinuousDraws >= prize.getGuaranteeDraws()) {
// 触发保底,直接中该奖品
if (prizeService.deductStock(prize.getId())) {
winPrize = prize;
isGuarantee = true;
break;
}
} }
PrizeOnline winPrize = doDrawLogic(user,drawPoint);
return winPrize;
} finally {
userLock.unlock();
// 6. 清理未使用的锁(防止内存溢出)
cleanUnusedLock(userId);
} }
} catch (InterruptedException e) {
// 3.2 未触发保底,按中奖率和最低中奖抽数规则抽奖 log.error("用户{}抽奖获取锁被中断", userId, e);
if (winPrize == null) { Thread.currentThread().interrupt(); // 恢复中断状态
winPrize = randomDraw(prizes, newContinuousDraws); throw new ServiceException("抽奖请求处理中,请稍后再试");
} } catch (Exception e) {
log.error("用户{}抽奖失败", userId, e);
// 4. 处理中奖结果 throw new ServiceException("抽奖失败,请重新刷新页面后在尝试");
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();
} }
} }
/** /**
* 随机抽奖(处理中奖率和最低中奖抽数 * 实际抽奖逻辑(抽离出来,便于维护
*/ */
private Prize randomDraw(List<Prize> prizes, int continuousDraws) { private PrizeOnline doDrawLogic(User user,Integer drawPoint) {
// 计算总中奖率 Long userId = user.getId();
double totalProbability = 0.0; // 步骤1获取用户当前累计抽数缓存+数据库兜底)
for (Prize prize : prizes) { int currentContinuousDraws = getContinuousDraws(userId);
// 检查最低中奖抽数规则未达到则该奖品中奖率为0 int newContinuousDraws = currentContinuousDraws + 1;
if (continuousDraws < prize.getMinWinDraws()) { log.info("用户{}当前累计抽数:{},本次抽数:{}", userId, currentContinuousDraws, newContinuousDraws);
continue; // 步骤2获取有效奖品列表启用状态排除谢谢惠顾
} List<PrizeOnline> validPrizes = prizeOnlineService.selectPrizeOnlineList(user.getGender());
totalProbability += prize.getWinProbability(); if (validPrizes.isEmpty()) {
throw new ServiceException("无有效奖品,请刷新页面后再次抽奖");
} }
// 步骤3执行抽奖规则保底→最低抽数过滤→概率抽奖
if (totalProbability <= 0) { PrizeOnline winPrize = null;
// 所有奖品都未达到最低中奖抽数返回null谢谢惠顾 // 3.1 保底规则判断(优先触发
return null; 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; * @param validPrizes 有效奖品列表
* @param currentDraws 当前累计抽数
for (Prize prize : prizes) { * @return 保底中奖的奖品无则返回null
if (continuousDraws < prize.getMinWinDraws()) { */
continue; private PrizeOnline checkGuaranteeRule(List<PrizeOnline> validPrizes, int currentDraws) {
} // 按保底抽数升序排序,优先触发保底抽数小的奖品
current += prize.getWinProbability(); validPrizes.sort(Comparator.comparingInt(PrizeOnline::getGuaranteeDraws));
if (random <= current) { for (PrizeOnline prize : validPrizes) {
// 抽中该奖品,检查库存 int guaranteeDraws = prize.getGuaranteeDraws();
if (prizeService.deductStock(prize.getId())) { if (guaranteeDraws > 0 && currentDraws >= guaranteeDraws) {
return prize; log.info("触发保底规则,用户抽中奖品:{}", prize.getPrizeName());
} else { return prize;
// 库存不足,重新抽奖(递归)
return randomDraw(prizes, continuousDraws);
}
} }
} }
return null; 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) { private int getContinuousDraws(Long userId) {
String key = String.format(USER_DRAW_COUNT_KEY, userId); String cacheKey = String.format(USER_DRAW_COUNT_KEY, userId);
// 先从缓存获取 // 1. 从Redis缓存获取
Integer count = (Integer) redisTemplate.opsForValue().get(key); RBucket<Integer> bucket = redissonClient.getBucket(cacheKey);
if (count != null) { Integer cacheCount = bucket.get();
return count; if (cacheCount != null) {
return cacheCount;
} }
// 从数据库查询 // 2. 缓存未命中,从数据库查询最后一次累计抽数
count = userDrawRecordMapper.selectLastContinuousDraws(userId); // Integer dbCount = userDrawRecordMapper.selectLastContinuousDraws(userId);
count = count == null ? 0 : count; Integer dbCount = 0;
// 存入缓存 int finalCount = dbCount == null ? 0 : dbCount;
redisTemplate.opsForValue().set(key, count); // 3. 存入缓存(设置过期时间)
return count; 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) @Transactional(rollbackFor = Exception.class)
private void saveDrawRecord(Long userId, Long prizeId, int continuousDraws) { public void winPrizeAfter(PrizeOnline prizeOnline, User user,Integer drawPoint, int continuousDraws) {
UserDrawRecord record = new UserDrawRecord(); // 扣减积分
record.setUserId(userId); String traceId = IdManager.nextIdStr();
record.setPrizeId(prizeId); PointChangeLog pointChangeLog = accountService.drawPoint(prizeOnline, user, drawPoint, traceId);
record.setDrawTime(LocalDateTime.now()); // 记录用户抽奖记录
record.setContinuousDraws(continuousDraws); // UserDrawRecord record = new UserDrawRecord();
userDrawRecordMapper.insert(record); // 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<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);
}
} }
} }

View File

@@ -10,6 +10,7 @@ import com.ruoyi.cai.enums.ConsumeLogType;
import com.ruoyi.cai.enums.account.AccountChangeCodeEnum; import com.ruoyi.cai.enums.account.AccountChangeCodeEnum;
import com.ruoyi.cai.ws.bean.Room; import com.ruoyi.cai.ws.bean.Room;
import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.domain.PageQuery;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 用户账户Service接口 * 用户账户Service接口
@@ -31,6 +32,9 @@ public interface AccountService extends IService<Account> {
void withdrawFail(Long userId, Long incomeCoin, Long traceId); 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); PointRecordLog rechargePoint(RechargeOrder rechargeOrder, User user);
void recharge(ConsumeLog consumeLog); void recharge(ConsumeLog consumeLog);

View File

@@ -2,6 +2,8 @@ package com.ruoyi.cai.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cai.domain.PointChangeLog; import com.ruoyi.cai.domain.PointChangeLog;
import com.ruoyi.cai.domain.PrizeOnline;
import com.ruoyi.cai.domain.User;
/** /**
* 积分记录Service接口 * 积分记录Service接口
@@ -18,4 +20,6 @@ public interface PointChangeLogService extends IService<PointChangeLog> {
void adminChange(Long userId, Long givePoint); void adminChange(Long userId, Long givePoint);
void adminInvite(Long userId, Long givePoint, Long inviteUserId, String traceId); void adminInvite(Long userId, Long givePoint, Long inviteUserId, String traceId);
PointChangeLog drawPoint(PrizeOnline prizeOnline, User user, Integer drawPoint, String traceId);
} }

View File

@@ -3,6 +3,8 @@ package com.ruoyi.cai.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cai.domain.PrizeOnline; import com.ruoyi.cai.domain.PrizeOnline;
import java.util.List;
/** /**
* 已发布奖品Service接口 * 已发布奖品Service接口
* *
@@ -11,4 +13,5 @@ import com.ruoyi.cai.domain.PrizeOnline;
*/ */
public interface PrizeOnlineService extends IService<PrizeOnline> { public interface PrizeOnlineService extends IService<PrizeOnline> {
List<PrizeOnline> selectPrizeOnlineList(Integer gender);
} }

View File

@@ -222,6 +222,17 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Autowired @Autowired
private PointChangeLogService pointChangeLogService; 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) @Transactional(rollbackFor = Exception.class)
@Override @Override
public PointRecordLog rechargePoint(RechargeOrder rechargeOrder, User user){ public PointRecordLog rechargePoint(RechargeOrder rechargeOrder, User user){
@@ -229,6 +240,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
return null; return null;
} }
log.info("开始赠送积分 orderNo={}", rechargeOrder.getOrderNo()); log.info("开始赠送积分 orderNo={}", rechargeOrder.getOrderNo());
baseMapper.incrPoint(rechargeOrder.getUserId(), rechargeOrder.getGivePoint());
pointChangeLogService.rechargeOrderChange(rechargeOrder.getOrderNo(), rechargeOrder.getUserId(),rechargeOrder.getGivePoint()); pointChangeLogService.rechargeOrderChange(rechargeOrder.getOrderNo(), rechargeOrder.getUserId(),rechargeOrder.getGivePoint());
PointRecordLog pointRecordLog = pointRecordLogService.initOrder(rechargeOrder, user); PointRecordLog pointRecordLog = pointRecordLogService.initOrder(rechargeOrder, user);
return pointRecordLog; return pointRecordLog;

View File

@@ -2,6 +2,7 @@ package com.ruoyi.cai.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.cai.domain.PointChangeLog; import com.ruoyi.cai.domain.PointChangeLog;
import com.ruoyi.cai.domain.PrizeOnline;
import com.ruoyi.cai.domain.User; import com.ruoyi.cai.domain.User;
import com.ruoyi.cai.enums.point.PointChangeLogActionTypeEnum; import com.ruoyi.cai.enums.point.PointChangeLogActionTypeEnum;
import com.ruoyi.cai.enums.point.PointChangeTraceTypeEnum; 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.mapper.PointChangeLogMapper;
import com.ruoyi.cai.service.PointChangeLogService; import com.ruoyi.cai.service.PointChangeLogService;
import com.ruoyi.cai.service.UserService; import com.ruoyi.cai.service.UserService;
import com.ruoyi.common.utils.ServletUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -105,4 +107,21 @@ public class PointChangeLogServiceImpl extends ServiceImpl<PointChangeLogMapper,
pointChangeLog.setTraceId(traceId); pointChangeLog.setTraceId(traceId);
this.save(pointChangeLog); this.save(pointChangeLog);
} }
@Override
public PointChangeLog drawPoint(PrizeOnline prizeOnline, User user, Integer drawPoint, String traceId) {
PointChangeLog pointChangeLog = new PointChangeLog();
pointChangeLog.setActionType(PointChangeLogActionTypeEnum.USE.getCode());
pointChangeLog.setUserId(user.getId());
pointChangeLog.setUsercode(user.getUsercode());
String message = String.format("抽中【%s】", prizeOnline.getPrizeName());
pointChangeLog.setMessage(message);
pointChangeLog.setChangeValue(Long.valueOf(drawPoint));
pointChangeLog.setOperateIp(ServletUtils.getClientIP());
pointChangeLog.setIsAdmin(false);
pointChangeLog.setTraceLinkType(PointChangeTraceTypeEnum.USE.getCode());
pointChangeLog.setTraceId(traceId);
this.save(pointChangeLog);
return pointChangeLog;
}
} }

View File

@@ -141,7 +141,7 @@ public class PointRecordLogServiceImpl extends ServiceImpl<PointRecordLogMapper,
if(!pointRecordLog.getOneJoin()){ if(!pointRecordLog.getOneJoin()){
return; return;
} }
pointChangeLogService.adminInvite(pointRecordLog.getOneUserId(),pointRecordLog.getOnePoints(),pointRecordLog.getSourceUserId(),traceId); pointChangeLogService.rechargeOrderInviteChange(pointRecordLog.getOneUserId(),pointRecordLog.getOnePoints(),pointRecordLog.getSourceUserId(),traceId);
} }

View File

@@ -7,6 +7,9 @@ import com.ruoyi.cai.service.PrizeOnlineService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/** /**
* 已发布奖品Service业务层处理 * 已发布奖品Service业务层处理
* *
@@ -17,4 +20,9 @@ import org.springframework.stereotype.Service;
@Service @Service
public class PrizeOnlineServiceImpl extends ServiceImpl<PrizeOnlineMapper,PrizeOnline> implements PrizeOnlineService { public class PrizeOnlineServiceImpl extends ServiceImpl<PrizeOnlineMapper,PrizeOnline> implements PrizeOnlineService {
@Override
public List<PrizeOnline> selectPrizeOnlineList(Integer gender){
return new ArrayList<>();
}
} }

View File

@@ -127,7 +127,7 @@ public class RechargeOrderServiceImpl extends ServiceImpl<RechargeOrderMapper,Re
consumeLog.setType(ConsumeLogType.RECHARGE.getCode()); consumeLog.setType(ConsumeLogType.RECHARGE.getCode());
consumeLog.setAmount(rechargeOrder.getRechargeCoin()); consumeLog.setAmount(rechargeOrder.getRechargeCoin());
accountService.recharge(consumeLog); accountService.recharge(consumeLog);
PointRecordLog pointRecordLog = accountService.rechargePoint(rechargeOrder, user); PointRecordLog pointRecordLog = accountService.rechargePoint(rechargeOrder, user); // 处理充值积分
RechargeConsumerResp resp = new RechargeConsumerResp(); RechargeConsumerResp resp = new RechargeConsumerResp();
resp.setSuccess(true); resp.setSuccess(true);
resp.setConsumeLogId(consumeLog.getId()); resp.setConsumeLogId(consumeLog.getId());

View File

@@ -4,22 +4,5 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.cai.mapper.PrizeInfoMapper"> <mapper namespace="com.ruoyi.cai.mapper.PrizeInfoMapper">
<resultMap type="com.ruoyi.cai.domain.PrizeInfo" id="PrizeInfoResult">
<result property="id" column="id"/>
<result property="prizeName" column="prize_name"/>
<result property="prizeDesc" column="prize_desc"/>
<result property="prizeImg" column="prize_img"/>
<result property="winProbability" column="win_probability"/>
<result property="guaranteeDraws" column="guarantee_draws"/>
<result property="minWinDraws" column="min_win_draws"/>
<result property="stock" column="stock"/>
<result property="prizeType" column="prize_type"/>
<result property="prizePrice" column="prize_price"/>
<result property="autoGive" column="auto_give"/>
<result property="isThank" column="is_thank"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
</mapper> </mapper>

View File

@@ -4,24 +4,5 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.cai.mapper.PrizeOnlineMapper"> <mapper namespace="com.ruoyi.cai.mapper.PrizeOnlineMapper">
<resultMap type="com.ruoyi.cai.domain.PrizeOnline" id="PrizeOnlineResult">
<result property="id" column="id"/>
<result property="prizeId" column="prize_id"/>
<result property="gender" column="gender"/>
<result property="prizeName" column="prize_name"/>
<result property="prizeDesc" column="prize_desc"/>
<result property="prizeImg" column="prize_img"/>
<result property="winProbability" column="win_probability"/>
<result property="guaranteeDraws" column="guarantee_draws"/>
<result property="minWinDraws" column="min_win_draws"/>
<result property="stock" column="stock"/>
<result property="prizeType" column="prize_type"/>
<result property="prizePrice" column="prize_price"/>
<result property="autoGive" column="auto_give"/>
<result property="isThank" column="is_thank"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
</mapper> </mapper>