中国立直麻将天梯榜 API

面向门店/雀庄的免费开放接口,用于雀士注册、对局录入与赛季排名查询。

REST JSON MD5 签名 HTTPS

概述

Base URL 1https://www.mj-rate.com

Base URL 2https://www.mj-rate.cn

所有接口均需在请求体中携带 rate_idsequenceparamstimestampsign,并通过签名校验。

参数类型必填说明
rate_idstring平台提供一个门店唯一的ID信息点击查看申请
sequencestring携带请求的序号,用于防重放,8位长度字符串(仅限数字或字母组合,不区分大小写)
paramsarray/object携带的包体信息
timestampint秒级时间戳
signstring32位大写字符串签名信息
请求头HEADER中需包含 Content-Type: application/json

认证与签名

平台为每个门店分配 rate_id(8 位非零数字串)与 secret点击查看申请

rate_id(8 位非零数字串)与 secret请向平台申请

secret 仅用于服务端签名计算,严禁下发前端或提交到公开仓库。

签名步骤:

  1. 将请求体除 sign 外的字段按键名升序排序;
  2. key=value&key=value&... 拼接,末尾追加 &secret=你的secret
  3. 对拼接串做 MD5 并转为大写,得到 sign
params 为对象时:需先对 params 递归按 key 升序排序,再 JSON.stringify(不转义 unicode、不转义 /)后参与拼接,否则验签失败。

timestamp 为秒级时间戳,与服务器时间差不得超过 ±300 秒。sequence 用于防重放,同一 rate_id + sequence 在 10 分钟内不可重复使用。

请求与响应

请求体统一结构:

{
  "rate_id": 12345678,
  "sequence": ABCD4321,
  "params": { 具体业务参数信息 },
  "timestamp": 1710000000,
  "sign": "XXXXXXXXXXXXXXXX"
}

响应统一结构:

{
  "code": 200,
  "msg": "success",
  "data": { "status": 1, "msg": "操作成功", ... }
}

code 为 200 表示请求被接受;data.status === 1 表示业务成功,0 表示业务失败。

错误码

code说明
200成功
400参数错误
405请求方法错误(需为 POST)
40001签名错误
40002时间戳过期
40003格式错误(缺参、类型错误等)
40004非授权IP地址(非授权IP请求超过10次,将封禁该IP地址24小时)
403禁止访问(如重复请求)
404资源不存在

雀士注册

POST /api/user/regedit

雀士信息注册。

params 参数

参数类型必填说明
usernamestring雀士昵称(中文昵称请使用URL编码,并删除所有空格,最大20个中文汉字,最小2个字符,不可以数字开头或纯数字)
phonestring手机号,国内 11 位 1 开头
avatarstring雀士头像 base64格式,解码后 ≤200KB
genderint雀士性别 0 女 / 1 男 / 2 未知(默认 2)
wechatstring微信号,后期绑定微信机器人使用
qqintQQ号码,一个QQ号码只能绑定一个雀士
emailstring雀士邮箱,后期可能发送广告信息等
birthdaystring雀士生日,格式为 YYYY-MM-DD,如果雀士已有生日,则不能修改

请求示例

{
  "rate_id": 12345678,
  "sequence": 87654321,
  "params": {
    "username": "Tom%20Cat",
    "phone": "13800138000",
    "avatar": "data:image/png;base64,iVBORw0KGgo...",
    "gender": 1
  },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应

{
  "code": 200,
  "msg": "success",
  "data": { "uuid": "01234567", "msg": "注册成功", "status": 1 }
}

雀士编辑

POST /api/user/edit

门店端修改雀士资料(仅限本门店或在本店有记录的雀士)。

params 参数

参数类型必填说明
phonestring雀士手机号
usernamestring雀士昵称,最大20个中文汉字,不可以数字开头,删除所有空格,中文昵称请使用URL编码
avatarstring雀士头像 base64格式,解码后 ≤200KB
genderint雀士性别,0 女 / 1 男 / 2 未知(默认 2),如果雀士已有性别,则不能修改
wechatstring微信号,后期绑定微信机器人使用
qqintQQ号码,一个QQ号码只能绑定一个雀士
emailstring雀士邮箱,后期可能发送广告信息等
birthdaystring雀士生日,格式为 YYYY-MM-DD,如果雀士已有生日,则不能修改

成功响应

{
  "code": 200,
  "msg": "success",
  "data": { "msg": "修改成功", "status": 1 }
}

查询雀士分数与排名

POST /api/user/getUserRateInfo

根据雀士手机号查询雀士的全局数据、各赛季数据,以及各赛季下的门店数据(区分四麻/三麻),并返回赛季全局排名与门店排名。

params 参数

参数类型必填说明
phonestring雀士手机号

请求示例

{
  "rate_id": 12345678,
  "sequence": 87654324,
  "params": {
    "phone": "13800138000"
  },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应

{
  "code": 200,
  "msg": "success",
  "data": {
    "status": 1,
    "msg": "处理成功",
    "userInfo": {
      "uuid": "01234567",
      "username": "Tom%20Cat",
      "avatarUrl": "https://xxx/xxx.png"
    },
    "recordData": {
      "global": {
        "four": {
          "total": 0,
          "one_total": 0, "two_total": 0, "three_total": 0, "four_total": 0, "fly_total": 0,
          "one_score": 0, "two_score": 0, "three_score": 0, "four_score": 0, "fly_score": 0
        },
        "three": {
          "total": 0,
          "one_total": 0, "two_total": 0, "three_total": 0, "four_total": 0, "fly_total": 0,
          "one_score": 0, "two_score": 0, "three_score": 0, "four_score": 0, "fly_score": 0
        }
      },
      "seasons": [
        {
          "seasonId": 1,
          "seasonName": "S1",
          "four": {
            "total": 0,
            "one_total": 0, "two_total": 0, "three_total": 0, "four_total": 0, "fly_total": 0,
            "one_score": 0, "two_score": 0, "three_score": 0, "four_score": 0, "fly_score": 0,
            "seasonRank": {
              "id": 1,
              "seasonTotal": 0,
              "bestStartIndex": 0,
              "bestWin": 0,
              "bestSum": 0,
              "bestValid": 1,
              "bestGroup": [],
              "rank": 0
            }
          },
          "three": {
            "total": 0,
            "one_total": 0, "two_total": 0, "three_total": 0, "four_total": 0, "fly_total": 0,
            "one_score": 0, "two_score": 0, "three_score": 0, "four_score": 0, "fly_score": 0,
            "seasonRank": {
              "id": 2,
              "seasonTotal": 0,
              "bestStartIndex": 0,
              "bestWin": 0,
              "bestSum": 0,
              "bestValid": 1,
              "bestGroup": [],
              "rank": 0
            }
          },
          "stores": [
            {
              "storeId": 100,
              "storeName": "某某雀庄",
              "four": {
                "total": 0,
                "one_total": 0, "two_total": 0, "three_total": 0, "four_total": 0, "fly_total": 0,
                "one_score": 0, "two_score": 0, "three_score": 0, "four_score": 0, "fly_score": 0,
                "seasonRank": {
                  "id": 3,
                  "seasonTotal": 0,
                  "bestStartIndex": 0,
                  "bestWin": 0,
                  "bestSum": 0,
                  "bestValid": 1,
                  "bestGroup": [],
                  "rank": 0
                }
              },
              "three": {
                "total": 0,
                "one_total": 0, "two_total": 0, "three_total": 0, "four_total": 0, "fly_total": 0,
                "one_score": 0, "two_score": 0, "three_score": 0, "four_score": 0, "fly_score": 0,
                "seasonRank": {
                  "id": 4,
                  "seasonTotal": 0,
                  "bestStartIndex": 0,
                  "bestWin": 0,
                  "bestSum": 0,
                  "bestValid": 1,
                  "bestGroup": [],
                  "rank": 0
                }
              }
            }
          ]
        }
      ]
    }
  }
}

新增对局记录

POST /api/record/add

新增一局天梯榜记录对局。name1~name4 为手机号,四个手机号均须已注册。

params 参数

参数类型必填说明
typeint对局类型,0为四人麻将,1为三人麻将
name1string第一位雀士手机号
score1float第一位雀士分数
name2string第二位雀士手机号
score2float第二位雀士分数
name3string第三位雀士手机号
score3float第三位雀士分数
name4string第四位雀士手机号,三人麻将时为0
score4float第四位雀士分数,三人麻将时为0
record_timestring对局时间 Y-m-d H:i:s,不传则当前时间
总分 1000 时,服务端会取整后 ×100 转为万点制。

您无需负责对对局分数的顺位排序,平台会根据分数一一匹配自行排序处理。

同分可用小数区分顺位(如 35000.2、35000.1),请注意使用降序,即0.2顺位在0.1前面。

请求示例

{
  "rate_id": 12345678,
  "sequence": 87654323,
  "params": {
    "type": 0,
    "name1": "13800138000", "score1": 35000,
    "name2": "13900139000", "score2": 25000,
    "name3": "13700137000", "score3": 22000,
    "name4": "13600136000", "score4": 18000,
    "record_time": "2026-03-11 20:00:00"
  },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应

{
  "code": 200,
  "msg": "success",
  "data": {
            "msg": "添加成功",
            "status": 1,
            "record_id": 64位字符串,请自行保存记录,后续可用此 ID 修改和删除记录
            "errCode": 200, // 错误码,见下表
           }
}

修改对局记录

POST /api/record/update

修改已有对局,仅限本门店记录。

params 参数

参数类型必填说明
record_idstring对局记录 ID,64位字符串
name1string第一位雀士手机号
score1float第一位雀士分数
name2string第二位雀士手机号
score2float第二位雀士分数
name3string第三位雀士手机号
score3float第三位雀士分数
name4string第四位雀士手机号,三人麻将时为0
score4float第四位雀士分数,三人麻将时为0

成功响应

{
  "code": 200,
  "msg": "success",
  "data": { "msg": "对局记录修改成功", "status": 1, "errCode": 200}
}

删除对局记录

POST /api/record/delete

删除对局,仅限本门店记录。params 只需 record_id

成功响应

{
  "code": 200,
  "msg": "success",
  "data": { "msg": "对局记录删除成功", "status": 1 }
}

查询对局记录

GET /api/record/getRecordList

查询门店的对局记录信息,默认查询门店全部对局记录。支持筛选功能,包括对局类型、雀士手机号、对局时间等。

params 参数

参数类型必填说明
typeint对局类型,0为四人麻将,1为三人麻将
phonestring雀士手机号
start_timeint/string查询的开始时间,支持时间戳和 Y-m-d H:i:s等多种格式
end_timeint/string查询的结束时间,支持时间戳和 Y-m-d H:i:s等多种格式
pageint查询页码,默认为1
page_sizeint每页数量,默认100,最大1000
如果设置时间筛选,结束时间必须大于开始时间,且单次时间跨度周期不可以超过3个月。

请求示例

{
  "rate_id": 12345678,
  "sequence": 87654323,
  "params": {
    "type": 0,
    "phone": "13800138000",
    "start_time": 1710000000,
    "end_time": "2026-03-21 20:00:00",
    "page": 1,
    "page_size": 100
  },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应

{
  "code": 200,
  "msg": "success",
  "data": {
            "msg": "查询成功",
            "status": 1,
            "total": 888, // 符合查询条件的总记录数据
            "recordList": {
                    {
                      "id": 64位字符串,
                      "storeName": 雀庄名称, "typeName": "天梯四麻" 或 "天梯三麻",
                      "name1": 一位雀士昵称, "score1": 一位雀士分数,
                      "name2": 二位雀士昵称, "score2": 二位雀士分数,
                      "name3": 三位雀士昵称, "score3": 三位雀士分数,
                      "name4": 四位雀士昵称 三麻时为空, "score4": 四位雀士分数 三麻时为空,
                      "recordTime": 对局时间, "createTime": 记录时间
                    },
                    {
                      "id": 64位字符串,
                      "storeName": 雀庄名称, "typeName": "天梯四麻" 或 "天梯三麻",
                      "name1": 一位雀士昵称, "score1": 一位雀士分数,
                      "name2": 二位雀士昵称, "score2": 二位雀士分数,
                      "name3": 三位雀士昵称, "score3": 三位雀士分数,
                      "name4": 四位雀士昵称 三麻时为空, "score4": 四位雀士分数 三麻时为空,
                      "recordTime": 对局时间, "createTime": 记录时间
                    },...
                },
           }
}

记录操作执行错误码

errCode说明
200成功
400name1~name4 或 score1~score4 数据有缺失
402name1~name4 中有重复雀士信息
305该对局信息在最近30天内完全重叠
403对局总分之和不为 1000 或 100000 (整数部分)
501、502、503、504name1~name4 中有未在平台注册雀士
601、602、603、604name1~name4 有雀士分数异常状态
701、702、703、704name1~name4 有雀士处于记录冷却期(5分钟)
801、802、803、804name1~name4 有雀士处于平台封禁期
500+错误时,服务器端将错误直接抛出,所以并不会检查是否会有多个雀士错误的情况。

查询用户赛季排名(该部分功能尚未完善)

GET /api/rank/getUserRank

按手机号查询雀士的赛季数据。

params 参数

参数类型必填说明
phonestring手机号
datestringYYYY-MM-DD,传则查该日所在赛季;不传返回所有赛季

成功响应示例(单赛季)

{
  "code": 200,
  "msg": "success",
  "data": {
    "username": "张三",
    "avatarUrl": "",
    "status": 1,
    "seasonId": 1,
    "seasonName": "S1",
    "seasonTotal": 12,
    "bestStartIndex": 2,
    "bestSum": 45.5,
    "bestGroup": [{ "score": 35000, "rank": 0, "point": 40.0 }],
    "createTime": "2026-03-01 00:00:00",
    "msg": "查询成功",
    "status": 1
  }
}

下载用户赛季证书

POST /api/season/downloadUserSeasonCertificate

按雀士手机号生成并下载指定赛季的证书 JPG。证书内容基于该雀士在全国榜territory=0storeId=0)对应模式下的 bestSum 计算段位与排名后渲染。

当前仅对已开放证书模板与渲染逻辑的赛季、模式生效(现阶段为四麻,赛季 S1、S2)。三麻或尚未开放的赛季将返回业务失败。

params 参数

参数类型必填说明
season_idint赛季 ID,如 1 表示 S1
phonestring雀士手机号(数字串,一般为国内 11 位)
typeint对局类型:0 四麻,1 三麻

请求示例

{
  "rate_id": 12345678,
  "sequence": "ABCD1234",
  "params": {
    "season_id": 2,
    "phone": "13800138000",
    "type": 0
  },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应

HTTP 200,Content-Type: image/jpeg,响应体为 JPG 二进制文件;建议客户端按 Content-Disposition 文件名保存(形如 赛季证书_S2_U12345_四麻.jpg)。

失败响应

仍为统一 JSON 结构,常见 data.msg

  • 雀士信息不存在
  • 当前赛季或模式暂不支持证书下载(需已开放证书模板与渲染)
  • 该雀士暂无对应模式的全国榜成绩,无法生成证书
  • 赛季证书图片不存在 / 证书生成失败
{
  "code": 200,
  "msg": "fail",
  "data": {
    "status": 0,
    "msg": "该雀士暂无对应模式的全国榜成绩,无法生成证书"
  }
}

获取用户赛季证书图片地址

POST /api/season/getUserSeasonCertificate

与下载接口参数一致;成功时不返回文件流,而是在 JSON 的 data.imageUrl 中给出证书 JPG 的完整 URL,便于前端直接展示或二次下载。

成功响应示例

{
  "code": 200,
  "msg": "success",
  "data": {
    "status": 1,
    "msg": "赛季证书已存在",
    "imageUrl": "www.mj-rate.com/static/resources/season/certificate/s2/gradeId_5_userId_12345.jpg"
  }
}

生涯模块

GET /api/career/getCareerConfig

获取生涯模式完整配置:段位规则列表、活动配置、连胜奖励、降段豁免、活跃度等说明文案。

params 参数

参数类型必填说明
(无)可传空对象 {}

请求示例

{
  "rate_id": 12345678,
  "sequence": "ABCD1234",
  "params": {},
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应(结构示意)

{
  "code": 200,
  "msg": "success",
  "data": {
    "status": 1,
    "msg": "查询成功",
    "data": {
      "title": "规则说明标题",
      "lastUpdateTime": "2026-05-01",
      "ruleList": [ { "name": "段位名", "tarot": "塔罗", "floors": 0, "star": 0, "rank": [], "win": 0, "exempt": "降段说明" } ],
      "activityList": [],
      "tips_win": "连胜奖励说明",
      "tips_exempt": "降段豁免说明",
      "tips_activity": "活跃度说明"
    }
  }
}
GET /api/career/getUserCareerData

根据雀士手机号查询该用户的生涯天梯数据:当前段位、星数、对局统计、可见段位配置列表、全局排名文案等。若用户无生涯记录会自动初始化。

params 参数

参数类型必填说明
phonestring雀士联系方式(数字串,校验为 require|number|max:13,一般为国内 11 位手机号)

请求示例

{
  "rate_id": 12345678,
  "sequence": "ABCD1235",
  "params": { "phone": "13800138000" },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应(字段示意)

data.data 主要包含:gradeIdstarrecord_totalseason_record_totalone_totalcareerConfig(段位列表,含 selected 标记当前段位)、globalRankText(全局排名相关)。

业务失败

手机号对应用户不存在时,data.status === 0data.msg 为「雀士信息不存在」。

POST /api/career/getUserCareerAnalysis

根据雀士手机号查询生涯数据分析:详细竞技数据、最近星数变化日志、当前段位配置等,并附带活动配置列表。

params 参数

参数类型必填说明
phonestring同「查询用户生涯数据」

请求示例

{
  "rate_id": 12345678,
  "sequence": "ABCD1236",
  "params": { "phone": "13800138000" },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应(结构示意)

{
  "code": 200,
  "msg": "success",
  "data": {
    "status": 1,
    "msg": "查询成功",
    "data": {
      "careerAnalysis": {
        "gradeId": 0, "star": 0, "win": 0,
        "record_total": 0, "season_record_total": 0,
        "one_total": 0, "two_total": 0, "three_total": 0, "four_total": 0,
        "exempt": 0, "globalRankText": 0,
        "starLog": [], "gradeInfo": { }
      },
      "activityList": []
    }
  }
}
GET /api/career/getCareerRankList

分页查询生涯天梯排行榜(按星数、一位数、总局数等排序,具体以后端实现为准)。

params 参数

参数类型必填说明
pageint页码,≥ 1
page_sizeint每页条数,1~100

请求示例

{
  "rate_id": 12345678,
  "sequence": "ABCD1237",
  "params": { "page": 1, "page_size": 20 },
  "timestamp": 1710000000,
  "sign": "YOUR_SIGN"
}

成功响应(结构示意)

{
  "code": 200,
  "msg": "success",
  "data": {
    "status": 1,
    "msg": "查询成功",
    "data": {
      "careerList": [
        {
          "userId": 1, "gradeId": 0, "star": 0,
          "uuid": "01234567", "username": "昵称", "avatarUrl": "https://...",
          "storeId": 0, "rank": 1, "gradeTxt": "段位·塔罗", "storeName": "雀庄名", "gradeImageUrl": "https://..."
        }
      ],
      "count": 100
    }
  }
}

门店接口

提示:门店注册接口并不对外开放。
POST /api/store/regedit

用于注册雀庄门店基础信息,并返回该门店的 API 凭证(rate_id / secret)。

params 参数

参数类型必填说明
licenseTypeint企业性质(0=个体户,1=企业)
companyNamestring企业名称
companyCodestring统一社会信用代码
idCardNamestring法人姓名
companyPhonestring法人联系方式(数字,最多 11 位)
namestring门店名称(需唯一)
phonestring门店联系方式(数字,最多 11 位)
ipstring门店对接服务器 IP(用于生成/绑定白名单)
logostring门店 LOGO(Base64 图片)

请求示例

{
  "rate_id": "12345678",
  "sequence": "87654321",
  "params": {
    "licenseType": 1,
    "companyName": "示例科技有限公司",
    "companyCode": "91310000XXXXXXXXXX",
    "idCardName": "张三",
    "companyPhone": "13800138000",
    "name": "示例雀庄(上海店)",
    "phone": "02112345678",
    "ip": "1.2.3.4",
    "logo": "data:image/png;base64,...."
  },
  "timestamp": 1700000000,
  "sign": "YOUR_SIGN"
}

成功响应

{
  "code": 200,
  "msg": "success",
  "data": {
    "rate_id": "23456789",
    "secret": "YOUR_RATE_SECRET",
    "msg": "注册成功",
    "status": 1
  }
}
POST /api/store/edit

用于编辑门店信息(门店必须存在且为启用状态)。可按需提交要修改的字段。

params 参数

参数类型必填说明
namestring门店名称(需唯一)
phonestring门店联系方式
logostring门店 LOGO(Base64 图片)
cityCodestring城市代码
addressstring详细地址
longitudestring经度
latitudestring纬度

请求示例

{
  "rate_id": "23456789",
  "sequence": "11223344",
  "params": {
    "name": "示例雀庄(上海店)- 新名称",
    "phone": "02100001111",
    "address": "上海市浦东新区XX路XX号",
    "longitude": "121.500000",
    "latitude": "31.200000"
  },
  "timestamp": 1700000000,
  "sign": "YOUR_SIGN"
}

成功响应

{
  "code": 200,
  "msg": "success",
  "data": {
    "msg": "编辑成功",
    "status": 1
  }
}

服务器消息推送

当门店在平台侧已配置推送接收地址时,系统在特定业务事件触发后会向该地址异步发起 POST 请求,请求体为 Content-Type: application/json; charset=utf-8 的 JSON 数据。推送任务经队列投递,与开放 API 的签名校验无关。

开通方式:推送接收地址及校验令牌需在平台后台与门店 API 信息绑定。目前请向平台管理员申请配置;未配置或地址为空时,平台不会发起推送。

成功响应约定

接口应返回 HTTP 状态码 2xx,且响应正文经去除首尾空白后必须为字符串 ok(区分大小写)。否则视为失败;平台会间隔约 10 秒重试,同一任务最多尝试 3 次。单次请求超时约 30 秒。

请求体结构

平台 POST 的 JSON 结构如下。其中 data 为业务负载:除各推送类型自有字段外,平台会合并 uuid(雀士编号)、phone(手机号)、storeId(门店对外 rate_id)等公共字段。

{
  "type": "season_enter_top100",
  "token": "与门店绑定的 serverToken",
  "data": {
    "uuid": "01234567",
    "phone": "13800138000",
    "storeId": 12345678,
    ... 各 type 对应的业务字段 ...
  }
}

推送类型(type)与触发场景

以下为当前平台会发起的推送类型;具体 data 内业务字段可能随版本扩展,接入时请做好未知字段兼容。

type说明data 中主要业务字段(除 uuid / phone / storeId 外)
season_enter_top100雀士赛季排名进入前 100 名seasonNamestoreNameusernameranklevelNamepointseasonTotalbestStartIndexbestWinrankList(组别等级列表)等
season_enter_top10雀士赛季排名进入前 10 名(进入前 10 时除本类型外,通常还会收到 season_enter_top100同上
season_fall_top100雀士赛季排名跌出前 100 名同上
career_promote生涯升段gradeIdBeforegradeIdAftergradeNameBeforegradeNameAfterstar
career_demote生涯掉段同上
career_retention生涯保段(触发降段保护)gradeIdgradeNamestar
abnormal_score异常分数 / 预警(如连续一位或分数触阈)storeNameusernametypeId(0 四人 / 1 三人)、rankscorerankList(顺位历史)
平台会记录推送日志(成功/失败、响应摘要等)便于排查;若需核对某次投递,可联系平台管理员协助查询。

签名伪代码

客户端需与服务端保持一致的 params 序列化与字段排序。

input = { rate_id, sequence, params(object), timestamp }
secret = 门店 secret

params_str = JSON.stringify(sortKeysRecursively(params))

data = {
  "params": params_str,
  "rate_id": rate_id,
  "sequence": sequence,
  "timestamp": timestamp
}
sort keys of data asc
signStr = join(key=value, "&") + "&secret=" + secret
sign = upper(MD5(signStr))

PHP 调用示例(cURL)

安全提醒:secret 仅用于服务端签名计算,严禁下发前端或提交到公开仓库。
<?php

class MjRateApiClient
{
    const RATE_ID = 'YOUR_RATE_ID';
    const RATE_SECRET = 'YOUR_RATE_SECRET';
    const RATE_URL = 'https://www.mj-rate.com/api/';

    public static function send(string $path, array $bizParams): array
    {
        $body = [
            'rate_id' => self::RATE_ID,
            'sequence' => (string)random_int(10000000, 99999999),
            'params' => $bizParams,
            'timestamp' => time(),
        ];
        $body['sign'] = self::buildSign($body);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, self::RATE_URL . $path);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=utf-8']);
        $res = curl_exec($ch);
        curl_close($ch);

        $json = json_decode((string)$res, true);
        return is_array($json) ? $json : ['code' => 0, 'message' => '解析失败', 'data' => $res];
    }

    private static function buildSign(array $body): string
    {
        $sign = strtoupper((string)($body['sign'] ?? ''));
        unset($body['sign']);

        $data = [];
        foreach ($body as $k => $v) {
            $data[$k] = is_array($v)
                ? json_encode(self::sortKeysRecursive($v), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
                : (string)$v;
        }
        ksort($data, SORT_STRING);

        $parts = [];
        foreach ($data as $k => $v) {
            $parts[] = $k . '=' . $v;
        }
        $parts[] = 'secret=' . self::RATE_SECRET;
        return strtoupper(md5(implode('&', $parts)));
    }

    private static function sortKeysRecursive(array $arr): array
    {
        ksort($arr, SORT_STRING);
        foreach ($arr as $k => $v) {
            if (is_array($v)) $arr[$k] = self::sortKeysRecursive($v);
        }
        return $arr;
    }
}

// 示例:注册雀士
$resp = MjRateApiClient::send('user/regedit', [
    'username' => rawurlencode('Tom Cat'),
    'phone' => '13800138000',
    'gender' => 1,
    // 'avatar' => 'data:image/png;base64,...' // ≤200KB(解码后)
]);
var_dump($resp);

Java 调用示例(OkHttp)

安全提醒:secret 仅用于服务端签名计算,严禁下发前端或提交到公开仓库。
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;

public class MjRateApiClient {
    static final String RATE_ID = "YOUR_RATE_ID";
    static final String RATE_SECRET = "YOUR_RATE_SECRET";
    static final String RATE_URL = "https://www.mj-rate.com/api/";

    static final ObjectMapper MAPPER = new ObjectMapper();
    static final OkHttpClient HTTP = new OkHttpClient.Builder()
            .callTimeout(java.time.Duration.ofSeconds(30))
            .build();

    public static Map<String, Object> send(String path, Map<String, Object> bizParams) throws Exception {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("rate_id", RATE_ID);
        body.put("sequence", String.valueOf(10000000 + new Random().nextInt(90000000)));
        body.put("params", bizParams);
        body.put("timestamp", System.currentTimeMillis() / 1000);
        body.put("sign", buildSign(body));

        String json = MAPPER.writeValueAsString(body);
        Request req = new Request.Builder()
                .url(RATE_URL + path)
                .post(RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")))
                .build();
        try (Response resp = HTTP.newCall(req).execute()) {
            String res = resp.body() != null ? resp.body().string() : "";
            return MAPPER.readValue(res, new TypeReference<Map<String, Object>>() {});
        }
    }

    private static String buildSign(Map<String, Object> body) throws Exception {
        Map<String, String> data = new TreeMap<>();
        for (Map.Entry<String, Object> e : body.entrySet()) {
            if ("sign".equals(e.getKey())) continue;
            Object v = e.getValue();
            if (v instanceof Map || v instanceof List) {
                data.put(e.getKey(), MAPPER.writeValueAsString(sortKeysRecursive(v)));
            } else {
                data.put(e.getKey(), String.valueOf(v));
            }
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> e : data.entrySet()) {
            if (sb.length() > 0) sb.append('&');
            sb.append(e.getKey()).append('=').append(e.getValue());
        }
        sb.append("&secret=").append(RATE_SECRET);
        return md5Upper(sb.toString());
    }

    private static Object sortKeysRecursive(Object obj) {
        if (obj instanceof Map) {
            Map<String, Object> m = new TreeMap<>();
            ((Map<?, ?>) obj).forEach((k, v) -> m.put(String.valueOf(k), sortKeysRecursive(v)));
            return m;
        }
        if (obj instanceof List) {
            List<Object> out = new ArrayList<>();
            for (Object v : (List<?>) obj) out.add(sortKeysRecursive(v));
            return out;
        }
        return obj;
    }

    private static String md5Upper(String s) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] dig = md.digest(s.getBytes(StandardCharsets.UTF_8));
        StringBuilder hex = new StringBuilder();
        for (byte b : dig) hex.append(String.format("%02x", b));
        return hex.toString().toUpperCase(Locale.ROOT);
    }

    public static void main(String[] args) throws Exception {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("username", java.net.URLEncoder.encode("Tom Cat", "UTF-8"));
        params.put("phone", "13800138000");
        params.put("gender", 1);
        Map<String, Object> resp = send("user/regedit", params);
        System.out.println(resp);
    }
}

Node.js 调用示例(fetch)

安全提醒:secret 仅用于服务端签名计算,严禁下发前端或提交到公开仓库。
import crypto from 'node:crypto';

const RATE_ID = 'YOUR_RATE_ID';
const RATE_SECRET = 'YOUR_RATE_SECRET';
const RATE_URL = 'https://www.mj-rate.com/api/';

function sortKeysRecursive(obj) {
  if (Array.isArray(obj)) return obj.map(sortKeysRecursive);
  if (obj && typeof obj === 'object') {
    const out = {};
    Object.keys(obj).sort().forEach((k) => { out[k] = sortKeysRecursive(obj[k]); });
    return out;
  }
  return obj;
}

function buildSign(body) {
  const copy = { ...body };
  delete copy.sign;
  const data = {};
  Object.keys(copy).forEach((k) => {
    const v = copy[k];
    data[k] = (v && typeof v === 'object') ? JSON.stringify(sortKeysRecursive(v)) : String(v);
  });
  const keys = Object.keys(data).sort();
  const parts = keys.map((k) => `${k}=${data[k]}`);
  parts.push(`secret=${RATE_SECRET}`);
  return crypto.createHash('md5').update(parts.join('&'), 'utf8').digest('hex').toUpperCase();
}

async function send(path, bizParams) {
  const body = {
    rate_id: RATE_ID,
    sequence: String(Math.floor(10000000 + Math.random() * 90000000)),
    params: bizParams,
    timestamp: Math.floor(Date.now() / 1000),
  };
  body.sign = buildSign(body);

  const res = await fetch(RATE_URL + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=utf-8' },
    body: JSON.stringify(body),
  });
  return await res.json();
}

// 示例:注册雀士
const resp = await send('user/regedit', {
  username: encodeURIComponent('Tom Cat'),
  phone: '13800138000',
  gender: 1,
  // avatar: 'data:image/png;base64,...'
});
console.log(resp);

Python 调用示例(requests)

安全提醒:secret 仅用于服务端签名计算,严禁下发前端或提交到公开仓库。
import hashlib
import json
import random
import time
import urllib.parse

import requests

RATE_ID = "YOUR_RATE_ID"
RATE_SECRET = "YOUR_RATE_SECRET"
RATE_URL = "https://www.mj-rate.com/api/"

def sort_keys_recursive(obj):
    if isinstance(obj, list):
        return [sort_keys_recursive(v) for v in obj]
    if isinstance(obj, dict):
        return {k: sort_keys_recursive(obj[k]) for k in sorted(obj.keys())}
    return obj

def build_sign(body: dict) -> str:
    data = {}
    for k, v in body.items():
        if k == "sign":
            continue
        if isinstance(v, (dict, list)):
            data[k] = json.dumps(sort_keys_recursive(v), ensure_ascii=False, separators=(",", ":"))
        else:
            data[k] = str(v)
    parts = [f"{k}={data[k]}" for k in sorted(data.keys())]
    parts.append(f"secret={RATE_SECRET}")
    raw = "&".join(parts)
    return hashlib.md5(raw.encode("utf-8")).hexdigest().upper()

def send(path: str, biz_params: dict) -> dict:
    body = {
        "rate_id": RATE_ID,
        "sequence": str(random.randint(10000000, 99999999)),
        "params": biz_params,
        "timestamp": int(time.time()),
    }
    body["sign"] = build_sign(body)
    resp = requests.post(RATE_URL + path, json=body, timeout=30)
    return resp.json()

# 示例:注册雀士
resp = send("user/regedit", {
    "username": urllib.parse.quote("Tom Cat"),
    "phone": "13800138000",
    "gender": 1,
    # "avatar": "data:image/png;base64,..."
})
print(resp)

技术支持与联系方式

如需对接支持、接口开通、问题排查,请通过以下方式联系我们:

邮箱

提交对接信息与错误截图

电话

适合紧急问题与现场对接

微信

点击微信号并添加

QQQ

点击直接发起会话