商户 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 字段进行签名校验。

签名生成步骤

  1. 将所有发送或接收到的参数(除 sign 字段本身以及值为空的参数外)按参数名(Key)的 ASCII 码从小到大排序(即字典序 a-z)。
  2. 使用 URL 键值对的格式(即 key1=value1&key2=value2...)拼接成字符串 StringA
  3. StringA 最后拼接商户专属密钥 key 得到 StringSignTemp(即 StringA&key=YOUR_SECRET_KEY)。
  4. 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...

签名实现代码

JavaScript
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
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..."
  }'

成功响应示例

JSON
{
  "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 中只返回纯文本字符串

Response
success
重试机制

如果商户响应的不是 success(如抛出 500 错误、超时或返回其他字符串),本系统会认为通知失败,并将在随后的定时任务中尝试重新发送回调(最多重试 5 次)。

回调处理示例

Node.js
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),以保障资金流的真实匹配。

一、接入原理与优势

安全校验拦截机制
如果请求中携带了实名信息参数,收银台会在用户进入页面时,强制弹出“实名转账确认”警示框,并要求用户承诺使用系统记录的相同名字名下的账户进行支付。这一特性可以极大程度削弱洗黑钱的利用。

二、商户系统前后端对接最佳实践

商户在开发前端应用时的最佳流程步骤如下:

  1. 确保您的平台具备用户真实姓名强制绑定的基础功能。
  2. 引导用户在您的前端“充值配置”页面中预先绑定其本人合法持有的付款账号(例如支付宝、微信号等)。
  3. 在展示给用户“扫码支付”或需要匹配信息的支付方式时,应该从后端接口获取该用户已绑定的账号列表。让用户通过选择预绑定的渠道完成下单,而非临时手动填写,尽可能避免输入错误和代付操作。
  4. 商户服务端接收到用户下单请求后,从自身数据库读出该账号实名数据,附加到 统一下单API 后交至本收银台。

前端逻辑对接范例 (Vue 3 示例)

Vue 3 Frontend
<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 Backend
<?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 当前系统无空闲通道或没有匹配金额的可用额度