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发请求,不验证签名,你就分不清真假。

签名验证的步骤:

  1. 从XML中取出所有参数(sign 本身除外)
  2. 按参数名ASCII码从小到大排序
  3. 拼接成 key1=value1&key2=value2 格式
  4. 在末尾拼接上 &key=你的API密钥
  5. 计算MD5,转大写
  6. 与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_coderesult_code 是不是SUCCESS。如果不是,直接返回失败,不用继续处理。

我在项目中遇到过一个问题:签名验证一直失败,排查了半天才发现是API密钥配错了。嗯,这种低级错误其实挺常见的,建议把API密钥放在配置中心,别硬编码在代码里。

4.4 处理重复通知

微信支付的通知机制是「至少一次」,不是「仅一次」。也就是说,同一笔订单可能会收到多次通知。为什么会这样?因为微信为了保证通知可靠送达,如果没收到你的成功响应,它会重试。

重试策略是这样的:

  • 首次通知后,如果没收到 SUCCESS 响应
  • 会在15秒、30秒、1分钟、3分钟、10分钟后重试
  • 最多重试9次

所以,你的回调接口必须能处理重复通知。怎么做?

核心思路就一句话:幂等性处理

具体做法:

  1. 收到通知后,先查数据库,看这个 transaction_idout_trade_no 是否已经处理过
  2. 如果已经处理过,直接返回 SUCCESS,不再重复执行业务逻辑
  3. 如果没处理过,才执行订单更新、发货等操作

代码示例:

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让微信重试。

总结一下回调处理的完整流程

  1. 接收XML数据
  2. 验证签名
  3. 检查业务结果(return_code和result_code)
  4. 幂等性检查(防止重复处理)
  5. 执行业务逻辑(更新订单、发货等)
  6. 返回SUCCESS或FAIL

嗯,到这里支付结果通知的核心内容就讲完了。你想想看,其实逻辑并不复杂,但细节特别多。签名验证、重复通知处理,这两个环节最容易出问题。我建议你在测试环境多模拟几次回调场景,把各种边界情况都覆盖到。

下一章咱们聊聊退款功能,那个坑也不少……