我来为你提供一个完整的抽奖系统案例,包含需求分析、数据库设计、核心代码实现和业务逻辑。

抽奖系统完整案例
需求分析
核心功能:
- 用户抽奖(普通抽/高级抽)
- 奖品管理(库存管理、概率控制)
- 中奖记录查询
- 抽奖概率设置
数据库设计
-- 奖品表 CREATE TABLE `prize` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '奖品名称', `level` int(4) NOT NULL COMMENT '奖品等级(1-特等奖,2-一等奖等)', `probability` decimal(5,2) NOT NULL COMMENT '中奖概率(百分比)', `quantity` int(11) NOT NULL DEFAULT 0 COMMENT '奖品数量', `remaining` int(11) NOT NULL DEFAULT 0 COMMENT '剩余数量', `image` varchar(255) DEFAULT NULL COMMENT '奖品图片', `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态(1-启用,0-停用)', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='奖品表'; -- 抽奖记录表 CREATE TABLE `lottery_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `prize_id` int(11) DEFAULT NULL COMMENT '奖品ID', `prize_name` varchar(50) DEFAULT NULL COMMENT '奖品名称', `is_win` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否中奖(0-未中奖,1-中奖)', `lottery_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '抽奖类型(1-普通抽,2-高级抽)', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖记录表';
核心代码实现 (Java)
抽奖算法实现
@Service
public class LotteryService {
private static final Logger logger = LoggerFactory.getLogger(LotteryService.class);
@Autowired
private PrizeMapper prizeMapper;
@Autowired
private LotteryRecordMapper recordMapper;
/**
* 执行抽奖
*/
@Transactional(rollbackFor = Exception.class)
public LotteryResult draw(Long userId, Integer lotteryType) {
// 1. 获取所有启用的奖品
List<Prize> prizeList = prizeMapper.selectEnabledPrizes();
// 2. 计算总概率
double totalProbability = prizeList.stream()
.mapToDouble(Prize::getProbability)
.sum();
// 3. 随机数生成
double randomValue = Math.random() * totalProbability;
// 4. 遍历确定中奖奖品
double cumulativeProbability = 0.0;
Prize selectedPrize = null;
for (Prize prize : prizeList) {
if (prize.getRemaining() <= 0) {
continue; // 跳过库存不足的奖品
}
cumulativeProbability += prize.getProbability();
if (randomValue <= cumulativeProbability) {
selectedPrize = prize;
break;
}
}
// 5. 处理抽奖结果
boolean isWin = selectedPrize != null;
String prizeName = isWin ? selectedPrize.getName() : "未中奖";
if (isWin) {
// 6. 扣减库存(乐观锁)
int updateResult = prizeMapper.decreaseStock(selectedPrize.getId());
if (updateResult == 0) {
throw new RuntimeException("奖品库存不足");
}
}
// 7. 保存抽奖记录
LotteryRecord record = new LotteryRecord();
record.setUserId(userId);
record.setPrizeId(isWin ? selectedPrize.getId() : null);
record.setPrizeName(prizeName);
record.setIsWin(isWin ? 1 : 0);
record.setLotteryType(lotteryType);
recordMapper.insert(record);
// 8. 返回结果
return LotteryResult.builder()
.isWin(isWin)
.prizeName(prizeName)
.prizeLevel(isWin ? selectedPrize.getLevel() : null)
.build();
}
}
概率控制算法
/**
* 加权随机算法 - 改进版
* 支持动态概率调整和库存检查
*/
public class WeightedRandomAlgorithm {
/**
* 根据权重随机选择奖品
* @param prizes 奖品列表
* @return 选中的奖品索引,如果没有则返回-1
*/
public static int selectPrize(List<Prize> prizes) {
// 过滤掉库存为0和未启用的奖品
List<Prize> availablePrizes = prizes.stream()
.filter(p -> p.getRemaining() > 0 && p.getStatus() == 1)
.collect(Collectors.toList());
if (availablePrizes.isEmpty()) {
return -1;
}
// 计算总权重
double totalWeight = availablePrizes.stream()
.mapToDouble(Prize::getProbability)
.sum();
// 生成随机数
double randomValue = Math.random() * totalWeight;
// 累积权重判断
double cumulativeWeight = 0.0;
for (int i = 0; i < availablePrizes.size(); i++) {
cumulativeWeight += availablePrizes.get(i).getProbability();
if (randomValue <= cumulativeWeight) {
return availablePrizes.get(i).getId();
}
}
return -1;
}
}
库存扣减(乐观锁方式)
@Mapper
public interface PrizeMapper {
@Select("SELECT * FROM prize WHERE status = 1")
List<Prize> selectEnabledPrizes();
/**
* 扣减库存 - 使用乐观锁防止超卖
*/
@Update("UPDATE prize SET remaining = remaining - 1 " +
"WHERE id = #{prizeId} AND remaining > 0")
int decreaseStock(@Param("prizeId") Integer prizeId);
}
抽奖概率配置
# application.yml 配置
lottery:
# 普通抽奖
normal:
daily-limit: 3 # 每日抽奖次数限制
points-cost: 100 # 每次消耗积分
# 高级抽奖
advanced:
daily-limit: 1 # 每日抽奖次数限制
points-cost: 300 # 每次消耗积分
# 奖品概率设置(示例数据)
prizes:
- name: iPhone 15 Pro
level: 1
probability: 0.5
quantity: 10
- name: iPad Mini
level: 2
probability: 2.0
quantity: 30
# ... 其他奖品
Controller层实现
@RestController
@RequestMapping("/api/lottery")
public class LotteryController {
@Autowired
private LotteryService lotteryService;
@Autowired
private UserService userService;
/**
* 执行抽奖
*/
@PostMapping("/draw")
public Result<LotteryResult> draw(@RequestParam("userId") Long userId,
@RequestParam("type") Integer lotteryType) {
try {
// 检查用户抽奖次数
int drawCount = lotteryService.getTodayDrawCount(userId, lotteryType);
int dailyLimit = lotteryType == 1 ? 3 : 1;
if (drawCount >= dailyLimit) {
return Result.error(400, "今日抽奖次数已用完");
}
// 扣减积分
int pointsCost = lotteryType == 1 ? 100 : 300;
boolean deductSuccess = userService.deductPoints(userId, pointsCost);
if (!deductSuccess) {
return Result.error(400, "积分不足");
}
// 执行抽奖
LotteryResult result = lotteryService.draw(userId, lotteryType);
return Result.success(result);
} catch (Exception e) {
logger.error("抽奖失败", e);
return Result.error(500, "抽奖失败");
}
}
/**
* 查询中奖记录
*/
@GetMapping("/records/{userId}")
public Result<List<LotteryRecord>> getRecords(@PathVariable("userId") Long userId) {
List<LotteryRecord> records = lotteryService.getUserRecords(userId);
return Result.success(records);
}
}
前端示例(Vue.js)
<template>
<div class="lottery-container">
<!-- 抽奖转盘 -->
<div class="wheel-section">
<canvas ref="wheelCanvas" width="400" height="400"></canvas>
<button @click="handleDraw" :disabled="isDrawing" class="draw-btn">
{{ isDrawing ? '抽奖中...' : '开始抽奖' }}
</button>
</div>
<!-- 奖品列表 -->
<div class="prize-list">
<h3>奖品列表</h3>
<div v-for="prize in prizes" :key="prize.id" class="prize-item">
<span>{{ prize.name }}</span>
<span v-if="prize.remaining > 0">剩余:{{ prize.remaining }}</span>
<span v-else class="sold-out">已抽完</span>
</div>
</div>
<!-- 中奖提示 -->
<el-dialog v-model="showResult" title="抽奖结果" width="300px">
<div class="result-content">
<el-icon v-if="result.isWin" class="win-icon" color="#67c23a">
<span>🎉</span>
</el-icon>
<div class="result-text">
{{ result.isWin ? `恭喜获得:${result.prizeName}` : '很遗憾,未中奖' }}
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const prizes = ref([])
const result = ref({})
const showResult = ref(false)
const isDrawing = ref(false)
// 获取奖品列表
const fetchPrizes = async () => {
try {
const res = await fetch('/api/prizes')
prizes.value = await res.json()
} catch (error) {
console.error('获取奖品失败:', error)
}
}
// 执行抽奖
const handleDraw = async () => {
if (isDrawing.value) return
isDrawing.value = true
try {
const res = await fetch('/api/lottery/draw', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 12345, type: 1 })
})
const data = await res.json()
if (data.code === 0) {
result.value = data.data
showResult.value = true
} else {
ElMessage.error(data.message)
}
} catch (error) {
ElMessage.error('抽奖失败,请稍后重试')
} finally {
isDrawing.value = false
}
}
onMounted(() => {
fetchPrizes()
})
</script>
注意事项与优化建议
| 方面 | 建议 |
|---|---|
| 并发处理 | 使用Redis分布式锁或乐观锁防止超卖 |
| 性能优化 | 奖品概率计算使用缓存,避免频繁查询数据库 |
| 安全防护 | 限制IP访问频率,防止恶意刷奖 |
| 异常处理 | 事务回滚保证数据一致性 |
| 监控告警 | 实时监控奖品库存,库存不足时及时预警 |
这个完整的抽奖系统案例涵盖了从数据库设计到前端展示的完整流程,你可以根据实际需求进行调整和扩展,需要我详细解释某个部分吗?