openlivegame · 商户对接文档(Dev)

游戏供应商对接与游戏数据监控平台。本文档定义了商户与 openlivegame 之间的 双向 HTTP 协议:商户通过 Provider API 获取游戏启动链接, openlivegame 通过 Seamless Wallet 回调实时完成余额变动。

Dev 环境 HTTP / Form MD5 双向签名 Seamless Wallet
Dev 环境域名
Provider API(商户 → openlivegame)https://merchant.caca789.com/provider/v1
玩家游戏入口(gameURL 落地)https://pp-client.caca789.com
Wallet 回调(openlivegame → 商户)由商户提供 callback_url,例如 https://merchant-wallet.example.com/wallet

概述 #

openlivegame 采用 Seamless Wallet(无缝钱包)集成模式。商户保留玩家资金账户的唯一真实来源, 玩家在游戏中的每一次下注 / 派奖 / 退款都由 openlivegame 实时回调商户钱包接口完成扣款与加款, openlivegame 自身不托管任何真实资金。

① 商户 → openlivegame

商户调用 Provider API 获取玩家游戏启动 URL,将玩家导向游戏客户端。

② openlivegame → 商户

openlivegame 在玩家下注结算过程中回调商户 Wallet API 完成资金变动。

③ 双向安全

两侧均使用统一的 MD5 签名算法 + 商户密钥验证请求真实性。

系统架构 #

整体交互分为同步链路(商户主动)与异步链路(openlivegame主动回调):

┌──────────────┐         ①  POST /provider/v1/gameurl        ┌──────────────┐
│              │ ────────────────────────────────────────▶   │              │
│              │ ◀────────────────────────────────────────   │              │
│              │         返回 gameURL (含 JSESSIONID)         │              │
│   商户       │                                              │   openlivegame    │
│   Merchant   │         ②  玩家跳转游戏客户端 (WS)            │   Platform   │
│              │                                              │              │
│              │ ◀────────────────────────────────────────   │              │
│              │    ③  POST {callback_url}/authenticate       │              │
│              │    ④  POST {callback_url}/bet / result ...   │              │
│              │ ────────────────────────────────────────▶   │              │
└──────────────┘         返回 cash / transactionId            └──────────────┘

商户上下文字段

openlivegame 为每个商户保存以下关键配置,请在对接前向 openlivegame 索取或提交:

字段用途来源
secureLogin商户 API 身份标识,Provider API 中用来定位商户openlivegame 分配
secretKey双向签名密钥,禁止泄露openlivegame 分配
callback_url商户 Seamless Wallet 回调基础 URL,例如 https://merchant-wallet.example.com/wallet商户提供
ip_whitelist调用 Provider API 的出口 IP 白名单(逗号分隔,可选)商户提供
currency商户使用的币种(须在平台货币支持列表内)商户提供

完整对接时序 #

1
商户生成玩家 token 商户为登录后的玩家生成一个短期有效、一次性的 token(建议 5–30 分钟有效期), 并将该 token 与玩家绑定,后续 Authenticate 回调凭此查询玩家。
2
商户调用 POST /provider/v1/gameurl 传入 secureLogintokenexternalPlayerIdcurrencygameId(可选), 以及按签名规则计算得到的 hash
3
openlivegame 验签后回调商户 POST {callback_url}/authenticate openlivegame 用同一份 token 反向询问商户,商户必须返回 userId(需 == externalPlayerId)、 currency 和当前余额 cash
4
openlivegame 返回 gameURL URL 中附带平台会话 JSESSIONID,商户将玩家 302 跳转至该 URL 即可进入游戏。
5
游戏中实时资金交互 玩家每次下注、派奖、退款时,openlivegame 主动回调商户对应接口(/bet / /result / /refund), 商户执行账户变动后返回最新余额。
6
必要时查询余额 openlivegame 在会话恢复 / 余额异常场景会调用 POST {callback_url}/balance 拉取最新余额。

货币支持 #

当前平台支持以下币种。gameurlcurrency 入参与 authenticate 回调返回的 currency 都必须取自此列表:

currency名称符号说明
USD美元$基准币种
EUR欧元
BRL巴西雷亚尔R$
IDR印尼盾Rp
INR印度卢比
USDT泰达币$按 1:1 锚定 USD 处理
注意:
  • 币种需在 openlivegame 平台与游戏上游双侧开通后方可投运(限红、投注档位均按币种独立配置)。 传入未开通的 currency 可能导致进入游戏后限红 / 余额显示异常,请勿自行尝试列表外币种。
  • 需要接入新币种时请提前联系 openlivegame 运营开通,开通后此列表同步更新。
  • 一个 externalPlayerId 终身绑定一种币种(详见 gameurl 的「一用户一币种」约束)。

Provider API · openlivegame 提供给商户 #

基础地址:https://merchant.caca789.com/provider/v1。 下文各端点路径(如 /gameurl)均为相对于该基础地址的相对路径, 实际请求 URL 形如 https://merchant.caca789.com/provider/v1/gameurl

所有接口均为 HTTP POST(除 healthcheck)、 Content-Type: application/x-www-form-urlencoded。 签名字段名为 hash,算法见 签名算法

返回格式统一为 JSON,error 为字符串错误码,"0" 表示成功。

路径说明: Provider API 统一使用 /provider/v1/* 前缀,本环境只接受该前缀, 历史路径一律不可用。
POST /gameurl 获取玩家游戏启动链接

商户侧玩家发起进入游戏请求时调用此接口。openlivegame 将通过商户预留的 callback_url 反向回调 authenticate 验证玩家身份,验证通过后返回一个附带 JSESSIONID 的游戏客户端 URL。

请求参数

字段类型必填说明
secureLoginstring必填商户 API 身份标识
tokenstring必填商户侧生成的玩家一次性 token,将透传给 authenticate 回调
externalPlayerIdstring必填商户侧玩家唯一 ID,格式 ^[A-Za-z0-9_\-]{1,64}$一个 externalPlayerId 只能绑定一种 currency;多币种用户请为每个币种使用不同的 externalPlayerId(详见下方)
currencystring必填ISO 币种代码,取值见 货币支持。必须与 authenticate 回调返回的 currency 一致,否则返回 error=20
hashstring必填请求签名,MD5 小写十六进制
gameIdstring可选游戏机台代码(即 getcasinogames 返回的 gameId);为空则进入游戏大厅,非空则直接进入指定机台。无论是否传值都参与签名(见下方签名说明)
languagestring可选语言代码,默认 en建议始终显式传值:不传时openlivegame按 language=en 参与签名校验(见下方签名说明)。用户在游戏内设置过的语言偏好会覆盖此字段
countrystring可选ISO 国家代码
platformstring可选平台标识(mobile / desktop 等)
lobbyUrlstring可选玩家退出游戏后跳转回的大厅 URL
lobbystring可选1 表示只进大厅 / 分类列表(按 gameId 对应机台的类型列桌,不绑定具体桌台);不传则直接进入 gameId 对应桌台。只在传 1 时参与签名,请勿传 0
签名参与规则(与服务端验签实现严格一致):
  • secureLogin / gameId / token / externalPlayerId / currency 恒参与签名——gameId 不传时也要以空值(gameId=)参与拼接。
  • language 恒参与:不传时openlivegame按 language=en 重算签名,因此要么显式传 language,要么把 language=en 计入签名。推荐显式传。
  • country / platform / lobbyUrl 仅在非空时参与;lobby 仅在值为 1 时参与。空值字段请不要发送、也不要计入签名。

响应字段

字段类型说明
errorstring错误码,"0" 表示成功
descriptionstring结果描述
gameURLstring成功时返回的游戏客户端 URL,仅当 error="0" 存在

请求示例

curl -X POST https://merchant.caca789.com/provider/v1/gameurl \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "secureLogin=merchant001" \
  -d "token=player-token-xyz" \
  -d "externalPlayerId=player-001" \
  -d "currency=USD" \
  -d "gameId=545" \
  -d "language=en" \
  -d "hash=e768a4a5f34aec8263db58b7d2775f30"
# 示例 hash 以 secretKey=mysecretkey 计算,可用来自校验你的签名实现

成功响应

{
  "error": "0",
  "description": "OK",
  "gameURL": "https://pp-client.caca789.com/desktop/baccarat/?JSESSIONID=sess-abc&table_id=545&lang=en&..."
}

失败响应示例

{
  "error": "5",
  "description": "invalid signature"
}
关键校验: authenticate 回调返回的 userId 必须与本次请求中的 externalPlayerId 严格相等, 否则 gameurl 返回 error=4;返回的 currency 必须与本次请求的 currency 一致(大小写不敏感),否则返回 error=20。openlivegame 不做任何容错匹配。
一用户一币种(强约束): openlivegame 侧每个 externalPlayerId 只允许绑定一种 currency。 首次进入游戏时 (externalPlayerId, currency) 会被固化,后续若用相同 externalPlayerId 传入不同的 currency,将返回 error=20 拒绝进入。

如果商户侧同一个自然人用户拥有多个币种账户(例如一个用户同时持有 USD 与 EUR 钱包), 商户必须为每一种币种提供一个不同externalPlayerId, 哪怕在商户系统中它们对应同一个真实用户。

推荐命名规范:{原用户ID}_{币种},例如 u12345_USD / u12345_EUR。 商户自行在内部维护「自然人 ↔ 多币种 externalPlayerId」的映射关系。
POST /getcasinogames 获取全量机台列表

返回当前 openlivegame 平台已启用的机台清单。商户可用于构建自己的游戏大厅页。

请求参数

字段类型必填说明
secureLoginstring必填商户 API 身份标识
hashstring必填请求签名
gameTypestring可选按游戏类型过滤,如 roulette / baccarat / blackjack。仅非空时参与签名

响应字段

字段类型说明
errorstring错误码
descriptionstring结果描述
gameList[]array机台列表
gameList[].gameIdstring机台代码,用作 gameurl 的 gameId 入参;也是钱包回调中的 tableCode
gameList[].operatorGameIdstring上游游戏 ID(供应商平台的游戏 ID,跨账号一致)
gameList[].gameNamestring机台展示名
gameList[].gameTypestring游戏类型
gameList[].typeDescriptionstring游戏类型描述(大厅分类)
gameList[].vendorTypestring供应商类型(pp / evo),供商户按供应商分组展示;不需要可忽略
gameList[].statusstring机台状态,恒为 open(接口只返回启用机台)

成功响应示例

{
  "error": "0",
  "description": "OK",
  "gameList": [
    {
      "gameId": "545",
      "operatorGameId": "101",
      "gameName": "Speed Baccarat A",
      "gameType": "baccarat",
      "typeDescription": "baccarat",
      "vendorType": "pp",
      "status": "open"
    },
    {
      "gameId": "227",
      "operatorGameId": "221",
      "gameName": "Auto Roulette",
      "gameType": "roulette",
      "typeDescription": "roulette",
      "vendorType": "pp",
      "status": "open"
    }
  ]
}
POST /roundreport 获取单局报表一次性 URL

roundId(即钱包回调中携带的业务局号)换取一个该局报表页的一次性访问 URL, 供商户客服核单 / 玩家查看本局详情。URL 内附带短期有效 token,过期需重新调用本接口。

请求参数

字段类型必填说明
secureLoginstring必填商户 API 身份标识
roundIdstring必填业务局号(钱包 bet/result/refund 回调中的 roundId,16 位纯数字)
hashstring必填请求签名

响应字段

字段类型说明
errorstring错误码;"4" 表示本商户名下无此局,"7" 表示 roundId 格式非法
descriptionstring结果描述
urlstring报表页一次性 URL,仅当 error="0" 存在

成功响应示例

{
  "error": "0",
  "description": "OK",
  "url": "https://pp-client.caca789.com/reports/545/index.html?token=..."
}
归属隔离:roundId 只能查询本商户名下的局;查询其他商户的局一律返回 error=4(round not found)。
GET /healthcheck 活性检测

探活接口,无需参数、无需签名。返回 error="0" 表示服务正常。

curl https://merchant.caca789.com/provider/v1/healthcheck

# =>
{ "error": "0", "description": "OK" }

Seamless Wallet · 商户提供给 openlivegame #

基础地址:商户预留的 callback_url(例如 https://merchant-wallet.example.com/wallet)。 下文各端点路径(如 /authenticate)均为相对于该 callback_url 的相对路径, 实际请求 URL 形如 https://merchant-wallet.example.com/wallet/authenticate

Dev 环境下 openlivegame 出口 IP 由出网网关决定,如商户在自己的 WAF / 防火墙上对入站做了白名单限制, 请向 openlivegame 索取最新的出口 IP 列表。

请求格式

  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • 签名字段: hash(MD5,对全部非 hash 字段计算)
  • 超时时间: /bet 5 秒;其余接口 10 秒(openlivegame 侧 client timeout)

响应格式

  • Content-Type: application/json
  • HTTP Status: 业务错误也请使用 200 OK,错误信息写入 error 字段
  • error 字段: 整型(注意:与 Provider API 的字符串错误码不同)
  • 金额字段: JSON number,最多 2 位小数
强制幂等: bet / result / refund 三个接口必须基于 reference 字段实现幂等。 同一 reference 的重复请求必须返回 同一条流水(相同 transactionIdcash), 禁止重复扣款或重复加款。重复时建议 error=5 (duplicate),但返回 error=0 也视为幂等成功。
POST /authenticate 玩家身份校验 · 返回初始余额

openlivegame 在收到商户 gameurl 请求后立即调用此接口, 用同一份 token 询问商户:此 token 是否有效、对应哪个玩家、当前余额是多少。

请求参数(form)

字段类型说明
tokenstring商户生成的玩家 token(与 gameurl 的 token 相同)
providerIdstring固定值 ppgame
hashstring请求签名

响应字段(JSON)

字段类型说明
userIdstring玩家唯一 ID,必须等于 gameurl 请求中的 externalPlayerId
currencystring玩家账户币种。必须与 gameurl 请求的 currency 一致;同一 userId 的 currency 一经确立不可更改
cashnumber当前现金余额,2 位小数
errorint错误码,0 表示成功,见 错误码表
descriptionstring错误描述

请求样例

POST /wallet/authenticate HTTP/1.1
Host: merchant-wallet.example.com
Content-Type: application/x-www-form-urlencoded

token=player-token-xyz&providerId=ppgame&hash=60dacf2fedfea2114586cea38f5685e0
# 示例 hash 以 secretKey=mysecretkey 计算

成功响应

{
  "userId":      "player-001",
  "currency":    "USD",
  "cash":        1000.50,
  "error":       0,
  "description": "OK"
}

失败响应

{
  "userId":      "",
  "currency":    "",
  "cash":        0,
  "error":       3,
  "description": "invalid token"
}
POST /balance 查询玩家最新余额

会话恢复、余额刷新等场景下 openlivegame 会调用此接口拉取商户侧玩家的最新现金余额。

请求参数(form)

字段类型说明
userIdstring玩家 ID(即 authenticate 返回的 userId)
providerIdstring固定值 ppgame
hashstring请求签名

响应字段(JSON)

字段类型说明
currencystring币种
cashnumber当前余额
errorint错误码
descriptionstring错误描述

成功响应

{
  "currency":    "USD",
  "cash":        1050.75,
  "error":       0,
  "description": "OK"
}
POST /bet 下注扣款

玩家在游戏中确认下注时调用,商户需从玩家余额中 扣减 amount

请求参数(form)

字段类型必填说明
userIdstring必填玩家 ID(= externalPlayerId)
tableCodestring必填机台编号(= getcasinogames 的 gameId / gameurl 的 gameId 入参)。注意字段名是 tableCode,不是 tableId
gameIdstring必填供应商局号(raw,同机台内唯一;跨机台可重号,唯一定位一局请用 (tableCode, gameId) 组合)
roundIdstring必填业务局号(一玩家一局唯一,16 位纯数字);由 (tableCode, gameId, userId) 派生,同三元组恒返同值
amountstring(decimal)必填下注金额,2 位小数字符串,如 "10.50"
referencestring必填幂等键 = "B" + roundId(17 字符);bet/result/refund 分别用 B/R/F 前缀隔离
providerIdstring必填固定值 ppgame
timestampstring(int64)必填请求时间戳,毫秒
hashstring必填请求签名
roundDetailsstring(json)预留局级附加信息(JSON 字符串)。当前版本不会发送,商户实现不必处理;如出现请按可选字段兼容(仍参与签名)

响应字段(JSON)

字段类型说明
transactionIdstring商户侧交易流水号
currencystring币种
cashnumber扣款后的玩家余额
errorint错误码,余额不足时返回 1
descriptionstring错误描述

成功响应

{
  "transactionId": "txn-20260423-001",
  "currency":      "USD",
  "cash":          989.50,
  "error":         0,
  "description":   "OK"
}

余额不足响应

{
  "transactionId": "",
  "currency":      "USD",
  "cash":          5.00,
  "error":         1,
  "description":   "insufficient balance"
}
余额不足处理: 检测到余额不足时,不做任何扣款,cash 返回 当前真实余额error=1。 openlivegame 将据此拒绝该笔下注并通知客户端。
响应时限与不确定状态: /bet 的调用超时是 5 秒(下注在牌局倒计时内完成,无法长等)。 若商户响应超时、或返回 error=100 / 未知错误码,openlivegame 视本笔扣款为 状态不确定:会将该笔写入退款确认队列,由后台任务发起对应的 /refund 对冲(reference = "F"+roundId)。 因此商户 /bet 必须保证「要么落账要么干净失败」,且 /refund 幂等必须健壮。
POST /result 结算派奖

游戏结算后对每一个有成功下注的玩家调用,商户需将 amount 加到玩家余额上。 未中奖的局也会调用,此时 amount="0.00"——商户应照常落一条加款 0 的流水并返回成功, 以便双方对账确认该局已结算。

请求参数(form)

字段类型必填说明
userIdstring必填玩家 ID
tableCodestring必填机台编号(与 bet 同)
gameIdstring必填供应商局号(与 bet 同)
roundIdstring必填业务局号(与 bet 同)
amountstring(decimal)必填派奖金额(≥ 0),2 位小数;未中奖为 "0.00"
referencestring必填派奖幂等键 = "R" + roundId(17 字符);与 bet 的 "B"+roundId 不同,保证端点隔离
providerIdstring必填固定值 ppgame
timestampstring(int64)必填毫秒时间戳
hashstring必填请求签名

响应字段(JSON)

bettransactionId / currency / cash / error / description

成功响应

{
  "transactionId": "result-20260423-001",
  "currency":      "USD",
  "cash":          1014.50,
  "error":         0,
  "description":   "OK"
}
重试策略: 网络层失败(超时 / 连接错误)时 openlivegame 即时重试,至多 3 次尝试(间隔 2s / 4s 递增); 业务错误码不做即时重试。最终失败的结算任务进入异步重试队列(约每 15 秒扫描一次,至多 5 次), 超过上限转人工介入。商户务必保证 R+roundId 的幂等。
资金安全保证: openlivegame 在调用 /result 前强制校验本局存在对应的成功 /bet 扣款流水, 不存在则拒绝结算并转人工。商户不会收到「没有 bet 的 result」(如收到,按下文 refund 的建议返回 error=2)。
POST /refund 下注回滚

两种场景触发:① 整局被作废(游戏中断 / 上游取消本局);② /bet 调用超时或返回不确定错误(error=100 / 未知码),平台无法确认扣款是否生效, 按「可能已扣款」入队退款。openlivegame 调用 refund 请求商户把 bet 扣掉的金额 退还给玩家。

请求参数(form)

字段与 result 完全相同:userId / tableCode / gameId / roundId / amount(退款金额,正数) / reference(退款幂等键 = "F" + roundId,17 字符,与 bet/result 端点隔离) / providerId / timestamp / hash

响应字段(JSON)

bettransactionId / currency / cash / error / description

成功响应

{
  "transactionId": "refund-20260423-001",
  "currency":      "USD",
  "cash":          1000.00,
  "error":         0,
  "description":   "OK"
}
重试与终态: refund 任务由后台队列驱动(约每 15 秒扫描一次),失败自动重试至上限(作废退款 3 次), 超限标记失败并转人工介入。error=0error=5(duplicate)都视为退款成功。 注意「bet 超时」场景:若商户实际并未扣款(bet 没有落账),收到对应 refund 时应返回 error=2 或携带原余额的失败响应,而不是凭空加款。

签名算法 #

双向通用(openlivegame验商户签名、商户验openlivegame签名)。签名字段名为 hash

步骤

  1. 收集所有请求参数的 原始值(表单解码后),排除 hash 本身
  2. 对参数名按 字母序(ASCII 升序)排序。
  3. 按顺序拼接为 k1=v1&k2=v2&...&kn=vn
  4. 在末尾 直接追加 secretKey(无分隔符)。
  5. 对整串做 MD5,取 小写十六进制

Go 参考实现

func CalcSign(params map[string]string, secretKey string) string {
    keys := make([]string, 0, len(params))
    for k := range params {
        if k == "hash" { continue }
        keys = append(keys, k)
    }
    sort.Strings(keys)

    var b strings.Builder
    for i, k := range keys {
        if i > 0 { b.WriteByte('&') }
        b.WriteString(k)
        b.WriteByte('=')
        b.WriteString(params[k])
    }
    b.WriteString(secretKey)

    sum := md5.Sum([]byte(b.String()))
    return hex.EncodeToString(sum[:])
}

PHP 参考实现

function calcSign(array $params, string $secretKey): string {
    unset($params['hash']);
    ksort($params);
    $parts = [];
    foreach ($params as $k => $v) {
        $parts[] = $k . '=' . $v;
    }
    return md5(implode('&', $parts) . $secretKey);
}

Python 参考实现

import hashlib

def calc_sign(params: dict, secret_key: str) -> str:
    items = sorted((k, v) for k, v in params.items() if k != "hash")
    payload = "&".join(f"{k}={v}" for k, v in items) + secret_key
    return hashlib.md5(payload.encode()).hexdigest()

Node.js 参考实现

const crypto = require('crypto');

function calcSign(params, secretKey) {
  const payload = Object.keys(params)
    .filter(k => k !== 'hash')
    .sort()
    .map(k => `${k}=${params[k]}`)
    .join('&') + secretKey;
  return crypto.createHash('md5').update(payload).digest('hex');
}

完整示例

假设:

secureLoginmerchant001
tokenplayer-token-123
externalPlayerIdplayer-001
currencyUSD
gameId545
secretKeymysecretkey

排序后拼接:

currency=USD&externalPlayerId=player-001&gameId=545&secureLogin=merchant001&token=player-token-123

追加 secretKey:

currency=USD&externalPlayerId=player-001&gameId=545&secureLogin=merchant001&token=player-token-123mysecretkey

MD5 结果即为 hash

ce14a7aa5f8c5c23d92d088e94f76f19

你的签名实现对上述输入应输出完全相同的结果(注意本例未含 language, 实际调用 gameurl 时按上文 签名参与规则 把 language 计入)。

注意事项:
  • 参数值若含 & / = / 空格等字符,签名时使用 解码后的原始值,HTTP 传输时才 URL 编码。
  • 商户验证openlivegame回调时:对收到的全部非 hash 字段计算(openlivegame发送什么就签什么,包括 timestamp)。
  • 商户调用 Provider API 时:可选字段遵循各端点的「签名参与规则」,空值字段不发送、不计入。
  • secretKey 只在签名末尾出现, 通过 HTTP 传输。

错误码 #

Provider API 错误码(字符串类型)

用于openlivegame返回给商户的响应中的 error 字段。

error说明触发场景
"0"成功
"1"内部错误服务异常、数据库错误等
"2"无效 secureLogin商户未注册或 secureLogin 错误
"3"商户已禁用商户或其所在组织被禁用
"4"资源不存在authenticate 业务失败、userId 与 externalPlayerId 不匹配、roundreport 查无此局
"5"签名错误hash 校验失败
"7"参数格式错误externalPlayerId 不符合正则、roundId 格式非法
"14"缺少必填字段必填参数未传或绑定失败
"20"币种不匹配externalPlayerId 已绑定其他币种、或 authenticate 返回的 currency 与请求不一致

Seamless Wallet 错误码(整型)

用于商户返回给openlivegame的响应中的 error 字段。

error说明openlivegame处理逻辑
0成功继续业务流程
1余额不足拒绝下注,提示玩家
2用户不存在会话终止
3无效 tokengameurl 返回 error=4,拒绝进入游戏
4签名错误错误日志,检查 secretKey
5重复交易视为幂等成功,以此次响应为准
100内部错误状态不确定:bet 触发 refund 对冲;result/refund 进入重试队列

幂等、重试与时序 #

幂等键设计建议

bet / result / refund,商户应把 (operatorId, reference) 作为联合唯一键。 收到请求时先查此唯一键,若已存在则直接返回该流水的 transactionId + cash_after, 避免重复扣加款。

超时设定

openlivegame 调用商户的 client timeout:/bet 5 秒,其余接口 10 秒。 商户回调接口应保证 P99 响应时间 < 3s;bet 超时会被视为状态不确定并触发 refund 对冲, 带来资金状态不一致风险。

重试策略

result 网络失败先即时重试(至多 3 次尝试);仍失败与 refund 任务一起进入 b_wallet_callback_tasks 重试队列(每 15 秒扫描,result 至多 5 次 / refund 至多 3 次), 超限转人工介入。因此商户必须对同一 reference 做严格幂等。

时序关系

同一 roundId 的正常时序为 bet → result(含 amount=0 的未中奖结算); 异常时序为 bet → refund(整局作废或 bet 状态不确定)。 正常情况下同一局不会同时出现 result 与 refund。

金额精度: HTTP 传输层 amount 为字符串(2 位小数)。商户实现必须使用 Decimal / BigDecimal 等精确类型计算,严禁用 float 做中间计算,以避免余额累积误差。

商户幂等实现(必读) #

openlivegame 的重试机制保证了失败请求一定会重试,商户若不做正确的幂等处理,会出现重复扣款 / 重复加款 / 资金对不平的严重事故。 本章给出强制性的实现要求。

1. 幂等键与唯一键

适用接口

/bet/result/refund 三个接口必须强制幂等。/authenticate/balance 为只读接口,不写入状态,天然幂等。

幂等键

请求中的 reference 字段即幂等键(1 位类型前缀 B/R/F + 16 位 roundId,共 17 字符),由 openlivegame 生成,在同一商户范围内全局唯一

数据库唯一键

商户交易流水表(建议命名 wallet_transactions)必须建立以下数据库级唯一索引

CREATE UNIQUE INDEX uk_operator_reference
  ON wallet_transactions (operator_id, reference);

-- 同时建议加一个业务维度索引用于查询和对账
CREATE INDEX ix_user_round_type
  ON wallet_transactions (user_id, round_id, txn_type);

说明reference 在 openlivegame 侧对单个商户全局唯一,因此唯一键用 (operator_id, reference) 足以防止所有重复提交。依靠代码层 if exists then skip没有 DB 唯一索引的实现, 在并发场景下会失效。

2. 三种重复请求场景的正确响应

商户收到带有已见过的 reference 的请求时,必须按实际状态返回。不要重复扣 / 加款。

场景 识别条件 推荐响应
A 已成功 DB 中存在相同 reference 且状态为 success 返回当时那笔流水的 transactionId 和成功时的余额 cash
error=0(推荐)或 error=5(duplicate,语义更准确),两者 openlivegame 都视为幂等成功
B 已失败 DB 中存在相同 reference 且状态为 failed(如余额不足) 返回与当时一致的失败响应(例如 error=1 + 当前余额)。不要重新执行业务逻辑,避免"之前余额不足、现在余额够了"导致迟到下注生效。
C 并发中 两个线程几乎同时收到同一 reference 请求 依赖 DB 唯一索引兜底:一条成功插入,另一条插入时命中唯一冲突,按场景 A 回查并返回已有流水。

3. 标准实现伪代码

Bet 接口的推荐流程

// 关键不变量:唯一键 (operator_id, reference) 限制下,一个 reference 只能有一条记录。

BEGIN TRANSACTION;

// Step 1: 先查幂等键,命中则直接返回该流水(场景 A / B)
tx := SELECT * FROM wallet_transactions
      WHERE operator_id = $op AND reference = $ref
      FOR UPDATE;  // 行级锁保证并发安全
if tx exists {
    return { transactionId: tx.id, cash: tx.cash_after,
             currency: tx.currency, error: tx.error_code,
             description: tx.description };
}

// Step 2: 锁定玩家账户行
player := SELECT * FROM players
          WHERE operator_id = $op AND user_id = $userId
          FOR UPDATE;

// Step 3: 业务判断
if player.cash < $amount {
    // 余额不足也要落一条失败流水,重试命中时场景 B 返回
    INSERT INTO wallet_transactions(operator_id, reference, user_id,
        table_code, game_id, round_id, txn_type, amount, cash_before, cash_after,
        error_code, description, status)
    VALUES ($op, $ref, $userId,
            $tableCode, $gameId, $roundId, 'bet',
            $amount, player.cash, player.cash,
            1, 'insufficient balance', 'failed');
    COMMIT;
    return { cash: player.cash, error: 1,
             description: "insufficient balance" };
}

// Step 4: 扣减余额 + 写流水(同一事务内)
UPDATE players SET cash = cash - $amount
                 WHERE id = player.id;

txId := INSERT INTO wallet_transactions(operator_id, reference, user_id,
          table_code, game_id, round_id, txn_type, amount, cash_before, cash_after,
          error_code, description, status)
        VALUES ($op, $ref, $userId,
                $tableCode, $gameId, $roundId, 'bet',
                $amount, player.cash, player.cash - $amount,
                0, 'OK', 'success')
        ON CONFLICT (operator_id, reference) DO NOTHING
        RETURNING id;

// 并发冲突兜底:另一个线程已插入 → 回滚本次扣款,查询已有流水返回(场景 C)
if txId is null {
    ROLLBACK;
    goto Step 1;
}

COMMIT;
return { transactionId: txId, cash: player.cash - $amount,
         currency: player.currency, error: 0, description: "OK" };

Result 与 Refund 同构,区别仅是 txn_type 以及余额是加而非减,且无余额不足分支。 注意 result 的 amount 可能为 0(未中奖结算)——照常落流水、加 0、返回成功。

4. Refund 的特殊要求

三端点 reference 用 B/R/F 前缀隔离"B" + roundId / "R" + roundId / "F" + roundId), 所以 refund 的 reference 与 bet/result 的 reference 不同。商户判定某次 refund 是否合法时,不能直接用 refund 的 reference 去比对 bet 表。应基于 (tableCode, gameId, userId) 三件套 + txn_type='bet' 定位原始 bet (等价地,也可以基于 roundId + txn_type='bet',因为 roundId 是三件套的派生),校验后再按 refund 的 reference 做幂等。
  • Refund 的 amount 总是正数,表示加款金额
  • 商户收到 Refund 时建议:定位 bet 流水 → 校验 amount <= bet.amount → 写 refund 流水并加款 → 可选地把 bet 标记为 refunded
  • Refund 对应的 bet 可能在商户侧不存在(bet 超时、商户实际未落账的场景)。此时不要凭空加款, 应返回 error=2,openlivegame 侧按任务失败转人工核对

5. 常见反模式(不要这么写)

反模式后果正确做法
只在应用层 if exists,不建 DB 唯一索引 并发下两个请求都先查到不存在,双方都插入,扣两次款 DB 唯一索引 + ON CONFLICT DO NOTHING 兜底
重复请求时"重新跑一遍业务" 第一次余额不足失败,重试时因用户充值已成功,导致迟到下注生效 重复请求必须严格返回首次结果,包括首次的错误
扣款和写流水不在同一事务 扣款成功但流水写入失败 → 幂等失效 → 下次重试再扣一次 扣款、写流水必须在同一个 DB 事务内提交
用 float 做余额计算 10.10 - 10.00 ≠ 0.10,长期累计后余额对不平 Decimal / BigDecimal / numeric(18,2) 等精确类型
失败请求返回 HTTP 5xx openlivegame 无法区分业务失败与网络失败,全部进入重试队列,加剧雪崩 业务错误 一律 HTTP 200,错误码放 error 字段
Authenticate 返回的 userId 与 externalPlayerId 不一致 gameurl 直接返回 error=4,玩家进不了游戏 严格使用同一份玩家标识,在 token 表和玩家表中保持映射一致
同一 externalPlayerId 在不同场次传入不同 currency gameurl 返回 error=20,玩家进不了游戏 一个 externalPlayerId 终身绑定一种币种。多币种用户用 {uid}_{ccy} 这类派生 ID,由商户维护自然人与多 ID 的映射
把回调里的 gameId 当全局唯一局号入唯一索引 gameId 跨机台可重号,撞唯一键后丢单 跨机台唯一请用 (tableCode, gameId) 组合,或直接用 roundId

6. 每日对账

强烈建议商户实现与 openlivegame 的 T+1 对账机制(通过 openlivegame 运维后台的报表或导出功能)。 对账维度:(operator_id, date, txn_type) 下的总金额与总笔数。 任何差异都应在当日发现、当日修复,避免问题滚雪球。

上线前自查清单 #

商户侧实现核对

  • ☐ 已与 openlivegame 协商并记录 secureLogin / secretKey / callback_url 三项配置
  • ☐ 实现了 5 个回调端点:/authenticate / /balance / /bet / /result / /refund
  • ☐ 所有回调对 hash 字段做了签名校验(对全部非 hash 字段计算),签名失败返回 error=4
  • ☐ 签名实现用本文档「完整示例」的输入跑出了相同的 MD5 结果
  • ☐ 生成的 token 是一次性的、短期有效、与玩家绑定
  • ☐ authenticate 返回的 userId 一定等于商户传给 gameurl 的 externalPlayerIdcurrency 一定等于 gameurl 请求的 currency
  • ☐ 多币种用户已使用不同的 externalPlayerId(如 uid_USD / uid_EUR),并在内部维护了自然人到多 ID 的映射
  • ☐ 解析回调时机台字段读的是 tableCode(不是 tableId)
  • ☐ bet / result / refund 基于 reference 实现了数据库唯一索引级幂等(UNIQUE (operator_id, reference)
  • ☐ result 在 amount="0.00"(未中奖结算)时也能正确落流水并返回成功
  • ☐ 扣款 / 加款与流水写入在同一 DB 事务内提交
  • ☐ 重复请求不重跑业务,严格返回首次落库的流水与错误码
  • ☐ 失败场景(余额不足等)也会落一条 status=failed 流水,保证重试幂等
  • ☐ 找不到对应 bet 的 refund 返回 error=2,不凭空加款
  • ☐ 所有业务错误均使用 HTTP 200 + JSON error 字段返回,未用 HTTP 4xx/5xx 代替业务错误
  • ☐ 余额计算使用 Decimal 类型,未使用 float
  • ☐ 回调接口 P99 响应 < 3 秒(bet 的硬超时只有 5 秒)
  • ☐ 玩家账户与资金流水表设计了幂等隔离,不会因并发下注导致透支
  • ☐ 生产环境配好了服务器 IP 白名单(如有)并向 openlivegame 报备
  • ☐ 已完成 openlivegame 提供的联调沙箱测试,包含成功、余额不足、重复 reference、退款等场景

建议的监控指标

  • 回调接口 QPS / P99 延迟 / 错误率(按 endpoint 拆分)
  • error=1(余额不足)的日占比趋势
  • error=5(重复交易)次数 —— 少量正常(对应openlivegame重试),激增需联系openlivegame排查
  • 未匹配到 bet 却请求 refund 的次数 —— 对应「bet 超时未落账」场景,激增说明商户侧 bet 链路过慢
  • 签名校验失败次数 —— 可能是密钥泄露或被攻击
openlivegame · Merchant Integration Guide · Dev · 2026