4、支付结果通知:回调URL配置、通知参数解析、签名验证、处理重复通知
支付结果通知,说白了就是微信服务器主动给你发消息,告诉你「这笔订单用户付了没」。我刚开始做支付集成时,总觉得只要前端调起支付就完事了。结果呢?用户付了钱,后台没收到通知,订单状态死活没更新……那叫一个尴尬。
所以今天咱们就把这个环节彻底讲透。你想想看,支付通知要是处理不好,轻则订单状态错乱,重则资金对不上账。嗯,这里要注意的东西还真不少。
4.1 回调URL配置
首先,你得告诉微信服务器,支付结果往哪儿发。这个地址就是回调URL,也叫通知地址。
配置方式有两种:
- 在商户平台配置:登录微信支付商户平台 → 产品中心 → 开发配置 → 设置支付回调URL。这个配置是全局的,所有订单的通知都会发到这个地址。
- 在统一下单接口中指定:调用下单API时,传一个
notify_url参数。这个优先级更高,会覆盖平台配置。
我个人习惯在统一下单时指定回调URL。为什么呢?因为不同业务场景可能需要不同的处理逻辑。比如普通商品订单和虚拟商品订单,回调处理可能不一样。
回调URL的要求:
- 必须是公网可访问的HTTPS地址
- 不能带参数(比如
?order_id=123是不行的) - 端口默认443,不能自定义
- 路径可以自定义,比如
https://yourdomain.com/pay/notify
注意:回调URL配置错了,微信根本不会给你发通知。我曾经在测试环境配了个内网地址,调试了一下午才发现问题……
4.2 通知参数解析
当用户支付成功后,微信服务器会以POST方式,把通知数据发到你的回调URL。数据格式是XML,没错,又是XML。
通知参数长这样:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
<sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
<is_subscribe><![CDATA[Y]]></is_subscribe>
<trade_type><![CDATA[JSAPI]]></trade_type>
<bank_type><![CDATA[CFT]]></bank_type>
<total_fee>1</total_fee>
<fee_type><![CDATA[CNY]]></fee_type>
<transaction_id><![CDATA[1008450740201411110005824339]]></transaction_id>
<out_trade_no><![CDATA[2014112615474100000072]]></out_trade_no>
<time_end><![CDATA[20141126170006]]></time_end>
<trade_state><![CDATA[SUCCESS]]></trade_state>
<cash_fee>1</cash_fee>
</xml>
关键参数说明:
| 参数名 | 说明 | 是否必传 |
|---|---|---|
return_code |
通信标识,SUCCESS/FAIL | 是 |
return_msg |
返回信息,如OK | 否 |
appid |
小程序ID | 是 |
mch_id |
商户号 | 是 |
nonce_str |
随机字符串 | 是 |
sign |
签名 | 是 |
result_code |
业务结果,SUCCESS/FAIL | 是 |
openid |
用户标识 | 是 |
trade_type |
交易类型,JSAPI等 | 是 |
total_fee |
订单金额,单位分 | 是 |
transaction_id |
微信支付订单号 | 是 |
out_trade_no |
商户订单号 | 是 |
time_end |
支付完成时间 | 是 |
trade_state |
交易状态,SUCCESS等 | 是 |
解析XML时,我建议用官方推荐的解析方式,别自己拼字符串。很多坑都是手写XML解析搞出来的。
4.3 签名验证
收到通知后,第一件事不是处理业务,而是验证签名。为什么?因为任何人都可以往你的回调URL发请求,不验证签名,你就分不清真假。
签名验证的步骤:
- 从XML中取出所有参数(
sign本身除外) - 按参数名ASCII码从小到大排序
- 拼接成
key1=value1&key2=value2格式 - 在末尾拼接上
&key=你的API密钥 - 计算MD5,转大写
- 与XML中的
sign字段比对
代码示例(Node.js):
function verifySign(xmlData, apiKey) {
// 解析XML
const params = parseXML(xmlData);
const sign = params.sign;
delete params.sign;
// 排序并拼接
const keys = Object.keys(params).sort();
let stringA = keys.map(key => `${key}=${params[key]}`).join('&');
let stringSignTemp = `${stringA}&key=${apiKey}`;
// 计算签名
const calculatedSign = crypto.createHash('md5')
.update(stringSignTemp)
.digest('hex')
.toUpperCase();
return calculatedSign === sign;
}
小技巧:验证签名前,先检查 return_code 和 result_code 是不是SUCCESS。如果不是,直接返回失败,不用继续处理。
我在项目中遇到过一个问题:签名验证一直失败,排查了半天才发现是API密钥配错了。嗯,这种低级错误其实挺常见的,建议把API密钥放在配置中心,别硬编码在代码里。
4.4 处理重复通知
微信支付的通知机制是「至少一次」,不是「仅一次」。也就是说,同一笔订单可能会收到多次通知。为什么会这样?因为微信为了保证通知可靠送达,如果没收到你的成功响应,它会重试。
重试策略是这样的:
- 首次通知后,如果没收到
SUCCESS响应 - 会在15秒、30秒、1分钟、3分钟、10分钟后重试
- 最多重试9次
所以,你的回调接口必须能处理重复通知。怎么做?
核心思路就一句话:幂等性处理。
具体做法:
- 收到通知后,先查数据库,看这个
transaction_id或out_trade_no是否已经处理过 - 如果已经处理过,直接返回
SUCCESS,不再重复执行业务逻辑 - 如果没处理过,才执行订单更新、发货等操作
代码示例:
function handleNotify(xmlData) {
// 1. 验证签名
if (!verifySign(xmlData, API_KEY)) {
return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>';
}
const params = parseXML(xmlData);
const outTradeNo = params.out_trade_no;
// 2. 检查是否已处理(幂等性检查)
const order = db.query('SELECT status FROM orders WHERE out_trade_no = ?', [outTradeNo]);
if (order && order.status === 'PAID') {
// 已处理过,直接返回成功
return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
}
// 3. 处理业务逻辑(加锁防止并发)
try {
// 更新订单状态
db.update('UPDATE orders SET status = ? WHERE out_trade_no = ?', ['PAID', outTradeNo]);
// 其他业务逻辑...
} catch (e) {
// 处理失败,返回FAIL让微信重试
return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理失败]]></return_msg></xml>';
}
// 4. 返回成功
return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
}
重要提醒:返回给微信的响应必须是纯XML格式,不能包含任何其他字符。我曾经在响应里加了个换行符,结果微信一直重试……
另外,处理重复通知时要注意并发问题。如果两次通知同时到达,你的幂等性检查可能失效。我建议用数据库的唯一索引或分布式锁来保证。
举个例子,可以在订单表上给 transaction_id 加唯一索引。这样即使并发插入,也只有一个能成功,另一个会报错,然后返回FAIL让微信重试。
总结一下回调处理的完整流程:
- 接收XML数据
- 验证签名
- 检查业务结果(return_code和result_code)
- 幂等性检查(防止重复处理)
- 执行业务逻辑(更新订单、发货等)
- 返回SUCCESS或FAIL
嗯,到这里支付结果通知的核心内容就讲完了。你想想看,其实逻辑并不复杂,但细节特别多。签名验证、重复通知处理,这两个环节最容易出问题。我建议你在测试环境多模拟几次回调场景,把各种边界情况都覆盖到。
下一章咱们聊聊退款功能,那个坑也不少……