SpringBoot 實現多場景抽獎活動全攻略
作者:一安
本文將基于?SpringBoot?框架,詳細介紹如何實現多種常見的抽獎活動,提供開箱即用的技術方案,助力開發者快速搭建抽獎系統。
前言
在互聯網營銷場景中,抽獎活動是吸引用戶、提升用戶活躍度的有效方式。從簡單的隨機抽獎到復雜的概率抽獎、階梯抽獎等,不同類型的抽獎活動能滿足多樣化的運營需求。
本文將基于 SpringBoot 框架,詳細介紹如何實現多種常見的抽獎活動,提供開箱即用的技術方案,助力開發者快速搭建抽獎系統。
實現
效果圖
圖片
數據庫設計
#活動 ID、活動名稱、活動開始時間、活動結束時間、活動狀態(進行中、已結束等)、抽獎次數限制、活動描述
CREATE TABLE lottery_activity (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
activity_name VARCHAR(255) NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
status TINYINT NOT NULL COMMENT '0:進行中, 1:已結束',
description TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
#獎品 ID、活動 ID(關聯活動表)、獎品名稱、獎品數量、獎品圖片地址、中獎概率
CREATE TABLE lottery_prize (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
activity_id BIGINT NOT NULL,
prize_name VARCHAR(255) NOT NULL,
prize_count INT NOT NULL,
image_url VARCHAR(255),
probability DECIMAL(5, 2) NOT NULL COMMENT '中獎概率,范圍0-1',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (activity_id) REFERENCES lottery_activity(id)
);
#參與 ID、用戶 ID、活動 ID、抽獎時間、是否中獎、抽中獎品 ID
CREATE TABLE lottery_participation (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
activity_id BIGINT NOT NULL,
participate_time DATETIME DEFAULT CURRENT_TIMESTAMP,
is_winner TINYINT NOT NULL COMMENT '0:未中獎, 1:中獎',
prize_id BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (activity_id) REFERENCES lottery_activity(id),
FOREIGN KEY (prize_id) REFERENCES lottery_prize(id)
);功能實現
隨機抽獎
隨機抽獎是最基礎的抽獎方式,從所有獎品中隨機選取一個作為中獎結果
@Service
public class RandomLotteryService {
public LotteryPrize randomDraw(List<LotteryPrize> prizes) {
if (prizes == null || prizes.isEmpty()) {
return null;
}
Random random = new Random();
int index = random.nextInt(prizes.size());
return prizes.get(index);
}
}概率抽獎
概率抽獎根據每個獎品設置的中獎概率,按照概率分布決定用戶是否中獎以及抽中哪個獎品
@Service
public class ProbabilityLotteryService {
public LotteryPrize probabilityDraw(List<LotteryPrize> prizes) {
if (prizes == null || prizes.isEmpty()) {
return null;
}
// 計算總概率
double totalProbability = 0;
for (LotteryPrize prize : prizes) {
totalProbability += prize.getProbability();
}
// 生成0到總概率之間的隨機數
Random random = new Random();
double randomValue = random.nextDouble() * totalProbability;
// 根據概率權重選擇獎品
double currentProbability = 0;
for (LotteryPrize prize : prizes) {
currentProbability += prize.getProbability();
if (randomValue < currentProbability) {
return prize;
}
}
return null;
}
}階梯抽獎
階梯抽獎根據用戶的參與次數或消費金額等條件,設置不同的抽獎階梯,每個階梯對應不同的獎品池
@Service
public class TieredLotteryService {
public LotteryPrize tieredDraw(int drawCount, Map<Integer, List<LotteryPrize>> tieredPrizes) {
for (Map.Entry<Integer, List<LotteryPrize>> entry : tieredPrizes.entrySet()) {
if (drawCount >= entry.getKey()) {
List<LotteryPrize> prizes = entry.getValue();
if (prizes != null &&!prizes.isEmpty()) {
// 可以結合隨機抽獎或概率抽獎從對應獎品池中抽取獎品
RandomLotteryService randomLotteryService = new RandomLotteryService();
return randomLotteryService.randomDraw(prizes);
}
}
}
return null;
}
}抽獎流程整合
- 抽獎次數限制:通過
Redis原子操作記錄和檢查用戶抽獎次數 - 分布式鎖:使用
Redisson實現分布式鎖,防止并發問題 - 緩存預熱:活動開始前將獎品庫存和配置加載到
Redis - Redis 庫存扣減:使用
Lua腳本實現原子性庫存扣減,避免超賣 - 異步庫存更新:
Redis扣減后異步更新數據庫,減輕數據庫壓力
@Service
public class LotteryProcessService {
public static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyyMMdd");
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private DefaultRedisScript<Long> stockDeductLuaScript;
@Autowired
private DefaultRedisScript<Long> incrementIfNotExistLuaScript;
@Autowired
private RandomLotteryService randomLotteryService;
@Autowired
private ProbabilityLotteryService probabilityLotteryService;
@Autowired
private TieredLotteryService tieredLotteryService;
@Autowired
private LotteryActivityService lotteryActivityService;
@Autowired
private LotteryPrizeService lotteryPrizeService;
@Autowired
private LotteryParticipationService lotteryParticipationService;
@Autowired
private RedissonClient redissonClient;
private static final String STOCK_KEY_PREFIX = "lottery:stock:";
private static final String DRAW_COUNT_KEY_PREFIX = "lottery:drawCount:";
private static final String LOCK_KEY_PREFIX = "lottery:lock:";
public LotteryResult draw(Long userId, Long activityId, String lotteryType) {
// 活動校驗
LotteryActivity activity = lotteryActivityService.getById(activityId);
if (activity == null || activity.getStatus() != 0) {
return new LotteryResult(false, null, "活動不存在或已結束");
}
// 檢查用戶抽獎次數
if (!checkUserDrawCount(userId, activityId, activity.getDailyDrawLimit())) {
return new LotteryResult(false, null, "今日抽獎次數已用完");
}
// 獲取獎品列表
List<LotteryPrize> prizes = lotteryPrizeService.listByActivityId(activityId);
if (prizes == null || prizes.isEmpty()) {
return new LotteryResult(false, null, "獎品列表為空");
}
// 預熱獎品庫存到Redis
warmUpPrizesStock(activityId, prizes);
// 執行抽獎邏輯
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + activityId + ":" + userId);
try {
// 嘗試獲取鎖,等待10秒,自動釋放時間30秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
return new LotteryResult(false, null, "系統繁忙,請稍后再試");
}
// 從Redis中扣減庫存
LotteryPrize prize = deductStockAndDraw(userId, activityId, prizes, lotteryType);
if (prize == null) {
return new LotteryResult(false, null, "獎品已抽完");
}
// 記錄用戶參與抽獎信息
recordLotteryParticipation(userId, activityId, prize);
// 異步更新數據庫庫存
asyncUpdateDbStock(prize.getId(), 1);
return new LotteryResult(true, prize, "抽獎成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new LotteryResult(false, null, "系統繁忙,請稍后再試");
} finally {
lock.unlock();
}
}
/**
* 檢查用戶抽獎次數
*/
private boolean checkUserDrawCount(Long userId, Long activityId, int dailyLimit) {
String drawCountKey = DRAW_COUNT_KEY_PREFIX + userId + ":" + activityId + ":" + DTF.format(LocalDate.now());
// 使用Lua腳本原子性檢查并增加計數
Long count = redisTemplate.execute(incrementIfNotExistLuaScript,
Arrays.asList(drawCountKey), 24);
return count != null && count <= dailyLimit;
}
/**
* 預熱獎品庫存到Redis
*/
private void warmUpPrizesStock(Long activityId, List<LotteryPrize> prizes) {
for (LotteryPrize prize : prizes) {
String stockKey = STOCK_KEY_PREFIX + activityId + ":" + prize.getId();
// 如果Redis中不存在該獎品庫存,則從數據庫加載
if (!redisTemplate.hasKey(stockKey)) {
// 考慮到并發預熱,使用分布式鎖
RLock lock = redissonClient.getLock("lottery:warmup:" + stockKey);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
// 再次檢查,避免其他線程已經預熱
if (!redisTemplate.hasKey(stockKey)) {
// 從數據庫獲取當前庫存
Integer dbStock = lotteryPrizeService.getStockById(prize.getId());
if (dbStock != null) {
redisTemplate.opsForValue().set(stockKey, dbStock);
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
}
/**
* 扣減庫存并抽獎
*/
private LotteryPrize deductStockAndDraw(Long userId, Long activityId,
List<LotteryPrize> prizes, String lotteryType) {
// 過濾掉庫存為0的獎品
List<LotteryPrize> availablePrizes = new ArrayList<>();
for (LotteryPrize prize : prizes) {
String stockKey = STOCK_KEY_PREFIX + activityId + ":" + prize.getId();
Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (stock != null && stock > 0) {
availablePrizes.add(prize);
}
}
if (availablePrizes.isEmpty()) {
return null;
}
// 根據抽獎類型選擇抽獎算法
LotteryPrize selectedPrize;
switch (lotteryType) {
case"random":
selectedPrize = randomLotteryService.randomDraw(availablePrizes);
break;
case"probability":
selectedPrize = probabilityLotteryService.probabilityDraw(availablePrizes);
break;
case"tiered":
// 獲取用戶抽獎次數,用于階梯抽獎
String drawCountKey = DRAW_COUNT_KEY_PREFIX + userId + ":" + activityId + ":" + LocalDate.now();
Integer drawCount = (Integer) redisTemplate.opsForValue().get(drawCountKey);
if (drawCount == null) drawCount = 0;
// 獲取階梯獎品配置
Map<Integer, List<LotteryPrize>> tieredPrizes = getTieredPrizes(activityId);
selectedPrize = tieredLotteryService.tieredDraw(drawCount, tieredPrizes);
break;
default:
selectedPrize = null;
}
if (selectedPrize == null) {
return null;
}
// 使用Lua腳本原子性扣減庫存
String stockKey = STOCK_KEY_PREFIX + activityId + ":" + selectedPrize.getId();
Long result = redisTemplate.execute(stockDeductLuaScript, Arrays.asList(stockKey), 1);
// 返回扣減成功的獎品
return result != null && result > 0 ? selectedPrize : null;
}
/**
* 記錄用戶參與抽獎信息
*/
@Transactional
public void recordLotteryParticipation(Long userId, Long activityId, LotteryPrize prize) {
LotteryParticipation participation = new LotteryParticipation();
participation.setUserId(userId);
participation.setActivityId(activityId);
participation.setWinner(prize != null ? 1 : 0);
participation.setPrizeId(prize != null ? prize.getId() : null);
lotteryParticipationService.save(participation);
}
/**
* 異步更新數據庫庫存
*/
@Async
public void asyncUpdateDbStock(Long prizeId, int deductCount) {
lotteryPrizeService.deductStock(prizeId, deductCount);
}
/**
* 獲取階梯獎品配置
*/
private Map<Integer, List<LotteryPrize>> getTieredPrizes(Long activityId) {
// 從Redis緩存或數據庫獲取階梯獎品配置
String tieredPrizesKey = "lottery:tieredPrizes:" + activityId;
Map<Integer, List<LotteryPrize>> tieredPrizes =
(Map<Integer, List<LotteryPrize>>) redisTemplate.opsForValue().get(tieredPrizesKey);
if (tieredPrizes == null) {
// 從數據庫加載
tieredPrizes = lotteryPrizeService.getTieredPrizesByActivityId(activityId);
// 緩存到Redis,有效期24小時
if (tieredPrizes != null) {
redisTemplate.opsForValue().set(tieredPrizesKey, tieredPrizes, 24, TimeUnit.HOURS);
}
}
return tieredPrizes;
}
}責任編輯:武曉燕
來源:
一安未來



















