API Documenation
本页面包含了 2J Game API 的详细说明,包括全局接口说明、错误码定义、金额参数、游戏类型等内容。通过阅读本页面,开发者可以了解如何与 2J Game 的服务器进行交互,如何处理不同类型的游戏请求,以及如何处理 API 返回的错误码。
Seamless Wallet 模式 和 Transfer Wallet 模式 是 2J Game 提供的两种钱包模式,OP 可根据自身需求选择合适的模式。
无缝钱包模式
无缝钱包(Seamless Wallet)模式是指用户的账户余额在 OP 和 2J 之间实现无缝对接,用户无需在 OP 和 2J 之间进行充值和提现操作。玩家在 2J 平台进行游玩时(如下注、派彩行为),2J 会通过调用 OP 的接口取操作玩家的账户。
交互流程
接入文档
点击查看 无缝钱包模式接入文档
转账钱包模式
转账钱包(Transfer Wallet)模式是指用户的账户余额在 OP 和 2J 之间实现转账操作,OP 需要调用 2J 的接口对玩家账号进行充值和提现操作,玩家在 2J 平台进行游玩时(如下注、派彩行为),2J 会直接操作玩家的账户,OP 可以通过调用 2J 的接口查询玩家的账户余额。
交互流程
接入文档
点击查看 转账钱包模式接入文档
全局接口说明
通用参数
OP 和 2J 服务器之间每个请求 querystring 中都必须携带参数,如下:
| 参数 | 类型 | 必要 | 说明 |
|---|---|---|---|
| mch | int | 是 | 2J分配给接入OP的标识id(通常为三位或者四位id) |
| ts | int | 是 | 毫秒时间戳(milliseconds) |
| sign | string | 是 | 签名,计算与验证规则参见 签名计算与验证 |
| sweepstakes | string | 否 | 指定使用的货币类型:sc/gc;开户为 SweepStakes 的商户必传此参数 |
金额参数
OP 和 2J服务器之间接口交互中涉及到的金额参数都以厘为单位,数据类型为int64。
1元 = 1000厘
1分 = 10厘签名计算与验证
每次请求都需要计算 sign,并将它与 mch、ts 一起放在 URL 的 querystring 中。参数定义参见 通用参数。
计算规则
sign = lowercase_hex(MD5(rawBodyBytes + UTF8(ts) + UTF8(key)))+ 表示按顺序直接拼接字节,中间不添加分隔符。
| 输入 / 输出 | 约定 |
|---|---|
jsonbody / rawBodyBytes | 实际发送或收到的原始 HTTP 请求体,使用 UTF-8 编码 |
ts | URL query 中毫秒时间戳的十进制字符串,计算和发送时使用同一个值 |
key | 商户对应的密钥,按 UTF-8 编码参与计算,不随请求发送 |
sign | MD5 摘要的 32 位小写十六进制字符串 |
参与摘要的内容只有原始请求体、ts 和 key;URL 路径、mch、sweepstakes 及 sign 本身不加入待签名内容。
接收方使用收到的原始 body、query 中的 ts 和对应商户的 key 按相同规则计算,将结果与 query 中的 sign 完整比较。
jsonbody 必须是原始请求体
jsonbody 指原始 HTTP body,不是解析后的 JSON 对象,也不是将该对象重新序列化得到的字符串。
不同语言、框架或序列化配置可能产生不同的字段顺序、空格、换行、Unicode 转义和数字格式。例如,{"amount":1}、{ "amount": 1 } 与 {"amount":1.0} 表达的业务数据可以相同,但原始字节不同,签名也不同。
计算前不要解析后重新序列化、排序字段、格式化、压缩或 trim 请求体。必须保留原文中的空格、换行和转义字符。
多语言示例
每份示例只演示两件事:计算 sign,以及把它放入 URL query 后发起 POST 请求。原始 body 只准备一次,同一份字节用于签名和发送,不执行 JSON 解析或重新序列化。
示例中的完整 API 地址 https://api.example.com/open2j/c/create、mch = 108 和 key = example_key 为占位值,请替换为测试环境对应的值。ts 使用本次请求的毫秒时间戳;sign 随 body、ts 或 key 的变化而变化。展开对应语言查看代码,源码文件保存为 UTF-8。
Go · Go 1.17+ / 标准库
HTTP 服务中可使用 io.ReadAll(r.Body) 读取原始字节,并处理读取错误;计算签名时使用读取到的字节,不使用结构体重新生成 JSON。
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"time"
)
// rawBody must be the exact bytes sent or received over HTTP.
func computeSign(rawBody []byte, ts, key string) string {
h := md5.New()
h.Write(rawBody)
h.Write([]byte(ts))
h.Write([]byte(key))
return hex.EncodeToString(h.Sum(nil))
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
rawBody := []byte(`{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}`)
mch, key := "108", "example_key"
ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
// Sign the original UTF-8 bytes; do not parse or reserialize the JSON.
sign := computeSign(rawBody, ts, key)
query := url.Values{"mch": {mch}, "ts": {ts}, "sign": {sign}}.Encode()
endpoint := "https://api.example.com/open2j/c/create?" + query
// Send the exact same bytes used above to compute the signature.
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(rawBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("Status: %s\n%s\n", resp.Status, body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP request failed with status %d", resp.StatusCode)
}
return nil
}Java · JDK 11+ / 标准库
Servlet 服务中从 request.getInputStream() 读取原始字节,不从 DTO 或 Map 重建 body。包含非 ASCII 字符时,使用 javac -encoding UTF-8 编译源码。
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
public class SignatureExample {
// rawBody must be the exact bytes sent or received over HTTP.
public static String computeSign(byte[] rawBody, String ts, String key)
throws NoSuchAlgorithmException {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(rawBody);
md5.update(ts.getBytes(StandardCharsets.UTF_8));
md5.update(key.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(32);
for (byte value : md5.digest()) {
hex.append(String.format("%02x", value & 0xff));
}
return hex.toString();
}
public static void main(String[] args) throws Exception {
byte[] rawBody = ("{\"op_id\":\"player01\",\"user_info\":{\"nickname\":\"Alice\","
+ "\"gender\":0,\"cnt\":\"ph\",\"lan\":\"en\"}}").getBytes(StandardCharsets.UTF_8);
String mch = "108";
String key = "example_key";
String ts = Long.toString(System.currentTimeMillis());
// Sign the original UTF-8 bytes; do not parse or reserialize the JSON.
String sign = computeSign(rawBody, ts, key);
String query = "mch=" + URLEncoder.encode(mch, StandardCharsets.UTF_8)
+ "&ts=" + URLEncoder.encode(ts, StandardCharsets.UTF_8)
+ "&sign=" + URLEncoder.encode(sign, StandardCharsets.UTF_8);
URI endpoint = URI.create("https://api.example.com/open2j/c/create?" + query);
HttpRequest request = HttpRequest.newBuilder(endpoint)
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json; charset=utf-8")
// Send the exact same bytes used above to compute the signature.
.POST(HttpRequest.BodyPublishers.ofByteArray(rawBody))
.build();
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println("Status: " + response.statusCode());
System.out.println(response.body());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IllegalStateException("HTTP request failed with status " + response.statusCode());
}
}
}Node.js · Node.js 18+ / ES Modules
保留原始请求体 Buffer;使用 Express 等框架时,在 JSON 解析中间件之前获取原始 body,不对解析后的 req.body 调用 JSON.stringify。
import { createHash } from 'node:crypto'
import { pathToFileURL } from 'node:url'
// rawBody must be the exact Buffer sent or received over HTTP.
export function computeSign(rawBody, ts, key) {
return createHash('md5')
.update(rawBody)
.update(ts, 'utf8')
.update(key, 'utf8')
.digest('hex')
}
async function main() {
const rawBody = Buffer.from('{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}', 'utf8')
const mch = '108'
const key = 'example_key'
const ts = Date.now().toString()
const url = new URL('https://api.example.com/open2j/c/create')
url.search = new URLSearchParams({ mch, ts, sign: computeSign(rawBody, ts, key) }).toString()
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10_000)
try {
// Node.js 18+: send the same Buffer that was signed.
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: rawBody,
signal: controller.signal,
})
console.log(`HTTP ${response.status}`)
console.log(await response.text())
if (!response.ok) process.exitCode = 1
} finally {
clearTimeout(timeout)
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(() => {
console.error('Request failed or exceeded the 10-second timeout.')
process.exitCode = 1
})
}Python 3 · Python 3.7+ / 标准库
签名函数接收 bytes。例如 Flask 的 request.get_data()、Django 的 request.body 可用于获取原始请求体;不要对解析结果调用 json.dumps。
import hashlib
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
def compute_sign(raw_body: bytes, ts: str, key: str) -> str:
"""raw_body must be the exact bytes sent or received over HTTP."""
md5 = hashlib.md5()
md5.update(raw_body)
md5.update(ts.encode("utf-8"))
md5.update(key.encode("utf-8"))
return md5.hexdigest()
def main() -> int:
raw_body = '{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}'.encode("utf-8")
mch = "108"
key = "example_key"
ts = str(time.time_ns() // 1_000_000)
query = urlencode({"mch": mch, "ts": ts, "sign": compute_sign(raw_body, ts, key)})
url = "https://api.example.com/open2j/c/create?" + query
try:
# Send the same bytes that were signed.
request = Request(url, data=raw_body, method="POST",
headers={"Content-Type": "application/json; charset=utf-8"})
with urlopen(request, timeout=10) as response:
print(f"HTTP {response.status}")
print(response.read().decode("utf-8", errors="replace"))
return 0 if 200 <= response.status < 300 else 1
except HTTPError as error:
with error:
print(f"HTTP {error.code}")
print(error.read().decode("utf-8", errors="replace"))
return 1
except (URLError, OSError, ValueError):
print("Request failed or exceeded the 10-second timeout.", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())PHP · PHP 7+ / 内置函数
可使用 file_get_contents('php://input') 读取原始请求体,并处理读取失败。PHP 字符串按字节参与计算,不经过 json_decode / json_encode。
<?php
declare(strict_types=1);
// Sign the original body bytes, followed by ts and key, with no separators.
function computeSign(string $rawBody, string $ts, string $key): string
{
return md5($rawBody . $ts . $key);
}
$url = 'https://api.example.com/open2j/c/create';
$mch = '108';
$key = 'example_key';
$rawBody = '{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}';
$ts = sprintf('%.0f', floor(microtime(true) * 1000));
$sign = computeSign($rawBody, $ts, $key);
$query = http_build_query([
'mch' => $mch,
'ts' => $ts,
'sign' => $sign,
], '', '&', PHP_QUERY_RFC3986);
$context = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json; charset=utf-8\r\n",
'content' => $rawBody, // Send the same bytes that computeSign hashed.
'timeout' => 10,
'ignore_errors' => true, // Read non-2xx response bodies too.
'follow_location' => 0,
]]);
$responseBody = @file_get_contents($url . '?' . $query, false, $context);
if ($responseBody === false) {
fwrite(STDERR, "HTTP request or response read failed.\n");
exit(1);
}
$status = 0;
foreach ($http_response_header as $header) {
if (preg_match('/^HTTP\/\S+\s+(\d{3})\b/', $header, $matches)) {
$status = (int) $matches[1];
}
}
echo 'HTTP ', $status, PHP_EOL, $responseBody, PHP_EOL;
if ($status < 200 || $status >= 300) {
fwrite(STDERR, "HTTP request returned a non-success status.\n");
exit(1);
}验签失败排查
| 检查项 | 核对方式 |
|---|---|
| 原始请求体 | 对照实际发送或收到的原始字节;优先检查是否重新序列化、改变字段顺序、增加空格或末尾换行。 |
| 字符编码 | 请求体、ts 和 key 均使用 UTF-8;中文原文与 \uXXXX 转义文本的字节不同。 |
| 时间戳 | 使用 query 中同一个 ts 字符串,不在验签时生成新时间戳,也不要将毫秒换成秒。 |
| 密钥与拼接 | 核对商户密钥,按 body → ts → key 拼接,不添加分隔符或其他 query 参数。 |
| 签名格式 | 使用 32 位小写十六进制,保留前导 0,不使用大写、Base64 或二进制摘要。 |
在线签名计算与校验
默认已填入示例 body、ts、key 和对应的 sign,可直接点击计算并校验。也可以替换为自己的原始请求数据;清空待验证的 sign 时仅计算签名。计算在当前浏览器内完成,不发送网络请求。
响应结构
所有接口的响应结构体都包含以下字段:
{
"header": {
"code": 0, // 错误编码,0表示正常, 非0表示异常
"msg": "", // 错误提示
"timestamp": 1709716095516
},
}通用接口
1. 用户注册 /open2j/c/create
Method: POST
URI: /open2j/c/create
Description: 用于在 2J 服务器上注册用户信息。
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa", // op侧用户的唯一标识, required
"user_info": {
"nickname": "player01", // op 玩家昵称。注意:不支持 emoji 特殊字符
"gender": 0, // 0-unknow, 1-famale, 2-male
"cnt": "ph", // 默认国家
"lan": "en" // 默认语言
}
}参数说明:
| 参数 | 类型 | 说明 | 备注 |
|---|---|---|---|
| op_id | string | op侧用户唯一标识 | 必要,最长64字符 |
| user_info | object | 用户信息 | 必要 |
| user_info.nickname | string | op 玩家昵称 | 必要 |
| user_info.gender | int | op 玩家性别 | 必要 |
| user_info.cnt | string | 默认国家 | 可选 |
| user_info.lan | string | 默认语言 | 可选 |
Response Body:
Content-Type: application/json
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
}// 通用响应header
}2. 获取游戏链接 /open2j/c/launch
Method: POST
URI: /open2j/c/launch
Description: 用于获取游戏链接接口。
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa", // 必填。op侧用户的唯一标识
"game_id": 65005, // 必填。游戏id
"lang": "en-US",
"backlink": "http://xxxxx.com?a=ss", // 返回 OP 地址
"device_type": 2, // 可选。玩家客户端类型 1: h5, 2: android, 3: ios
"device_id": "AD19939", // 可选。玩家客户端设备唯一id
"ret_lobby_btn": false, // 可选。是否显示返回大厅按钮
"full_screen": 1, // 可选。控制在使用浏览器情况下,是否自动进入全屏 1: 不进入全屏 2: 自动进入全屏,该参数未传由2j服务器控制是否自动进入全屏
"auto_create_account": false, // 可选。是否自动创建账号, true: 自动创建账号, false: 不自动创建账号
"keep_token": false // 可选。是否保持 token 不变
}参数说明:
| 参数 | 类型 | 说明 | 备注 |
|---|---|---|---|
| op_id | string | op侧用户唯一标识 | 必要 |
| game_id | int | 游戏id | 必要 |
| lang | string | 语言 | 可选 |
| backlink | string | 返回 OP 地址 | 可选 |
| device_type | int | 玩家客户端类型 1: h5, 2: android, 3: ios | 可选 |
| device_id | string | 玩家客户端设备唯一id | 可选 |
| ret_lobby_btn | bool | 是否显示返回大厅按钮 | 可选 |
| full_screen | int | 控制在使用浏览器情况下,是否自动进入全屏 | 可选 |
| auto_create_account | bool | 是否自动创建账号 | 可选 |
| keep_token | bool | 是否保持 token 不变 | 可选 |
- game_id 详细清单可参考这里
- lang 参考:https://zh.wikipedia.org/wiki/ISO_639-1
- backlink: 控制游戏中退出游戏按钮执行的逻辑
- 如果传 about:blank 或者为空 触发代码:
window.close() - 如果传链接 触发代码:
window.location.href=backlink - 如果是用Iframe的方式打开游戏,可以使用设置backlink参数为脚本的方式使用,例如:javascript:window.parent.postMessage({ type: 'exit_customer_page'}, '*'), 这样会触发父页面的onmessage事件,然后父页面可以根据type参数做相应的处理。
- 如果传 about:blank 或者为空 触发代码:
Response Body:
Content-Type: application/json
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
},
"url": "http://abc.zzz?brd=default&chn=&lang=en-US&launch=blackjack&mch=108&opid=demo10087&token=6defed39fd6a4e98add31a4e1ead5172&version=1"
}3. 踢出用户 /open2j/c/evict
Method: POST
URI: /open2j/c/evict
Description: 提供给 OP 用于踢出用户, 如果玩家在游戏中,会被强制退出游戏。
注意:这里玩家被踢出后,会收到一个弹窗通知告知玩家被踢出,但如果 OP 在调用
/open2j/c/launch时没有提供backlink参数,会导致玩家无法返回 OP 端。
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa", // 必填。op侧用户的唯一标识
}Response Body:
Content-Type: application/json
注意:如果玩家同时在多个游戏中,那么 game_info 只会返回最近活跃的游戏。
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
},
"game_info": {
"game_id": 1006, // 游戏id
"join_at": 1720686368, // 加入时间
"leave_at": 1720686368, // 离开时间
}
}4. 查询玩家游戏状态 /open2j/c/detect-user-gaming
Method: POST
URI: /open2j/c/detect-user-gaming
Description: 用于查询玩家游戏状态。
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa" // 必填。op侧用户的唯一标识
}Response Body:
Content-Type: application/json
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
},
"is_gaming": false, // 是否在游戏中
"game_ids": [1006] // 游戏id列表
}