商户 API 对接文档
商户通过调用统一下单 API 获取支付收银台链接,用户完成支付后,系统将通过异步回调 API 实时通知商户支付结果。
简介
系统采用 RESTful API 设计,使用 JSON 作为数据交换格式。所有请求都需要进行签名验证以确保安全性。
1. 获取商户 ID 和密钥 → 2. 实现签名算法 → 3. 调用统一下单接口 → 4. 处理支付回调
基础规范
接口协议
| 配置项 | 值 | 说明 |
|---|---|---|
| 数据格式 | application/json |
所有请求和响应均为 JSON 格式 |
| 请求方式 | POST |
统一下单和回调接口均为 POST |
| 字符编码 | UTF-8 |
所有字符串使用 UTF-8 编码 |
| 时间格式 | ISO 8601 |
日期时间格式示例:2024-03-25T14:30:45+08:00 |
签名机制
为了保证交易安全,所有发往本系统的请求,以及本系统发往商户的回调,均包含 sign 字段进行签名校验。
签名生成步骤
- 将所有发送或接收到的参数(除
sign字段本身以及值为空的参数外)按参数名(Key)的 ASCII 码从小到大排序(即字典序 a-z)。 - 使用 URL 键值对的格式(即
key1=value1&key2=value2...)拼接成字符串StringA。 - 在
StringA最后拼接商户专属密钥key得到StringSignTemp(即StringA&key=YOUR_SECRET_KEY)。 - 对
StringSignTemp进行 MD5 运算,将得到的字符串所有字符转换为小写,即得到最终的sign值。
参数:mch_id=1001, amount=100.00, out_trade_no=ORDER123
排序后:amount=100.00&mch_id=1001&out_trade_no=ORDER123
拼接密钥:amount=100.00&mch_id=1001&out_trade_no=ORDER123&key=YOUR_SECRET_KEY
MD5结果:c3e5...
签名实现代码
const crypto = require('crypto');
function generateSign(params, secretKey) {
// 1. 过滤 sign 字段和空值
const filtered = {};
for (const [key, value] of Object.entries(params)) {
if (key !== 'sign' && value !== '' && value !== null && value !== undefined) {
filtered[key] = value;
}
}
// 2. 按键名 ASCII 排序
const sortedKeys = Object.keys(filtered).sort();
// 3. 拼接字符串
const stringA = sortedKeys.map(key => `${key}=${filtered[key]}`).join('&');
const stringSignTemp = `${stringA}&key=${secretKey}`;
// 4. MD5加密并转小写
const sign = crypto.createHash('md5')
.update(stringSignTemp)
.digest('hex')
.toLowerCase();
return sign;
}
// 使用示例
const params = {
mch_id: '1001',
amount: '100.00',
out_trade_no: 'ORDER123',
pay_type: 'ALIPAY',
notify_url: 'https://your-domain.com/notify'
};
const secretKey = 'YOUR_SECRET_KEY';
const sign = generateSign(params, secretKey);
console.log('Sign:', sign);
统一下单 API
用于商户向系统发起代收请求并获取收银台支付链接。
POST https://api.1117.xin/api/v1/gateway/create-order
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mch_id | Integer |
是 |
分配给商户的唯一 ID |
| out_trade_no | String |
是 |
商户系统内部的唯一订单号,最长 64 位 |
| amount | String |
是 |
订单金额,保留两位小数,如 100.00 |
| pay_type | String |
是 |
支付方式。固定枚举:ALIPAY (支付宝) |
| notify_url | String |
是 |
接收异步支付结果通知的回调地址(需公网可访问) |
| payer_name | String |
否 |
付款人姓名,实名转账校验(可选) |
| payer_account | String |
否 |
付款人账号,实名转账校验(可选) |
| sign | String |
是 |
签名,见 签名机制 章节 |
系统除了校验 Sign 签名,还会校验发起请求的服务器 IP 是否在后台配置的白名单中,否则返回 401 未授权。
响应参数
| 参数名 | 类型 | 说明 |
|---|---|---|
| code | Integer |
状态码。200 表示成功,非 200 表示失败 |
| message | String |
响应信息说明 |
| data | Object |
成功时返回的业务数据对象 |
| └ sys_trade_no | String |
本支付系统的内部唯一订单号 |
| └ pay_url | String |
收银台支付链接。引导用户跳转或在 App 内打开 |
请求示例
curl -X POST "https://api.1117.xin/api/v1/gateway/create-order" \
-H "Content-Type: application/json" \
-d '{
"mch_id": "1001",
"out_trade_no": "ORDER_20240325143045",
"amount": "100.00",
"pay_type": "ALIPAY",
"notify_url": "https://your-domain.com/pay/notify",
"sign": "c3e5..."
}'
成功响应示例
{
"code": 200,
"message": "success",
"data": {
"sys_trade_no": "S202403251430451234",
"pay_url": "https://p.1117.fun/cashier/S202403251430451234"
}
}
异步回调通知 API (Webhook)
当用户在收银台付款,并且本平台专员确认资金到账后,本系统会主动通过 POST 请求向商户下单时传入的 notify_url 发送支付结果通知。
回调参数
| 参数名 | 类型 | 说明 |
|---|---|---|
| mch_id | Integer |
商户 ID |
| out_trade_no | String |
商户下单时传的外部订单号 |
| sys_trade_no | String |
本支付系统的内部单号 |
| amount | String |
订单原始金额(商户请求时的金额) |
| actual_amount | String |
买家实际支付的金额(可能含尾数) |
| status | String |
订单状态:固定返回 PAID |
| sign | String |
本系统生成的 MD5 签名,商户必须验证 |
商户应以 amount(原始金额)作为给用户上分的依据,而不是 actual_amount。
商户返回规范
商户系统接收到回调并处理完业务逻辑(如给用户加余额)后,必须在 HTTP 响应的 Body 中只返回纯文本字符串:
success
如果商户响应的不是 success(如抛出 500 错误、超时或返回其他字符串),本系统会认为通知失败,并将在随后的定时任务中尝试重新发送回调(最多重试 5
次)。
回调处理示例
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// 回调接收端点
app.post('/pay/notify', (req, res) => {
const payload = req.body;
const receivedSign = payload.sign;
// 1. 验证签名(重要!防止伪造回调)
const calculatedSign = generateSign(payload, 'YOUR_SECRET_KEY');
if (receivedSign !== calculatedSign) {
console.error('签名验证失败');
return res.status(400).send('Invalid signature');
}
// 2. 验证订单状态
if (payload.status === 'PAID') {
// 3. 处理业务逻辑(给用户加余额等)
// 注意:使用 payload.amount 而不是 payload.actual_amount
const orderAmount = payload.amount;
const outTradeNo = payload.out_trade_no;
// TODO: 在这里更新您的数据库,给用户加余额
console.log(`订单 ${outTradeNo} 支付成功,金额:${orderAmount}`);
// 4. 必须返回 success 字符串
return res.send('success');
}
res.status(400).send('Invalid status');
});
function generateSign(params, secretKey) {
const filtered = {};
for (const [key, value] of Object.entries(params)) {
if (key !== 'sign' && value !== '' && value !== null) {
filtered[key] = value;
}
}
const sortedKeys = Object.keys(filtered).sort();
const stringA = sortedKeys.map(key => `${key}=${filtered[key]}`).join('&');
const stringSignTemp = `${stringA}&key=${secretKey}`;
return crypto.createHash('md5').update(stringSignTemp).digest('hex').toLowerCase();
}
app.listen(8080, () => console.log('Webhook server is listening on port 8080')); // 配置您的服务端口
实名验证接入
为了防止诈骗资金混入,收银台现已支持基于用户“付款方式”的实名校验。商户需要在统一下单接口中附加买家的真实姓名 (payer_name) 以及付款绑定的账号
(payer_account),以保障资金流的真实匹配。
一、接入原理与优势
二、商户系统前后端对接最佳实践
商户在开发前端应用时的最佳流程步骤如下:
- 确保您的平台具备用户真实姓名强制绑定的基础功能。
- 引导用户在您的前端“充值配置”页面中预先绑定其本人合法持有的付款账号(例如支付宝、微信号等)。
- 在展示给用户“扫码支付”或需要匹配信息的支付方式时,应该从后端接口获取该用户已绑定的账号列表。让用户通过选择预绑定的渠道完成下单,而非临时手动填写,尽可能避免输入错误和代付操作。
- 商户服务端接收到用户下单请求后,从自身数据库读出该账号实名数据,附加到
统一下单API后交至本收银台。
前端逻辑对接范例 (Vue 3 示例)
<script setup lang="ts">
import { ref, computed } from 'vue';
import api from '@/utils/request';
// 用户身份与选择状态
const payerName = ref('张三');
const paymentType = ref('ALIPAY'); // 用户选择支付宝作为当前通道
const payerAccount = ref('');
// 建议:从服务端拉取用户在商户端提前绑定的钱包白名单
const userWallets = ref([
{ type: 'ALIPAY', account: 'my_alipay_acc', accountName: '张三' },
{ type: 'WECHAT', account: 'wx_123456', accountName: '张三' }
]);
// 动态筛选出选中类型的对应预绑账户
const filteredUserWallets = computed(() => {
return userWallets.value.filter(m => m.type === paymentType.value);
});
async function handleSubmitOrder() {
if (!payerAccount.value) return alert('请选择实名绑定的付款账户');
// 发起创建订单请求,并将选定的账号参数透传给服务端,再由服务端调用API
await api.post('/api/recharge/create', {
amount: 100,
payerName: payerName.value,
payerAccount: payerAccount.value
});
}
</script>
后端对接范例 (PHP 示例)
<?php
/**
* 创建带有实名验证的支付订单
*/
function createRealNameOrder($mchId, $key, $orderNo, $amount, $payerName, $payerAccount) {
// 基础下单参数
$params = [
'mch_id' => $mchId,
'out_trade_no' => $orderNo,
'amount' => strval(number_format($amount, 2, '.', '')),
'pay_type' => 'ALIPAY',
'notify_url' => 'https://your-domain.com/pay/notify',
//附加实名验证参数
'payer_name' => $payerName, // 必须与用户在支付平台实名的姓名一致
'payer_account' => $payerAccount // 必须与用户在支付平台的收付款账号一致
];
// 生成签名 (去除空值/排完序拼装字符串 + key 的 md5)
$params['sign'] = generateSign($params, $key);
// 发起 HTTP 请求
$ch = curl_init('https://api.1117.xin/api/v1/gateway/create-order');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// 商户后端接收到前端传来的实名认证信息后进行调用
$result = createRealNameOrder(
'10001',
'your_secret_key_here',
'ORDER_' . time(),
100.00,
$_POST['payerName'],
$_POST['payerAccount']
);
if ($result['statusCode'] === 200) {
echo "下单成功,请重定向至收银台: " . $result['data']['payment_url'];
} else {
echo "下单失败: " . $result['message'];
}
?>
错误码参考
| 状态码 | 错误信息 | 排查建议 |
|---|---|---|
401 |
Unauthorized IP: 1.1.1.1 | 请求服务器的 IP 不在商户配置的白名单中,请登录后台添加 |
401 |
Invalid Signature | Sign 签名验证失败,请检查排序、拼接或 Key 是否正确 |
400 |
Merchant Not Active | 商户状态被禁用,请联系平台管理员 |
400 |
Duplicate out_trade_no | 订单号重复,该 out_trade_no 已存在 |
503 |
No available payment channel | 当前系统无空闲通道或没有匹配金额的可用额度 |