外部支付对接手册

面向需要接入知趣商城支付网关的第三方开发者

概述

知趣商城提供支付即服务(Payment as a Service),允许外部网站通过 API 创建支付宝 / 微信支付订单。用户在托管收银台页面完成扫码支付,支付成功后系统自动回调通知你的服务器。

1. 管理员在后台「应用管理」创建应用 → 获取 app_id + app_secret

2. 你的服务端调用「创建订单」API → 获取收银台链接或二维码

3. 用户跳转收银台扫码支付

4. 支付成功 → 系统 POST 回调通知你的服务器

5. 你的服务器确认收款,完成业务逻辑

安全提醒

app_secret 是签名密钥,请妥善保管,不要暴露在前端代码或公开仓库中。所有 API 调用和签名验证必须在服务端完成。

签名算法

所有 API 请求必须携带 HMAC-SHA256 签名,防止参数篡改。

  1. 将所有参数(不含 sign)按 key 的 ASCII 字母序排列
  2. 拼接为 key1=value1&key2=value2&... 格式
  3. 使用 app_secret 作为密钥,计算 HMAC-SHA256
  4. 输出 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=测试商品&timestamp=1700000000

使用 app_secret 对上述字符串计算 HMAC-SHA256,得到 sign 值。

创建订单

POST /api/service/external/orders

请求参数(JSON Body):

参数类型必填说明
app_idstring应用 ID
out_trade_nostring你的订单号(同一应用内唯一,最长 64 字符)
subjectstring商品名称(最长 200 字符)
amountnumber金额,单位:分(正整数,如 100 = ¥1.00)
pay_methodstring支付方式:alipay 或 wechat
notify_urlstring回调地址(不传则使用应用默认回调地址)
timestampnumberUnix 时间戳(秒),与服务器时间差 ≤ 5 分钟
signstringHMAC-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_idstring应用 ID
timestampnumberUnix 时间戳(秒)
signstring签名(参与签名字段: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..."
}

你需要做的:

  1. 验证 sign 签名(除 sign 外所有字段参与签名)
  2. 检查 out_trade_noamount 是否与你的订单匹配
  3. 处理业务逻辑(发货、开通权限等)
  4. 响应纯文本 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 — 事件唯一 ID
  • timestamp — 事件发生时间戳(秒)
  • data — 事件数据(取决于事件类型)
  • sign — HMAC-SHA256 签名

签名验证

Webhook 通知的签名验证方式与 API 请求相同。使用 app_secret 对除 sign 外的所有字段进行签名验证。

错误码

错误响应格式:

{ "error": "Missing required fields", "code": "INVALID_PARAMS" }
code说明
INVALID_PARAMS缺少必填参数
INVALID_AMOUNT金额必须为正整数(单位:分)
INVALID_PAY_METHODpay_method 只能是 alipay 或 wechat
TIMESTAMP_EXPIRED时间戳过期(与服务器时间差超过 5 分钟)
INVALID_APPapp_id 无效或应用已停用
INVALID_SIGN签名验证失败
DUPLICATE_ORDERout_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 判断是否已处理过。