首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 實作加權隨機抽獎演算法:機率可控、程式碼可複用的完整方案

     編輯:PHP 綜合知識

抽獎系統的核心在於「加權隨機」:每個獎項擁有不同中獎機率,總和為 100%。本文提供一個經典、高效且易擴充的 PHP 實作,採用「區間映射法」,將機率轉換為整數區間,隨機生成一個數字落入哪個區間即中該獎。適合活動行銷、遊戲掉寶、優惠券發放等場景。

核心演算法邏輯

  1. 定義獎項陣列 $prize[key] = 機率%,總和 = 100
  2. 將機率 × 放大倍數(如 10 或 100)轉為整數,建立累積區間陣列 $prizeZone
  3. 隨機取得 mt_rand(0, $max-1)
  4. 遍歷區間陣列,找出隨機數落入的區間,回傳對應獎項 Key

完整可執行程式碼

<?php
/**
 * 加權隨機抽獎函數
 *
 * @param array  $prizes      獎項機率陣列 [key => 機率%],總和需為 100
 * @param array  $prizeList   獎項詳細資訊 [key => ['name', 'desc', ...]]
 * @param int    $precision   放大精度(10=小數點1位,100=小數點2位)
 * @return mixed  中獎獎項的 key,若失敗回傳 false
 */
function weighted_lottery(array $prizes, array $prizeList, int $precision = 10) {
    // 1. 驗證機率總和
    $totalProb = array_sum($prizes);
    if (abs($totalProb - 100) > 0.0001) {
        trigger_error("獎項機率總和必須為 100%,目前為:$totalProb%", E_USER_WARNING);
        return false;
    }

    // 2. 建立累積區間
    $prizeZone = [];
    $max = 0;
    foreach ($prizes as $k => $v) {
        $max += $v * $precision;
        $prizeZone[] = ['max' => $max, 'key' => $k];
    }
    $max--; // 最大隨機值

    // 3. 產生隨機數
    $rand = mt_rand(0, $max);

    // 4. 二分搜尋區間(效能優於線性遍歷,獎項多時顯著)
    $low = 0;
    $high = count($prizeZone) - 1;
    while ($low <= $high) {
        $mid = (($low + $high) >> 1);
        if ($rand > $prizeZone[$mid]['max']) {
            $low = $mid + 1;
        } else {
            $high = $mid - 1;
        }
    }
    $winnerKey = $prizeZone[$low]['key'] ?? false;

    return $winnerKey;
}

/* ===== 使用範例 ===== */

// 獎項機率設定(總和 100%)
$prizes = [
    1 => 3.5,   // 3.5%
    2 => 2.5,   // 2.5%
    3 => 6,     // 6%
    4 => 2,     // 2%
    5 => 42,    // 42%(謝謝參與)
    6 => 36,    // 36%
    7 => 2,     // 2%
    8 => 4,     // 4%
    9 => 2,     // 2%
];

// 獎項詳細資料
$prizeList = [
    1 => ['name' => '小仙子',           'type' => 'virtual'],
    2 => ['name' => '5 Q幣',             'type' => 'currency'],
    3 => ['name' => '38,888 遊戲幣',     'type' => 'coin'],
    4 => ['name' => '88,888 遊戲幣',     'type' => 'coin'],
    5 => ['name' => '謝謝參與!',         'type' => 'nothing'],
    6 => ['name' => '歡迎下次再來!',     'type' => 'nothing'],
    7 => ['name' => '10 Q幣',             'type' => 'currency'],
    8 => ['name' => '888 銀元寶(綁定)',   'type' => 'item'],
    9 => ['name' => '隨機高級坐騎(綁定)', 'type' => 'item'],
];

// 執行抽獎
$winnerKey = weighted_lottery($prizes, $prizeList, 10); // 精度 10(支援小數點 1 位)

if ($winnerKey !== false) {
    $prize = $prizeList[$winnerKey];
    echo "🎉 恭喜中獎:{$prize['name']} (Key: $winnerKey)";
    // 這裡可接入資料庫寫入中獎紀錄、扣除庫存、發送通知等邏輯
} else {
    echo "抽獎失敗,請檢查機率設定。";
}
?>

進階擴充功能

1. 動態調整機率(後台管理介面)

// 從資料庫讀取獎項設定
$prizes = [];
$prizeList = [];
$stmt = $pdo->query("SELECT `id`, `probability`, `name`, `type`, `stock` FROM `lottery_prizes` WHERE `status`=1");
while ($row = $stmt->fetch()) {
    $prizes[$row['id']] = (float)$row['probability'];
    $prizeList[$row['id']] = $row;
}

2. 庫存控制(獎項送完自動下架)

function weighted_lottery_with_stock(array $prizes, array $prizeList, int $precision = 10) {
    // 過濾掉庫存為 0 的獎項
    $available = [];
    foreach ($prizes as $k => $v) {
        if (($prizeList[$k]['stock'] ?? 1) > 0) {
            $available[$k] = $v;
        }
    }
    // 重新正規化機率(使總和仍為 100%)
    $sum = array_sum($available);
    if ($sum === 0) return false; // 全部送完
    foreach ($available as $k => $v) {
        $available[$k] = round($v * 100 / $sum, 4);
    }
    // 微調最後一項確保總和精確 100
    $keys = array_keys($available);
    $lastKey = end($keys);
    $available[$lastKey] = 100 - array_sum(array_slice($available, 0, -1));

    return weighted_lottery($available, $prizeList, $precision);
}

3. 防刷機制(同一用戶/ IP 限制)

// Redis 實作:每日限抽 3 次
$redisKey = "lottery:limit:{$userId}:".date('Ymd');
$todayCount = $redis->incr($redisKey);
if ($todayCount === 1) $redis->expire($redisKey, 86400);
if ($todayCount > 3) {
    die(json_encode(['code' => 429, 'msg' => '今日抽獎次數已用完']));
}

4. 中獎紀錄寫入(事務保證一致性)

$pdo->beginTransaction();
try {
    // 1. 扣減庫存
    $pdo->prepare("UPDATE `lottery_prizes` SET `stock`=`stock`-1 WHERE `id`=? AND `stock`>0")->execute([$winnerKey]);
    // 2. 寫入中獎記錄
    $pdo->prepare("INSERT INTO `lottery_log` (`user_id`, `prize_id`, `prize_name`, `created_at`) VALUES (?, ?, ?, NOW())")
        ->execute([$userId, $winnerKey, $prizeList[$winnerKey]['name']]);
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
    // 記錄錯誤日誌
}

效能與精度說明

精度最大區間值適用場景記憶體佔用
10 (0.1%)1,000一般活動、遊戲掉寶極低
100 (0.01%)10,000精細機率控制低
1000 (0.001%)100,000極高精度需求中

建議:一般商業活動 precision = 100 即足夠(支援 0.01% 機率),且 mt_rand 最大值 2^31-1 遠大於所需區間。


常見陷阱與除錯

機率總和不等於 100
函數內建檢查,會觸發 Warning 並回傳 false。上線前請用單元測試驗證。
浮點數精度導致區間計算偏差
使用整數運算(機率 × precision),避免浮點數誤差累積。
獎項 Key 非連續整數
演算法使用 Key 作為識別,支援任意字串/整數 Key,不需連續。
高併發下庫存超賣
必須在資料庫層面使用 UPDATE ... WHERE stock > 0 + 事務,或引入 Redis Lua 腳本原子扣減。

單元測試範例

function test_lottery_distribution() {
    $prizes = ['A' => 70, 'B' => 20, 'C' => 10];
    $prizeList = ['A' => ['name' => 'A'], 'B' => ['name' => 'B'], 'C' => ['name' => 'C']];
    $counts = ['A' => 0, 'B' => 0, 'C' => 0];
    $trials = 100000;

    for ($i = 0; $i < $trials; $i++) {
        $k = weighted_lottery($prizes, $prizeList, 10);
        $counts[$k]++;
    }

    foreach ($counts as $k => $v) {
        $actual = round($v / $trials * 100, 2);
        $expected = $prizes[$k];
        echo "$k: 期望 $expected%, 實際 $actual% (誤差 " . abs($actual - $expected) . "%)\n";
    }
}
// 執行測試:誤差通常在 ±0.2% 內,符合統計學預期
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved