Skip to content

API Documenation ​

本页面包含了 2J Game API 的详细说明,包括全局接口说明、错误码定义、金额参数、游戏类型等内容。通过阅读本页面,开发者可以了解如何与 2J Game 的服务器进行交互,如何处理不同类型的游戏请求,以及如何处理 API 返回的错误码。

Seamless Wallet 模式 和 Transfer Wallet 模式 是 2J Game 提供的两种钱包模式,OP 可根据自身需求选择合适的模式。

无缝钱包模式 ​

无缝钱包(Seamless Wallet)模式是指用户的账户余额在 OP 和 2J 之间实现无缝对接,用户无需在 OP 和 2J 之间进行充值和提现操作。玩家在 2J 平台进行游玩时(如下注、派彩行为),2J 会通过调用 OP 的接口取操作玩家的账户。

交互流程 ​

uml diagram

接入文档 ​

点击查看 无缝钱包模式接入文档

转账钱包模式 ​

转账钱包(Transfer Wallet)模式是指用户的账户余额在 OP 和 2J 之间实现转账操作,OP 需要调用 2J 的接口对玩家账号进行充值和提现操作,玩家在 2J 平台进行游玩时(如下注、派彩行为),2J 会直接操作玩家的账户,OP 可以通过调用 2J 的接口查询玩家的账户余额。

交互流程 ​

uml diagram

接入文档 ​

点击查看 转账钱包模式接入文档

全局接口说明 ​

通用参数 ​

OP 和 2J 服务器之间每个请求 querystring 中都必须携带参数,如下:

参数类型必要说明
mchint是2J分配给接入OP的标识id(通常为三位或者四位id)
tsint是毫秒时间戳(milliseconds)
signstring是签名,计算与验证规则参见 签名计算与验证
sweepstakesstring否指定使用的货币类型:sc/gc;开户为 SweepStakes 的商户必传此参数

金额参数 ​

OP 和 2J服务器之间接口交互中涉及到的金额参数都以厘为单位,数据类型为int64。

1元 = 1000厘
1分 = 10厘

签名计算与验证 ​

每次请求都需要计算 sign,并将它与 mch、ts 一起放在 URL 的 querystring 中。参数定义参见 通用参数。

计算规则 ​

text
sign = lowercase_hex(MD5(rawBodyBytes + UTF8(ts) + UTF8(key)))

+ 表示按顺序直接拼接字节,中间不添加分隔符。

输入 / 输出约定
jsonbody / rawBodyBytes实际发送或收到的原始 HTTP 请求体,使用 UTF-8 编码
tsURL query 中毫秒时间戳的十进制字符串,计算和发送时使用同一个值
key商户对应的密钥,按 UTF-8 编码参与计算,不随请求发送
signMD5 摘要的 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。

go
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 编译源码。

java
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。

js
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。

py
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
<?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 时仅计算签名。计算在当前浏览器内完成,不发送网络请求。

粘贴实际发送或收到的 body 原文,保留空格和转义,不要解析后重新序列化。文本框换行按 LF 计算;原文含 CRLF 时请使用代码示例。

响应结构 ​

所有接口的响应结构体都包含以下字段:

json
{
    "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

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_idstringop侧用户唯一标识必要,最长64字符
user_infoobject用户信息必要
user_info.nicknamestringop 玩家昵称必要
user_info.genderintop 玩家性别必要
user_info.cntstring默认国家可选
user_info.lanstring默认语言可选

Response Body:

Content-Type: application/json

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

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_idstringop侧用户唯一标识必要
game_idint游戏id必要
langstring语言可选
backlinkstring返回 OP 地址可选
device_typeint玩家客户端类型 1: h5, 2: android, 3: ios可选
device_idstring玩家客户端设备唯一id可选
ret_lobby_btnbool是否显示返回大厅按钮可选
full_screenint控制在使用浏览器情况下,是否自动进入全屏可选
auto_create_accountbool是否自动创建账号可选
keep_tokenbool是否保持 token 不变可选
  1. game_id 详细清单可参考这里
  2. lang 参考:https://zh.wikipedia.org/wiki/ISO_639-1
  3. backlink: 控制游戏中退出游戏按钮执行的逻辑
    • 如果传 about:blank 或者为空 触发代码: window.close()
    • 如果传链接 触发代码: window.location.href=backlink
    • 如果是用Iframe的方式打开游戏,可以使用设置backlink参数为脚本的方式使用,例如:javascript:window.parent.postMessage({ type: 'exit_customer_page'}, '*'), 这样会触发父页面的onmessage事件,然后父页面可以根据type参数做相应的处理。

Response Body:

Content-Type: application/json

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

json
{
    "op_id": "xxx-xxx-aaa", // 必填。op侧用户的唯一标识
}

Response Body:

Content-Type: application/json

注意:如果玩家同时在多个游戏中,那么 game_info 只会返回最近活跃的游戏。

json
{
    "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

json
{
    "op_id": "xxx-xxx-aaa" // 必填。op侧用户的唯一标识
}

Response Body:

Content-Type: application/json

json
{
    "header": {
        "code": 0,
        "trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
        "msg": "",
        "timestamp": 1710310573038
    },
    "is_gaming": false, // 是否在游戏中
    "game_ids": [1006] // 游戏id列表
}

2J Game Inc.