💰 支付对接文档(商户版)

v1.0 HMAC-SHA256 JSON RESTful

本文档面向接入商户,说明如何通过签名接口完成下单、查单、接收支付结果通知。

接口域名:http://127.0.0.1:3456(生产环境替换为你的公网域名)
通信方式:HTTP POST + JSON  |  编码:UTF-8  |  鉴权:merchant_id + secret_key + HMAC-SHA256 签名

一、商户凭证

接入前由平台方分配两个参数:

参数说明
merchant_id商户号,全局唯一
secret_key商户密钥,仅商户与平台知晓,严禁泄露

凭证在后台「商户管理」中创建:POST /api/merchants,返回 merchant_id 和 secret_key。

二、签名算法

所有接口的请求和响应都带 sign 签名,用于身份校验和防篡改。

2.1 签名生成步骤

  1. 收集所有参数(除 sign 外的全部字段,含 merchant_id、timestamp、nonce、业务字段)。
  2. 参数按 key 的 ASCII 字典序升序排序。
  3. 拼接成字符串:key1=value1&key2=value2&...(value 取原始值,不转义;空字符串不参与签名)。
  4. 用 secret_key 计算 HMAC-SHA256,输出 16 进制小写字符串,即 sign。

2.2 公共参数

参数类型必填说明
merchant_idstring✓商户号
timestampint✓当前时间戳(秒)。与服务器时间差超过 ±300 秒会被拒绝(防重放)
noncestring✓随机字符串,建议 16 位,防重放
signstring✓签名结果(不参与签名计算)

2.3 签名示例

假设商户号 M1001,密钥 abc123,下单参数:

{
  "merchant_id": "M1001",
  "timestamp": 1793018723,
  "nonce": "8fb066158f90fe71",
  "amount": 1000
}

排序后拼接串(空值 notify_url 不参与):

amount=1000&merchant_id=M1001&nonce=8fb066158f90fe71&timestamp=1793018723
sign = HMAC_SHA256("abc123", 上面字符串) 的 16 进制小写

三、接口 1:下单

POST /api/v1/order/create

请求参数

参数类型必填说明
merchant_idstring✓商户号
timestampint✓时间戳(秒)
noncestring✓随机串
signstring✓签名
amountint✓下单金额(单位:分),必须为商品单价的整数倍。如商品单价 100 分(¥1),传 1000 即购买 10 件
notify_urlstring✗支付结果回调地址;不传则用商品默认配置

响应

{
  "code": 0,
  "msg": "ok",
  "data": {
    "order_no": "E20261026204529052006191",
    "price": 100,
    "pay_url": "https://cashier.youzan.com/pay/buyer?...",
    "alipay_h5_url": "https://cashier.youzan.com/assets/c-cashier/alipay-h5?...",
    "alipay_open_url": "https://render.alipay.com/p/s/i?scheme=..."
  },
  "sign": "4c90fd7a32538d6ac17a1b6de15c2abf4f7e30f620ff015c2243488571e6bea0"
}

响应字段

字段说明
order_no订单号,查单主键,务必保存
price金额(单位:分)
pay_url有赞收银台(需登录有赞,自用/调试)
alipay_h5_url支付宝 H5 页(copy-link)
alipay_open_url给买家付款用:浏览器点开 → 唤起支付宝 App → 免登录直接付款
sign服务端对 data 字段的签名(用商户密钥验签)
买家付款用 alipay_open_url:任意浏览器/微信点开,自动唤起支付宝 App 收银台,全程无需登录有赞。

四、接口 2:查单

POST /api/v1/order/query

请求参数

参数类型必填说明
merchant_idstring✓商户号
timestampint✓时间戳(秒)
noncestring✓随机串
signstring✓签名
order_nostring✓下单返回的订单号

响应

{
  "code": 0,
  "msg": "ok",
  "data": {
    "order_no": "E20261026204529052006191",
    "paid": false,
    "status": 0,
    "status_text": "初始/未支付",
    "price": 100,
    "real_pay": 100,
    "pay_state": "ALL_UNPAID"
  },
  "sign": "b8cfb9e831082711209babaeb5b4c85f5a2dce9d595dce5d933a70afe1e55f04"
}

状态枚举 status

值含义
0初始/未支付
1申请失败
2处理中
3已付款 ✅
4成功 ✅
5失败
6已关闭
7已退票
paid = true 即视为支付成功。status >= 5 为终态(失败/关闭)。

五、接口 3:支付结果通知(异步回调)

商户在下单时传 notify_url,平台 worker 轮询到订单支付成功(或进入终态)后,主动 POST 到该地址。

通知请求体(支付成功)

{
  "merchant_id": "M1001",
  "order_no": "E20261026204529052006191",
  "kdt_id": "213478012",
  "price": 100,
  "paid": true,
  "status": 3,
  "statusText": "已付款",
  "payState": "ALL_PAID",
  "orderStatus": 20,
  "orderStatusText": "待发货",
  "realPay": 100,
  "paidAt": 1793018723000,
  "timestamp": 1793018723,
  "nonce": "a1b2c3d4e5f60718",
  "sign": "计算出的签名"
}

通知请求体(失败/关闭终态)

{
  "merchant_id": "M1001",
  "order_no": "E20261026204529052006191",
  "kdt_id": "213478012",
  "price": 100,
  "paid": false,
  "status": 6,
  "statusText": "已关闭",
  "timestamp": 1793018723,
  "nonce": "a1b2c3d4e5f60718",
  "sign": "计算出的签名"
}

商户处理要求

  1. 必须验签:按第二节算法,用 secret_key 重算 sign 并比对,防止伪造回调。
  2. 校验 timestamp 在合理时间窗口内。
  3. 校验 order_no 是已下单的订单号。
  4. 处理幂等:paidAt 相同或已处理过则直接忽略。
  5. 必须返回 {"code":0,"msg":"success"} 或 HTTP 200,否则平台会按轮询间隔重试。

签名字段说明

通知里的 sign 是对除 sign 外的全部字段(含 merchant_id、timestamp、nonce、业务字段)按 key 字典序排序后计算 HMAC-SHA256。空值字段不参与签名。

六、错误码

code含义
0成功
400参数错误(商品不存在 / 无可用商品 / 订单不存在)
401鉴权失败(签名错误 / 商户不存在 / 商户停用 / 时间戳超时)
403无权查询该订单
500服务端异常

七、各语言签名示例

Node.js

const crypto = require("crypto");

function sign(params, secretKey) {
  const keys = Object.keys(params)
    .filter((k) => params[k] !== undefined && params[k] !== null && params[k] !== "")
    .sort();
  const str = keys.map((k) => `${k}=${params[k]}`).join("&");
  return crypto.createHmac("sha256", secretKey).update(str, "utf8").digest("hex");
}

// 下单
const body = {
  merchant_id: "M1001",
  timestamp: Math.floor(Date.now() / 1000),
  nonce: crypto.randomBytes(8).toString("hex"),
  amount: 1000,  // 下单金额(分), 如 ¥10 = 1000, 必须是商品单价的整数倍
  notify_url: "https://你的回调地址/paid",
};
body.sign = sign(body, "你的secret_key");

const res = await fetch("http://127.0.0.1:3456/api/v1/order/create", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(body),
}).then((r) => r.json());

console.log(res.data.alipay_open_url); // 给买家付款的链接

Python

import hmac, hashlib, time, secrets, json, requests

def sign(params, secret_key):
    items = {k: v for k, v in params.items() if v not in (None, "")}
    s = "&".join(f"{k}={items[k]}" for k in sorted(items))
    return hmac.new(secret_key.encode(), s.encode(), hashlib.sha256).hexdigest()

body = {
    "merchant_id": "M1001",
    "timestamp": int(time.time()),
    "nonce": secrets.token_hex(8),
    "amount": 1000,  # 下单金额(分)
    "notify_url": "https://你的回调地址/paid",
}
body["sign"] = sign(body, "你的secret_key")

r = requests.post("http://127.0.0.1:3456/api/v1/order/create", json=body).json()
print(r["data"]["alipay_open_url"])

PHP

function sign(array $params, string $secretKey): string {
    $params = array_filter($params, fn($v) => $v !== null && $v !== '');
    ksort($params);
    $s = '';
    foreach ($params as $k => $v) $s .= "$k=$v&";
    $s = rtrim($s, '&');
    return hash_hmac('sha256', $s, $secretKey);
}

$body = [
    'merchant_id' => 'M1001',
    'timestamp'   => time(),
    'nonce'       => bin2hex(random_bytes(8)),
    'amount'      => 1000,  // 下单金额(分)
    'notify_url'  => 'https://你的回调地址/paid',
];
$body['sign'] = sign($body, '你的secret_key');

$ch = curl_init('http://127.0.0.1:3456/api/v1/order/create');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$res = json_decode(curl_exec($ch), true);
echo $res['data']['alipay_open_url'];

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "sort"
    "strings"
)

func Sign(params map[string]string, secretKey string) string {
    keys := make([]string, 0, len(params))
    for k, v := range params {
        if v != "" {
            keys = append(keys, k)
        }
    }
    sort.Strings(keys)
    parts := make([]string, 0, len(keys))
    for _, k := range keys {
        parts = append(parts, k+"="+params[k])
    }
    str := strings.Join(parts, "&")
    h := hmac.New(sha256.New, []byte(secretKey))
    h.Write([]byte(str))
    return hex.EncodeToString(h.Sum(nil))
}

八、回调验签示例(Node.js)

商户接收支付通知后,用同样算法验证 sign:

const crypto = require("crypto");

function verify(notifyBody, secretKey) {
  const { sign, ...rest } = notifyBody;
  const keys = Object.keys(rest)
    .filter((k) => rest[k] !== undefined && rest[k] !== null && rest[k] !== "")
    .sort();
  const str = keys.map((k) => `${k}=${rest[k]}`).join("&");
  const expected = crypto.createHmac("sha256", secretKey).update(str, "utf8").digest("hex");
  return expected === sign;
}

app.post("/paid", (req, res) => {
  if (!verify(req.body, "你的secret_key")) {
    return res.status(401).json({ code: 401, msg: "签名错误" });
  }
  // 校验时间戳
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - req.body.timestamp) > 300) {
    return res.json({ code: 400, msg: "时间戳超时" });
  }
  // 处理订单发货逻辑...
  console.log(`订单 ${req.body.order_no} 已支付, 实付 ${req.body.realPay} 分`);
  res.json({ code: 0, msg: "success" });  // 必须返回成功, 否则会重试
});

九、接入检查清单