体育投注是全球博彩行业最大的细分市场,2026年市场规模预计超过800亿美元。一套完整的体育投注系统需要处理海量实时数据、动态赔率计算、高并发投注。
本文将从赔率引擎、数据源接入、风险控制、结算系统等核心环节,结合实际代码,拆解体育投注系统的技术实现。
┌─────────────────────────────────────────────────────────┐
│ 客户端层 │
│ Web (React) · App (Flutter) · H5 · 投注终端 │
└──────────────────────┬──────────────────────────────────┘
│ WSS/HTTP
┌──────────────────────▼──────────────────────────────────┐
│ 投注网关 (Gateway) │
│ 连接管理 · 验签 · 限流 · 抗重放 │
└──┬──────┬──────┬──────┬──────┬──────┬───────────────────┘
│ │ │ │ │ │
┌──▼┐ ┌──▼──┐ ┌▼───┐ ┌▼───┐ ┌▼───┐ ┌▼────────┐
│赛事│ │赔率 │ │投注 │ │结算 │ │风控 │ │数据源 │
│服务│ │引擎 │ │引擎 │ │中心 │ │系统 │ │适配器 │
└──┬┘ └──┬──┘ └──┬─┘ └──┬─┘ └──┬─┘ └──┬─────┘
│ │ │ │ │ │
┌──▼──────▼───────▼───────▼───────▼───────▼────────┐
│ 数据层 │
│ MySQL · Redis · Kafka · InfluxDB(赔率走势) │
└──────────────────────────────────────────────────┘
赔率引擎需要从多个数据源(如Sportradar、StatsPerform、BetRadar)获取赛事数据,结合平台利润模型计算最终赔率。下面是赔率计算的核心实现:
// odds/engine.go - 赔率计算引擎
type OddsEngine struct {
ProfitMargin float64 // 平台抽水比例,通常5-10%
MinOdds float64 // 最低赔率
MaxOdds float64 // 最高赔率
}
type MarketOdds struct {
MatchID string
MarketType string // 1X2 / handicap / over_under / correct_score
Home float64
Draw float64
Away float64
UpdatedAt int64
}
// 从外部源获取初始赔率,加入抽水后推送到客户端
func (e *OddsEngine) CalculateMarket(sourceOdds MarketOdds) MarketOdds {
// 1. 验证原始赔率
if sourceOdds.Home <= 1 || sourceOdds.Away <= 1 {
return sourceOdds // 无效赔率
}
// 2. 计算原始概率总和(应该接近1)
impliedProb := 1/sourceOdds.Home + 1/sourceOdds.Draw + 1/sourceOdds.Away
// 3. 加入抽水比例
margin := 1 + e.ProfitMargin
adjustedProb := impliedProb * margin
// 4. 按比例分配抽水
result := sourceOdds
result.Home = 1 / ((1/sourceOdds.Home) / impliedProb * adjustedProb)
result.Draw = 1 / ((1/sourceOdds.Draw) / impliedProb * adjustedProb)
result.Away = 1 / ((1/sourceOdds.Away) / impliedProb * adjustedProb)
// 5. 限制赔率范围
result.Home = clamp(result.Home, e.MinOdds, e.MaxOdds)
result.Draw = clamp(result.Draw, e.MinOdds, e.MaxOdds)
result.Away = clamp(result.Away, e.MinOdds, e.MaxOdds)
result.UpdatedAt = time.Now().UnixMilli()
return result
}
// 动态调整:根据投注量实时调整赔率
func (e *OddsEngine) DynamicAdjust(market MarketOdds, bets []Bet) MarketOdds {
var totalHome, totalDraw, totalAway float64
for _, bet := range bets {
switch bet.Selection {
case "home": totalHome += bet.Amount
case "draw": totalDraw += bet.Amount
case "away": totalAway += bet.Amount
}
}
total := totalHome + totalDraw + totalAway
if total == 0 {
return market
}
// 如果某选项投注额过高,降低该选项赔率,避免赔付风险
homeRatio := totalHome / total
if homeRatio > 0.5 {
market.Home -= 0.1 * (homeRatio - 0.5)
}
// 类似逻辑处理draw和away...
return market
}
func clamp(val, min, max float64) float64 {
if val < min { return min }
if val > max { return max }
return val
}
赔率引擎需要支持以下赔率格式转换:
| 格式 | 说明 | 示例 | 计算公式 |
|---|---|---|---|
| Decimal | 欧洲赔率 | 2.50 | 回报 = 本金 × 赔率 |
| Hong Kong | 香港赔率 | 1.50 | 回报 = 本金 × (1 + 赔率) |
| Malay | 马来赔率 | -0.67 | 负数表示需投注该额贏1 |
| Indo | 印尼赔率 | -2.00 | 负数表示需投注该额贏1 |
体育博彩需要对接多家数据提供商获取实时赛事和赔率数据。下面是Node.js实现的WebSocket数据源适配器:
// adapters/sportradar.js - 数据源接入适配器
const WebSocket = require('ws');
class SportradarAdapter {
constructor(apiKey) {
this.apiKey = apiKey;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnect = 10;
this.subscribers = new Map();
}
connect() {
this.ws = new WebSocket(`wss://push.sportradar.com/main?api_key=${this.apiKey}`);
this.ws.on('open', () => {
console.log('Sportradar 连接成功');
this.reconnectAttempts = 0;
// 订阅所有足球赛事实时赔率
this.ws.send(JSON.stringify({
action: "subscribe",
sport: "soccer",
markets: ["1x2", "handicap", "over_under", "correct_score"]
}));
});
this.ws.on('message', (data) => {
try {
const msg = JSON.parse(data);
this.handleMessage(msg);
} catch (e) {
console.error('消息解析失败:', e.message);
}
});
this.ws.on('close', () => {
if (this.reconnectAttempts < this.maxReconnect) {
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => this.connect(), delay);
this.reconnectAttempts++;
}
});
this.ws.on('error', (err) => {
console.error('Sportradar连接错误:', err.message);
});
}
handleMessage(msg) {
// 将外部数据转换为系统内部格式
if (msg.type === "odds_update") {
const internalOdds = {
matchId: msg.match_id,
marketType: this.normalizeMarket(msg.market),
home: parseFloat(msg.odds.home),
draw: parseFloat(msg.odds.draw),
away: parseFloat(msg.odds.away),
timestamp: Date.now()
};
this.subscribers.forEach((cb, key) => {
cb(internalOdds);
});
}
}
normalizeMarket(market) {
const mapping = {
"match_result": "1X2",
"asian_handicap": "handicap",
"total_goals": "over_under",
};
return mapping[market] || market;
}
onOddsUpdate(callback) {
const key = `sub_${Date.now()}`;
this.subscribers.set(key, callback);
return () => this.subscribers.delete(key);
}
}
投注引擎需要支持多种投注类型:单关、串关、滚球投注。下面是串关赔率计算:
// betting/parlay.go - 串关计算
type ParlayBet struct {
UserID int
Selections []Selection
Stake float64
TotalOdds float64
PotentialWin float64
}
type Selection struct {
MatchID string
Market string
Selection string // home/draw/away/over/under
Odds float64
Status string // pending/won/lost/void
}
func CalculateParlay(selections []Selection) float64 {
totalOdds := 1.0
for _, sel := range selections {
if sel.Odds > 1 {
totalOdds *= sel.Odds
}
}
return totalOdds
}
// 串关回滚逻辑:如果一场取消,串关按剩余场次重新计算
func RecalculateVoidedParlay(bet ParlayBet, voidedMatchID string) ParlayBet {
var activeSelections []Selection
for _, sel := range bet.Selections {
if sel.MatchID != voidedMatchID {
activeSelections = append(activeSelections, sel)
}
}
if len(activeSelections) >= 2 {
bet.TotalOdds = CalculateParlay(activeSelections)
} else {
// 不足2场,按单关退款
bet.TotalOdds = 1.0
}
bet.PotentialWin = bet.Stake * bet.TotalOdds
return bet
}
体育投注风控包括以下关键策略:
// risk/monitor.go - 套利检测
func DetectArbitrage(markets []MarketOdds) []ArbitrageOpportunity {
var opportunities []ArbitrageOpportunity
for i, m1 := range markets {
for j := i + 1; j < len(markets); j++ {
m2 := markets[j]
if m1.MatchID != m2.MatchID {
continue
}
// 计算无风险套利回报率
inv1 := 1/m1.Home + 1/m1.Draw + 1/m1.Away
inv2 := 1/m2.Home + 1/m2.Draw + 1/m2.Away
// 找出最优组合
best := math.Min(inv1, inv2)
if best > 0 {
returnRate := (1/best - 1) * 100
if returnRate > 2 { // 回报率超过2%的套利机会
opportunities = append(opportunities, ArbitrageOpportunity{
MatchID: m1.MatchID,
ReturnRate: returnRate,
})
}
}
}
}
return opportunities
}
滚球(In-Play)投注对延迟要求极高。系统需要在比赛进行中实时更新赔率并接受投注:
欧洲赔率又称十进制赔率,是国际最通用的赔率表示方式,简单直观。其核心逻辑是:回报额 = 投注本金 × 赔率。例如赔率2.50,投注100元,猜中后获得250元(含本金100元),净盈利150元。欧洲赔率的倒数代表市场隐含概率,例如赔率2.00对应的隐含概率为50%,赔率4.00对应25%。将所有选项隐含概率相加,得到的值减去1就是平台的抽水比例(Overround),通常在5%到12%之间。
亚洲盘口与欧洲赔率最大的区别在于引入了让球机制,将比赛的两个结果选项(上盘和下盘)的胜率拉到接近50%。以让半球(-0.5)盘口为例:如果强队让半球,投注上盘的玩家只有在强队净胜至少1球时才能赢;强队平局或输球则上盘全输,下盘全赢。亚洲盘口的精髓在于消除了平局这个结果——通过四分之一球(0/0.5)和四分之三球(0.75)等复杂盘口实现赢半输半的细腻赔付机制,这使得亚洲盘口的抽水通常比欧洲赔率更低(约2%到5%),对玩家更为友好。在实际系统开发中,亚洲盘口的赔率引擎需要处理盘口类型的自动生成和动态调整,包括平手盘(0)、平半盘(0/0.5)、半球盘(0.5)、半一盘(0.5/1)等十余种盘口类型的实时计算。
串关(Parlay/Accumulator)是将多场比赛的投注组合在一起,所有选择全部正确才能赢得奖金,串关赔率为各场赔率的乘积。以三串一为例:选择三场比赛的胜平负结果,赔率分别为2.00、1.80和2.50,则串关总赔率为2.00 × 1.80 × 2.50 = 9.00。投注100元,三场全部猜中可获得900元。串关的吸引力在于用较小的本金博取高额回报,但风险也呈指数级增长——三串一的胜率为各场胜率的乘积,假设每场胜率50%,三场全中的概率仅为12.5%。
实际运营中经常遇到串关中某场比赛因天气、安全问题等原因取消或延期的情况。此时系统需要自动进行串关拆分:如果剩余有效场次大于等于2场,则按剩余场次重新计算串关赔率;如果只剩1场,则按单关计算退还本金。例如一个四串一的投注,其中一场取消后自动变为三串一,总赔率重新计算。系统实现中,串关的拆分逻辑需要精确到每场比赛的状态变更事件——当数据源推送比赛取消事件时,投注引擎应实时重新计算受影响的所有串关投注,并将变更结果通过WebSocket推送给用户。同时,每张串关票据的原始赔率和调整后的赔率都应保留在数据库中,以便对账和用户查询。
滚球投注中,暂停期管理是维持平台公平性的关键技术环节。当比赛出现重大事件(如进球、红牌、点球、伤停等)时,系统必须在极短时间内关闭对应市场的投注通道,防止用户利用信息差获利。从技术实现角度看,暂停期管理依赖多个维度的协同:数据源推送事件信号后,赔率引擎立即将该比赛的所有滚球市场标记为暂停状态,投注网关拒绝任何新投注并返回清晰的状态提示,前端WebSocket推送市场关闭消息并在用户界面灰显对应的赔率按钮。暂停状态的恢复需要等待赔率引擎重新计算和推送调整后的赔率——以进球后为例,赔率引擎需要根据实时比分、剩余时间、控球率等重新生成赔率,这个过程通常在500毫秒到3秒内完成。
暂停期管理的另一个核心挑战是防止恶意用户利用系统延迟套利。常见攻击手段包括:在临进球前使用高延迟通道大量下注、通过多账户并行下单抢占暂停前窗口、在暂停期通过API直接提交已被关闭市场的投注等。应对方案包括:第一,投注网关层实施双阶段校验——第一步在内存中快速校验市场状态,第二步在数据库层做最终校验防止竞态条件;第二,采用投注序列号(Bet Sequence Number)机制,每个用户的投注请求附带时间戳和顺序号,系统严格按序处理;第三,建立投注时间与比赛事件的毫秒级回溯机制,任何发生在事件时间戳之后的投注请求自动判定为无效并退款。这一整套暂停期管理机制的核心指标是事件到关闭投注的端到端延迟——行业标准要求低于800毫秒,头部平台已优化到200毫秒以内。
青禾技术提供完整的皇冠体育系统搭建方案,支持PC端、移动端H5和原生APP。系统已对接多家国际体育数据提供商,赔率更新延迟低于1秒。支持中文、英文、泰语、越南语等多语种,多币种结算。
了解更多请访问 皇冠体育系统 页面。
📕 需要相关系统搭建服务?青禾技术提供一站式解决方案,欢迎咨询。
✈ Telegram: @guanshui549© 2026 青禾技术服务 | lilesc88.top