外部支付对接手册
面向需要接入知趣商城支付网关的第三方开发者
概述
知趣商城提供支付即服务(Payment as a Service),允许外部网站通过 API 创建支付宝 / 微信支付订单。用户在托管收银台页面完成扫码支付,支付成功后系统自动回调通知你的服务器。
1. 管理员在后台「应用管理」创建应用 → 获取 app_id + app_secret
2. 你的服务端调用「创建订单」API → 获取收银台链接或二维码
3. 用户跳转收银台扫码支付
4. 支付成功 → 系统 POST 回调通知你的服务器
5. 你的服务器确认收款,完成业务逻辑
安全提醒
app_secret 是签名密钥,请妥善保管,不要暴露在前端代码或公开仓库中。所有 API 调用和签名验证必须在服务端完成。
签名算法
所有 API 请求必须携带 HMAC-SHA256 签名,防止参数篡改。
- 将所有参数(不含
sign)按 key 的 ASCII 字母序排列 - 拼接为
key1=value1&key2=value2&...格式 - 使用
app_secret作为密钥,计算 HMAC-SHA256 - 输出 hex 编码的签名值
示例参数:
{
"app_id": "abc123",
"out_trade_no": "ORDER001",
"subject": "测试商品",
"amount": 100,
"pay_method": "alipay",
"timestamp": 1700000000
}排序拼接后:
amount=100&app_id=abc123&out_trade_no=ORDER001&pay_method=alipay&subject=测试商品×tamp=1700000000使用 app_secret 对上述字符串计算 HMAC-SHA256,得到 sign 值。
创建订单
POST /api/service/external/orders
请求参数(JSON Body):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_id | string | 是 | 应用 ID |
| out_trade_no | string | 是 | 你的订单号(同一应用内唯一,最长 64 字符) |
| subject | string | 是 | 商品名称(最长 200 字符) |
| amount | number | 是 | 金额,单位:分(正整数,如 100 = ¥1.00) |
| pay_method | string | 是 | 支付方式:alipay 或 wechat |
| notify_url | string | 否 | 回调地址(不传则使用应用默认回调地址) |
| timestamp | number | 是 | Unix 时间戳(秒),与服务器时间差 ≤ 5 分钟 |
| sign | string | 是 | HMAC-SHA256 签名 |
成功响应(200):
{
"order_no": "EX1A2B3C4D5E",
"qr_code_url": "https://qr.alipay.com/xxx",
"pay_url": "/pay/EX1A2B3C4D5E",
"expires_at": "2024-01-01T01:00:00.000Z"
}order_no — 系统订单号(EX 前缀)
qr_code_url — 支付二维码链接,可自行生成二维码图片展示
pay_url — 托管收银台路径,拼接域名后跳转用户扫码
expires_at — 订单过期时间(30 分钟有效)
两种使用方式:
A. 跳转收银台 — 将用户重定向到 https://域名 + pay_url,在托管页面扫码
B. 自行展示二维码 — 用 qr_code_url 在你的页面生成二维码
查询订单
GET /api/service/external/orders/{out_trade_no}
Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_id | string | 是 | 应用 ID |
| timestamp | number | 是 | Unix 时间戳(秒) |
| sign | string | 是 | 签名(参与签名字段:app_id, out_trade_no, timestamp) |
成功响应(200):
{
"order_no": "EX1A2B3C4D5E",
"out_trade_no": "ORDER20240101001",
"amount": 2990,
"pay_status": "paid",
"trade_no": "2024010122001...",
"paid_at": "2024-01-01T00:35:00.000Z"
}pay_status — pending(待支付)/ paid(已支付)/ closed(已关闭)/ refunded(已退款)
trade_no — 支付宝/微信交易号(支付成功后才有)
支付回调通知
支付成功后,系统会向 notify_url(订单级优先,否则使用应用默认地址)发送 POST 请求。
回调请求体(JSON):
{
"order_no": "EX1A2B3C4D5E",
"out_trade_no": "ORDER20240101001",
"amount": 2990,
"pay_status": "paid",
"trade_no": "2024010122001...",
"paid_at": "2024-01-01T00:35:00.000Z",
"timestamp": 1700000000,
"sign": "a1b2c3d4..."
}你需要做的:
- 验证
sign签名(除 sign 外所有字段参与签名) - 检查
out_trade_no和amount是否与你的订单匹配 - 处理业务逻辑(发货、开通权限等)
- 响应纯文本
success
重要
响应体必须是纯文本 success(不区分大小写),不是 JSON。只有收到 success 才视为通知成功。
重试机制:
| 重试次数 | 间隔 |
|---|---|
| 第 1 次 | 立即 |
| 第 2 次 | 5 秒后 |
| 第 3 次 | 30 秒后 |
| 第 4 次 | 5 分钟后 |
| 第 5 次及以后 | 每 5 分钟一次 |
通知持续 48 小时,超时后标记为失败。建议同时使用「查询订单」接口作为补充确认。
Webhook 事件
系统支持多种事件类型的 Webhook 通知,帮助你实时获取业务数据变化。
事件类型:
| 事件类型 | 说明 |
|---|---|
| order.created | 订单创建成功 |
| order.paid | 订单支付成功 |
| order.refunded | 订单退款成功 |
| order.closed | 订单已关闭 |
| license.activated | 授权码激活成功 |
| license.deactivated | 授权码注销成功 |
| product.updated | 产品信息更新 |
Webhook 通知格式:
{
"event_type": "order.paid",
"event_id": "evt_1700000000abc123",
"timestamp": 1700000000,
"data": {
"order_no": "EX1A2B3C4D5E",
"out_trade_no": "ORDER20240101001",
"amount": 2990,
"pay_status": "paid",
"trade_no": "2024010122001...",
"paid_at": "2024-01-01T00:35:00.000Z"
},
"sign": "a1b2c3d4..."
}字段说明:
event_type— 事件类型event_id— 事件唯一 IDtimestamp— 事件发生时间戳(秒)data— 事件数据(取决于事件类型)sign— HMAC-SHA256 签名
签名验证
Webhook 通知的签名验证方式与 API 请求相同。使用 app_secret 对除 sign 外的所有字段进行签名验证。
错误码
错误响应格式:
{ "error": "Missing required fields", "code": "INVALID_PARAMS" }| code | 说明 |
|---|---|
| INVALID_PARAMS | 缺少必填参数 |
| INVALID_AMOUNT | 金额必须为正整数(单位:分) |
| INVALID_PAY_METHOD | pay_method 只能是 alipay 或 wechat |
| TIMESTAMP_EXPIRED | 时间戳过期(与服务器时间差超过 5 分钟) |
| INVALID_APP | app_id 无效或应用已停用 |
| INVALID_SIGN | 签名验证失败 |
| DUPLICATE_ORDER | out_trade_no 重复 |
| RATE_LIMITED | 请求频率超限(每应用 30 次/分钟) |
| PAYMENT_GATEWAY_ERROR | 支付网关异常 |
| ORDER_NOT_FOUND | 订单不存在 |
| INTERNAL_ERROR | 服务器内部错误 |
代码示例
Node.js
const crypto = require('crypto');
const APP_ID = 'your_app_id';
const APP_SECRET = 'your_app_secret';
const BASE_URL = 'https://your-domain.com';
function generateSign(params, secret) {
const sorted = Object.keys(params).sort()
.map(k => `${k}=${params[k]}`).join('&');
return crypto.createHmac('sha256', secret)
.update(sorted).digest('hex');
}
// 创建订单
async function createOrder(outTradeNo, subject, amount, payMethod) {
const timestamp = Math.floor(Date.now() / 1000);
const params = {
app_id: APP_ID, out_trade_no: outTradeNo,
subject, amount, pay_method: payMethod, timestamp,
};
params.sign = generateSign(params, APP_SECRET);
const res = await fetch(`${BASE_URL}/api/service/external/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
return res.json();
}
// 验证回调签名
function verifyNotify(body) {
const { sign, ...rest } = body;
return sign === generateSign(rest, APP_SECRET);
}Python
import hmac, hashlib, time, requests
APP_ID = 'your_app_id'
APP_SECRET = 'your_app_secret'
BASE_URL = 'https://your-domain.com'
def generate_sign(params: dict, secret: str) -> str:
sorted_str = '&'.join(f'{k}={params[k]}' for k in sorted(params))
return hmac.new(
secret.encode(), sorted_str.encode(), hashlib.sha256
).hexdigest()
def create_order(out_trade_no, subject, amount, pay_method):
params = {
'app_id': APP_ID, 'out_trade_no': out_trade_no,
'subject': subject, 'amount': amount,
'pay_method': pay_method, 'timestamp': int(time.time()),
}
params['sign'] = generate_sign(params, APP_SECRET)
return requests.post(f'{BASE_URL}/api/service/external/orders', json=params).json()
# Flask 回调处理
from flask import Flask, request
app = Flask(__name__)
@app.route('/notify', methods=['POST'])
def notify():
body = request.json
sign = body.pop('sign', '')
if generate_sign(body, APP_SECRET) != sign:
return 'fail', 400
# 处理业务逻辑...
return 'success'PHP
<?php
$APP_SECRET = 'your_app_secret';
function generateSign(array $params, string $secret): string {
ksort($params);
$pairs = [];
foreach ($params as $k => $v) { $pairs[] = "$k=$v"; }
return hash_hmac('sha256', implode('&', $pairs), $secret);
}
// 回调处理
$body = json_decode(file_get_contents('php://input'), true);
$sign = $body['sign'];
unset($body['sign']);
if (generateSign($body, $APP_SECRET) === $sign) {
// 处理业务逻辑...
echo 'success';
} else {
http_response_code(400);
echo 'fail';
}FAQ
Q: 金额单位是什么?
A: 分。¥29.90 应传 2990。
Q: 回调一直收不到怎么办?
A: 检查 notify_url 是否可公网访问、是否返回了纯文本 success。同时可用「查询订单」接口主动查询。
Q: 可以同时支持支付宝和微信吗?
A: 可以。每次创建订单时通过 pay_method 指定。同一个 out_trade_no 只能用一次,切换支付方式请用不同订单号。
Q: 签名一直验证失败?
A: 常见原因:① 参数未按 key 字母序排列 ② 数字类型拼接时未转字符串 ③ sign 字段参与了签名 ④ app_secret 不正确
Q: 订单有效期多久?
A: 30 分钟。超时未支付自动关闭。
Q: 频率限制是多少?
A: 每个应用每分钟最多 30 次请求。
Q: 回调通知可能重复吗?
A: 可能。请做好幂等处理,根据 out_trade_no 判断是否已处理过。