聚合支付网关 - 商户接入指南
本平台提供统一的支付API接口,商户只需对接一套API即可使用多个支付渠道。
| 项目 | 说明 |
|---|---|
| API地址 | |
| 请求方式 | POST(Content-Type: application/x-www-form-urlencoded) |
| 字符编码 | UTF-8 |
| 签名算法 | MD5 |
| 支持币种 | HKD(港币)、CNY(人民币),默认 HKD |
| 返回格式 | JSON(code=0 表示成功,其他表示失败) |
所有API接口返回统一的JSON格式:
// 成功 { "code": 0, "msg": "success", "data": { ... } } // 失败 { "code": -1, "msg": "错误信息" }
所有API请求都必须携带签名参数,用于验证请求的合法性和完整性。
sign 外)按参数名 ASCII 码升序排列key=value 格式用 & 拼接(空值不参与签名)&secret=你的API_SecretMD5(32位小写)得到签名值假设参数为:api_key=abc123, timestamp=1709284800, channel=kpay, amount=100.00,Secret 为 my_secret_key
// 1. 按key排序: amount, api_key, channel, timestamp // 2. 拼接: amount=100.00&api_key=abc123&channel=kpay×tamp=1709284800 // 3. 追加secret: amount=100.00&api_key=abc123&channel=kpay×tamp=1709284800&secret=my_secret_key // 4. MD5 => "e10adc3949ba59abbe56e057f20f883e"
<?php $api_key = '你的API_KEY'; $api_secret = '你的API_SECRET'; $params = [ 'api_key' => $api_key, 'timestamp' => time(), 'channel' => 'kpay', 'order_no' => 'ORD' . date('YmdHis') . rand(1000, 9999), 'amount' => '100.00', 'currency' => 'HKD', 'pay_method' => 'alipay_scan', 'subject' => '商品购买', 'notify_url' => 'https://www.example.com/notify.php', 'return_url' => 'https://www.example.com/return.php', ]; // 签名 ksort($params); $sign_str = ''; foreach ($params as $k => $v) { if ($v !== '') $sign_str .= "$k=$v&"; } $sign_str .= "secret=$api_secret"; $params['sign'] = md5($sign_str); // 发送请求 $ch = curl_init('https://平台地址/api/pay.php'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = json_decode(curl_exec($ch), true); curl_close($ch); if ($result['code'] === 0) { $pay_url = $result['data']['pay_url']; // 将 pay_url 返回给前端处理(生成二维码或跳转) }
import hashlib, time, random, requests API_KEY = "你的API_KEY" API_SECRET = "你的API_SECRET" API_URL = "https://平台地址/api/" def make_sign(params, secret): filtered = {k: v for k, v in params.items() if v != '' and k != 'sign'} sign_str = '&'.join(f'{k}={v}' for k, v in sorted(filtered.items())) sign_str += f'&secret={secret}' return hashlib.md5(sign_str.encode()).hexdigest() params = { 'api_key': API_KEY, 'timestamp': str(int(time.time())), 'channel': 'kpay', 'order_no': f'ORD{int(time.time())}{random.randint(1000,9999)}', 'amount': '100.00', 'currency': 'HKD', 'pay_method': 'alipay_scan', 'subject': '商品购买', 'notify_url': 'https://www.example.com/notify', 'return_url': 'https://www.example.com/return', } params['sign'] = make_sign(params, API_SECRET) resp = requests.post(API_URL + 'pay.php', data=params, verify=False) result = resp.json() if result['code'] == 0: pay_url = result['data']['pay_url'] # 将 pay_url 返回给前端
import java.security.MessageDigest; import java.util.*; import java.net.http.*; import java.net.URI; public class PayClient { static String API_KEY = "你的API_KEY"; static String API_SECRET = "你的API_SECRET"; static String API_URL = "https://平台地址/api/"; public static String sign(Map<String, String> params, String secret) { TreeMap<String, String> sorted = new TreeMap<>(params); sorted.remove("sign"); StringBuilder sb = new StringBuilder(); for (var e : sorted.entrySet()) { if (e.getValue() != null && !e.getValue().isEmpty()) sb.append(e.getKey()).append("=").append(e.getValue()).append("&"); } sb.append("secret=").append(secret); return md5(sb.toString()); } static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] hash = md.digest(input.getBytes("UTF-8")); StringBuilder hex = new StringBuilder(); for (byte b : hash) hex.append(String.format("%02x", b)); return hex.toString(); } catch (Exception e) { throw new RuntimeException(e); } } public static void main(String[] args) throws Exception { Map<String, String> params = new HashMap<>(); params.put("api_key", API_KEY); params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("channel", "kpay"); params.put("order_no", "ORD" + System.currentTimeMillis()); params.put("amount", "100.00"); params.put("currency", "HKD"); params.put("pay_method", "alipay_scan"); params.put("subject", "商品购买"); params.put("notify_url", "https://www.example.com/notify"); params.put("sign", sign(params, API_SECRET)); // 拼接 form body 并发送 POST StringBuilder body = new StringBuilder(); params.forEach((k, v) -> { if (body.length() > 0) body.append("&"); body.append(k).append("=").append(java.net.URLEncoder.encode(v, java.nio.charset.StandardCharsets.UTF_8)); }); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(API_URL + "pay.php")) .header("Content-Type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(body.toString())) .build(); HttpResponse<String> resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(resp.body()); } }
const crypto = require('crypto'); const https = require('https'); const querystring = require('querystring'); const API_KEY = '你的API_KEY'; const API_SECRET = '你的API_SECRET'; const API_URL = 'https://平台地址/api/'; function makeSign(params, secret) { const keys = Object.keys(params).filter(k => k !== 'sign' && params[k] !== '').sort(); let str = keys.map(k => k + '=' + params[k]).join('&'); str += '&secret=' + secret; return crypto.createHash('md5').update(str).digest('hex'); } const params = { api_key: API_KEY, timestamp: String(Math.floor(Date.now() / 1000)), channel: 'kpay', order_no: 'ORD' + Date.now() + Math.floor(Math.random() * 10000), amount: '100.00', currency: 'HKD', pay_method: 'alipay_scan', subject: '商品购买', notify_url: 'https://www.example.com/notify', return_url: 'https://www.example.com/return', }; params.sign = makeSign(params, API_SECRET); const postData = querystring.stringify(params); const url = new URL(API_URL + 'pay.php'); const req = https.request({ hostname: url.hostname, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, (res) => { let body = ''; res.on('data', d => body += d); res.on('end', () => { const result = JSON.parse(body); if (result.code === 0) { console.log('pay_url:', result.data.pay_url); } }); }); req.write(postData); req.end();
timestamp 与服务器时间差不能超过 5分钟,否则请求会被拒绝。请确保服务器时间准确。商户可在商户后台「API密钥」页面设置IP白名单。设置后,只有白名单中的IP才能调用API。
| 配置项 | 说明 |
|---|---|
| 格式 | 多个IP用英文逗号分隔,如 1.2.3.4,5.6.7.8 |
| 留空 | 不限制,所有IP均可访问 |
| 生效范围 | 统一下单、订单查询、退款等所有API接口 |
IP不在白名单中: x.x.x.x 错误,请将提示中的IP地址加入白名单。| 维度 | 默认值 | 说明 |
|---|---|---|
| 每分钟 | 60次 | 超出返回 请求过于频繁 |
| 每天 | 5000次 | 超出返回 已达每日调用上限 |
timestamp(Unix 秒级时间戳)与服务器时间差不能超过 5分钟,否则返回 请求已过期。建议配置 NTP 时间同步。
所有API请求必须使用 HTTPS 协议,不支持 HTTP 明文传输。
/api/pay.php
创建支付订单,返回支付链接。商户后端将链接返回给前端,由前端展示(生成二维码或跳转)。
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
| api_key | 是 | string | 商户 API Key |
| timestamp | 是 | int | 当前 Unix 时间戳(秒) |
| sign | 是 | string | 签名(见签名认证) |
| channel | 否 | string | 渠道编码,如 kpay、wonder、shengpay。留空则自动在所有已配置渠道中轮询选择 |
| order_no | 是 | string | 商户订单号(同一商户下不可重复,建议 6~32 位字母数字) |
| amount | 是 | string | 订单金额(保留2位小数,如 100.00) |
| currency | 否 | string | 币种,支持 HKD(港币)和 CNY(人民币),默认 HKD |
| pay_method | 是 | string | 支付方式,详见下方支付方式表 |
| subject | 否 | string | 订单描述(将展示在支付页面) |
| notify_url | 是 | string | 异步通知地址(订单状态变化时平台向此地址发送POST通知,包括支付成功、失败、退款、关闭等) |
| return_url | 建议 | string | 支付完成后用户跳转地址,H5 和卡类支付时强烈建议传入 |
| payment_institution | 否 | string | 支付宝机构,默认 ALIPAYCN,可选 ALIPAYHK |
channel 参数留空时,系统会在商户所有已开通且支持该支付方式的渠道中自动轮询选择。
CNY 等非HKD币种时,平台会按支付类型(微信/支付宝/收银台)单独配置的汇率自动换算为HKD后提交给上游。例如汇率设为 0.91,则 100 HKD = 91 CNY。订单详情中会记录原始金额、原始币种和使用的汇率。请在商户后台「渠道配置」中按支付类型分别设置汇率。
POST /api/pay.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded api_key=4f5d22c3dffccf067a377871f06e350a ×tamp=1709284800 &channel=kpay &order_no=ORD20260301001 &amount=100.00 ¤cy=HKD &pay_method=alipay_scan &subject=商品购买 ¬ify_url=https://www.example.com/notify.php &return_url=https://www.example.com/return.php &sign=e10adc3949ba59abbe56e057f20f883e
{
"code": 0,
"msg": "success",
"data": {
"order_no": "20260301143000123456", // 平台订单号(用于后续查询/退款)
"pay_url": "https://..." // 支付链接
}
}
{ "code": -1, "msg": "签名错误", "data": null }
pay_url 为微信协议链接(weixin://wxpay/...),必须生成二维码供用户用微信扫一扫,不能直接跳转。pay_url 为微信支付直链,必须在微信内置浏览器中打开。pay_url 为支付宝链接,建议生成二维码供用户扫描使用,浏览器直接跳转不保证能百分百唤醒支付宝App。pay_url 为支付宝收银台链接,可以生成二维码或在支付宝内打开此链接。pay_url 为收银台页面链接,浏览器跳转后用户选择支付方式完成支付(KPay卡类支付 + Wonder收银台均统一为此方式)。pay_url 为 Wonder 收银台页面链接,浏览器跳转后用户自行选择支付方式。
商户后端下单成功后,将 pay_url 返回给前端,前端根据支付方式做不同处理。
pay_url 是微信支付协议链接(形如 weixin://wxpay/bizpayurl?pr=xxx),不能直接在浏览器打开。// 推荐使用 qrcode.js 库生成二维码 // CDN: https://cdn.jsdelivr.net/npm/qrcodejs/qrcode.min.js var qr_url = res.data.pay_url; new QRCode(document.getElementById('qrcode-container'), { text: qr_url, width: 256, height: 256 }); // 展示二维码后,前端轮询自己的后端接口查询订单状态 // 当后端返回已支付时自动跳转成功页面
pay_url 是微信 H5 支付链接,必须在微信内置浏览器中打开才能唤起支付。
window.location.href = res.data.pay_url;
// 建议下单时传入 return_url,支付完成后自动跳回
pay_url 是支付宝标准链接。建议生成二维码供用户使用支付宝扫描付款。// 推荐方式:生成二维码供用户扫描 new QRCode(document.getElementById('qrcode-container'), { text: res.data.pay_url, width: 256, height: 256 }); // 提示用户"请打开支付宝扫一扫付款"
pay_url 是拼装好的支付宝收银台链接。// 方式一:生成二维码 new QRCode(document.getElementById('qrcode-container'), { text: res.data.pay_url, width: 256, height: 256 }); // 方式二:支付宝内直接打开 window.location.href = res.data.pay_url;
pay_url 是收银台页面链接。
window.location.href = res.data.pay_url;
// 支付完成后会跳转回 return_url
pay_url 是 Wonder 收银台页面链接。
window.location.href = res.data.pay_url;
// 支付完成后会跳转回 return_url
| 渠道 | pay_method | 名称 | 说明 |
|---|---|---|---|
kpay | wechat_scan | 微信扫码 | 返回微信协议链接,须生成二维码 |
wechat_h5 | 微信H5 | 返回微信支付直链,须在微信内打开 | |
alipay_scan | 支付宝扫码 | 建议生成二维码供用户扫描 | |
alipay_h5 | 支付宝H5 | 可生成二维码或支付宝内打开链接 | |
cashier | 收银台支付 | 跳转至收银台页面完成支付(统一KPay卡类与Wonder收银台) | |
wonder | wechat_scan | 微信扫码 | 返回微信协议链接(weixin://),须生成二维码 |
wechat_h5 | 微信H5 | 微信内打开直接支付,非微信跳转支付页面 | |
alipay_scan | 支付宝扫码 | 返回支付宝二维码链接,须生成二维码 | |
alipay_h5 | 支付宝H5 | 返回支付宝H5直接支付链接 | |
cashier | 收银台支付 | 跳转至Wonder收银台,用户自选支付方式 | |
shengpay | wechat_scan | 微信扫码 | 返回微信Native支付链接,须生成二维码 |
wechat_h5 | 微信H5 | 返回微信H5支付链接,手机浏览器跳转支付 | |
alipay_scan | 支付宝扫码 | 返回支付宝二维码链接,须生成二维码 | |
alipay_h5 | 支付宝H5 | 返回支付宝H5支付链接,手机浏览器跳转支付 | |
alipay_pc | 支付宝PC | 返回支付宝电脑网站支付链接,PC浏览器跳转支付 |
/api/query.php
查询订单当前状态。支持通过平台订单号或商户订单号查询。
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
| api_key | 是 | string | 商户 API Key |
| timestamp | 是 | int | 当前 Unix 时间戳(秒) |
| sign | 是 | string | 签名 |
| order_no | 二选一 | string | 平台订单号(下单时返回的 order_no) |
| merchant_order_no | 二选一 | string | 商户订单号(下单时传入的 order_no) |
order_no 和 merchant_order_no 至少传一个。如果两个都传,优先使用 order_no。
{
"code": 0,
"msg": "success",
"data": {
"order_no": "20260301143000123456", // 平台订单号
"merchant_order_no": "ORD20260301001", // 商户订单号
"channel": "kpay", // 渠道编码
"channel_order_no": "2026030114350000001", // 上游渠道订单号(支付成功后返回)
"amount": "100.00", // 订单金额(HKD)
"currency": "HKD", // 币种
"subject": "测试商品", // 订单描述
"status": 1, // 状态码(见状态码表)
"trade_status": "PAID", // 交易状态
"status_text": "已支付", // 状态中文
"pay_method": "wechat_scan", // 支付方式
"refund_amount": "0.00", // 退款金额
"original_amount": "91.00", // 原始金额(仅非HKD下单时返回)
"original_currency": "CNY", // 原始币种(仅非HKD下单时返回)
"exchange_rate": "0.910000", // 下单时汇率(仅非HKD下单时返回)
"pay_url": "https://...", // 支付链接
"return_url": "https://...", // 支付完成跳转地址
"notify_url": "https://...", // 商户通知回调地址
"notify_status": 1, // 通知状态:0=未通知 1=成功 2=失败
"notify_status_text": "成功", // 通知状态中文
"notify_count": 1, // 已通知次数
"paid_at": "2026-03-01 14:35:00", // 支付时间
"failed_at": null, // 失败时间
"refunded_at": null, // 退款时间
"closed_at": null, // 关闭时间
"created_at": "2026-03-01 14:30:00" // 下单时间
}
}
| 参数 | 类型 | 说明 |
|---|---|---|
| order_no | string | 平台订单号 |
| merchant_order_no | string | 商户订单号 |
| channel | string | 渠道编码 |
| channel_order_no | string | 上游渠道订单号(支付成功后才有) |
| amount | string | 订单金额(HKD) |
| currency | string | 币种 |
| subject | string | 订单描述 |
| status | int | 状态码:0=待支付 1=已支付 2=失败 3=已退款 4=已关闭 |
| trade_status | string | 交易状态:WAIT_PAY / PAID / FAIL / REFUNDED / CLOSED |
| status_text | string | 状态中文描述 |
| pay_method | string | 支付方式 |
| refund_amount | string | 退款金额 |
| original_amount | string | 原始金额(仅非HKD下单时返回) |
| original_currency | string | 原始币种(仅非HKD下单时返回) |
| exchange_rate | string | 下单时汇率(仅非HKD下单时返回) |
| pay_url | string | 支付链接 |
| return_url | string | 支付完成跳转地址 |
| notify_url | string | 商户通知回调地址 |
| notify_status | int | 通知状态:0=未通知 1=成功 2=失败 |
| notify_status_text | string | 通知状态中文 |
| notify_count | int | 已通知次数 |
| paid_at | string | 支付时间(未支付为null) |
| failed_at | string | 失败时间(未失败为null) |
| refunded_at | string | 退款时间(未退款为null) |
| closed_at | string | 关闭时间(未关闭为null) |
| created_at | string | 下单时间 |
商户后端封装一个接口(如 /check_order),内部调用平台查询API并将结果返回给前端。
<?php // check_order.php - 商户后端接口,供前端轮询调用 $order_no = $_GET['order_no'] ?? ''; if (!$order_no) { echo json_encode(['status' => -1]); exit; } $params = [ 'api_key' => API_KEY, 'timestamp' => time(), 'order_no' => $order_no, ]; // 签名(复用签名函数) ksort($params); $sign_str = ''; foreach ($params as $k => $v) { if ($v !== '') $sign_str .= "$k=$v&"; } $sign_str .= "secret=" . API_SECRET; $params['sign'] = md5($sign_str); // 调用平台查询接口 $ch = curl_init('https://平台地址/api/query.php'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = json_decode(curl_exec($ch), true); curl_close($ch); // 返回给前端 header('Content-Type: application/json'); echo json_encode([ 'status' => $result['data']['status'] ?? 0, 'paid_at' => $result['data']['paid_at'] ?? '', ]);
# Flask 示例 - 商户后端查询接口 from flask import Flask, request, jsonify @app.route('/check_order') def check_order(): order_no = request.args.get('order_no', '') if not order_no: return jsonify({'status': -1}) params = { 'api_key': API_KEY, 'timestamp': str(int(time.time())), 'order_no': order_no, } params['sign'] = make_sign(params, API_SECRET) resp = requests.post(API_URL + 'query.php', data=params, verify=False) result = resp.json() return jsonify({ 'status': result.get('data', {}).get('status', 0), 'paid_at': result.get('data', {}).get('paid_at', ''), })
// Spring Boot 示例 - 商户后端查询接口 @GetMapping("/check_order") public Map<String, Object> checkOrder(@RequestParam String order_no) { Map<String, String> params = new HashMap<>(); params.put("api_key", API_KEY); params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("order_no", order_no); params.put("sign", sign(params, API_SECRET)); // 发送 POST 到平台查询接口 String response = httpPost(API_URL + "query.php", params); JSONObject result = new JSONObject(response); JSONObject data = result.optJSONObject("data"); Map<String, Object> ret = new HashMap<>(); ret.put("status", data != null ? data.optInt("status", 0) : 0); ret.put("paid_at", data != null ? data.optString("paid_at", "") : ""); return ret; }
// Express 示例 - 商户后端查询接口 app.get('/check_order', async (req, res) => { const { order_no } = req.query; if (!order_no) return res.json({ status: -1 }); const params = { api_key: API_KEY, timestamp: String(Math.floor(Date.now() / 1000)), order_no, }; params.sign = makeSign(params, API_SECRET); const resp = await fetch(API_URL + 'query.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(params), }); const result = await resp.json(); const data = result.data || {}; res.json({ status: data.status || 0, paid_at: data.paid_at || '', }); });
// 前端轮询商户自己的后端接口(不是直接调平台API) var order_no = '20260301143000123456'; // 下单时拿到的平台订单号 var timer = setInterval(function() { fetch('/check_order?order_no=' + order_no) .then(function(r) { return r.json(); }) .then(function(res) { if (res.status === 1) { clearInterval(timer); alert('支付成功!'); window.location.href = '/success.html'; } }); }, 3000); // 每3秒查询一次 // 5分钟后停止轮询 setTimeout(function() { clearInterval(timer); }, 300000);
status=1 时自动跳转成功页面。最长轮询 5分钟,超时提示用户重新下单。
/api/refund.php
对已支付的订单发起退款。退款金额不能超过原订单金额。
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
| api_key | 是 | string | 商户 API Key |
| timestamp | 是 | int | 当前 Unix 时间戳(秒) |
| sign | 是 | string | 签名 |
| order_no | 是 | string | 平台订单号 |
| refund_amount | 是 | string | 退款金额(不能超过原订单金额,保留2位小数) |
$params = [ 'api_key' => API_KEY, 'timestamp' => time(), 'order_no' => '20260301143000123456', 'refund_amount' => '100.00', ]; // 签名(同下单签名逻辑) ksort($params); $sign_str = ''; foreach ($params as $k => $v) { if ($v !== '') $sign_str .= "$k=$v&"; } $sign_str .= "secret=" . API_SECRET; $params['sign'] = md5($sign_str); $ch = curl_init('https://平台地址/api/refund.php'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = json_decode(curl_exec($ch), true);
params = {
'api_key': API_KEY,
'timestamp': str(int(time.time())),
'order_no': '20260301143000123456',
'refund_amount': '100.00',
}
params['sign'] = make_sign(params, API_SECRET)
resp = requests.post(API_URL + 'refund.php', data=params, verify=False)
result = resp.json()
if result['code'] == 0:
print('退款成功', result['data'])
Map<String, String> params = new HashMap<>(); params.put("api_key", API_KEY); params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("order_no", "20260301143000123456"); params.put("refund_amount", "100.00"); params.put("sign", sign(params, API_SECRET)); String response = httpPost(API_URL + "refund.php", params);
const params = { api_key: API_KEY, timestamp: String(Math.floor(Date.now() / 1000)), order_no: '20260301143000123456', refund_amount: '100.00', }; params.sign = makeSign(params, API_SECRET); const resp = await fetch(API_URL + 'refund.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(params), }); const result = await resp.json();
{
"code": 0,
"msg": "success",
"data": {
"order_no": "20260301143000123456",
"refund_amount": "100.00",
"refund_order_no": "R20260301..."
}
}
3(已退款),不可撤回notify_url 发送 trade_status=REFUNDED 的异步通知当订单状态发生变化时(支付成功、支付失败、退款、关闭等),平台会主动向商户下单时传入的 notify_url 发送 POST 通知。
| trade_status | status | 场景 | 说明 |
|---|---|---|---|
PAID | 1 | 支付成功 | 用户完成支付,需要发货或执行业务逻辑 |
FAIL | 2 | 支付失败 | 支付过程出现错误,可提示用户重新支付 |
REFUNDED | 3 | 退款成功 | 退款已到账,refund_amount 字段为实际退款金额 |
CLOSED | 4 | 订单关闭 | 订单被撤销或超时关闭 |
平台以 application/x-www-form-urlencoded 格式 POST 以下参数到商户的 notify_url:
| 参数 | 类型 | 说明 |
|---|---|---|
| order_no | string | 平台订单号 |
| merchant_order_no | string | 商户订单号 |
| amount | string | 订单金额 |
| currency | string | 币种 |
| status | int | 订单状态码(0=待支付 1=已支付 2=失败 3=已退款 4=已关闭) |
| trade_status | string | 交易状态(PAID / FAIL / REFUNDED / CLOSED)建议优先使用此字段判断业务逻辑 |
| channel | string | 渠道编码 |
| pay_method | string | 支付方式(如 wechat_scan、alipay_h5 等) |
| refund_amount | string | 退款金额(仅退款时有值,如 50.00;未退款时为空) |
| original_amount | string | 商户原始金额(仅非HKD下单时返回,如 91.00) |
| original_currency | string | 商户原始币种(仅非HKD下单时返回,如 CNY) |
| exchange_rate | string | 下单时使用的汇率(仅非HKD下单时返回) |
| paid_at | string | 支付时间(仅支付成功时有值) |
| timestamp | int | 通知发送时间戳 |
| sign | string | 签名(验签方式与请求签名一致) |
支付成功通知:
amount=100.00&channel=kpay¤cy=HKD&merchant_order_no=ORD20260301001 &order_no=20260301143000123456&paid_at=2026-03-01+14%3A35%3A00&pay_method=wechat_scan &status=1&trade_status=PAID×tamp=1709285700&sign=a1b2c3d4e5...
退款成功通知:
amount=100.00&channel=kpay¤cy=HKD&merchant_order_no=ORD20260301001 &order_no=20260301143000123456&paid_at=2026-03-01+14%3A35%3A00&pay_method=wechat_scan &refund_amount=50.00&status=3&trade_status=REFUNDED×tamp=1709287500&sign=b2c3d4e5f6...
amount 仍为原订单金额(100.00),refund_amount 为实际退款金额(50.00)。trade_status 字段判断业务类型并处理对应逻辑200,且响应体为纯文本 successsuccess,平台会按重试策略重新发送通知<?php // notify.php - 商户端通知接收地址 $params = $_POST; $sign = $params['sign']; unset($params['sign']); // 1. 验证签名 ksort($params); $sign_str = ''; foreach ($params as $k => $v) { if ($v !== '') $sign_str .= "$k=$v&"; } $sign_str .= "secret=" . API_SECRET; if (md5($sign_str) !== $sign) { echo 'sign error'; exit; } $trade_status = $params['trade_status']; $order_no = $params['merchant_order_no']; // 2. 根据 trade_status 分别处理 switch ($trade_status) { case 'PAID': // 支付成功:发货/充值等 db_update("UPDATE orders SET status='paid' WHERE order_no=?", [$order_no]); break; case 'REFUNDED': // 退款成功:更新订单状态、退回库存等 db_update("UPDATE orders SET status='refunded' WHERE order_no=?", [$order_no]); break; case 'CLOSED': // 订单关闭:释放库存等 db_update("UPDATE orders SET status='closed' WHERE order_no=?", [$order_no]); break; case 'FAIL': // 支付失败:可提示用户重新支付 db_update("UPDATE orders SET status='failed' WHERE order_no=?", [$order_no]); break; } // 3. 必须返回 success(无论哪种通知类型) echo 'success';
# Flask 示例 from flask import Flask, request import hashlib @app.route('/notify', methods=['POST']) def notify(): params = dict(request.form) sign = params.pop('sign', '') # 1. 验签 filtered = {k: v for k, v in sorted(params.items()) if v != ''} sign_str = '&'.join(f'{k}={v}' for k, v in filtered.items()) sign_str += f'&secret={API_SECRET}' if hashlib.md5(sign_str.encode()).hexdigest() != sign: return 'sign error', 200 # 2. 根据 trade_status 分别处理 trade_status = params['trade_status'] order_no = params['merchant_order_no'] if trade_status == 'PAID': # 发货/充值等 pass elif trade_status == 'REFUNDED': # 退款:更新订单状态 pass elif trade_status == 'CLOSED': # 订单关闭 pass elif trade_status == 'FAIL': # 支付失败 pass return 'success', 200
// Spring Boot 示例 @PostMapping("/notify") public String notify(@RequestParam Map<String, String> params) { String sign = params.remove("sign"); // 1. 验签 String expected = sign(new HashMap<>(params), API_SECRET); if (!expected.equals(sign)) { return "sign error"; } // 2. 根据 trade_status 分别处理 String tradeStatus = params.get("trade_status"); String orderNo = params.get("merchant_order_no"); switch (tradeStatus) { case "PAID": // 发货/充值等 break; case "REFUNDED": // 退款处理 break; case "CLOSED": // 订单关闭 break; case "FAIL": // 支付失败 break; } return "success"; }
// Express 示例 const express = require('express'); app.use(express.urlencoded({ extended: true })); app.post('/notify', (req, res) => { const params = { ...req.body }; const sign = params.sign; delete params.sign; // 1. 验签 const expected = makeSign(params, API_SECRET); if (expected !== sign) { return res.send('sign error'); } // 2. 根据 trade_status 分别处理 const { trade_status, merchant_order_no } = params; switch (trade_status) { case 'PAID': // 发货/充值等 break; case 'REFUNDED': // 退款处理 break; case 'CLOSED': // 订单关闭 break; case 'FAIL': // 支付失败 break; } res.send('success'); });
若商户 notify_url 未返回 success,平台会按以下间隔自动重试:
| 次数 | 间隔 | 说明 |
|---|---|---|
| 第1次 | 立即 | 状态变化时立即发送 |
| 第2次 | 1 分钟后 | 第1次重试 |
| 第3次 | 5 分钟后 | 第2次重试 |
| 第4次 | 30 分钟后 | 第3次重试 |
| 第5次 | 1 小时后 | 最后一次自动重试 |
200,且响应体为纯文本 success(不含引号、空格、HTML 标签、BOM 头等)。"success"(带引号)、SUCCESS(大写)、{ "status": "ok" }(JSON)等均视为失败。
merchant_order_no + trade_status 查询本地订单状态success,不要重复处理successPAID 和 REFUNDED 等不同类型的通知,这是正常的状态流转| status | trade_status | 状态 | 说明 |
|---|---|---|---|
| 0 | WAIT_PAY | 待支付 | 订单已创建,等待用户支付 |
| 1 | PAID | 已支付 | 用户支付成功 |
| 2 | FAIL | 支付失败 | 支付过程中出现错误 |
| 3 | REFUNDED | 已退款 | 订单已成功退款(由上游渠道通知或商户发起退款) |
| 4 | CLOSED | 已关闭 | 订单被撤销或超时关闭 |
status 为数字类型,trade_status 为字符串类型,两者含义一致。trade_status 字段,语义更明确。
| code | 说明 |
|---|---|
| 0 | 成功 |
| -1 | 业务错误(具体看 msg 字段) |
| 类别 | msg | 说明与排查 |
|---|---|---|
| 签名 | 缺少签名参数 | 请求中缺少 api_key / timestamp / sign |
| 请求已过期 | timestamp 与服务器时间差超过5分钟 | |
| 无效的api_key | api_key 不存在或商户已被禁用 | |
| 商户已过期 | 商户有效期已过,联系管理员续期 | |
| 签名错误 | 检查:(1)参数排序 (2)空值排除 (3)Secret 是否正确 | |
| 安全 | IP不在白名单中: x.x.x.x | 将提示的IP加入白名单 |
| 请求过于频繁 / 已达每日调用上限 | 超出频率限制,联系管理员调整 | |
| 下单 | 渠道不可用 | 渠道已关闭或不存在 |
| 您未开通此渠道 | 管理员未给该商户开通此渠道 | |
| 该渠道不支持此支付方式 | pay_method 值错误 | |
| 订单号重复 | 同一 order_no 已存在 | |
| 退款 | 订单状态不允许退款 | 仅 status=1 可退款 |
| 退款金额不能超过订单金额 | 检查退款金额 |
&secret=xxx(注意有 &)notify_url 是否可从外网访问notify_url 返回的是纯文本 success,HTTP状态码 200wechat_scan 返回的 pay_url 是微信支付协议链接(weixin://wxpay/...),必须生成二维码让用户用微信扫一扫。
HKD(港币)和 CNY(人民币),默认 HKD。0.91,表示 1 HKD = 0.91 CNY。original_amount、original_currency、exchange_rate 字段,方便商户对账。
平台为每个商户提供一个通用的在线付款页面,客户打开后可自行选择支付方式并完成付款。
https://您的域名/api/payment.php?merchant=M202603040336
merchant 参数自动识别商户