概述

PayHub 支付平台为第三方商户提供统一收单 API。商户通过简单的 HTTP 接口即可快速接入多渠道支付能力。

支付方式接口适用场景
聚合扫码支付POST /api/cashier/qrcode-payPC / H5 展示二维码,用户扫码
微信 JSAPIPOST /api/cashier/wechat-js微信公众号 / 小程序内唤起支付

接入准备

获取凭证

在 PayHub 管理后台完成商户入驻后,获取以下信息:

凭证说明
app_id商户应用标识(唯一)
app_secret应用密钥,用于 HMAC-SHA256 签名
callback_url支付结果回调地址
⚠ 重要app_secret 仅创建时展示一次,请妥善保管。遗失可在管理后台重置。

环境地址

环境Base URL
生产https://payment.qiquan18.cn
测试https://payment.qiquan18.cn

安全机制

机制说明
HMAC-SHA256 签名每个请求必须携带签名,防篡改
IP 白名单商户可配置允许请求的服务器 IP 列表,非白名单 IP 直接返回 401
提单限频同一 IP 3 秒内仅允许提交 1 次订单(基于 Redis),防止恶意刷单
时间戳校验timestamp 与服务器时间偏差超过 ±5 分钟则拒绝请求
随机串防重放nonce 随机字符串,防止请求被截获后重放

签名算法(HMAC-SHA256)

签名步骤

  1. 取所有请求参数(不含 sign),去掉值为空的参数
  2. 按 key 的 ASCII 升序排列
  3. 拼接成 key1=value1&key2=value2&... 格式
  4. app_secret 为密钥,做 HMAC-SHA256 运算,输出 64 位小写 hex

签名示例

原始参数:
  app_id            = pk_live_51H8xK2eZvKYlo2C9xK2eZvKYlo2C
  amount            = 1000
  merchant_order_no = ORD20260315001
  notify_url        = https://example.com/notify
  timestamp         = 1710489862
  nonce             = a1b2c3d4

排序拼接:
  amount=1000&app_id=pk_live_51H8xK2eZvKYlo2C9xK2eZvKYlo2C&merchant_order_no=ORD20260315001&nonce=a1b2c3d4&notify_url=https://example.com/notify&timestamp=1710489862

签名:
  sign = hmac_sha256(拼接字符串, app_secret) → "e5b7a3f1c9d2..."

多语言实现

// PHP
$params = [
    'app_id'            => $appId,
    'amount'            => 1000,
    'merchant_order_no' => 'ORD20260315001',
    'notify_url'        => 'https://example.com/notify',
    'timestamp'         => time(),
    'nonce'             => bin2hex(random_bytes(16)),
];

// 1. 去空值 + 排序
$params = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($params);

// 2. 拼接
$signStr = http_build_query($params);    // 自动 key=value&key=value

// 3. HMAC-SHA256
$params['sign'] = hash_hmac('sha256', $signStr, $appSecret);
# Python
import hmac, hashlib, time, secrets, urllib.parse

params = {
    'app_id': app_id,
    'amount': 1000,
    'merchant_order_no': 'ORD20260315001',
    'notify_url': 'https://example.com/notify',
    'timestamp': int(time.time()),
    'nonce': secrets.token_hex(16),
}

# 去空值 + 排序 + 拼接
filtered = {k: v for k, v in params.items() if v not in ('', None)}
sign_str = '&'.join(f'{k}={v}' for k, v in sorted(filtered.items()))

# HMAC-SHA256
params['sign'] = hmac.new(
    app_secret.encode(), sign_str.encode(), hashlib.sha256
).hexdigest()
// Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
import org.apache.commons.codec.binary.Hex;

Map<String, String> params = new TreeMap<>();  // TreeMap 自动排序
params.put("app_id", appId);
params.put("amount", "1000");
params.put("merchant_order_no", "ORD20260315001");
params.put("notify_url", "https://example.com/notify");
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("nonce", UUID.randomUUID().toString().replace("-", ""));

// 拼接
StringBuilder sb = new StringBuilder();
params.forEach((k, v) -> {
    if (v != null && !v.isEmpty()) {
        if (sb.length() > 0) sb.append("&");
        sb.append(k).append("=").append(v);
    }
});

// HMAC-SHA256
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(), "HmacSHA256"));
String sign = Hex.encodeHexString(mac.doFinal(sb.toString().getBytes()));
// Node.js
const crypto = require('crypto');

const params = {
  app_id: appId,
  amount: 1000,
  merchant_order_no: 'ORD20260315001',
  notify_url: 'https://example.com/notify',
  timestamp: Math.floor(Date.now() / 1000),
  nonce: crypto.randomBytes(16).toString('hex'),
};

// 去空值 + 排序 + 拼接
const signStr = Object.keys(params)
  .filter(k => params[k] !== '' && params[k] != null)
  .sort()
  .map(k => `${k}=${params[k]}`)
  .join('&');

// HMAC-SHA256
params.sign = crypto.createHmac('sha256', appSecret)
  .update(signStr).digest('hex');
// Go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "sort"
    "strings"
)

params := map[string]string{
    "app_id":            appId,
    "amount":            "1000",
    "merchant_order_no": "ORD20260315001",
    "notify_url":        "https://example.com/notify",
    "timestamp":         fmt.Sprintf("%d", time.Now().Unix()),
    "nonce":             hex.EncodeToString(randomBytes(16)),
}

// 排序 + 拼接
keys := make([]string, 0, len(params))
for k, v := range params {
    if v != "" { keys = append(keys, k) }
}
sort.Strings(keys)
pairs := make([]string, len(keys))
for i, k := range keys { pairs[i] = k + "=" + params[k] }
signStr := strings.Join(pairs, "&")

// HMAC-SHA256
h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(signStr))
sign := hex.EncodeToString(h.Sum(nil))

通用规范

请求格式

项目要求
协议HTTPS(生产环境强制)
方法POST
Content-Typeapplication/json
字符编码UTF-8

公共请求参数

每个 API 请求都必须包含:

参数类型必填说明
app_idstring商户应用 ID
timestampintUnix 秒级时间戳,±5 分钟有效
noncestring随机字符串,≤32 位
signstringHMAC-SHA256 签名

统一响应结构

{
  "code": 200,
  "message": "ok",
  "data": { ... }
}

聚合扫码支付

生成聚合二维码,用户可使用 支付宝 / 微信 / 云闪付 扫码完成支付。

POST /api/cashier/qrcode-pay

请求参数

参数类型必填说明
app_idstring商户应用 ID
merchant_order_nostring商户订单号,最长 64 位,回调原样返回
amountint支付金额,单位:,最小值 1
notify_urlstring商户方回调地址,支付成功后通知此地址
bodystring商品描述 JSON,可为空
extrastring自定义透传参数,回调原样返回
notestring订单备注,最长 250 字符,默认 "在线支付"
timestampintUnix 时间戳
noncestring随机字符串
signstringHMAC-SHA256 签名

请求示例

POST /api/cashier/qrcode-pay HTTP/1.1
Content-Type: application/json

{
  "app_id": "pk_live_51H8xK2eZvKYlo2C9xK2eZvKYlo2C",
  "merchant_order_no": "ORD20260315001",
  "amount": 1000,
  "notify_url": "https://your-domain.com/pay/callback",
  "body": "{\"goods\":\"VIP月卡\",\"sku\":\"vip_monthly\"}",
  "extra": "{\"user_id\":12345,\"channel\":\"ios\"}",
  "note": "VIP月卡充值",
  "timestamp": 1710489862,
  "nonce": "a1b2c3d4e5f6",
  "sign": "e5b7a3f1c9d2e8b4a7f6..."
}

成功响应

{
  "code": 200,
  "message": "下单成功",
  "data": {
    "order_no": "QR20260315120000ABCD1234",
    "merchant_order_no": "ORD20260315001",
    "amount": 1000,
    "extra": "{\"user_id\":12345,\"channel\":\"ios\"}",
    "qr_code_url": "https://qr.example.com/xxxx.png",
    "source_url": "alipays://platformapi/startapp?...",
    "expire_minutes": 30
  }
}

响应字段说明

字段类型说明
order_nostring平台订单号
merchant_order_nostring商户订单号(原样返回)
amountint金额(分)
extrastring透传参数(原样返回)
qr_code_urlstring二维码图片 URL,前端直接 <img> 展示
source_urlstring支付宝跳转直链(H5 场景可直跳唤起)
expire_minutesint二维码过期时间(分钟)
💡 集成建议 PC 场景 — 将 qr_code_url 渲染为图片让用户扫码
H5 场景 — 可直接 location.href = source_url 唤起支付宝

错误响应示例

// 参数缺失 (422)
{"code":422,"message":"The merchant_order_no field is required.","data":{...}}

// 业务错误 (400)
{"code":400,"message":"金额必须大于 0","data":null}

// 签名错误 (401)
{"code":401,"message":"签名验证失败","data":null}

// 请求过频 (429)
{"code":429,"message":"请求过于频繁,请3秒后重试","data":null}

微信 JSAPI 支付 - 暂时未开通

用于微信公众号或小程序内唤起微信支付。

POST /api/cashier/wechat-js

请求参数

参数类型必填说明
app_idstring商户应用 ID
merchant_order_nostring商户订单号
amountint金额(分)
notify_urlstring回调地址
payeeMercIdstring收款方子商户号
subOpenIdstring用户在子商户公众号/小程序下的 OpenID
wxAppIdstring微信公众号/小程序 AppID
busiCodestring业务代码
isMiniPgstring"1" 小程序、"2" 公众号(默认 "2")
bodystring商品描述 JSON
extrastring透传参数
timestampint时间戳
noncestring随机字符串
signstring签名

成功响应

{
  "code": 200,
  "message": "下单成功",
  "data": {
    "order_no": "JS20260315120000ABCD1234",
    "merchant_order_no": "ORD20260315001",
    "jsapi_pay_info": {
      "appId": "wx1234567890abcdef",
      "timeStamp": "1710489862",
      "nonceStr": "a1b2c3d4e5f6g7",
      "package": "prepay_id=wx15120000000000",
      "signType": "RSA",
      "paySign": "BASE64_SIGN_STRING..."
    }
  }
}

前端唤起支付

// 微信公众号 H5
WeixinJSBridge.invoke(
  'getBrandWCPayRequest',
  response.data.jsapi_pay_info,
  function(res) {
    if (res.err_msg === "get_brand_wcpay_request:ok") {
      alert('支付成功');
    }
  }
);
// 微信小程序
wx.requestPayment({
  ...response.data.jsapi_pay_info,
  success(res) { console.log('支付成功', res); },
  fail(res)    { console.log('支付失败', res); }
});

异步回调 (Webhook)

支付成功后,平台向商户的 notify_url 发送 POST 请求通知支付结果。

回调机制

项目说明
触发条件渠道确认支付成功
请求方式POST + application/json
重试策略商户未返回 "success" 时,1 分钟后重试,最多 3 次
重试间隔1min → 1min → 1min(共 3 次推送)
实现方式Laravel Queue Job + 延迟派发
幂等要求商户须保证同一订单多次通知的幂等处理

回调参数

字段类型说明
order_nostring平台订单号
merchant_order_nostring商户订单号(原样返回
amountstring订单金额(元)
actual_amountstring实际支付金额(元)
statusint1 = 已支付
channel_codestring支付渠道编码
pay_timestring支付时间 YYYY-MM-DD HH:mm:ss
extrastring透传参数(原样返回
signstring回调签名(HMAC-SHA256)

回调示例

POST https://your-domain.com/pay/callback HTTP/1.1
Content-Type: application/json

{
  "order_no": "QR20260315120000ABCD1234",
  "merchant_order_no": "ORD20260315001",
  "amount": "10.00",
  "actual_amount": "10.00",
  "status": 1,
  "channel_code": "YINSHENG_QRCODE",
  "pay_time": "2026-03-15 12:30:00",
  "extra": "{\"user_id\":12345,\"channel\":\"ios\"}",
  "sign": "a1b2c3d4e5f6..."
}

商户验签 & 响应

收到回调后,用与请求签名相同的算法验证 sign,处理业务后返回:

HTTP 200
Content-Type: text/plain

success
⚠ 注意 必须返回纯文本 success(不是 JSON),否则平台将触发重试。

订单状态枚举

status含义标识
0待支付PENDING
1已支付PAID
2已关闭(超时)CLOSED
3已退款REFUNDED
4支付失败FAILED

支付渠道编码

channel_code名称类型
alipay_h5支付宝 H5alipay
alipay_pc支付宝 PCalipay
alipay_app支付宝 APPalipay
wxpay_h5微信 H5wxpay
wxpay_native微信 Nativewxpay
wxpay_jsapi微信 JSAPIwxpay
wxpay_app微信 APPwxpay
unionpay_h5银联 H5unionpay

错误码

HTTP Statuscodemessage 示例处理建议
200200下单成功
400400商户订单号已存在,请勿重复提交更换 merchant_order_no 后重试
400400金额必须大于 0检查 amount 参数
401401缺少 app_id 参数请求中加入 app_id
401401商户不存在 / 商户已停用确认 app_id 是否正确
401401请求已过期,请检查 timestamptimestamp 与服务器时间差 ±5 分钟内
401401IP 未授权检查 IP 白名单配置
401401签名验证失败检查签名算法与 app_secret
422422商户订单号不能为空补充必填参数
429429请求过于频繁,请3秒后重试同一 app_id + IP 3 秒内限 1 次
500500服务异常联系平台技术支持

接入流程

聚合扫码支付时序图

sequenceDiagram participant M as 商户服务器 participant P as PayHub 平台 participant C as 支付渠道(银盛) participant U as 用户 M->>P: 1. POST /api/cashier/qrcode-pay (带签名) P->>P: 2. 验签 + IP白名单 + 订单防重 + 限频 P->>C: 3. 调用渠道下单 C-->>P: 4. 返回二维码URL P-->>M: 5. 返回 qr_code_url + order_no M->>U: 6. 展示二维码 U->>C: 7. 扫码支付 C->>P: 8. 渠道回调通知 P->>P: 9. 更新订单状态 P->>M: 10. Webhook (merchant_order_no + extra) M-->>P: 11. 返回 "success"

步骤说明

  1. 商户服务器 → PayHub:POST /api/cashier/qrcode-pay(携带签名)
  2. PayHub:验签 → 商户状态 → 时间戳 → IP 白名单 → 订单防重 → 限频检查
  3. PayHub → 支付渠道:调用渠道下单接口
  4. 支付渠道 → PayHub:返回二维码 URL
  5. PayHub → 商户服务器:返回 qr_code_url + order_no
  6. 商户前端 → 用户:展示二维码
  7. 用户:扫码完成支付
  8. 支付渠道 → PayHub:异步回调通知
  9. PayHub:更新订单状态
  10. PayHub → 商户 notify_url:Webhook 推送(含 merchant_order_no + extra)
  11. 商户服务器 → PayHub:返回 "success"

SDK & 示例代码

以下为各语言的核心调用代码,包含签名计算与下单请求。

// PHP — 签名 + 下单
$params = [
    'app_id'            => $appId,
    'merchant_order_no' => 'ORD' . date('YmdHis') . mt_rand(1000, 9999),
    'amount'            => 1000,
    'notify_url'        => 'https://your-domain.com/pay/callback',
    'extra'             => json_encode(['user_id' => 12345]),
    'timestamp'         => time(),
    'nonce'             => bin2hex(random_bytes(16)),
];

// 签名
$filtered = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($filtered);
$signStr = urldecode(http_build_query($filtered));
$params['sign'] = hash_hmac('sha256', $signStr, $appSecret);

// 下单
$ch = curl_init($baseUrl . '/api/cashier/qrcode-pay');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode($params),
    CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
// $result['data']['qr_code_url'] → 二维码图片
// Java — 签名 + 下单 (需引入 okhttp、commons-codec)
TreeMap<String, String> params = new TreeMap<>();
params.put("app_id", appId);
params.put("merchant_order_no", "ORD" + System.currentTimeMillis());
params.put("amount", "1000");
params.put("notify_url", "https://your-domain.com/pay/callback");
params.put("extra", "{\"user_id\":12345}");
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("nonce", UUID.randomUUID().toString().replace("-", ""));

// 签名
StringBuilder sb = new StringBuilder();
params.forEach((k, v) -> {
    if (v != null && !v.isEmpty()) {
        if (sb.length() > 0) sb.append("&");
        sb.append(k).append("=").append(v);
    }
});
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(), "HmacSHA256"));
String sign = Hex.encodeHexString(mac.doFinal(sb.toString().getBytes()));
params.put("sign", sign);

// 下单
RequestBody body = RequestBody.create(
    new Gson().toJson(params), MediaType.parse("application/json"));
Request request = new Request.Builder()
    .url(baseUrl + "/api/cashier/qrcode-pay")
    .post(body).build();
Response response = new OkHttpClient().newCall(request).execute();
// 解析 response.body().string()
# Python — 签名 + 下单
import hmac, hashlib, time, secrets, requests, json

params = {
    'app_id': app_id,
    'merchant_order_no': f'ORD{int(time.time())}',
    'amount': 1000,
    'notify_url': 'https://your-domain.com/pay/callback',
    'extra': json.dumps({'user_id': 12345}),
    'timestamp': int(time.time()),
    'nonce': secrets.token_hex(16),
}

# 签名
filtered = {k: v for k, v in params.items() if v not in ('', None)}
sign_str = '&'.join(f'{k}={v}' for k, v in sorted(filtered.items()))
params['sign'] = hmac.new(
    app_secret.encode(), sign_str.encode(), hashlib.sha256
).hexdigest()

# 下单
resp = requests.post(
    f'{base_url}/api/cashier/qrcode-pay', json=params
).json()
# resp['data']['qr_code_url'] → 二维码图片
// Node.js — 签名 + 下单
const crypto = require('crypto');
const axios = require('axios');

const params = {
  app_id: appId,
  merchant_order_no: `ORD${Date.now()}`,
  amount: 1000,
  notify_url: 'https://your-domain.com/pay/callback',
  extra: JSON.stringify({ user_id: 12345 }),
  timestamp: Math.floor(Date.now() / 1000),
  nonce: crypto.randomBytes(16).toString('hex'),
};

// 签名
const signStr = Object.keys(params)
  .filter(k => params[k] !== '' && params[k] != null)
  .sort()
  .map(k => `${k}=${params[k]}`)
  .join('&');
params.sign = crypto.createHmac('sha256', appSecret)
  .update(signStr).digest('hex');

// 下单
const { data } = await axios.post(
  `${baseUrl}/api/cashier/qrcode-pay`, params
);
// data.data.qr_code_url → 二维码图片
// Go — 签名 + 下单
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "net/http"
    "sort"
    "strings"
    "time"
)

params := map[string]string{
    "app_id":            appId,
    "merchant_order_no": fmt.Sprintf("ORD%d", time.Now().Unix()),
    "amount":            "1000",
    "notify_url":        "https://your-domain.com/pay/callback",
    "extra":             `{"user_id":12345}`,
    "timestamp":         fmt.Sprintf("%d", time.Now().Unix()),
    "nonce":             hex.EncodeToString(randomBytes(16)),
}

// 签名
keys := make([]string, 0)
for k, v := range params {
    if v != "" { keys = append(keys, k) }
}
sort.Strings(keys)
pairs := make([]string, len(keys))
for i, k := range keys { pairs[i] = k + "=" + params[k] }
signStr := strings.Join(pairs, "&")

h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(signStr))
params["sign"] = hex.EncodeToString(h.Sum(nil))

// 下单
jsonData, _ := json.Marshal(params)
resp, _ := http.Post(
    baseUrl+"/api/cashier/qrcode-pay",
    "application/json",
    bytes.NewBuffer(jsonData),
)
// 解析 resp.Body → data.qr_code_url
<?php
/**
 * 聚合扫码支付 API 完整调用示例
 *
 * 包含签名计算、下单请求、响应处理的完整流程
 */

$appId     = 'your_app_id';     // 公钥
$appSecret = 'your_app_secret'; // 秘钥
$baseUrl   = 'https://payment.qiquan18.cn/api/cashier'; // 生产环境地址

// ─── 正确签名下单 ───
$orderNo = 'ORD_' . date('YmdHis') . '_' . mt_rand(1000, 9999);
$params = buildParams($appId, 1000, $orderNo); // 1000分 = 10元
$params['sign'] = makeSign($params, $appSecret);

echo "商户订单号: {$orderNo}\n";
$res = httpPost($baseUrl . '/qrcode-pay', $params);
echo "响应: " . json_encode($res['json'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";

if ($res['json']['code'] === 200) {
    $qrCodeUrl = $res['json']['data']['qr_code_url'];
    echo "二维码: {$qrCodeUrl}\n";
}

// ────── 工具函数 ──────

function buildParams(string $appId, int $amount, string $merchantOrderNo): array
{
    return [
        'app_id'            =&gt; $appId,
        'merchant_order_no' =&gt; $merchantOrderNo,
        'amount'            =&gt; $amount,
        'notify_url'        =&gt; 'https://your-domain.com/pay/callback',
        'extra'             =&gt; json_encode(['user_id' =&gt; 12345]),
        'note'              =&gt; '在线支付',
        'timestamp'         =&gt; time(),
        'nonce'             =&gt; bin2hex(random_bytes(16)),
    ];
}

function makeSign(array $params, string $secret): string
{
    unset($params['sign']);
    $params = array_filter($params, fn($v) =&gt; $v !== '' &amp;&amp; $v !== null);
    ksort($params);

    $pairs = [];
    foreach ($params as $k =&gt; $v) {
        $pairs[] = "{$k}={$v}";
    }
    $signStr = implode('&amp;', $pairs);

    return hash_hmac('sha256', $signStr, $secret);
}

function httpPost(string $url, array $data): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           =&gt; true,
        CURLOPT_HTTPHEADER     =&gt; ['Content-Type: application/json', 'Accept: application/json'],
        CURLOPT_POSTFIELDS     =&gt; json_encode($data),
        CURLOPT_RETURNTRANSFER =&gt; true,
        CURLOPT_TIMEOUT        =&gt; 30,
    ]);
    $body = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    return [
        'http_code' =&gt; $httpCode,
        'body'      =&gt; $body,
        'error'     =&gt; $error,
        'json'      =&gt; json_decode($body, true),
    ];
}