Appearance
异步通知
异步通知是嗖付支付系统向商户系统推送支付结果、退款结果等重要信息的机制。商户需要正确处理异步通知以确保订单状态的准确性。
通知机制
通知方式
- HTTP POST:嗖付系统向商户指定的回调地址发送 POST 请求
- 重试策略:如果通知失败,系统会按照一定间隔重试
- 超时设置:每次通知请求超时时间为 30 秒
重试机制
通知失败时,系统会按以下时间间隔进行重试:
| 重试次数 | 重试间隔 |
|---|---|
| 1 | 立即重试 |
| 2 | 1 分钟后 |
| 3 | 5 分钟后 |
| 4 | 15 分钟后 |
| 5 | 30 分钟后 |
| 6 | 60 分钟后 |
| 7 | 2 小时后 |
| 8 | 4 小时后 |
注意
超过 8 次重试仍失败的通知将不再重试,请确保通知接口的稳定性。
支付结果通知
通知时机
- 用户支付成功后立即发送
- 订单状态发生变化时发送
通知参数
| 参数名 | 类型 | 描述 |
|---|---|---|
| mch_id | String | 商户号 |
| trade_no | String | 嗖付交易号 |
| out_trade_no | String | 商户订单号 |
| trade_status | String | 交易状态 |
| total_amount | String | 订单总金额,单位:元 |
| pay_amount | String | 实际支付金额,单位:元 |
| pay_type | String | 支付类型 |
| pay_time | String | 支付完成时间 |
| attach | String | 附加数据 |
| buyer_info | Object | 买家信息 |
| timestamp | String | 通知时间戳 |
| nonce_str | String | 随机字符串 |
| sign | String | 签名 |
通知示例
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_id | String | 商户号 |
| trade_no | String | 嗖付交易号 |
| out_trade_no | String | 商户订单号 |
| refund_no | String | 嗖付退款单号 |
| out_refund_no | String | 商户退款单号 |
| refund_amount | String | 退款金额,单位:元 |
| refund_status | String | 退款状态 |
| refund_time | String | 退款完成时间 |
| refund_reason | String | 退款原因 |
| timestamp | String | 通知时间戳 |
| nonce_str | String | 随机字符串 |
| sign | String | 签名 |
通知示例
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"
}签名验证
验证步骤
- 提取签名:从通知数据中提取
sign字段 - 参数排序:将除
sign外的所有参数按 key 的字典序升序排列 - 拼接字符串:按照
key1=value1&key2=value2的格式拼接 - 添加密钥:在拼接字符串末尾添加
&key=API_KEY - MD5 加密:对最终字符串进行 MD5 加密,并转换为大写
- 验证签名:将计算得到的签名与通知中的签名进行比较
验证示例
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 可以访问通知接口
- 签名验证:务必验证每个通知的签名
- 超时设置:设置合理的接口响应超时时间
- 日志记录:记录所有通知的详细日志
调试工具
通知日志查看
可以通过商户后台查看通知发送记录:
- 登录商户后台
- 进入"交易管理" → "通知记录"
- 查看通知状态、重试次数、错误信息等
本地调试
开发环境下可以使用 ngrok 等工具将本地服务暴露到外网:
bash
# 安装ngrok
npm install -g ngrok
# 暴露本地3000端口
ngrok http 3000然后将生成的外网地址配置为通知地址进行调试。
常见问题
Q: 收不到通知怎么办?
A: 请检查:
- 通知地址是否可以从外网访问
- 接口是否返回
SUCCESS - 签名验证是否正确
- 服务器防火墙是否开放相应端口
Q: 通知重复怎么处理?
A: 建议实现幂等性处理,确保同一笔订单的多次通知不会造成重复处理。
Q: 通知延迟很久才收到?
A: 可能的原因:
- 商户接口响应缓慢
- 网络问题导致重试
- 系统高峰期处理延迟
建议优化接口性能,确保快速响应。