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

PHP 實作可配置中獎機率的抽獎程式(加權隨機、陣列展開、效能優化)

     編輯:PHP 相關知識

需求分析

依機率分布抽獎:一等獎 0.12%、二等獎 3%、三等獎 12%、其餘「謝謝惠顧」。核心挑戰:機率精確控制、大量抽獎效能、機率動態調整。

方法一:陣列展開法(簡單、小規模適用)

function drawPrizeArrayExpansion(array $prizes, int $total = 10000): string {
    // $prizes = ['一等獎' => 0.12, '二等獎' => 3, '三等獎' => 12, '謝謝惠顧' => 84.88]; // 百分比
    $pool = [];
    foreach ($prizes as $prize => $percent) {
        $count = (int) round($total * $percent / 100);
        $pool = array_merge($pool, array_fill(0, $count, $prize));
    }
    shuffle($pool);
    return $pool[array_rand($pool)];
}
  • 優點:直觀、程式碼少。
  • 缺點:$total 需足夠大以精確表達小數機率(0.12% 需至少 10000)、記憶體隨 $total 線性增長、shuffle O(n) 耗時。

方法二:加權隨機算法(推薦、O(1) 空間、O(n) 時間、精確)

/**
 * 加權隨機抽獎
 * @param array $prizes ['獎項名' => 機率權重(整數或浮點數), ...]
 * @return string 中獎獎項
 */
function drawPrizeWeighted(array $prizes): string {
    // 1. 計算總權重
    $totalWeight = array_sum($prizes);
    if ($totalWeight <= 0) throw new InvalidArgumentException('Total weight must be > 0');

    // 2. 產生隨機點
    $rand = mt_rand(1, (int) $totalWeight * 10000) / 10000; // 支援小數權重

    // 3. 區間查找
    $accumulated = 0;
    foreach ($prizes as $prize => $weight) {
        $accumulated += $weight;
        if ($rand <= $accumulated) {
            return $prize;
        }
    }
    // 理論不會到達,保底回傳最後一項
    return array_key_last($prizes);
}

// 使用範例
$prizes = [
    '一等獎' => 0.12,
    '二等獎' => 3,
    '三等獎' => 12,
    '謝謝惠顧' => 84.88,
];

// 測試 100000 次驗證機率
$stats = [];
for ($i = 0; $i < 100000; $i++) {
    $stats[drawPrizeWeighted($prizes)]++;
}
print_r($stats);
/*
Array
(
    [一等獎] => 119   // 0.119% ≈ 0.12%
    [二等獎] => 3002  // 3.002% ≈ 3%
    [三等獎] => 12015 // 12.015% ≈ 12%
    [謝謝惠顧] => 84864 // 84.864% ≈ 84.88%
)
*/

方法三:別名法 / Vose's Alias Method(超大規模、O(1) 查找、預處理 O(n))

適合獎項數極多(>1000)、高頻抽獎(每秒萬次)場景。此處略過實作,可參考 php-alias-method 套件。

進階需求:防刷、日誌、庫存扣減

class LotteryService {
    private $pdo;
    private $prizes = [
        '一等獎' => ['weight' => 0.12, 'stock' => 10],
        '二等獎' => ['weight' => 3,    'stock' => 100],
        '三等獎' => ['weight' => 12,   'stock' => 500],
        '謝謝惠顧' => ['weight' => 84.88, 'stock' => INF],
    ];

    public function draw(int $userId): array {
        // 1. 防刷:Redis 限流 + 用戶每日上限
        $key = "lottery:user:$userId:" . date('Ymd');
        $count = $this->redis->incr($key);
        if ($count === 1) $this->redis->expire($key, 86400);
        if ($count > 5) throw new RuntimeException('Daily limit exceeded');

        // 2. 過濾無庫存獎項
        $available = [];
        foreach ($this->prizes as $name => $info) {
            if ($info['stock'] === INF || $info['stock'] > 0) {
                $available[$name] = $info['weight'];
            }
        }
        if (empty($available)) return ['prize' => '活動結束'];

        // 3. 抽獎
        $prize = drawPrizeWeighted($available);

        // 4. 扣減庫存(Redis 原子操作 + DB 最終一致性)
        if ($prize !== '謝謝惠顧') {
            $this->redis->decr("lottery:stock:$prize");
            // 異步寫入 DB 訂單表
        }

        // 5. 記錄日誌
        $this->log($userId, $prize);

        return ['prize' => $prize];
    }
}

機率配置最佳實踐

  • 資料庫儲存機率:prize_config 表存 name, weight, stock, start_time, end_time,後台可視化調整、即時生效。
  • 權重用整數:避免浮點精度問題,如 [12, 300, 1200, 8488] 對應萬分比。
  • 版本控制:每次調整機率產生新版本,支援灰度發布、回滾。
  • 審計日誌:記錄每次抽獎的 user_id, prize, weight_snapshot, timestamp, ip,防舞弊、支援爭議查證。

結語

抽獎核心在「加權隨機」演算法,陣列展開法適合原型開發,生產環境請使用加權隨機(區間查找),配合 Redis 防刷、庫存原子扣減、機率資料庫化管理、完整審計日誌,構建公平、高效、可審計的抽獎系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved