¥ 聚合支付中心 · 接入文档

聚合支付中心接入文档

所有业务应用通过本中台统一收款:支付密钥集中托管,业务侧只做「签名下单 + 接收回调」两件事。

版本 v1.0 微信支付 APIv3(公钥模式) 支付宝 RSA2 PHP 7.4 / uni-app 已联调

一、方案概述

业务系统不再保存任何微信/支付宝商户密钥。下单、签名、验签、回调分发、对账全部由中台完成:

业务后端 支付中心 微信/支付宝 聚合收银小程序 │ ① unifiedorder(签名) │ │ │ │─────────────────────────▶│ 建单 │ │ │ ② order_no + status_token│ │ │ │◀─────────────────────────│ │ │ │ ③ 跳转小程序(order_no, token) ────────────────────────────────────────────▶ │ │ │ ④ wx.login → openid │ │ │ │◀─────────────────────────────────────────────────│ │ │ ⑤ clientprepay(order_no,token,openid) │ │ │◀─────────────────────────────────────────────────│ │ │ ⑥ 下单 ────────────────────▶│ │ │ │◀──────────── prepay_id/签名串 │ │ │ ⑦ paySign ───────────────────────────────────────▶│ │ │ │ ⑧ 用户支付 │ │ │◀── ⑨ 支付结果通知 ──────────│ │ │◀── ⑩ notify_url 回调 ────│ 验签+入账 │ │ │ 返回 success │ │ │

二、5 分钟接入

第 1 步:后台创建应用

登录 管理后台 → 「应用管理 → 新增应用」,只需填 4 项:

字段必填说明
应用名称是展示在收银小程序与首页
支付结果回调地址是https://你的后端/pay/notify,必须公网可访问
开通渠道是勾选微信支付 / 支付宝
支付完成返回地址否H5 支付完成后跳回的前端页面;留空则落到中台结果页
图标 / 简介 / 分账比例 / 排序 / 备注否仅展示与统计用途,可留空
保存后会自动弹出密钥窗口,里面有 app_id、app_secret、回调地址, 每项都可一键复制,也可「复制全部信息」直接发给你。
之后随时可在列表「密钥」列点 查看密钥 再次复制;忘记或怀疑泄露时点 重置密钥(旧密钥立即失效)。

应用默认「待审核」,点击 激活 后即可正式收款(激活状态会实时出现在收银小程序与门户首页)。

第 2 步:后端调用统一下单

POST /api/v1/pay/unifiedorder
Content-Type: application/x-www-form-urlencoded

app_id=a1b2c3d4e5f6g7h8
out_trade_no=ORDER20261020001
amount=0.01
subject=测试商品
channel=wechat            # wechat | alipay
trade_type=jsapi          # jsapi(小程序) | app | h5 | native
attach=任意透传参数
timestamp=1770000000
nonce_str=7f3a9c1b2d4e
sign=9A8B7C6D5E4F...      # 见「签名规则」

返回关键字段:

{
  "code": 0,
  "msg": "ok",
  "data": {
    "order_no": "PC20261020103000123456",
    "status_token": "9f8a7b6c5d4e3f21",
    "amount": "0.01",
    "status": 0,
    "status_text": "待支付",
    "expire_time": "2026-10-20 11:00:00",
    "need_client_prepay": true
  }
}

第 3 步:引导用户到聚合收银小程序

业务小程序内调用(需先在「小程序后台 → 设置 → 第三方设置 → 关联小程序」里关联聚合收银小程序):

wx.navigateToMiniProgram({
  appId: '聚合收银小程序AppID',
  path: 'pages/index/index?order_no=' + order_no + '&token=' + status_token,
  // 也可以放在 extraData 里
  extraData: { order_no: order_no, token: status_token }
})
小程序 openid 按 AppID 隔离,所以 支付必须发生在聚合收银小程序内,业务方不需要传 openid。

第 4 步:接收支付结果回调

中台以 application/x-www-form-urlencoded 形式 POST 到你的 notify_url,处理成功须输出纯文本 success(不区分大小写)。详见「异步通知与验签」。

三、环境与域名

项目值
联调地址http://localhost:8100(内网联调;生产请换成 HTTPS 域名)
接口前缀/api/v1
后台/admin
门户首页/

通道回调地址一览(由中台统一提供,业务方无需配置)

通道类型地址用途
微信支付 异步接收
notify_url
http://localhost:8100/notify/wechat
生产:https://你的域名/notify/wechat
微信服务器 POST 支付结果,中台验签解密→入账→再回调业务方
同步回调
return_url
http://localhost:8100/return/wechat
生产:https://你的域名/return/wechat
H5 支付完成后用户浏览器跳转落点(仅展示,不入账)
支付宝 异步接收
notify_url
http://localhost:8100/notify/alipay
生产:https://你的域名/notify/alipay
支付宝服务器 POST 通知,中台 RSA2 验签→入账→分发
同步回调
return_url
http://localhost:8100/return/alipay
生产:https://你的域名/return/alipay
网页/手机网站支付完成后跳转落点(仅展示,不入账)
关键区别:异步接收地址是入账唯一依据,必须公网 HTTPS 可达;同步回调地址是用户看得见的页面,跳转慢或丢失都不影响入账。
若业务应用在后台配置了自己的「支付完成返回地址」,H5 支付完成后会直接跳回业务页,不再经过中台的 /return/xxx。
不要用浏览器打开异步接收地址来「测试」。 /notify/wechat、/notify/alipay 只接受 POST,用浏览器(GET)访问会看到一页 「回调地址正常」的说明,页面提示 HTTP 200 并不表示出错,也不表示支付成功。
验证部署是否正常请访问 /api/ping,返回内容里会带上当前域名解析出的回调地址。
生产要求:必须使用 HTTPS 域名,且该域名需同时配置到微信小程序「request 合法域名」;微信支付异步通知地址也必须公网可达(localhost 只能跑通下单与模拟支付)。

四、签名规则

业务方调用需要签名的接口(下单、查单、关单、退款)时必须携带签名,算法与微信支付 V2 一致:

  1. 参数中必须包含 app_id、timestamp(秒级时间戳)、nonce_str(随机串);
  2. 剔除 sign 以及值为空字符串或 null 的参数;
  3. 按参数名 ASCII 升序排序,拼成 k1=v1&k2=v2;
  4. 末尾追加 &key=你的app_secret;
  5. 做 MD5,转大写,即 sign。
时间戳与服务端相差超过 300 秒会被拒绝(错误码 1005),请确保服务器时间同步(NTP)。

PHP

function payCenterSign(array $params, string $secret): string {
    unset($params['sign']);
    $params = array_filter($params, fn($v) => $v !== '' && $v !== null);
    ksort($params);
    $pairs = [];
    foreach ($params as $k => $v) {
        $pairs[] = $k . '=' . $v;
    }
    return strtoupper(md5(implode('&', $pairs) . '&key=' . $secret));
}

Node.js

const crypto = require('crypto');

function sign(params, secret) {
  const { sign, ...rest } = params;
  const str = Object.keys(rest)
    .filter(k => rest[k] !== '' && rest[k] !== null && rest[k] !== undefined)
    .sort()
    .map(k => `${k}=${rest[k]}`)
    .join('&');
  return crypto.createHash('md5').update(str + '&key=' + secret, 'utf8')
    .digest('hex').toUpperCase();
}

Python

import hashlib

def sign(params: dict, secret: str) -> str:
    params.pop('sign', None)
    items = [(k, v) for k, v in params.items() if v not in ('', None)]
    raw = '&'.join(f'{k}={v}' for k, v in sorted(items))
    return hashlib.md5((raw + '&key=' + secret).encode('utf-8')).hexdigest().upper()

五、统一下单

POST/api/v1/pay/unifiedorder

参数必填说明
app_id是后台分配的应用 ID
out_trade_no是业务订单号,≤64 字符;同一 app 内唯一,用于幂等
amount是金额(元,两位小数),0.01 ~ 1000000
subject否商品标题,默认「应用名 + 消费」
body否商品描述
channel否wechat(默认)/ alipay
trade_type否jsapi(小程序,默认)/ app / h5 / native
attach否透传参数,回调原样返回,≤200 字符
notify_url否覆盖应用配置的回调地址(单次生效)
timestamp / nonce_str / sign是签名三要素

幂等:相同 app_id + out_trade_no 重复请求返回同一笔订单;金额不一致会返回 2008。

返回:

字段说明
order_no平台订单号(对账、查单、退款都用它)
status_token小程序查询/支付令牌,随跳转链接传给收银小程序
status0 待支付 / 1 已支付 / 2 已关闭 / 3 已退款 / 4 部分退款 / 5 支付失败
need_client_prepaytrue 表示需由收银小程序补 openid 后再预支付(jsapi 场景)
pay_paramsapp / h5 / native 场景直接返回可用的唤起参数

curl 示例

curl -X POST 'http://localhost:8100/api/v1/pay/unifiedorder' \
  -d 'app_id=a1b2c3d4e5f6g7h8' \
  -d 'out_trade_no=TEST20261020001' \
  -d 'amount=0.01' \
  -d 'subject=联调测试' \
  -d 'channel=wechat' \
  -d 'trade_type=jsapi' \
  -d 'timestamp=1770000000' \
  -d 'nonce_str=abc123456789' \
  -d 'sign=计算出来的大写MD5'

六、支付唤起(收银小程序内部接口)

以下接口由聚合收银小程序调用,业务方无需实现,仅供理解链路与排查问题。

POST/api/v1/wx/login

用 wx.login 的 code 换取 openid。参数:code。返回 openid、unionid。

POST/api/v1/pay/clientprepay

参数必填说明
order_no是平台订单号
token是下单返回的 status_token
openid微信必填收银小程序自己的 openid

返回 pay_params,微信小程序可直接传给 uni.requestPayment:

{
  "appId": "wx555d0fc8251f5c36",
  "timeStamp": "1770000000",
  "nonceStr": "8f2c...",
  "package": "prepay_id=wx2010...",
  "signType": "RSA",
  "paySign": "MEUCIQ..."
}
渠道开启「模拟模式」时 pay_params.mock = true,可调 POST /api/v1/pay/mockpay(仅 debug 模式可用)直接把订单置为已支付,用于打通回调链路。

七、查询支付状态(前端轮询)

GET/api/v1/pay/status?order_no=xxx&token=xxx

无需签名,只需订单号与令牌。返回 status、pay_time、transaction_id 等。业务前端若想自己轮询(例如 H5 收银页),可用它。

服务端对渠道主动查单做了 5 秒节流,前端建议 2~3 秒轮询一次即可。

八、业务方查单(服务端,签名)

POST/api/v1/pay/query

参数:app_id、order_no 或 out_trade_no、sync(1 = 同时向渠道同步状态)。

用途:回调丢失时补偿、对账核验、发货前二次确认。

九、关闭订单

POST/api/v1/pay/close

参数:app_id、order_no。仅待支付订单可关闭,同时会尽力关闭渠道侧订单。

十、退款

POST/api/v1/pay/refund

参数必填说明
app_id是应用 ID
order_no是平台订单号
refund_amount是本次退款金额(元),不超过剩余可退金额
reason否退款原因
{
  "code": 0,
  "data": {
    "refund_no": "RF202610201100001",
    "channel_refund_id": "50300...",
    "refund_amount": "0.01",
    "status": 1
  }
}

支持部分退款多次申请;全部退完后订单状态变为「已退款」。

十一、应用列表(公开)

GET/api/v1/app/list?keyword=

返回所有已激活应用(不含任何密钥),用于小程序首页与门户展示。

十二、异步通知与验签

支付成功后中台会向应用的 notify_url 发送 POST(form 表单):

字段说明
app_id应用 ID,需与本地比对防串单
order_no平台订单号
out_trade_no业务订单号
channelwechat / alipay
transaction_id渠道流水号
amount订单金额(元)
status1 已支付 / 4 部分退款
attach下单透传参数
pay_time支付时间
notify_time通知时间
nonce_str随机串
sign签名,算法同上(用你的 app_secret)

处理要求(重要)

  1. 必须验签,并校验 app_id;
  2. 用 out_trade_no 做幂等:已处理过直接返回 success;
  3. 校验金额与本地订单一致,不一致必须告警并拒绝;
  4. 输出纯文本 success 才算成功,否则中台会重试(最多 3 次);
  5. 接口需在 5 秒内响应,重逻辑请异步处理。

PHP 示例

<?php
// notify_url 指向本文件
$secret = '你的app_secret';
$post   = $_POST;

// 1. 验签
$sign = $post['sign'] ?? '';
unset($post['sign']);
$post = array_filter($post, fn($v) => $v !== '' && $v !== null);
ksort($post);
$raw = implode('&', array_map(fn($k) => "$k={$post[$k]}", array_keys($post)));
if (!hash_equals(strtoupper(md5($raw . '&key=' . $secret)), strtoupper($sign))) {
    http_response_code(400);
    exit('sign error');
}

// 2. 幂等 + 金额校验(示例)
$outTradeNo = $_POST['out_trade_no'];
$amount     = $_POST['amount'];
// $order = DB::row('SELECT * FROM orders WHERE order_no = ?', [$outTradeNo]);
// if (!$order) { log_error(); exit('order not found'); }
// if ($order['status'] === 'paid') { exit('success'); }
// if (bccomp($order['amount'], $amount, 2) !== 0) { log_error(); exit('amount mismatch'); }
// DB::update('UPDATE orders SET status="paid", transaction_id=? WHERE order_no=?', [$_POST['transaction_id'], $outTradeNo]);

// 3. 必须输出 success
echo 'success';

十三、同步回调处理

同步回调是用户浏览器的跳转,用来给用户看结果,不能作为发货依据:

通道触发场景跳转地址
微信H5 支付(trade_type=h5)完成后/return/wechat?out_trade_no=xx
支付宝网页/手机网站支付完成后/return/alipay?out_trade_no=xx&trade_no=xx&sign=xx

中台的处理逻辑:

  1. 读取 out_trade_no,查本地订单;
  2. 若仍为「待支付」,主动向渠道查一次单(回调可能比跳转晚到),已支付则补入账并分发业务通知;
  3. 渲染响应式结果页:显示金额、订单状态、订单号,已支付且有业务 return_url 时提供「返回业务应用」按钮;
  4. 支付宝同步参数中的 trade_no / total_amount 会做交叉校验并记录日志。
业务方如需自定义结果页,在后台「应用管理」填写「支付完成返回地址」,中台下单时会把它作为渠道 return_url 传递(微信 H5 的 app_url、支付宝的 return_url),用户付完直接回到你的页面。
这种情况下你的页面仍需自行确认订单状态:调用 业务方查单 并传 sync=1,或者依赖异步通知。

十四、错误码

code含义处理建议
0成功—
1001应用不存在检查 app_id
1002缺少 app_id补参数
1003应用未激活/已禁用后台激活
1004签名校验失败核对 app_secret 与排序规则
1005请求已过期校准服务器时间(±300 秒)
1006缺少 timestamp / nonce_str补参数
2001out_trade_no 非法非空且 ≤64 字符
2002金额非法0.01 ~ 1000000,两位小数
2003不支持的渠道wechat / alipay
2004应用未开通该渠道后台勾选渠道
2005不支持的 trade_typejsapi / app / h5 / native
2008订单号已存在但金额不一致换单号或核对金额
2009缺少支付者标识小程序场景由收银端处理,无需业务方传
3001订单不存在检查 order_no
3002订单状态异常已关闭/已退款不可再支付
3003订单已过期重新下单(默认 30 分钟)
3005查询令牌无效token 与订单不匹配
3007无权操作该订单订单不属于该 app_id
4001~4003退款错误状态不可退 / 金额超限 / 渠道失败
5001~5004渠道未配置 / 未开启 / 不支持联系中台管理员
500系统异常稍后重试,持续失败联系管理员

十四、联调清单

#检查项预期
1签名自测相同参数在业务侧与中台计算出的 sign 一致
2下单成功返回 code=0 且有 order_no、status_token
3幂等同单号重复下单返回同一 order_no
4错误签名返回 1004
5跳转收银小程序打开即显示金额与倒计时
6支付成功结果页显示「支付成功」并可回跳业务小程序
7回调到达业务日志收到 POST,验签通过,返回 success
8重复回调业务侧不重复发货
9退款退款接口返回 code=0,订单状态更新
10金额校验回调金额与本地订单一致

十六、常见问题

1. 下单报 1004 签名失败?

九成是排序或空值处理不对:必须剔除空值后再按 key 升序;拼接末尾是 &key=secret;结果转大写。

2. 支付完成但业务没收到回调?

3. 能否不跳小程序、直接在业务小程序里支付?

不行。微信 openid 按小程序 AppID 隔离,业务小程序的 openid 无法在聚合小程序中使用;同理聚合小程序的 openid 也不能用于业务小程序的 JSAPI 下单。统一收银必须走聚合小程序。

4. 支付宝能在小程序里用吗?

微信小程序内只能唤起微信支付。支付宝适用于 App(trade_type=app)与 H5(trade_type=h5)。

5. 生产环境注意事项