Skip to content

异步通知

异步通知是嗖付支付系统向商户系统推送支付结果、退款结果等重要信息的机制。商户需要正确处理异步通知以确保订单状态的准确性。

通知机制

通知方式

  • HTTP POST:嗖付系统向商户指定的回调地址发送 POST 请求
  • 重试策略:如果通知失败,系统会按照一定间隔重试
  • 超时设置:每次通知请求超时时间为 30 秒

重试机制

通知失败时,系统会按以下时间间隔进行重试:

重试次数重试间隔
1立即重试
21 分钟后
35 分钟后
415 分钟后
530 分钟后
660 分钟后
72 小时后
84 小时后

注意

超过 8 次重试仍失败的通知将不再重试,请确保通知接口的稳定性。

支付结果通知

通知时机

  • 用户支付成功后立即发送
  • 订单状态发生变化时发送

通知参数

参数名类型描述
mch_idString商户号
trade_noString嗖付交易号
out_trade_noString商户订单号
trade_statusString交易状态
total_amountString订单总金额,单位:元
pay_amountString实际支付金额,单位:元
pay_typeString支付类型
pay_timeString支付完成时间
attachString附加数据
buyer_infoObject买家信息
timestampString通知时间戳
nonce_strString随机字符串
signString签名

通知示例

json
{
  "mch_id": "your_mch_id",
  "trade_no": "SF20231119001234567890",
  "out_trade_no": "ORDER20231119001",
  "trade_status": "TRADE_SUCCESS",
  "total_amount": "100.00",
  "pay_amount": "100.00",
  "pay_type": "WECHAT_PAY",
  "pay_time": "2023-11-19 14:32:15",
  "attach": "store_id:001",
  "buyer_info": {
    "buyer_id": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o",
    "buyer_name": "张三"
  },
  "timestamp": "1700380935",
  "nonce_str": "1234567890abcdef",
  "sign": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6"
}

退款结果通知

通知时机

  • 退款处理完成后发送
  • 退款状态发生变化时发送

通知参数

参数名类型描述
mch_idString商户号
trade_noString嗖付交易号
out_trade_noString商户订单号
refund_noString嗖付退款单号
out_refund_noString商户退款单号
refund_amountString退款金额,单位:元
refund_statusString退款状态
refund_timeString退款完成时间
refund_reasonString退款原因
timestampString通知时间戳
nonce_strString随机字符串
signString签名

通知示例

json
{
  "mch_id": "your_mch_id",
  "trade_no": "SF20231119001234567890",
  "out_trade_no": "ORDER20231119001",
  "refund_no": "RF20231119001234567890",
  "out_refund_no": "REFUND20231119001",
  "refund_amount": "50.00",
  "refund_status": "REFUND_SUCCESS",
  "refund_time": "2023-11-19 15:32:15",
  "refund_reason": "用户取消订单",
  "timestamp": "1700383935",
  "nonce_str": "1234567890abcdef",
  "sign": "B2C3D4E5F6G7H8I9J0K1L2M3N4O5P7Q8"
}

签名验证

验证步骤

  1. 提取签名:从通知数据中提取sign字段
  2. 参数排序:将除sign外的所有参数按 key 的字典序升序排列
  3. 拼接字符串:按照key1=value1&key2=value2的格式拼接
  4. 添加密钥:在拼接字符串末尾添加&key=API_KEY
  5. MD5 加密:对最终字符串进行 MD5 加密,并转换为大写
  6. 验证签名:将计算得到的签名与通知中的签名进行比较

验证示例

java
public boolean verifyNotifySign(Map<String, String> params, String apiKey) {
    // 1. 提取并移除签名参数
    String receivedSign = params.remove("sign");

    // 2. 过滤空值并排序
    Map<String, String> filteredParams = params.entrySet().stream()
        .filter(entry -> StringUtils.isNotBlank(entry.getValue()))
        .collect(Collectors.toMap(
            Map.Entry::getKey,
            Map.Entry::getValue,
            (e1, e2) -> e1,
            TreeMap::new
        ));

    // 3. 拼接参数
    String stringA = filteredParams.entrySet().stream()
        .map(entry -> entry.getKey() + "=" + entry.getValue())
        .collect(Collectors.joining("&"));

    // 4. 添加密钥
    String stringSignTemp = stringA + "&key=" + apiKey;

    // 5. MD5加密并转大写
    String calculatedSign = DigestUtils.md5Hex(stringSignTemp).toUpperCase();

    // 6. 验证签名
    return calculatedSign.equals(receivedSign);
}

商户接口实现

接口规范

  • HTTP 方法:POST
  • Content-Type:application/json
  • 响应格式:纯文本
  • 成功响应:返回字符串SUCCESS
  • 失败响应:返回其他任何内容

实现示例

Java Spring Boot 示例

java
@RestController
@RequestMapping("/notify")
public class NotifyController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/payment")
    public String paymentNotify(@RequestBody Map<String, Object> params) {
        try {
            // 1. 验证签名
            if (!verifySign(params)) {
                logger.warn("支付通知签名验证失败: {}", params);
                return "SIGN_ERROR";
            }

            // 2. 获取关键参数
            String outTradeNo = (String) params.get("out_trade_no");
            String tradeStatus = (String) params.get("trade_status");
            String payAmount = (String) params.get("pay_amount");

            // 3. 验证订单状态
            Order order = orderService.getByOutTradeNo(outTradeNo);
            if (order == null) {
                logger.warn("支付通知订单不存在: {}", outTradeNo);
                return "ORDER_NOT_EXISTS";
            }

            // 4. 处理业务逻辑
            if ("TRADE_SUCCESS".equals(tradeStatus)) {
                // 支付成功处理
                orderService.handlePaymentSuccess(order, params);
                logger.info("订单支付成功: {}", outTradeNo);
            }

            return "SUCCESS";
        } catch (Exception e) {
            logger.error("处理支付通知异常", e);
            return "ERROR";
        }
    }

    @PostMapping("/refund")
    public String refundNotify(@RequestBody Map<String, Object> params) {
        try {
            // 1. 验证签名
            if (!verifySign(params)) {
                logger.warn("退款通知签名验证失败: {}", params);
                return "SIGN_ERROR";
            }

            // 2. 获取关键参数
            String outRefundNo = (String) params.get("out_refund_no");
            String refundStatus = (String) params.get("refund_status");

            // 3. 处理退款结果
            if ("REFUND_SUCCESS".equals(refundStatus)) {
                orderService.handleRefundSuccess(outRefundNo, params);
                logger.info("退款成功: {}", outRefundNo);
            }

            return "SUCCESS";
        } catch (Exception e) {
            logger.error("处理退款通知异常", e);
            return "ERROR";
        }
    }
}

PHP 示例

php
<?php
// 支付结果通知处理
function handlePaymentNotify() {
    // 1. 获取通知数据
    $input = file_get_contents('php://input');
    $params = json_decode($input, true);

    // 2. 验证签名
    if (!verifySign($params, $apiKey)) {
        error_log('支付通知签名验证失败: ' . json_encode($params));
        echo 'SIGN_ERROR';
        return;
    }

    // 3. 获取关键参数
    $outTradeNo = $params['out_trade_no'];
    $tradeStatus = $params['trade_status'];
    $payAmount = $params['pay_amount'];

    // 4. 查询订单
    $order = getOrderByOutTradeNo($outTradeNo);
    if (!$order) {
        error_log('支付通知订单不存在: ' . $outTradeNo);
        echo 'ORDER_NOT_EXISTS';
        return;
    }

    // 5. 处理业务逻辑
    if ($tradeStatus === 'TRADE_SUCCESS') {
        // 更新订单状态
        updateOrderStatus($outTradeNo, 'PAID');
        // 其他业务处理...
        error_log('订单支付成功: ' . $outTradeNo);
    }

    echo 'SUCCESS';
}

function verifySign($params, $apiKey) {
    $sign = $params['sign'];
    unset($params['sign']);

    // 过滤空值并排序
    $params = array_filter($params, function($value) {
        return $value !== null && $value !== '';
    });
    ksort($params);

    // 拼接参数
    $stringA = http_build_query($params);
    $stringSignTemp = $stringA . '&key=' . $apiKey;

    // MD5加密并转大写
    $calculatedSign = strtoupper(md5($stringSignTemp));

    return $calculatedSign === $sign;
}

// 处理通知
handlePaymentNotify();
?>

Node.js 示例

javascript
const express = require("express");
const crypto = require("crypto");
const app = express();

app.use(express.json());

// 支付结果通知
app.post("/notify/payment", (req, res) => {
  try {
    const params = req.body;

    // 1. 验证签名
    if (!verifySign(params, API_KEY)) {
      console.warn("支付通知签名验证失败:", params);
      return res.send("SIGN_ERROR");
    }

    // 2. 获取关键参数
    const { out_trade_no, trade_status, pay_amount } = params;

    // 3. 查询订单
    const order = getOrderByOutTradeNo(out_trade_no);
    if (!order) {
      console.warn("支付通知订单不存在:", out_trade_no);
      return res.send("ORDER_NOT_EXISTS");
    }

    // 4. 处理业务逻辑
    if (trade_status === "TRADE_SUCCESS") {
      // 更新订单状态
      updateOrderStatus(out_trade_no, "PAID");
      console.log("订单支付成功:", out_trade_no);
    }

    res.send("SUCCESS");
  } catch (error) {
    console.error("处理支付通知异常:", error);
    res.send("ERROR");
  }
});

function verifySign(params, apiKey) {
  const sign = params.sign;
  delete params.sign;

  // 过滤空值并排序
  const filteredParams = Object.keys(params)
    .filter((key) => params[key] !== null && params[key] !== "")
    .sort()
    .reduce((obj, key) => {
      obj[key] = params[key];
      return obj;
    }, {});

  // 拼接参数
  const stringA = Object.keys(filteredParams)
    .map((key) => `${key}=${filteredParams[key]}`)
    .join("&");

  const stringSignTemp = `${stringA}&key=${apiKey}`;

  // MD5加密并转大写
  const calculatedSign = crypto
    .createHash("md5")
    .update(stringSignTemp)
    .digest("hex")
    .toUpperCase();

  return calculatedSign === sign;
}

最佳实践

1. 幂等性处理

java
@Transactional
public void handlePaymentSuccess(String outTradeNo, Map<String, Object> params) {
    // 使用数据库锁或分布式锁确保幂等性
    Order order = orderService.getByOutTradeNoForUpdate(outTradeNo);

    if (order.getStatus() != OrderStatus.PENDING) {
        // 订单已处理,直接返回
        return;
    }

    // 更新订单状态
    order.setStatus(OrderStatus.PAID);
    order.setPayTime(new Date());
    orderService.update(order);

    // 其他业务处理...
}

2. 异常处理

java
@PostMapping("/notify/payment")
public String paymentNotify(@RequestBody Map<String, Object> params) {
    try {
        // 业务处理逻辑
        processPaymentNotify(params);
        return "SUCCESS";
    } catch (BusinessException e) {
        // 业务异常,记录日志但返回SUCCESS(避免重复通知)
        logger.error("处理支付通知业务异常: {}", e.getMessage(), e);
        return "SUCCESS";
    } catch (Exception e) {
        // 系统异常,返回ERROR触发重试
        logger.error("处理支付通知系统异常", e);
        return "ERROR";
    }
}

3. 安全建议

  • HTTPS 通信:确保通知接口使用 HTTPS
  • IP 白名单:限制只有嗖付服务器 IP 可以访问通知接口
  • 签名验证:务必验证每个通知的签名
  • 超时设置:设置合理的接口响应超时时间
  • 日志记录:记录所有通知的详细日志

调试工具

通知日志查看

可以通过商户后台查看通知发送记录:

  1. 登录商户后台
  2. 进入"交易管理" → "通知记录"
  3. 查看通知状态、重试次数、错误信息等

本地调试

开发环境下可以使用 ngrok 等工具将本地服务暴露到外网:

bash
# 安装ngrok
npm install -g ngrok

# 暴露本地3000端口
ngrok http 3000

然后将生成的外网地址配置为通知地址进行调试。

常见问题

Q: 收不到通知怎么办?

A: 请检查:

  1. 通知地址是否可以从外网访问
  2. 接口是否返回SUCCESS
  3. 签名验证是否正确
  4. 服务器防火墙是否开放相应端口

Q: 通知重复怎么处理?

A: 建议实现幂等性处理,确保同一笔订单的多次通知不会造成重复处理。

Q: 通知延迟很久才收到?

A: 可能的原因:

  1. 商户接口响应缓慢
  2. 网络问题导致重试
  3. 系统高峰期处理延迟

建议优化接口性能,确保快速响应。

相关接口

嗖付聚合支付API文档