轉盤抽獎是行銷活動常見功能,核心挑戰在於:機率精確控制、獎品庫存扣減原子性、防刷與狀態一致性。本文提供生產級 PHP 實作方案,涵介面設計、機率演算法、資料庫事務與併發控制。
-- 獎品庫存表
CREATE TABLE prize_surplus (
id INT PRIMARY KEY AUTO_INCREMENT,
prize VARCHAR(50) NOT NULL,
surplus INT DEFAULT 0,
UNIQUE KEY uk_prize (prize)
);
-- 話費專用庫存(按日期)
CREATE TABLE telephone_charge_surplus (
id INT PRIMARY KEY AUTO_INCREMENT,
date DATE NOT NULL,
charge_surplus INT DEFAULT 0,
UNIQUE KEY uk_date (date)
);
-- 用戶積分表
CREATE TABLE zaszh_user (
id INT PRIMARY KEY,
answer_points INT DEFAULT 0
);
-- 積分消耗記錄
CREATE TABLE zaszh_answer_points_consume (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
points_consume INT,
consume_for VARCHAR(20),
create_date INT
);
-- 中獎記錄
CREATE TABLE zaszh_award (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
prize VARCHAR(50),
create_date INT
);
<?php
// 1. 參數驗證與初始化
$user_id = 1; // 實際應從 Session/Token 獲取
// 2. 獎品區塊配置(角度範圍 + 獎品名稱)
$arr_area = [
0 => ['min_angle' => 1, 'max_angle' => 45, 'prize' => '小米手機'],
1 => ['min_angle' => 46, 'max_angle' => 90, 'prize' => '拍立得'],
2 => ['min_angle' => 91, 'max_angle' => 135, 'prize' => '10元話費'],
3 => ['min_angle' => 136, 'max_angle' => 180, 'prize' => '5元話費'],
4 => ['min_angle' => 181, 'max_angle' => 225, 'prize' => '謝謝參與'],
5 => ['min_angle' => 226, 'max_angle' => 270, 'prize' => '謝謝參與'],
6 => ['min_angle' => 271, 'max_angle' => 315, 'prize' => '謝謝參與'],
7 => ['min_angle' => 316, 'max_angle' => 360, 'prize' => '謝謝參與'],
];
// 3. 機率抽獎演算法(兩階段權重)
$area_selected = [];
$num_rand = mt_rand(1, 10); // 1-10 權重基數
switch ($num_rand) {
case 1: $area_selected = $arr_area[0]; break; // 10% 小米
case 2: $area_selected = $arr_area[1]; break; // 10% 拍立得
case 3: $area_selected = $arr_area[2]; break; // 10% 10元話費
case 4: $area_selected = $arr_area[3]; break; // 10% 5元話費
default: // 60% 謝謝參與(再細分4個區塊)
$sub = mt_rand(1, 4);
$area_selected = $arr_area[3 + $sub];
break;
}
// 4. 資料庫連線
require('connect_database.php'); // 應回傳 $mysqli 實例
// 5. 事務處理:積分扣除 + 獎品發放
try {
$mysqli->begin_transaction();
// 5.1 扣除積分(原子操作:條件更新)
$sql = "UPDATE zaszh_user
SET answer_points = answer_points - 5
WHERE id = ? AND answer_points >= 5";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();
if ($mysqli->affected_rows === 0) {
throw new Exception('積分不足');
}
// 5.2 記錄積分消耗
$sql = "INSERT INTO zaszh_answer_points_consume
(user_id, points_consume, consume_for, create_date)
VALUES (?, 5, 'lottery', UNIX_TIMESTAMP())";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();
// 5.3 根據獎品類型處理庫存與發放
$prize = $area_selected['prize'];
$awarded = false;
switch ($prize) {
case '小米手機':
case '拍立得':
// 實物獎品:扣減 prize_surplus
$sql = "UPDATE prize_surplus
SET surplus = surplus - 1
WHERE prize = ? AND surplus > 0";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('s', $prize);
$stmt->execute();
if ($mysqli->affected_rows > 0) {
$awarded = recordAward($mysqli, $user_id, $prize);
} else {
$prize = '謝謝參與'; // 庫存不足降級
}
break;
case '10元話費':
case '5元話費':
// 話費:扣減 telephone_charge_surplus(按日期)
$amount = ($prize === '10元話費') ? 10 : 5;
$sql = "UPDATE telephone_charge_surplus
SET charge_surplus = charge_surplus - ?
WHERE date = CURDATE() AND charge_surplus >= ?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('ii', $amount, $amount);
$stmt->execute();
if ($mysqli->affected_rows > 0) {
$awarded = recordAward($mysqli, $user_id, $prize);
} else {
$prize = '謝謝參與';
}
break;
default:
$awarded = true; // 謝謝參與無需庫存
break;
}
$mysqli->commit();
// 6. 回傳結果
echo json_encode([
'status' => 'success',
'msg' => $prize,
'awarded' => $awarded
]);
} catch (Exception $e) {
$mysqli->rollback();
echo json_encode([
'status' => 'error',
'msg' => $e->getMessage()
]);
} finally {
$mysqli->close();
}
// 輔助函數:記錄中獎
function recordAward($mysqli, $user_id, $prize) {
$sql = "INSERT INTO zaszh_award (user_id, prize, create_date)
VALUES (?, ?, UNIX_TIMESTAMP())";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('is', $user_id, $prize);
return $stmt->execute() && $mysqli->affected_rows > 0;
}
?>
採用兩階段權重:
-- 關鍵:WHERE 條件包含庫存檢查,單條 SQL 完成「判斷+扣減」
UPDATE prize_surplus SET surplus = surplus - 1 WHERE prize = '小米手機' AND surplus > 0
優勢:
事務包含:積分扣除 → 消耗記錄 → 獎品庫存扣減 → 中獎記錄。任一步驟失敗全數回滾,確保:
<?php
// 活動開始前將庫存載入 Redis
$redis->set('prize:xiaomi:stock', 100);
$redis->set('prize:polaroid:stock', 50);
// 抽獎時原子遞減
$remaining = $redis->decr('prize:xiaomi:stock');
if ($remaining < 0) {
$redis->incr('prize:xiaomi:stock'); // 回滾
// 庫存不足處理
}
?>
CREATE TABLE lottery_config (
id INT PRIMARY KEY,
prize_name VARCHAR(50),
weight INT, -- 權重
min_angle INT,
max_angle INT,
daily_limit INT, -- 每日限量
total_limit INT -- 總限量
);
// 後端回傳獎品對應角度範圍
$angle = mt_rand($area_selected['min_angle'], $area_selected['max_angle']);
// 前端 CSS 動畫
transform: rotate(${angle}deg);
{
"status": "success",
"msg": "小米手機",
"angle": 23,
"awarded": true,
"remaining_points": 95
}
PHP 轉盤抽獎系統核心在於:
實戰中需根據流量規模選擇適當架構:中小型活動純 MySQL 事務足夠;大型高併發需引入 Redis 預扣庫存 + 異步寫入資料庫。安全性方面,務必加入防刷、簽名驗證與敏感操作審計日誌。