接入概述
- 开通账号:联系 Telegram @Crab2476 / @zhouv66,获取商户号
mchNo、应用 IDappId、应用密钥key以及网关地址。 - 沙箱联调:使用测试参数在沙箱环境完成下单、查询、回调全流程(见沙箱环境)。
- 配置生产参数:在商户后台配置异步通知地址、服务器出口 IP 白名单。
- 上线:切换为生产密钥与生产网关地址,小额验证后正式放量。
密钥只能保存在服务端,切勿写入 App、小程序或网页前端代码。
请求规范
- 协议:
HTTPS,请求方式POST - 格式:
Content-Type: application/json,字符编码UTF-8 - 金额:单位为分,整数,例如
100表示 1.00 元 - 时间戳:
reqTime为 13 位毫秒时间戳,与服务器时间相差超过 10 分钟的请求将被拒绝
公共参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mchNo | String | 是 | 商户号 |
| appId | String | 是 | 应用 ID |
| reqTime | Long | 是 | 请求时间,13 位毫秒时间戳 |
| version | String | 是 | 接口版本,固定 1.0 |
| signType | String | 是 | 签名类型,固定 MD5 |
| sign | String | 是 | 签名值,见签名规则 |
公共返回
{
"code": 0, // 0 表示成功,其余为失败
"msg": "SUCCESS", // 失败时为错误描述
"data": { ... }, // 业务数据
"sign": "..." // 对 data 内字段的签名,建议验签
}签名规则
- 取除
sign外所有非空参数,按参数名 ASCII 码从小到大排序。 - 以
key1=value1&key2=value2的格式拼接成字符串stringA。 - 在末尾拼接密钥:
stringSignTemp = stringA + "&key=" + 应用密钥。 - 对
stringSignTemp做 MD5,结果转为大写,即为sign。
示例
参数:amount=100, appId=A1001, mchNo=M1001, mchOrderNo=T20261016001, reqTime=1760600000000
stringA = amount=100&appId=A1001&mchNo=M1001&mchOrderNo=T20261016001&reqTime=1760600000000
stringSignTemp = stringA + "&key=你的应用密钥"
sign = MD5(stringSignTemp).toUpperCase()各语言签名代码见 SDK 下载。
统一下单
POST /api/pay/unifiedOrder
请求参数(另含公共参数)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mchOrderNo | String(64) | 是 | 商户订单号,商户系统内唯一 |
| wayCode | String | 是 | 支付方式编码,开通后提供可用列表 |
| amount | Long | 是 | 支付金额,单位分 |
| currency | String | 是 | 币种,如 cny |
| subject | String(64) | 是 | 商品标题 |
| body | String(256) | 是 | 商品描述 |
| notifyUrl | String | 否 | 异步通知地址,不填使用后台配置 |
| returnUrl | String | 否 | 支付完成后的跳转地址 |
| clientIp | String | 否 | 用户端 IP,用于风控 |
| expiredTime | Integer | 否 | 订单失效时间,单位秒,默认 7200 |
| extParam | String | 否 | 商户扩展参数,回调时原样返回 |
请求示例
POST https://{网关地址}/api/pay/unifiedOrder
{
"mchNo": "M1001",
"appId": "A1001",
"mchOrderNo": "T20261016001",
"wayCode": "ALI_WAP",
"amount": 128000,
"currency": "cny",
"subject": "会员充值",
"body": "会员充值 1280 元",
"notifyUrl": "https://merchant.example.com/pay/notify",
"reqTime": 1760600000000,
"version": "1.0",
"signType": "MD5",
"sign": "C380BEC2BFD727A4B6845133519F3AD6"
}返回 data 字段
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| payOrderId | String | 是 | 平台支付订单号 |
| mchOrderNo | String | 是 | 商户订单号 |
| orderState | Integer | 是 | 订单状态:0 订单生成,1 支付中,2 支付成功,3 支付失败,4 已撤销,5 已退款,6 订单关闭 |
| payDataType | String | 是 | 支付数据类型:payurl 跳转链接,codeUrl 二维码地址,form 表单 |
| payData | String | 否 | 支付数据,按 payDataType 使用 |
| errCode | String | 否 | 渠道错误码 |
| errMsg | String | 否 | 渠道错误描述 |
查询订单
POST /api/pay/query
payOrderId 与 mchOrderNo 二选一,同时传入时以 payOrderId 为准。 建议在未收到回调时主动查询,不要以查询代替回调作为唯一的到账依据。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| payOrderId | String | 二选一 | 平台支付订单号 |
| mchOrderNo | String | 二选一 | 商户订单号 |
返回 data 包含订单号、金额、orderState、successTime 等字段,含义同统一下单。
异步通知
订单状态变为成功或失败后,平台以 POST(application/x-www-form-urlencoded)方式请求notifyUrl,参数为订单信息加 sign。
- 先验签:用同样的签名规则计算后与
sign比对,一致才处理。 - 校验金额与订单号,并做幂等处理,同一订单可能收到多次通知。
- 处理成功后响应纯文本
SUCCESS;否则平台按 0s / 30s / 60s / 90s / 120s / 150s 递增间隔重试,最多 6 次。
payOrderId=P1981...&mchNo=M1001&appId=A1001&mchOrderNo=T20261016001
&amount=128000&state=2&successTime=1760600123000&extParam=&reqTime=1760600123456&sign=...代付下单
POST /api/transferOrder
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mchOrderNo | String(64) | 是 | 商户代付单号,商户系统内唯一 |
| ifCode | String | 是 | 代付通道编码,开通后提供 |
| entryType | String | 是 | 入账方式,如 BANK_CARD |
| amount | Long | 是 | 代付金额,单位分 |
| currency | String | 是 | 币种,如 cny |
| accountNo | String | 是 | 收款账号 |
| accountName | String | 否 | 收款人姓名 |
| bankName | String | 否 | 收款银行名称 |
| transferDesc | String | 是 | 代付备注 |
| notifyUrl | String | 否 | 代付结果通知地址 |
代付结果以异步通知或 POST /api/transfer/query 查询为准,状态:0 订单生成,1 转账中,2 转账成功,3 转账失败,4 订单关闭。
返回码
| code | 说明 | 处理建议 |
|---|---|---|
| 0 | 请求成功 | 订单是否支付成功以 orderState / 回调为准 |
| 非 0 | 请求失败,原因见 msg | 参数或签名错误请修正后重试,不要原单号重复提交已成功的订单 |
| 9999 | 系统异常 | 稍后使用查询接口确认订单状态,确认失败后再重试 |
常见失败原因:签名错误、商户或应用未启用、支付方式未开通、商户订单号重复、金额超出单笔限额、IP 不在白名单。
SDK 下载
以下为各语言的签名实现,配合任意 HTTP 客户端即可完成接入。Java / PHP / Python / Node.js / Go 完整 SDK 包与 Demo 工程请联系 Telegram @Crab2476 / @zhouv66 获取。
Java
public static String sign(Map<String, Object> params, String key) {
String raw = new TreeMap<>(params).entrySet().stream()
.filter(e -> e.getValue() != null && !"".equals(e.getValue().toString()) && !"sign".equals(e.getKey()))
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
return DigestUtils.md5Hex(raw + "&key=" + key).toUpperCase();
}PHP
function sign(array $params, string $key): string {
unset($params['sign']);
$params = array_filter($params, fn($v) => $v !== null && $v !== '');
ksort($params);
$raw = urldecode(http_build_query($params));
return strtoupper(md5($raw . '&key=' . $key));
}Python
import hashlib
def sign(params: dict, key: str) -> str:
items = sorted((k, v) for k, v in params.items() if k != "sign" and v not in (None, ""))
raw = "&".join(f"{k}={v}" for k, v in items)
return hashlib.md5(f"{raw}&key={key}".encode("utf-8")).hexdigest().upper()Node.js
const crypto = require("crypto");
function sign(params, key) {
const raw = Object.keys(params)
.filter((k) => k !== "sign" && params[k] !== undefined && params[k] !== null && params[k] !== "")
.sort()
.map((k) => `${k}=${params[k]}`)
.join("&");
return crypto.createHash("md5").update(`${raw}&key=${key}`, "utf8").digest("hex").toUpperCase();
}Go
func Sign(params map[string]string, key string) string {
keys := make([]string, 0, len(params))
for k, v := range params {
if k != "sign" && v != "" {
keys = append(keys, k)
}
}
sort.Strings(keys)
parts := make([]string, len(keys))
for i, k := range keys {
parts[i] = k + "=" + params[k]
}
sum := md5.Sum([]byte(strings.Join(parts, "&") + "&key=" + key))
return strings.ToUpper(hex.EncodeToString(sum[:]))
}沙箱环境
沙箱与生产环境接口、参数、签名规则完全一致,仅网关地址和密钥不同,不产生真实资金流动。
