本文目录导读:

在PHP项目中实现抽奖功能,通常涉及概率算法设计、数据库存储、并发控制和安全性处理,下面我会提供一个完整、可运行的实现方案。
核心概念
抽奖功能的关键要素包括:
- 奖品列表与概率:每个奖品有一个中奖概率,所有概率之和为100%
- 库存控制:奖品数量有限,需要实时扣减
- 用户限制:防止同一用户重复中奖或刷奖
- 公平随机:使用可靠的随机算法
概率算法实现
经典概率算法
<?php
/**
* 根据概率抽取奖品
*
* @param array $prizes 奖品列表,每个元素包含prize_id, name, probability, stock
* @return array 中奖结果
*/
function drawPrize(array $prizes): array {
// 1. 过滤库存为0的奖品
$availablePrizes = array_filter($prizes, function($p) {
return $p['stock'] > 0;
});
if (empty($availablePrizes)) {
return ['code' => 0, 'message' => '奖品已全部抽完'];
}
// 2. 重置索引为连续数字
$availablePrizes = array_values($availablePrizes);
// 3. 计算概率总和(应接近100%)
$totalProbability = array_sum(array_column($availablePrizes, 'probability'));
if ($totalProbability <= 0) {
return ['code' => 0, 'message' => '概率配置错误'];
}
// 4. 生成随机数
$randNum = mt_rand(1, $totalProbability * 100) / 100;
// 5. 遍历奖品,累加概率,判断落在哪个区间
$current = 0;
foreach ($availablePrizes as $prize) {
$current += $prize['probability'];
if ($randNum <= $current) {
return [
'code' => 1,
'prize' => $prize,
'message' => '恭喜您抽中了' . $prize['name']
];
}
}
// 理论上不会到这里,但安全起见
return ['code' => 0, 'message' => '未中奖'];
}
// 示例奖品配置(概率总和为100%)
$prizes = [
['prize_id' => 1, 'name' => '一等奖 iPhone', 'probability' => 1, 'stock' => 2],
['prize_id' => 2, 'name' => '二等奖 平板', 'probability' => 5, 'stock' => 10],
['prize_id' => 3, 'name' => '三等奖 耳机', 'probability' => 14, 'stock' => 50],
['prize_id' => 4, 'name' => '四等奖 优惠券', 'probability' => 30, 'stock' => 200],
['prize_id' => 5, 'name' => '谢谢参与', 'probability' => 50, 'stock' => 9999],
];
$result = drawPrize($prizes);
print_r($result);
带权重的备选实现(支持非百分比权重)
function weightedRandom(array $items): ?array {
// 计算总权重
$totalWeight = array_sum(array_column($items, 'weight'));
if ($totalWeight <= 0) return null;
$random = mt_rand(1, $totalWeight);
$current = 0;
foreach ($items as $item) {
$current += $item['weight'];
if ($random <= $current) {
return $item;
}
}
return null;
}
数据库设计与存储
SQL设计示例
CREATE TABLE `prizes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '奖品名称', `probability` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '中奖概率', `total_stock` int(11) NOT NULL DEFAULT '0' COMMENT '总库存', `remaining_stock` int(11) NOT NULL DEFAULT '0' COMMENT '剩余库存', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1启用 0禁用', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_prize_records` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `prize_id` int(11) NOT NULL COMMENT '奖品ID', `prize_name` varchar(100) NOT NULL COMMENT '奖品名称', `draw_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '抽奖时间', `ip_address` varchar(50) DEFAULT NULL COMMENT 'IP地址', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0待领取 1已领取 2已过期', `expire_time` datetime DEFAULT NULL COMMENT '过期时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_draw_time` (`draw_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
完整抽奖服务类
<?php
class LotteryService
{
private $db; // PDO实例或数据库连接
private $config;
public function __construct($db, $config = [])
{
$this->db = $db;
$this->config = array_merge([
'daily_limit' => 3, // 每日抽奖次数限制
'cooldown_seconds' => 5, // 抽奖间隔(秒)
'debug' => false
], $config);
}
/**
* 执行抽奖
*/
public function draw(int $userId): array
{
// 1. 检查用户当日抽奖次数
if (!$this->checkDailyLimit($userId)) {
return ['success' => false, 'message' => '今日抽奖次数已用完'];
}
// 2. 检查抽奖间隔
if (!$this->checkCooldown($userId)) {
return ['success' => false, 'message' => '操作太快,请稍后再试'];
}
// 3. 获取可用奖品列表(库存>0且启用)
$prizes = $this->getAvailablePrizes();
if (empty($prizes)) {
return ['success' => false, 'message' => '暂无可用奖品'];
}
// 4. 概率抽奖
$result = $this->calculatePrize($prizes);
$prize = $result['prize'] ?? null;
try {
// 5. 使用事务保证库存一致性
$this->db->beginTransaction();
if ($prize && $prize['prize_id'] > 0) {
// 检查是否有特殊限制(如一等奖限制)
if ($prize['is_rare'] ?? false) {
if (!$this->checkRarePrizeLimit($userId, $prize['prize_id'])) {
// 一等奖已被人抽中,降级处理
$prize = $this->getFallbackPrize($prizes);
}
}
// 扣减库存
$affected = $this->deductStock($prize['prize_id']);
if ($affected === 0) {
// 库存不足,回退
$this->db->rollBack();
return ['success' => false, 'message' => '奖品已被抽完'];
}
// 记录中奖记录
$this->recordWin($userId, $prize['prize_id'], $prize['name']);
} else {
// 未中奖,记录参与记录(谢谢参与)
$this->recordLose($userId);
}
$this->db->commit();
return [
'success' => true,
'is_win' => ($prize && $prize['prize_id'] > 0),
'prize_name' => $prize['name'] ?? '谢谢参与',
'prize_id' => $prize['prize_id'] ?? 0
];
} catch (\Exception $e) {
$this->db->rollBack();
if ($this->config['debug']) {
throw $e;
}
return ['success' => false, 'message' => '系统繁忙,请稍后再试'];
}
}
/**
* 检查每日抽奖次数
*/
private function checkDailyLimit(int $userId): bool
{
$today = date('Y-m-d');
$sql = "SELECT COUNT(*) FROM user_prize_records
WHERE user_id = ? AND DATE(draw_time) = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $today]);
$count = (int) $stmt->fetchColumn();
return $count < $this->config['daily_limit'];
}
/**
* 检查抽奖间隔
*/
private function checkCooldown(int $userId): bool
{
$sql = "SELECT MAX(draw_time) FROM user_prize_records
WHERE user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId]);
$lastDraw = $stmt->fetchColumn();
if (!$lastDraw) return true;
$diff = time() - strtotime($lastDraw);
return $diff >= $this->config['cooldown_seconds'];
}
/**
* 获取可用奖品
*/
private function getAvailablePrizes(): array
{
$sql = "SELECT id as prize_id, name, probability, remaining_stock
FROM prizes
WHERE status = 1 AND remaining_stock > 0";
$stmt = $this->db->query($sql);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* 概率计算核心
*/
private function calculatePrize(array $prizes): array
{
// 对概率进行处理(可能需要调整使总和等于100%)
$total = array_sum(array_column($prizes, 'probability'));
if ($total <= 0) return ['prize' => null];
$rand = mt_rand(1, $total * 100) / 100;
$current = 0;
foreach ($prizes as $prize) {
$current += $prize['probability'];
if ($rand <= $current) {
return ['prize' => $prize];
}
}
return ['prize' => null];
}
/**
* 扣减库存(带行锁保护)
*/
private function deductStock(int $prizeId): int
{
$sql = "UPDATE prizes
SET remaining_stock = remaining_stock - 1
WHERE id = ? AND remaining_stock > 0";
$stmt = $this->db->prepare($sql);
$stmt->execute([$prizeId]);
return $stmt->rowCount();
}
/**
* 记录中奖
*/
private function recordWin(int $userId, int $prizeId, string $prizeName): void
{
$sql = "INSERT INTO user_prize_records
(user_id, prize_id, prize_name, status, expire_time)
VALUES (?, ?, ?, 0, DATE_ADD(NOW(), INTERVAL 7 DAY))";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $prizeId, $prizeName]);
}
/**
* 记录未中奖
*/
private function recordLose(int $userId): void
{
$sql = "INSERT INTO user_prize_records
(user_id, prize_id, prize_name, status)
VALUES (?, 0, '谢谢参与', 0)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId, '谢谢参与']);
}
/**
* 检查稀有奖品限制(例如一等奖只允许一个人中)
*/
private function checkRarePrizeLimit(int $userId, int $prizeId): bool
{
$sql = "SELECT COUNT(*) FROM user_prize_records
WHERE prize_id = ? AND prize_id != 0";
$stmt = $this->db->prepare($sql);
$stmt->execute([$prizeId]);
return (int)$stmt->fetchColumn() === 0;
}
/**
* 降级奖品(一等奖已被领走时给二等奖)
*/
private function getFallbackPrize(array $prizes): array
{
// 排除一等奖,返回二等奖
foreach ($prizes as $p) {
if ($p['probability'] > 1) {
return $p;
}
}
return $prizes[array_rand($prizes)];
}
}
并发控制与安全性
使用Redis锁防止超发
// 在deductStock前检查Redis分布式锁
public function drawWithRedisLock(int $userId): array
{
$lockKey = 'lottery:draw:' . $userId;
$lock = $this->redis->setnx($lockKey, time(), 3); // 3秒超时
if (!$lock) {
return ['success' => false, 'message' => '操作太频繁'];
}
try {
return $this->draw($userId);
} finally {
$this->redis->del($lockKey);
}
}
用户抽奖限制检查
// 用Redis记录用户抽奖次数(比数据库快)
public function getUserDailyDraws(int $userId): int
{
$key = 'lottery:daily:' . $userId . ':' . date('Ymd');
$count = $this->redis->get($key);
if ($count === false) {
// 从数据库同步
$count = $this->db->getDailyDraws($userId);
$this->redis->setex($key, 86400, $count);
}
return (int)$count;
}
防止脚本刷奖
// 检查IP频率
public function checkIpFrequency(string $ip): bool
{
$key = 'lottery:ip:' . md5($ip);
$count = $this->redis->incr($key);
if ($count === 1) {
$this->redis->expire($key, 60); // 60秒内
}
return $count <= $this->config['ip_limit_per_minute'];
}
// 检查设备指纹
public function checkDeviceFingerprint(string $fingerprint): bool
{
$key = 'lottery:device:' . $fingerprint;
return (int)$this->redis->get($key) < $this->config['device_daily_limit'];
}
完整调用示例
<?php
// 1. 数据库配置
$pdo = new PDO('mysql:host=localhost;dbname=lottery;charset=utf8mb4', 'root', 'password');
// 2. 初始化服务
$lottery = new LotteryService($pdo, [
'daily_limit' => 5,
'cooldown_seconds' => 3
]);
// 3. 假设用户已登录
$userId = 123;
// 4. 检查是否还有抽奖资格
if (!$lottery->checkDailyLimit($userId)) {
die('今日抽奖次数已用完');
}
// 5. 抽奖
$result = $lottery->draw($userId);
// 6. 返回结果
if ($result['success']) {
if ($result['is_win']) {
echo "🎉 恭喜您获得:" . $result['prize_name'];
// 这里可以引导用户填写收货地址
} else {
echo "😅 谢谢参与,下次加油!";
}
} else {
echo "❌ 抽奖失败:" . $result['message'];
}
注意事项
-
概率精度:建议使用浮点数(如decimal(5,2))存储概率,计算时转换为整数处理小数误差
-
库存原子性:一定要在数据库层面使用行锁或乐观锁保证库存扣减的原子性
-
回退机制:高并发场景下,当用户抽中一等奖后发现已被领走,需要有一个降级容错机制
-
测试环境:建议在测试环境使用固定的随机种子进行概率测试
-
日志记录:记录所有抽奖日志,包括IP、设备信息、用户ID、抽奖结果等,便于事后检查和防刷
-
法律合规:确保抽奖活动符合当地法律法规关于「有奖销售」的规定
扩展优化思路
- 按固定时间段开放:用定时任务控制奖品的生效时段
- 权重动态调整:根据用户等级、活跃度调整中奖概率
- 保底机制:连续N次不中后必中一个奖品(使用计数器逻辑)
- 奖品分级放量:越早参与中奖率越高,后续递减
希望这个完整的实现方案能帮到你!如果需要针对特定场景(如微信公众号抽奖、高并发秒杀场景)的优化,可以进一步讨论。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播