GoodRoad 后端接口文档
这份文档详细说明 GoodRoad 插件与后端交互的所有 API。 第三方在接入时,需要在自己的后端实现这些接口,或者代理到我们的接口服务上。
接口概览
GoodRoad 分为四个主要阶段的接口调用:
| 阶段 | 接口 | 方法 | 说明 |
|---|---|---|---|
| 1. 初始化 | POST /plugin/v1/init-ticket |
POST | 创建一次性初始化票据 |
| 2. 会话交换 | POST /plugin/v1/session/exchange |
POST | 将 init_ticket 兑换为会话令牌 |
| 3. 获取快照 | GET /plugin/v1/tables/snapshot |
GET | 获取桌台列表及路盘数据 |
| 4. 进入游戏 | POST /plugin/v1/game/enter |
POST | 请求进入指定桌台 |
接口调用流程是链式的:先 init-ticket → exchange → snapshot → enter。
除 init-ticket 外,后续接口都需要有效的 session_token。
认证与签名
签名校验(可选)
如果后端启用了 GOODROAD_CHANNEL_SECRET 环境变量,所有请求都需要包含签名头。
签名算法
signText = "{channelId}|{timestamp}|{nonce}"
signature = HMAC-SHA256(signText, secret).hex()
必需头部
x-channel-id: 渠道号x-timestamp: 当前 Unix 时间戳(秒),允许偏差 ±300 秒x-nonce: 随机字符串,每个时间窗口内一次性使用x-signature: 上述签名结果
域名白名单(可选)
如果配置了 GOODROAD_CHANNEL_DOMAIN_MAP,请求时需要在 domain 字段里传当前页面域名,后端会校验是否在该 channel 的白名单内。
POST init-ticket
/plugin/v1/init-ticket
创建一次性初始化票据。返回的 init_ticket 用于后续的 session/exchange 阶段。
请求字段
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| channel_id | string | 是 | 渠道号 |
| domain | string | 是 | 当前页面域名或来源(可选校验) |
| user_type | string | 否 | 'member' | 'guest'(默认 guest) |
| token | string | 否 | 会员 token,传此字段则认为是会员用户 |
成功响应
result = 0
{
"result": 0,
"desc": "ok",
"data": {
"init_ticket": "hex_string_32_bytes",
"expires_in": 60,
"channel_id": "demo-channel",
"user_type": "guest"
}
}
常见错误
CHANNEL_ID_REQUIRED
channel_id 为空或缺失
DOMAIN_FORBIDDEN
当前域名未在该 channel 的白名单内
SIGNATURE_INVALID
签名校验失败或格式错误
TOKEN_INVALID
传入的 token 无法识别
POST session/exchange
/plugin/v1/session/exchange
将 init_ticket 兑换为持久会话令牌。session_token 有效期通常为 10 分钟(会员)或 2 小时(游客)。
请求字段
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| init_ticket | string | 是 | 来自 init-ticket 响应的 init_ticket |
成功响应
result = 0
{
"result": 0,
"desc": "ok",
"data": {
"session_token": "hex_string_48_bytes",
"ws_token": "hex_string_36_bytes",
"expires_in": 600,
"user_type": "guest"
}
}
常见错误
INIT_TICKET_REQUIRED
init_ticket 为空或缺失
INIT_TICKET_INVALID
init_ticket 已过期、已使用或不存在
GET tables/snapshot
/plugin/v1/tables/snapshot
获取可用桌台列表及其路盘数据。插件会定期调用此接口以刷新桌台列表。
请求字段
| 字段 | 来源 | 类型 | 说明 |
|---|---|---|---|
| session_token | Header: x-session-token Query: session_token | string | 会话令牌(推荐用 header 传递) |
| limit | Query | number | 最多返回多少个桌台(默认 6,范围 1-50) |
| include_good_road | Query | bool | 是否包含好路数据(默认 1) |
| include_history | Query | bool | 是否包含游戏历史(默认 1) |
成功响应
result = 0
{
"result": 0,
"desc": "ok",
"data": {
"generated_at": 1718358000,
"user_type": "guest",
"channel_id": "demo-channel",
"table_count": 3,
"table_list": [
{
"room_id": 1001,
"game_code": "baccarat_001",
"game_name": "百家乐 1 号桌",
"game_type": 4,
"online": 1,
"shoe_id": 42,
"round_count": 28,
"player_count": 10,
"banker_count": 16,
"tie_count": 2,
"good_road": {
"list": ["b", "p", "b", "b", "t", "p"],
"is_good_road": 1,
"rule": "BACCARAT_LONG_BANKER_4",
"rule_name": "长庄 >=4",
"duration_rounds": 4
},
"game_history": [ ... ],
"analy_data": "{...}"
}
]
}
}
重要:仅返回有路盘数据的桌台(即路盘列表不为空)。无任何落点的空桌将被过滤。
常见错误
SESSION_TOKEN_REQUIRED
session_token 为空或缺失
SESSION_INVALID
session_token 已过期或不存在
POST game/enter
/plugin/v1/game/enter
请求进入指定桌台。仅会员可进入,游客会收到拒绝。
请求字段
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| room_id | number | 是 | 目标桌台 ID |
| session_token | string | 是 | 会话令牌(header: x-session-token 优先) |
成功响应
result = 0
{
"result": 0,
"desc": "ok",
"data": {
"room_id": 1001,
"game_code": "baccarat_001",
"open_url": "https://game.example.com/baccarat?room=1001&token=..."
}
}
常见错误
SESSION_TOKEN_REQUIRED
session_token 为空或缺失
SESSION_INVALID
session_token 已过期或不存在
GUEST_FORBIDDEN_ENTER
游客无法进入游戏
ROOM_NOT_AVAILABLE
目标桌台不可用或不存在
最佳实践
错误处理
- 始终检查响应中的
result字段,非 0 表示出错。 - 对于 SESSION_INVALID 错误,前端应重新初始化流程(从 init-ticket 开始)。
- 对于网络错误,建议实现指数退避重试机制。
性能建议
- 缓存 session_token 直到过期,避免频繁的 exchange 调用。
- snapshot 列表通常可缓存 3-5 秒,再自动刷新。
- 对于高并发场景,使用 Redis 缓存会话和防重放 nonce。
安全建议
- 不要把 session_token 暴露在 URL query 里,优先使用 HTTP 请求头。
- 不要在浏览器控制台输出或记录完整的 token。
- 启用 GOODROAD_CHANNEL_SECRET 时,签名必须在后端完成,不要在前端暴露密钥。
- 定期轮换 channel secret,并通知接入方同步更新。
完整示例
JavaScript/TypeScript 联调示例
async function setupGoodRoadSession() {
const apiBase = 'http://127.0.0.1:13100';
// 1. 获取 init_ticket
const initResp = await fetch(`${apiBase}/plugin/v1/init-ticket`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel_id: 'demo-channel',
domain: window.location.origin,
user_type: 'guest'
})
}).then(r => r.json());
if (initResp.result !== 0) {
throw new Error(`Init failed: ${initResp.code}`);
}
const initTicket = initResp.data.init_ticket;
// 2. 交换 session_token
const sessionResp = await fetch(`${apiBase}/plugin/v1/session/exchange`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ init_ticket: initTicket })
}).then(r => r.json());
if (sessionResp.result !== 0) {
throw new Error(`Exchange failed: ${sessionResp.code}`);
}
const sessionToken = sessionResp.data.session_token;
// 3. 获取桌台快照
const snapshotResp = await fetch(
`${apiBase}/plugin/v1/tables/snapshot?limit=8`,
{
headers: { 'x-session-token': sessionToken }
}
).then(r => r.json());
console.log('Tables:', snapshotResp.data.table_list);
// 4. 进入游戏(需要会员 token)
// const enterResp = await fetch(`${apiBase}/plugin/v1/game/enter`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'x-session-token': sessionToken
// },
// body: JSON.stringify({ room_id: 1001 })
// }).then(r => r.json());
}
setupGoodRoadSession().catch(console.error);