微信支付统一下单回调处理

wen java案例 2

本文目录导读:

微信支付统一下单回调处理

  1. 详细步骤与代码示例(以 Node.js/Java 为例,但逻辑通用)
  2. 必须注意的 4 个“坑”
  3. 微信支付 V3 接口的差异
  4. 总结与建议

处理微信支付统一下单的回调(官方称为“支付结果通知”)是确保订单状态正确更新的关键环节,微信服务器会以 POST 方式将异步通知结果发送到你在统一下单接口中填写的 notify_url

以下是处理该回调的完整流程、核心逻辑以及需要注意的坑。

  1. 接收请求:微信服务器 POST 请求到你的 notify_url
  2. 验签:验证数据是否确实来自微信,防止伪造回调。
  3. 解密数据:从加密的 req_info 中获取订单详情。
  4. 检查业务逻辑:确认订单未处理、金额一致、商户号匹配等。
  5. 更新订单状态:将数据库中的订单状态标记为“已支付”。
  6. 返回成功应答:告诉微信别再重复通知了。

详细步骤与代码示例(以 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 字段本身除外)。
  • 规则
    1. 按字段名的 ASCII 码升序排序。
    2. 拼接成 key1=value1&key2=value2 格式。
    3. 在字符串末尾拼接 &key=你的API密钥(微信商户平台设置的密钥)。
    4. 计算 MD5HMAC-SHA256(根据统一下单时指定的 sign_type),结果转大写。
    5. 与微信传来的 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_idout_trade_nototal_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 个“坑”

  1. 幂等处理:必须判断订单是否已经支付,如果收到重复通知,直接返回 SUCCESS,不要重复发货或更新状态。
  2. 验签失败:不要先解密后验签。先验签,验证数据完整性。
  3. SSL/TLS:微信需要你的 notify_urlHTTPS 地址(生产环境)。
  4. 解密失败:AES 密钥是 API 密钥的 MD5,不是 API 密钥本身,也不是签名密钥。

微信支付 V3 接口的差异

微信支付 V3 接口(现在新接入推荐使用 V3)的回调处理有所不同:

  1. 数据格式:不再是 XML,而是 JSON
  2. 验签方式:不再使用 MD5 签名,而是基于 平台证书的非对称验签(RSA-SHA256)
  3. 数据位置:关键信息(如 transaction_id)直接包含在回调 JSON 的 resource 字段中,但 resource 字段本身是加密的(AES-256-GCM)。

V3 核心流程:

  1. 获取请求头 Wechatpay-SignatureWechatpay-Serial(证书序列号)、Wechatpay-TimestampWechatpay-Nonce
  2. 拼接签名串:timestamp + \n + nonce + \n + 请求体JSON + \n
  3. 使用微信支付平台证书(根据 Wechatpay-Serial 找到对应证书)的公钥,验证上述签名串。
  4. 验签通过后,使用 APIv3 密钥解密 resource 中的 ciphertext 字段(AES-256-GCM 解密,需要 associated_datanonce)。
  5. 业务逻辑同上。

总结与建议

  • 单元测试:利用微信官方的“模拟支付通知”工具或手动构造回调数据测试。
  • 日志:记录回调的原始数据、验签结果、解密结果、业务校验结果,出问题时这是唯一的排查手段。
  • 超时:回调处理应尽快完成(< 5秒),不要把大量数据库查询或外部 API 调用放在同步流程中。
  • 一致性:如果支付成功,但更新数据库失败(如网络问题),必须返回 FAIL 让微信重试。

如果你需要针对特定语言(Java、PHP、Go)的完整代码示例,或者 V3 接口的详细实现,请告诉我。

抱歉,评论功能暂时关闭!