本文目录导读:

处理微信支付统一下单的回调(官方称为“支付结果通知”)是确保订单状态正确更新的关键环节,微信服务器会以 POST 方式将异步通知结果发送到你在统一下单接口中填写的 notify_url。
以下是处理该回调的完整流程、核心逻辑以及需要注意的坑。
- 接收请求:微信服务器 POST 请求到你的
notify_url。 - 验签:验证数据是否确实来自微信,防止伪造回调。
- 解密数据:从加密的
req_info中获取订单详情。 - 检查业务逻辑:确认订单未处理、金额一致、商户号匹配等。
- 更新订单状态:将数据库中的订单状态标记为“已支付”。
- 返回成功应答:告诉微信别再重复通知了。
详细步骤与代码示例(以 Node.js/Java 为例,但逻辑通用)
接收请求并获取报文
微信使用的是 XML 格式 的 POST 请求(新旧版都是,V3 版本升级后通知格式虽改为 JSON,但 V2 仍是主流且兼容;以下基于 V2 接口的经典流程,V3 的验签方式不同,会单独说明)。
// Node.js (Express) 示例
const https = require('https');
const xml2js = require('xml2js'); // 需要安装
app.post('/wechat/pay/notify', async (req, res) => {
// 1. 获取请求的 XML 数据
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', async () => {
// body 就是微信发来的 XML 字符串
console.log('收到微信回调原始数据:', body);
// 后续处理...
});
});
验签(最重要的一步)
目标:验证接收到的数据中的 sign 字段是否与你自己使用本地秘钥计算的一致。
- 参数:提取所有
POST过来的 XML 节点(sign字段本身除外)。 - 规则:
- 按字段名的 ASCII 码升序排序。
- 拼接成
key1=value1&key2=value2格式。 - 在字符串末尾拼接
&key=你的API密钥(微信商户平台设置的密钥)。 - 计算 MD5 或 HMAC-SHA256(根据统一下单时指定的
sign_type),结果转大写。 - 与微信传来的
sign字段对比。
// 验签函数 (V2接口)
const crypto = require('crypto');
const apiKey = 'your_wechat_api_key_here'; // 商户平台->API安全->设置密钥
function verifyWechatSign(xmlData, apiKey) {
// 1. 解析 XML 为对象
const parser = new xml2js.Parser({ explicitArray: false });
const result = await parser.parseStringPromise(xmlData);
const data = result.xml;
// 2. 取出 sign 字段
const receivedSign = data.sign;
delete data.sign; // 参与签名的数据不包括 sign 本身
// 3. 排序并拼接
const keys = Object.keys(data).sort();
let stringA = '';
for (let key of keys) {
// 注意:value 为空字符串的字段需要剔除,或者微信官方文档有特殊说明,一般全量参与
if (data[key] !== '') {
stringA += `${key}=${data[key]}&`;
}
}
// 4. 拼接Key
const stringSignTemp = `${stringA}key=${apiKey}`;
// 5. 计算签名 (假设 sign_type 是 MD5)
const calcSign = crypto.createHash('md5')
.update(stringSignTemp, 'utf8')
.digest('hex')
.toUpperCase();
return calcSign === receivedSign;
}
// 在接收到数据后调用
if (!verifyWechatSign(body, apiKey)) {
console.error('验签失败,请求可能不是来自微信');
// 返回失败应答(见步骤6)
// res.end('<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>');
return;
}
解密 req_info(获取完整订单信息)
回调数据中,关键信息(如 transaction_id、out_trade_no、total_fee)通常是以 加密 形式放在 <req_info> 字段中的。
- 解密算法:AES-256-ECB。
- 密钥:
md5(你的API密钥)得到 32 位小写字符串,作为 AES 密钥。 - 加密数据:
req_info字段的值(Base64 编码)。 - 填充:
PKCS7Padding(加密)/PKCS7Unpadding(解密)。
// 解密 req_info
const crypto = require('crypto');
function decryptWechatReqInfo(encryptedBase64, apiKey) {
// 1. 计算 AES 密钥:MD5(API密钥) 转小写
const aesKey = crypto.createHash('md5')
.update(apiKey)
.digest('hex')
.toLowerCase();
// 2. 创建解密器
const decipher = crypto.createDecipheriv('aes-256-ecb', aesKey, null);
decipher.setAutoPadding(true); // 使用 PKCS7 填充
// 3. 解密(输入是 Base64,输出是 XML 字符串)
let decrypted = decipher.update(encryptedBase64, 'base64', 'utf8');
decrypted += decipher.final('utf8');
// 4. 解析解密后的 XML 得到订单详情
const parser = new xml2js.Parser({ explicitArray: false });
const result = await parser.parseStringPromise(decrypted);
return result.xml;
// 返回包含:transaction_id, out_trade_no, total_fee, openid, time_end 等字段
}
// 在验签通过后调用
const orderInfo = await decryptWechatReqInfo(result.req_info, apiKey);
console.log('解密后的订单信息:', orderInfo);
业务逻辑校验
拿到解密后的具体数据后,必须做以下检查,防止重复或错误支付:
- 商户号是否一致:
mch_id必须等于你的商户号。 - 订单号是否存在:
out_trade_no必须在你的数据库中有对应的记录。 - 订单状态是否为“未支付”:关键!如果订单已经是“已支付”状态,应该直接返回成功应答(而不是报错),避免微信重复通知。
- 金额是否一致:回调中的
total_fee必须与数据库中的订单金额完全一致(避免被篡改)。 - 支付结果是否为成功:检查
result_code是否为SUCCESS。
// 假设你有一个数据库查询函数 getOrderByOutTradeNo
const order = await getOrderByOutTradeNo(orderInfo.out_trade_no);
if (!order) {
// 订单不存在,返回失败
// ...
}
if (order.status === 'paid') {
// 已经支付过了,直接返回成功(幂等处理)
// ...
}
if (parseInt(orderInfo.total_fee) !== order.total_fee_in_cents) {
// 金额不一致,可能是恶意攻击
// ...
}
if (orderInfo.result_code !== 'SUCCESS') {
// 支付未成功,记录日志,返回成功(避免重试)
// ...
}
更新订单状态(原子操作)
使用事务或乐观锁,确保支付状态的改变是安全的。
-- SQL 示例
UPDATE orders
SET status = 'paid',
transaction_id = '回调中的微信单号',
pay_time = NOW()
WHERE out_trade_no = '订单号' AND status = 'pending'; -- 防止并发
确认更新成功(受影响的记录数 > 0)。
返回应答给微信(非常重要)
微信需要你返回一个固定的 XML 来确认收到通知。如果返回非成功结果或超时,微信会按照策略重试(最高重试 9 次,间隔递增)。
- 成功:
<xml><return_code><![CDATA[SUCCESS]]></return_code></xml> - 失败:
<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[参数错误]]></return_msg></xml>
// 成功处理的最后一步
res.writeHead(200, { 'Content-Type': 'application/xml' });
res.end('<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>');
千万不要返回错误,除非你希望微信重发。
必须注意的 4 个“坑”
- 幂等处理:必须判断订单是否已经支付,如果收到重复通知,直接返回
SUCCESS,不要重复发货或更新状态。 - 验签失败:不要先解密后验签。先验签,验证数据完整性。
- SSL/TLS:微信需要你的
notify_url是 HTTPS 地址(生产环境)。 - 解密失败:AES 密钥是 API 密钥的 MD5,不是 API 密钥本身,也不是签名密钥。
微信支付 V3 接口的差异
微信支付 V3 接口(现在新接入推荐使用 V3)的回调处理有所不同:
- 数据格式:不再是 XML,而是 JSON。
- 验签方式:不再使用 MD5 签名,而是基于 平台证书的非对称验签(RSA-SHA256)。
- 数据位置:关键信息(如
transaction_id)直接包含在回调 JSON 的resource字段中,但resource字段本身是加密的(AES-256-GCM)。
V3 核心流程:
- 获取请求头
Wechatpay-Signature、Wechatpay-Serial(证书序列号)、Wechatpay-Timestamp、Wechatpay-Nonce。 - 拼接签名串:
timestamp + \n + nonce + \n + 请求体JSON + \n。 - 使用微信支付平台证书(根据
Wechatpay-Serial找到对应证书)的公钥,验证上述签名串。 - 验签通过后,使用 APIv3 密钥解密
resource中的ciphertext字段(AES-256-GCM 解密,需要associated_data和nonce)。 - 业务逻辑同上。
总结与建议
- 单元测试:利用微信官方的“模拟支付通知”工具或手动构造回调数据测试。
- 日志:记录回调的原始数据、验签结果、解密结果、业务校验结果,出问题时这是唯一的排查手段。
- 超时:回调处理应尽快完成(< 5秒),不要把大量数据库查询或外部 API 调用放在同步流程中。
- 一致性:如果支付成功,但更新数据库失败(如网络问题),必须返回
FAIL让微信重试。
如果你需要针对特定语言(Java、PHP、Go)的完整代码示例,或者 V3 接口的详细实现,请告诉我。