PAYATHON 2026

支付异步回调返回 success 后,支付宝为何仍回调商户平台?

支付老李

结论

支付宝是否停止重试,要看它实际收到的 HTTP 响应,而不是应用日志里记录了什么返回值。

即使业务代码执行了 return "success",只要支付宝最终收到的响应正文不是纯文本 success,或者出现请求超时、HTTP 状态异常、网关改写响应等情况,支付宝仍可能继续发送异步通知。

先确认这两项:

  • HTTP 状态码是 200
  • 响应正文准确等于 success,不能包含引号、JSON 包装、HTML、空格、换行、BOM 或其他字符

常见原因

1. success 被包装成 JSON

接口实际返回的内容可能是:

{
  "result": "success"
}

也可能是:

"success"

这些响应虽然含有 success,但通常不符合支付宝要求的纯文本格式。

在 Spring MVC 中,如果方法返回统一响应对象,或者响应经过全局包装器处理,就容易出现这种情况。

2. HTTP 状态码不是 200

应用可能已经打印了“返回 success”,但之后又发生异常,导致最终响应变成:

HTTP/1.1 500 Internal Server Error

请求经过鉴权、限流或重定向后,也可能返回:

302 Found
401 Unauthorized
403 Forbidden
429 Too Many Requests
502 Bad Gateway
504 Gateway Timeout

支付宝看到的是最终 HTTP 响应,不会读取应用内部日志。

3. 接口处理时间过长

如果回调接口在返回前同步执行耗时操作,例如:

  • 更新多个业务系统
  • 调用第三方接口
  • 发送短信或邮件
  • 等待消息队列确认
  • 等待长时间未释放的数据库锁

支付宝可能在收到响应前就已经超时。商户服务稍后即使执行了 return "success",支付宝也没有成功收到这个响应,因此仍会重试。

不同支付宝产品和接口版本的超时时间、重试策略可能不同,应以对应产品的官方文档为准。

4. 网关或代理修改了响应

请求经过 Nginx、API 网关、WAF、CDN 或负载均衡器时,响应内容或状态可能发生变化,例如:

  • 自动追加错误页面
  • 压缩或包装响应
  • 强制跳转到 HTTPS
  • 命中安全拦截规则
  • 上游超时后返回网关错误
  • 回调路径被登录鉴权拦截

所以不能只检查应用日志,还要查看入口代理和网关日志。

5. 回调请求没有到达当前查看的实例

在多实例部署中,支付宝通知可能被负载均衡到不同节点。某个节点成功返回了 success,不代表其他通知也得到了正确处理。

常见情况有:

  • 部分节点仍在运行旧版本
  • 不同节点的配置不一致
  • 某个节点无法连接数据库
  • 灰度环境和正式环境共用回调地址
  • 日志只采集了部分实例

6. 返回内容中存在不可见字符

下面这些响应看起来很接近正确格式,但仍可能无法被识别:

 success
success 
success\n
success\r\n
success

最后一种情况可能含有 UTF-8 BOM。模板渲染、文件包含、调试输出或中间件都有可能引入这些字符。

7. 业务处理成功,但响应阶段发生异常

订单可能已经更新成功,但随后在记录日志、序列化响应或提交事务时发生异常。应用日志里已经出现“支付成功”,最终却没有正常返回 success。

因此,回调处理必须支持幂等。支付宝重复发送通知时,系统不能重复入账、发货或发券。

推荐排查步骤

第一步:记录完整的请求和最终响应

至少记录以下信息:

  • notify_id
  • trade_no
  • out_trade_no
  • trade_status
  • 请求到达时间
  • 处理完成时间
  • 当前服务器实例
  • 最终 HTTP 状态码
  • 最终响应正文
  • 处理耗时
  • 签名验证结果

敏感字段要按安全要求脱敏,日志中不能记录私钥或完整密钥材料。

只记录下面这样的日志远远不够:

准备返回 success

应尽量从网关层确认实际发出的状态码、响应大小和处理时间。

第二步:绕过统一响应包装

异步通知接口不要返回通用 JSON 对象,例如:

return Result.success();

也不要让全局响应处理器把字符串转换成:

{"code":0,"message":"success"}

最好为回调地址设置独立接口,并明确返回 text/plain。

第三步:检查网关访问日志

以 Nginx 为例,可以在访问日志中加入状态码、响应字节数和请求耗时:

log_format alipay_callback
    '$remote_addr [$time_local] "$request" '
    'status=$status bytes=$body_bytes_sent '
    'request_time=$request_time '
    'upstream_status=$upstream_status '
    'upstream_time=$upstream_response_time';

access_log /var/log/nginx/alipay_callback.log alipay_callback;

在持续重试期间,重点检查:

  • 是否出现非 200 状态码
  • request_time 是否过长
  • upstream_status 是否为 502 或 504
  • 响应正文长度是否异常
  • 请求是否被重定向到其他地址

第四步:检查回调地址是否被拦截

支付宝回调是服务器之间的通知,不能依赖浏览器登录状态。检查回调路径是否被以下机制阻断:

  • 登录认证
  • CSRF 校验
  • 验证码
  • 配置错误的 IP 白名单
  • User-Agent 限制
  • 请求频率限制
  • 强制页面跳转

如果需要控制访问,应使用支付宝签名验证,不能要求支付宝携带商户平台的登录凭证。

第五步:缩短同步处理链路

可以按下面的顺序处理:

  1. 读取支付宝通知参数。
  2. 验证签名。
  3. 校验 app_id、商户身份、订单号、金额和交易状态。
  4. 通过数据库事务完成必要的订单状态变更。
  5. 记录该通知已经处理。
  6. 尽快返回纯文本 success。
  7. 将发货、通知、积分等耗时操作交给可靠的异步任务。

不能为了停止重试,在完成验证前直接返回 success。如果对伪造或错误通知返回成功,支付宝通常不会再次发送该通知,商户也可能失去重新处理的机会。

Spring Boot 示例

下面的示例说明响应格式和基本处理结构。签名验证所需参数应根据实际使用的支付宝 SDK 和产品接口确定。

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class AlipayNotifyController {

    @PostMapping(
        value = "/payment/alipay/notify",
        produces = MediaType.TEXT_PLAIN_VALUE
    )
    public ResponseEntity<String> notify(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();

        request.getParameterMap().forEach((key, values) -> {
            if (values != null && values.length > 0) {
                params.put(key, String.join(",", values));
            }
        });

        try {
            boolean signatureValid = verifyAlipaySignature(params);
            if (!signatureValid) {
                return ResponseEntity.ok()
                    .contentType(MediaType.TEXT_PLAIN)
                    .body("failure");
            }

            validateNotification(params);
            processPaymentIdempotently(params);

            return ResponseEntity.ok()
                .contentType(MediaType.TEXT_PLAIN)
                .body("success");
        } catch (Exception exception) {
            // 记录 trade_no、out_trade_no、实例名称及异常信息
            // 不要记录私钥等敏感数据
            return ResponseEntity.ok()
                .contentType(MediaType.TEXT_PLAIN)
                .body("failure");
        }
    }

    private boolean verifyAlipaySignature(Map<String, String> params) {
        // 使用实际接入的支付宝 SDK 验证签名
        throw new UnsupportedOperationException("Implement with Alipay SDK");
    }

    private void validateNotification(Map<String, String> params) {
        // 校验 app_id、seller_id、out_trade_no、total_amount、trade_status 等
    }

    private void processPaymentIdempotently(Map<String, String> params) {
        // 使用 trade_no 或 out_trade_no 实现幂等更新
    }
}

如果项目使用了全局响应包装器,需要明确排除这个回调接口,否则返回的字符串仍可能被转换成 JSON。

Servlet 示例

直接写入响应,可以更准确地控制返回内容:

response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("success");
response.getWriter().flush();

写入前后不要输出其他内容,也不要执行:

response.sendRedirect("/success");

支付宝要求回调请求本身返回成功响应,而不是跳转后的页面。

PHP 示例

<?php

try {
    $params = $_POST;

    if (!verify_alipay_signature($params)) {
        header('Content-Type: text/plain; charset=utf-8');
        http_response_code(200);
        echo 'failure';
        exit;
    }

    validate_notification($params);
    process_payment_idempotently($params);

    header('Content-Type: text/plain; charset=utf-8');
    http_response_code(200);
    echo 'success';
    exit;
} catch (Throwable $e) {
    error_log($e->getMessage());

    header('Content-Type: text/plain; charset=utf-8');
    http_response_code(200);
    echo 'failure';
    exit;
}

PHP 文件的 <?php 之前不能出现空格或 BOM,也不要让调试信息、警告信息混入响应正文。

幂等处理要点

支付系统必须能正常处理重复通知。即使响应完全正确,受网络状态、并发请求或之前已经触发的重试影响,短时间内仍可能收到重复通知。

订单状态应通过条件更新或唯一约束修改,防止重复执行:

UPDATE payment_order
SET pay_status = 'PAID',
    alipay_trade_no = :trade_no,
    paid_at = CURRENT_TIMESTAMP
WHERE out_trade_no = :out_trade_no
  AND pay_status = 'WAIT_PAY';

根据受影响的行数判断处理结果:

  • 影响 1 行:本次完成了状态变更。
  • 影响 0 行:查询订单,确认是否已经支付。
  • 订单已支付且关键字段一致:视为重复通知,返回 success。
  • 金额、商户身份或支付宝交易号不一致:记录告警,不要直接更新订单。

注意事项

  • 不要通过浏览器访问回调地址来判断支付宝能否正常收到响应。
  • 不要只看控制器的返回值,要检查支付宝入口处最终发出的 HTTP 响应。
  • 验签失败、订单不存在或金额不一致时,不能为了停止通知直接返回 success。
  • 对已经正确处理过的重复通知,应快速返回 success,不能再次发货或入账。
  • 支付宝公钥、应用公钥和应用私钥的用途不同,验签配置错误会导致每次通知都失败。
  • 如果日志确认每次响应都是 HTTP 200,正文也严格等于 success,但同一通知仍被长期重复发送,应整理 notify_id、trade_no、通知时间、响应状态和网关日志,再联系对应支付宝产品的技术支持渠道核查。

备注:内容仅供参考。