支付宝回调返回 success 后为何仍持续收到通知?
结论
支付宝收到商户回调接口返回的纯文本 success 后,通常会停止重试。如果通知持续到达,问题往往不在业务处理结果,而在于支付宝没有识别到有效应答。
常见原因有:
- HTTP 状态码不是
200 - 响应正文并非严格的
success success前后带有空格、换行、HTML、JSON 或其他内容- 接口响应超时,支付宝未能及时收到结果
- 网关、反向代理或框架改写了响应
- 同一订单收到了不同类型的通知,却被误认为重复回调
仅凭订单号 2022062822001401591428498179 无法确定具体原因,还需要查看回调日志、HTTP 访问日志和支付宝侧的通知记录。
为什么返回了 success 仍可能重试
支付宝判断通知是否成功,依据的是最终收到的 HTTP 响应,而不是代码是否执行过 return "success" 之类的语句。
例如,下面这些响应通常都不等同于纯文本 success:
success\n
{"message":"success"}
success<br>
处理成功:success
业务代码可能已经生成了 success,但之后又发生异常,导致接口最终返回 500。统一响应包装器也可能把字符串转换成 JSON:
{
"code": 200,
"data": "success"
}
这种包装用于普通业务接口通常没有问题,但支付回调接口一般需要直接输出指定文本。
解决步骤
1. 检查真实 HTTP 响应
不要只看应用日志中记录的返回值。还要核对支付宝访问回调地址时实际得到的结果,包括:
- HTTP 状态码
- 响应正文的原始字节
- 响应耗时
- 是否发生重定向
- 请求是否经过 CDN、WAF、API 网关或反向代理
理想的响应应接近:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
success
避免返回 301、302、404、499、500、502 或 504。
2. 验签后再处理业务
收到回调参数后,应先验签,再校验以下业务字段:
app_idseller_idout_trade_notrade_nototal_amounttrade_status
金额、商户身份和订单归属必须与本地订单一致。如果验签失败或业务字段不匹配,不应更新订单状态。
3. 对重复通知做幂等处理
支付通知可能重复发送,所以多次收到通知并不代表发生了多次支付。
可以使用 trade_no 或本地 out_trade_no 建立唯一约束,并通过条件更新确保订单只处理一次:
UPDATE payment_order
SET status = 'PAID',
alipay_trade_no = :trade_no,
paid_at = CURRENT_TIMESTAMP
WHERE out_trade_no = :out_trade_no
AND status = 'UNPAID';
如果受影响行数为 0,应继续查询订单状态:
- 订单已支付:按重复通知处理,直接返回
success - 订单不存在或数据不一致:记录异常,暂不确认成功
退款、交易关闭等其他通知状态应按照各自的业务规则处理,不能一律当作支付成功。
4. 尽快返回 success
通知接口中不宜同步执行耗时操作,例如:
- 发送短信或邮件
- 调用多个下游系统
- 生成报表
- 执行复杂的库存计算
- 等待人工审核
较稳妥的处理方式是完成验签和必要的事务更新,将后续工作投递到消息队列,然后立即返回 success。
Java 示例
以下示例只展示响应方式和基本处理结构。验签参数需要根据项目实际使用的支付宝 SDK 配置:
@PostMapping(
value = "/alipay/notify",
produces = "text/plain;charset=UTF-8"
)
@ResponseBody
public String notify(HttpServletRequest request) {
Map<String, String> params = extractAlipayParams(request);
boolean verified = verifyAlipaySignature(params);
if (!verified) {
return "failure";
}
if (!validateBusinessFields(params)) {
return "failure";
}
try {
processPaymentIdempotently(params);
return "success";
} catch (Exception e) {
log.error("Failed to process Alipay notification", e);
return "failure";
}
}
如果项目启用了统一返回值包装,应将回调路径排除,否则返回的字符串可能被包装成 JSON。
PHP 示例
<?php
$params = $_POST;
if (!verifyAlipaySignature($params)) {
http_response_code(200);
header('Content-Type: text/plain; charset=utf-8');
echo 'failure';
exit;
}
if (!validateBusinessFields($params)) {
http_response_code(200);
header('Content-Type: text/plain; charset=utf-8');
echo 'failure';
exit;
}
try {
processPaymentIdempotently($params);
http_response_code(200);
header('Content-Type: text/plain; charset=utf-8');
echo 'success';
exit;
} catch (Throwable $e) {
error_log($e->getMessage());
http_response_code(200);
header('Content-Type: text/plain; charset=utf-8');
echo 'failure';
exit;
}
需要确认 PHP 文件在 <?php 之前没有输出 BOM、空格或其他内容,输出响应后也不要继续打印调试信息。
注意事项
- 验签失败或订单校验失败时,不要为了停止重试而盲目返回
success。 - 对于已经正确处理过的重复通知,应直接返回
success,不要重复扣库存、发货或记账。 - 回调日志应记录通知时间、
trade_no、out_trade_no、trade_status、验签结果、处理耗时和最终响应,但不要记录私钥等敏感信息。 - 检查回调地址是否发生了 HTTP 到 HTTPS、域名或路径重定向。
- 如果本地日志显示已经返回
success,还要检查 Nginx、负载均衡、网关和应用服务器的访问日志,确认最终响应没有被改写。 - 如果只有订单
2022062822001401591428498179持续出现问题,应对比每次通知的完整参数,确认是内容完全相同的重试,还是交易状态不同的多次通知。 - 如果响应状态、正文和耗时都正常,但通知仍在持续,应携带订单号、商户订单号、通知时间和服务端访问日志,通过支付宝商家平台或官方技术支持进一步核查。
备注:内容仅供参考。
