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

ASP.NET Core MVC 實戰:轉盤抽獎系統完整實現(前端動畫 + 後端概率算法 + 安全防作弊)

     編輯:關於C語言

系統架構概覽

  • 前端: HTML/CSS 轉盤 UI + jQuery Rotate 動畫 + AJAX 交互
  • 後端: ASP.NET Core MVC Controller + 概率算法服務 + 實體框架持久化
  • 核心邏輯: 後端集中計算中獎結果,前端僅負責視覺呈現,防止客戶端竄改

資料模型設計


public class Prize
{
    public int Id { get; set; }
    public string Name { get; set; }           // 獎品名稱
    public double Probability { get; set; }    // 權重/機率 (總和不需為 1)
    public int Level { get; set; }             // 獎等 (1=特獎 ... 7=安慰獎)
    public int Stock { get; set; }             // 庫存量 (-1 表無限)
    public bool IsActive { get; set; }         // 是否上架
}

後端核心:加權隨機算法


public interface ILotteryService
{
    LotteryResult Draw(int userId);
}

public class LotteryResult
{
    public bool Success { get; set; }
    public string ErrorMessage { get; set; }
    public Prize Prize { get; set; }
    public int Angle { get; set; } // 前端旋轉角度
}

public class WeightedLotteryService : ILotteryService
{
    private readonly AppDbContext _db;
    private static readonly Random _rng = new(); // 單例避免種子重複

    public WeightedLotteryService(AppDbContext db) => _db = db;

    public LotteryResult Draw(int userId)
    {
        // 1. 取得有效獎品池
        var prizes = _db.Prizes.Where(p => p.IsActive && (p.Stock == -1 || p.Stock > 0)).ToList();
        if (!prizes.Any()) return Fail("目前無可用獎品");

        // 2. 計算權重總和
        double totalWeight = prizes.Sum(p => p.Probability);
        if (totalWeight <= 0) return Fail("獎品權重配置錯誤");

        // 3. 產生隨機點
        double point = _rng.NextDouble() * totalWeight;

        // 4. 落點判定 (O(n) 線性掃描,獎品數少時足夠;大量獎品可用前綴和+二分搜尋 O(log n))
        double accum = 0;
        Prize hit = null;
        foreach (var p in prizes)
        {
            accum += p.Probability;
            if (point <= accum) { hit = p; break; }
        }

        // 5. 扣減庫存 (樂觀鎖/交易確保一致性)
        if (hit.Stock > 0)
        {
            hit.Stock--;
            _db.SaveChanges();
        }

        // 6. 映射角度 (前端扇區排序需與後端一致)
        int angle = MapLevelToAngle(hit.Level);
        
        return new LotteryResult { Success = true, Prize = hit, Angle = angle };
    }

    private int MapLevelToAngle(int level) => level switch
    {
        1 => 26,   // 特獎
        2 => 282,  // 一等獎
        3 => 185,  // 二等獎
        4 => 137,  // 三等獎
        5 => 337,  // 四等獎
        6 => 235,  // 五等獎
        7 => 88,   // 安慰獎
        _ => 0
    };

    private LotteryResult Fail(string msg) => new() { Success = false, ErrorMessage = msg };
}

Controller 端點


[ApiController]
[Route("api/lottery")]
public class LotteryController : ControllerBase
{
    private readonly ILotteryService _lottery;
    private readonly IUserService _user;

    public LotteryController(ILotteryService lottery, IUserService user)
    {
        _lottery = lottery;
        _user = user;
    }

    [HttpPost("draw")]
    public async Task Draw()
    {
        // 1. 身份驗證 (示範用 Session/JWT)
        var userId = GetCurrentUserId();
        if (userId == null) return Ok(new { code = 1, msg = "請先登入" });

        // 2. 業務檢查 (抽獎券、次數限制等)
        if (!await _user.CanDrawAsync(userId.Value))
            return Ok(new { code = -3, msg = "抽獎券不足" });

        // 3. 執行抽獎
        var result = _lottery.Draw(userId.Value);
        if (!result.Success) return Ok(new { code = -1, msg = result.ErrorMessage });

        // 4. 記錄中獎紀錄
        await _lottery.RecordAsync(userId.Value, result.Prize.Id);

        // 5. 回傳前端所需資料
        return Ok(new 
        { 
            code = 0, 
            angle = result.Angle, 
            prize = result.Prize.Name,
            id = result.Prize.Level // 對應前端 case
        });
    }
}

前端轉盤實現 (jQuery Rotate 插件)

HTML 結構


CSS 關鍵點:扇區角度與圖片對齊


.turntable-container { position: relative; width: 400px; height: 400px; margin: 0 auto; }
.pointer { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; cursor: pointer; }
.rotate { position: absolute; top: 0; left: 0; width: 100%; height: 100%; transform-origin: center center; }
/* 切圖時確保指針指向 0° 對應特獎區中心,Level→Angle 映射才會準確 */

JavaScript 交互邏輯


let isRotating = false;

$('#ptr').click(function() {
    if (isRotating) return;
    
    $.ajax({
        url: '/api/lottery/draw',
        type: 'POST',
        dataType: 'json',
        success: function(res) {
            if (res.code === 1) { alert(res.msg); return; } // 未登入
            if (res.code === -3) { alert(res.msg); return; } // 券不足
            if (res.code !== 0) { alert('系統錯誤'); return; }

            // 正常中獎流程
            isRotating = true;
            rotateWheel(res.angle, res.prize);
        },
        error: () => alert('網路錯誤')
    });
});

function rotateWheel(targetAngle, prizeName) {
    $('#wheel').rotate({
        angle: 0,
        animateTo: targetAngle + 1800, // 多轉 5 圈 (1800°) 增加懸念
        duration: 8000,                // 8 秒動畫
        easing: $.easing.easeOutCubic,
        callback: function() {
            $('#prize-name').text(`恭喜獲得:${prizeName}`);
            isRotating = false;
            // 可選:延遲刷新頁面更新餘額
            setTimeout(() => location.reload(), 3000);
        }
    });
}

安全與防作弊關鍵點

風險對策
前端 JS 竄改角度/獎品後端唯一權威決定結果,前端僅呈現
重放攻擊 / 多次點擊前端 isRotating 鎖 + 後端冪等 Token / Redis 分散式鎖
概率配置洩露不在前端輸出機率,僅回傳中獎結果
庫存超賣資料庫 RowLock / 樂觀鎖 (Version) / Redis Lua 原子扣減
內部人員操控抽獎日誌不可篡改 (Append-only)、定期稽核

擴充:大流量下的效能優化

  • 前綴和陣列 + 二分搜尋: 獎品超過 100 種時,將 O(n) 改為 O(log n)。
  • Redis Lua 腳本原子抽獎: 將權重陣列、庫存、隨機點計算、扣減合為單一原子操作,避免 DB 熱點。
  • 非同步寫入中獎記錄: 回傳結果後,背景任務寫入歷史表,縮短關鍵路徑。
  • CDN 快取靜態資源: 轉盤圖片、JS、CSS 走 CDN,減輕源站壓力。

總結

轉盤抽獎系統的核心在於「後端決定命運,前端演繹過程」。加權隨機算法簡單高效,配合嚴格的身份驗證、庫存扣減原子性、前端動畫與後端角度映射同步,即可構建公平、流暢、防作弊的抽獎體驗。記住:永遠不要信任客戶端傳來的任何參數。

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