回调与结算
webhook是登记的完整回调地址。提供方直接向该URL发送POST请求,不追加路径。模板提供POST /callback。三种事件统一由签名payload中的event字段区分:open、transaction、close。第三方负责自己的玩家、余额和账本;收到open不能自动创建任意玩家。
验签格式
所有回调使用JSON信封,payload是原始业务JSON字符串。
{
"payload": "{\"event\":\"open\",\"user\":\"test-player-001\",\"user_key\":\"SESSION\"}",
"timestamp": "UNIX_SECONDS",
"nonce": "RANDOM_HEX",
"sign": "HMAC_HEX"
}
sign = HMAC-SHA256(secret, timestamp + ":" + nonce + ":" + payload),结果为小写十六进制。必须使用收到的payload原文验签,不能先解析再重新序列化。验证时间误差不超过300秒,并使用恒定时间比较签名;验签通过才解析业务数据。
event包含在payload原文中,必须先验签再按event分发,不能根据未签名的请求头选择操作。每次新建回调请求都会签名。不能靠nonce代替业务幂等;重复投递合法,必须按业务事件唯一键处理。
open
payload包含event="open"、user和user_key。user_key标识一次钱包会话。用玩家行锁检查尚无其他未关闭会话,保存会话与初始余额。相同open重试返回第一次确认的余额,不能重新充值。
{
"success": true,
"message": 10000000
}
transaction
{
"event": "transaction",
"transaction_id": "123456",
"user_key": "SESSION",
"user": "test-player-001",
"game_code": "baccarat",
"amount": -100,
"balance": 9999900,
"timestamp": 1791900000
}
amount为带符号整数,本次余额应等于旧余额+amount。必须检查user_key是当前会话。以合作方、玩家、transaction_id作为唯一业务身份;相同事件重复返回原结果,编号相同但金额或报文冲突必须拒绝并对账。
余额、流水、防重记录必须在一个数据库事务提交。事务提交后即使HTTP应答丢失,也不能重复入账。应先检查历史事件,再判断当前会话,允许旧交易在钱包关闭后安全重试。
close
payload包含event="close"、user、user_key、balance。必须匹配当前会话并核对本地余额。迟到的旧close不能关闭新会话;相同close重试返回成功。
transaction和close成功均返回HTTP 200及{"success":true,"message":"ok"}。错误或非JSON响应不会被当作确认。
故障处理
transaction与close先写入提供方回调队列,再按同会话顺序签名发送。投递失败递增退避,20次失败后进入死信并阻塞后续事件;管理员核查后可重试原事件。已落库的事件可在后台重启后继续投递,每次重新签名,第三方必须按业务编号去重。open仍为即时请求。
提供方不维护独立钱包账本和钱包会话表,也不保留GameServer磁盘补发队列。回调队列不代表结算全链路的原子提交:游戏消息尚未写入队列时发生崩溃,仍需人工核对。
第三方必须保存独立账本,不依赖提供方的短期游戏日志。出现余额不一致时保留会话、暂停相关资金操作并提交事件编号;不要手动覆盖余额或把失败事件标成成功。
发生进程崩溃后,第三方保留未关闭的本地钱包状态,人工核对日志;提供方当前没有自动恢复接口。当前正式准入默认关闭,未经重启、应答丢失和结算顺序验收的游戏不得开放正式接入。
地址示例
登记 https://operator.example/callback 后,三个事件均发送到该地址。回调接口不使用浏览器登录态,应直接返回JSON,不要跳转到登录页面。服务器出口 IP 白名单约束第三方发起的游戏API请求;回调接收端则必须校验平台签名,二者用途不同。
字段类型与金额单位
所有事件发往登记的同一个完整 URL,HTTP 方法为 POST,请求与响应均为 application/json。以下表格的业务字段位于 payload 字符串内,不是信封顶层。
| 信封字段 | 类型 | 说明 |
|---|---|---|
| payload | string | 必填,原始业务 JSON 文本;必须保留原文验签 |
| timestamp | string | 必填,本次投递签名的 Unix 秒 |
| nonce | string | 必填,本次签名的随机文本 |
| sign | string | 必填,HMAC-SHA256 小写十六进制文本 |
| 业务字段 | 类型 | 所属事件及含义 |
|---|---|---|
| event | string | 所有事件:open、transaction 或 close |
| user | string | 所有事件:第三方稳定玩家编码 |
| user_key | string | 所有事件:本次钱包会话标识,必须与玩家一起核对 |
| transaction_id | string | transaction:唯一业务交易编号,原样保存,不转浮点数 |
| game_code | string | transaction:发生交易的游戏标识 |
| amount | integer(int64) | transaction:本次有符号余额变化,负数扣减、正数增加 |
| balance | integer(int64) | transaction / close:变更后/关闭时的余额,用于核对 |
| timestamp | integer | transaction:交易发生的 Unix 秒;区别于信封 timestamp 字符串 |
金额统一使用原始整数,1000 原始单位 = 1.00 金币。展示汇率只影响显示,回调不换算币种、不传小数金额。open 响应的 message 必须是非负 int64 JSON 数字,不能是字符串。处理端应使用支持 int64 精度的 JSON 解析器;JavaScript Number 超过安全整数范围会丢失精度,不能用于大额账务运算。
{
"event": "close",
"user": "test-player-001",
"user_key": "SESSION",
"balance": 9999900
}
确认响应与失败响应
{
"success": true,
"message": "ok"
}
{
"success": false,
"message": "wallet balance mismatch"
}
失败 message 由第三方定义,上例不是平台统一错误码。签名不合法应拒绝执行;事务未提交、玩家不存在、会话冲突或余额不符时,不得返回 success=true。不要先应答成功再异步入账。
transaction / close 投递端仅在 HTTP 200、响应为有效 JSON 且 success=true 时确认成功;message 不参与确认。单次请求超时 8 秒,响应最大 64 KiB,不跟随重定向。超时、非 200、无效 JSON、success=false 均视为失败。重试延迟按失败次数指数增加,最大约 256 秒;第 20 次失败进入死信。业务 payload 保持不变,信封 timestamp/nonce/sign 在每次投递时重新生成。
open 必须及时返回已确认的初始余额;它不进入 outbox 自动重试队列。open 失败会阻止游戏开户。请保存原始业务事件、处理结果和对账依据;敏感签名与密钥不写入普通日志。