离线/余额回收回调
VCT 平台在玩家退出游戏、后台强制踢线或切换同币种游戏时通知包网(Agent)关闭会话并完成最终对账。
环境地址
| 环境 | Base URL |
|---|---|
| 生产环境 | https://hub-panel-api.v7stech.com |
| 测试环境 | https://hub-panel-api-dev.v7stech.com |
必须确认收到
包网必须返回 JSON 响应,且 status 必须为 success。缺少 status、返回其他状态或响应无法解析,都会被 VCT 记录为离线回调失败。
接口信息
| 属性 | 值 |
|---|---|
| 路径 | POST /api/v1/callback/offline |
| 调用方 | VCT 平台 → 包网 (Agent) |
| Content-Type | application/json |
鉴权与签名
统一 Header Token 规则见认证机制。本接口的 Body sign 使用 HMAC-SHA256(username + "." + timestamp, agent_api_secret);Header 与 Body 的 timestamp 必须完全一致。
请求参数(VCT 发送)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | 是 | 玩家用户名 |
currency | string | 是 | 货币代码 |
session_id | string | 是 | 本次 VCT 游戏会话唯一 ID;必须用于幂等和对账 |
game_id | string | 否 | 游戏 GUID |
game_code | string | 否 | 游戏代码 |
game_name | string | 否 | 游戏名称 |
game_type | string | 否 | 游戏类型 |
provider | string | 否 | 游戏商代码 |
reclaimed_amount | decimal | 是 | Hub 在 cleared_at 清空本地钱包时保存的金额快照;仅用于本次 Offline 对账,不用于重复变更主钱包,也不代表所有迟到流水处理完成后的包网主钱包最终余额 |
wallet_cleared_total | decimal | 普通下分是 | Hub 实际清零的同币种全部钱包物理合计 |
mirrored_delta_total | decimal | 普通下分是 | 清空时仍同时存在于来源钱包和当前钱包的增量合计;以来源钱包实际清空金额为上限,可包含同一次下分前发生的负数冲正 |
net_balance_to_reconcile | decimal | 普通下分是 | wallet_cleared_total - mirrored_delta_total,用于与包网主钱包对账 |
wallet_breakdown | array | 普通下分是 | 每个被清空钱包的金额以及 session、Game、Provider、Brand 快照 |
mirrored_adjustments | array | 普通下分是 | 已成功镜像的调整事件;每项包含 event_id、原始 delta、镜像额 mirrored_amount、来源/目标 session 与 Brand。最终可扣重总额以 mirrored_delta_total 为准 |
cleared_at | string | 否* | Hub 实际清空本地钱包的时间,RFC3339/RFC3339Nano;普通退出/踢出/切换游戏的新记录会提供,历史记录和 launch_failed 可能没有 |
action | string | 是 | exit_game、api_kick_exit、switch_game 或启动失败补偿 launch_failed |
sign | string | 是 | 请求签名 |
timestamp | long | 是 | 请求时间戳 |
示例请求
{
"username": "player001",
"currency": "MYR",
"session_id": "vct-session-uuid-001",
"game_id": "game-guid-001",
"game_code": "12345",
"game_name": "Example Game",
"game_type": "Slot",
"provider": "gsc",
"reclaimed_amount": 200.00,
"wallet_cleared_total": 200.00,
"mirrored_delta_total": 50.00,
"net_balance_to_reconcile": 150.00,
"wallet_breakdown": [
{"brand_code":"UU","session_id":"uu-current-session","is_active_session":true,"cleared_amount":150.00},
{"brand_code":"CMD","session_id":"cmd-old-session","is_active_session":false,"cleared_amount":50.00}
],
"mirrored_adjustments": [
{"event_id":"CMD_SETTLED_1","source_brand_code":"CMD","target_brand_code":"UU","delta":50.00,"mirrored_amount":50.00}
],
"cleared_at": "2026-10-04T23:30:12.123Z",
"action": "switch_game",
"sign": "9a2f64d0bcfa829...",
"timestamp": 1783324700
}响应要求(Agent 必须返回)
成功确认
{
"status": "success",
"message": "received",
"username": "player001",
"currency": "MYR",
"reclaimed_amount": 125.50
}status 必须严格为 success,表示 Agent 已收到并处理该离线/余额回收通知。钱包清空完成后 Hub 不会让 callback 成败阻止玩家再次启动;HTTP 2xx 且 Body 的 status 为 success 用于把 reclaim 标记完成并停止重试。
失败响应
{
"status": "error",
"message": "player not found"
}失败响应或 HTTP 非 2xx 会被 VCT 记录为回调失败;Agent 应使用 sign、timestamp 和 username 做验签及幂等处理。
如果 Agent 返回失败,VCT 会把该笔记录标记为 callback_failed,之后再次发送相同的 Offline 请求,或由管理员手动 Retry;Agent 必须按 session_id + action 幂等处理。失败记录不会阻止玩家再次启动游戏。
对于普通 Offline callback,VCT 会在失败后自动重试,最多 15 次,并使用递增退避间隔:1 分钟、5 分钟、15 分钟、30 分钟、1 小时、2 小时。达到上限后停止自动重试,但管理员仍可手动 Retry。每次实际 HTTP 调用都会单独记录,包网不能根据重复请求重复入账
重要说明
- 在 Seamless 模式下,玩家主钱包的扣款、加款和冲正已由
/bet-push实时完成;reclaimed_amount是 Hub 清空本地钱包时的金额快照,仅用于本次 Offline 对账,Agent 不得据此重复变更主钱包。 mirrored_adjustments只列出成功应用的同步事件。每项的mirrored_amount是该事件已镜像到当前游戏钱包的金额;正数代表加款镜像,负数代表冲正抵销。来源钱包可能已经被其他流水改变,因此最终实际可扣重总额以mirrored_delta_total为准(Hub 已按来源钱包下分时的实际余额封顶)。wallet_cleared_total可能同时包含来源钱包和这些已镜像金额。使用event_id核对本地事件账本,并以net_balance_to_reconcile作为最终总对账金额;不能把物理合计直接加回主钱包。cleared_at是 Hub 清空本地钱包的时间,不是Offline请求发送时间,也不是所有迟到日志处理完成的时间。清空之后才到达 Hub 的日志仍可能出现,Agent 应继续按bet-push的幂等规则处理,并使用session_id与cleared_at做审计。- VCT 会记录请求和响应,便于核对是否收到以及回收金额。
- Agent 应优先按
session_id + action(并结合请求签名)实现幂等,避免重复入账
启动失败补偿 (launch_failed)
当 VCT 已经收到 /balance 成功响应、但游戏商启动失败时,VCT 会调用本接口并发送:
{
"username": "player001",
"currency": "MYR",
"session_id": "vct-session-uuid-001",
"reclaimed_amount": 100.00,
"action": "launch_failed",
"sign": "9a2f64d0bcfa829...",
"timestamp": 1783324700
}包网处理要求:
session_id必须原样保存、匹配,不得使用玩家或币种推断替代。launch_failed只释放本次启动的临时会话、余额锁定或临时记录。launch_failed不得写入普通enter_game,也不得进入普通退出游戏 offline 平账流程。- 同一个玩家、币种和
session_id应建立唯一约束,重复启动记录必须被拒绝或幂等处理。 - 重复收到同一个
launch_failed时必须返回成功或幂等结果,不能重复加款、重复释放或重复创建记录。 - 只有普通
exit_game、api_kick_exit、switch_game等动作才进入普通 offline 平账逻辑。
AI 开发提示词(可直接复制)
请在当前包网项目中实现 VCT 离线/余额回收回调 POST /api/v1/callback/offline。
实现要求:
1. 校验 Authorization 三段式 Token,Header 与 Body timestamp 必须一致;Body sign=HMAC-SHA256(username + "." + timestamp, agent_api_secret),使用小写 hex 和常量时间比较。
2. 必须按 Agent + username + currency + session_id + action 幂等;VCT 会自动或人工重试相同请求。
3. 普通 exit_game、api_kick_exit、switch_game 用于关闭对应 session、释放临时状态并记录钱包快照;switch_game 汇总同币种全部 Provider 钱包。保存 wallet_breakdown 与 mirrored_adjustments,并按 event_id 对账。
4. Seamless 模式资金已由 bet-push 实时处理;普通 offline 不得再次给主钱包加款或扣款。wallet_cleared_total 是物理合计,net_balance_to_reconcile 才是扣除镜像后的对账值。
5. cleared_at 是 Hub 清空本地钱包的时间,不是回调发送时间;迟到 bet-push 仍要继续按幂等资金规则处理。
6. launch_failed 只撤销本次启动的 launch_pending、临时锁和记录,不得写 enter_game、不得进入普通退出流程、不得重复入账。
7. 只有数据库事务成功提交后才返回 HTTP 2xx 和 {"status":"success"};重复请求已处理也应幂等返回 success。
8. 暂时性故障返回非 2xx 或 status=error,让 VCT 重试;不能在处理失败时返回 success。
9. 日志保存 session_id、action、username、currency、reclaimed_amount、cleared_at 和处理结果,并脱敏 Secret、Token、sign。
10. 增加测试:三种普通退出、launch_failed、重复请求、乱序到达、签名错误、timestamp 过期、事务回滚以及 bet-push 后不重复改余额。
请复用现有会话表、钱包账本、事务、锁和认证组件,完成路由、Service、幂等约束及测试。