聚合支付中心接入文档
所有业务应用通过本中台统一收款:支付密钥集中托管,业务侧只做「签名下单 + 接收回调」两件事。
一、方案概述
业务系统不再保存任何微信/支付宝商户密钥。下单、签名、验签、回调分发、对账全部由中台完成:
- 业务后端:调用统一下单接口(用
app_secret签名),拿到order_no与status_token。 - 业务前端:把用户引导到「聚合收银」小程序,携带
order_no+token。 - 聚合收银小程序:用自身 openid 唤起微信支付,完成后回跳业务小程序。
- 中台:接收微信/支付宝异步通知,验签入账后,再回调业务侧的
notify_url。
二、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 }
})
第 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 |
网页/手机网站支付完成后跳转落点(仅展示,不入账) |
若业务应用在后台配置了自己的「支付完成返回地址」,H5 支付完成后会直接跳回业务页,不再经过中台的
/return/xxx。
/notify/wechat、/notify/alipay 只接受 POST,用浏览器(GET)访问会看到一页
「回调地址正常」的说明,页面提示 HTTP 200 并不表示出错,也不表示支付成功。验证部署是否正常请访问
/api/ping,返回内容里会带上当前域名解析出的回调地址。
四、签名规则
业务方调用需要签名的接口(下单、查单、关单、退款)时必须携带签名,算法与微信支付 V2 一致:
- 参数中必须包含
app_id、timestamp(秒级时间戳)、nonce_str(随机串); - 剔除
sign以及值为空字符串或 null 的参数; - 按参数名 ASCII 升序排序,拼成
k1=v1&k2=v2; - 末尾追加
&key=你的app_secret; - 做
MD5,转大写,即sign。
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 | 小程序查询/支付令牌,随跳转链接传给收银小程序 |
status | 0 待支付 / 1 已支付 / 2 已关闭 / 3 已退款 / 4 部分退款 / 5 支付失败 |
need_client_prepay | true 表示需由收银小程序补 openid 后再预支付(jsapi 场景) |
pay_params | app / 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 收银页),可用它。
八、业务方查单(服务端,签名)
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 | 业务订单号 |
channel | wechat / alipay |
transaction_id | 渠道流水号 |
amount | 订单金额(元) |
status | 1 已支付 / 4 部分退款 |
attach | 下单透传参数 |
pay_time | 支付时间 |
notify_time | 通知时间 |
nonce_str | 随机串 |
sign | 签名,算法同上(用你的 app_secret) |
处理要求(重要)
- 必须验签,并校验
app_id; - 用
out_trade_no做幂等:已处理过直接返回 success; - 校验金额与本地订单一致,不一致必须告警并拒绝;
- 输出纯文本
success才算成功,否则中台会重试(最多 3 次); - 接口需在 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 |
中台的处理逻辑:
- 读取
out_trade_no,查本地订单; - 若仍为「待支付」,主动向渠道查一次单(回调可能比跳转晚到),已支付则补入账并分发业务通知;
- 渲染响应式结果页:显示金额、订单状态、订单号,已支付且有业务
return_url时提供「返回业务应用」按钮; - 支付宝同步参数中的
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 | 补参数 |
| 2001 | out_trade_no 非法 | 非空且 ≤64 字符 |
| 2002 | 金额非法 | 0.01 ~ 1000000,两位小数 |
| 2003 | 不支持的渠道 | wechat / alipay |
| 2004 | 应用未开通该渠道 | 后台勾选渠道 |
| 2005 | 不支持的 trade_type | jsapi / 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. 支付完成但业务没收到回调?
- 确认
notify_url是公网可访问的 HTTPS 地址; - 确认回调返回了
success; - 后台「回调日志」可看到中台向业务方发起的请求与响应,含 HTTP 状态码与重试次数;
- 可用「业务方查单」接口 +
sync=1主动补偿。
3. 能否不跳小程序、直接在业务小程序里支付?
不行。微信 openid 按小程序 AppID 隔离,业务小程序的 openid 无法在聚合小程序中使用;同理聚合小程序的 openid 也不能用于业务小程序的 JSAPI 下单。统一收银必须走聚合小程序。
4. 支付宝能在小程序里用吗?
微信小程序内只能唤起微信支付。支付宝适用于 App(trade_type=app)与 H5(trade_type=h5)。
5. 生产环境注意事项
- 域名必须是 HTTPS,并加入小程序 request 合法域名;
- 中台
app.debug必须为 false; - 证书目录
cert/、runtime/禁止 Web 访问; - 业务服务器开启 NTP 时间同步。