PAYATHON 2026

支付宝“电脑网站支付”成功后未收到callback通知

支付阿杰

结论

“电脑网站支付”完成后,网站余额不应依靠浏览器跳转页面来更新。入账应以支付宝服务器发送到 notify_url 的异步通知为准。

排查时主要确认以下事项:

  1. 创建支付请求时是否正确设置了 notify_url。
  2. notify_url 能否从公网直接访问。
  3. 支付宝请求是否已经到达服务器,却被网关、鉴权、重定向或程序异常拦截。
  4. 通知验签和订单校验是否正确。
  5. 处理成功后是否向支付宝返回纯文本 success。
  6. 对未收到通知或处理失败的订单,是否通过 alipay.trade.query 主动补查。

return_url 只是用户浏览器的同步跳转地址。如果用户关闭页面、网络中断或浏览器拦截跳转,该地址可能不会被访问,因此不能作为确认支付成功和余额入账的依据。

先区分两个通知地址

电脑网站支付通常会用到两个地址:

  • return_url:支付完成后,支付宝引导用户浏览器跳回网站。
  • notify_url:支付宝服务器向商户服务器发送异步 POST 通知。

到账逻辑应放在 notify_url 中。典型流程如下:

创建本地待支付订单
    ↓
调用 alipay.trade.page.pay
    ↓
用户在支付宝完成付款
    ↓
支付宝服务器 POST notify_url
    ↓
网站验签并核对订单
    ↓
更新订单和用户余额
    ↓
返回 success

即使 return_url 正常跳转,也不能只凭跳转参数认定支付成功。

检查支付请求是否设置了 notify_url

以支付宝 Java SDK 的常见调用方式为例:

AlipayClient alipayClient = new DefaultAlipayClient(
    GATEWAY_URL,
    APP_ID,
    MERCHANT_PRIVATE_KEY,
    "json",
    "UTF-8",
    ALIPAY_PUBLIC_KEY,
    "RSA2"
);

AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("https://www.example.com/payment/return");
request.setNotifyUrl("https://www.example.com/api/alipay/notify");

AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo("ORDER_202609080001");
model.setTotalAmount("100.00");
model.setSubject("账户充值");
model.setProductCode("FAST_INSTANT_TRADE_PAY");

request.setBizModel(model);

AlipayTradePagePayResponse response =
    alipayClient.pageExecute(request, "POST");

String paymentForm = response.getBody();

检查时应以实际提交给支付宝的请求为准,不能只看配置文件:

  • notify_url 是否为空。
  • 地址是否拼写错误。
  • 测试环境和生产环境是否使用了不同配置。
  • URL 是否仍然指向旧域名、内网域名或临时地址。
  • 是否只设置了 return_url,却没有设置 notify_url。

不同语言和 SDK 版本的方法名可能略有差异,但支付请求必须正确传递 notify_url。

确认通知地址可以被公网访问

支付宝服务器必须能直接访问 notify_url。以下地址通常不能用于正式的异步通知:

http://localhost/alipay/notify
http://127.0.0.1/alipay/notify
http://192.168.1.10/alipay/notify

通知接口还需要满足以下条件:

  • 域名解析正确。
  • 端口允许公网访问。
  • HTTPS 证书有效,证书链完整。
  • 防火墙、WAF、CDN 和安全组没有拦截请求。
  • 接口不要求登录态、Cookie、验证码或 CSRF Token。
  • 请求不会被强制跳转到登录页。
  • 路由支持 POST。
  • 接口可以读取表单参数,而不是只接收 JSON。
  • 响应时间足够短,不在通知请求中执行耗时任务。

可以先查看 Web 服务器访问日志,搜索通知路径:

/api/alipay/notify

根据日志结果继续排查:

  • 完全没有访问记录:先检查 notify_url、公网连通性、DNS、防火墙和支付请求参数。
  • 有访问记录但没有入账:继续检查 HTTP 状态码、应用日志、验签、订单校验、数据库事务和响应内容。

使用 curl 只能确认接口是否可以访问,无法模拟有效的支付宝通知,因为伪造参数不能通过支付宝签名验证。例如:

curl -i -X POST https://www.example.com/api/alipay/notify \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'test=connectivity'

业务代码将这个请求判定为验签失败属于正常现象。

正确读取并验证异步通知

支付宝异步通知通常以 application/x-www-form-urlencoded 格式提交。服务端应读取全部表单参数,再使用支付宝公钥验签。

下面是一个简化的 Spring MVC 示例:

@PostMapping(
    value = "/api/alipay/notify",
    produces = "text/plain;charset=UTF-8"
)
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
    try {
        Map<String, String> params = new HashMap<>();

        for (Map.Entry<String, String[]> entry
                : request.getParameterMap().entrySet()) {
            String value = String.join(",", entry.getValue());
            params.put(entry.getKey(), value);
        }

        boolean verified = AlipaySignature.rsaCheckV1(
            params,
            ALIPAY_PUBLIC_KEY,
            "UTF-8",
            "RSA2"
        );

        if (!verified) {
            return "failure";
        }

        String appId = params.get("app_id");
        String sellerId = params.get("seller_id");
        String outTradeNo = params.get("out_trade_no");
        String tradeNo = params.get("trade_no");
        String tradeStatus = params.get("trade_status");
        String totalAmount = params.get("total_amount");

        if (!APP_ID.equals(appId)) {
            return "failure";
        }

        if (!EXPECTED_SELLER_ID.equals(sellerId)) {
            return "failure";
        }

        boolean paid =
            "TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus);

        if (!paid) {
            return "success";
        }

        paymentService.confirmPayment(
            outTradeNo,
            tradeNo,
            new BigDecimal(totalAmount)
        );

        return "success";
    } catch (Exception e) {
        // 应记录异常及订单号,但不要记录私钥等敏感信息
        return "failure";
    }
}

实际项目还需要核对这些信息:

  • app_id 是否为本系统使用的支付宝应用。
  • seller_id 是否属于预期收款方。
  • out_trade_no 是否存在于本地订单表。
  • total_amount 是否与本地订单金额完全一致。
  • 币种和业务类型是否符合订单预期。
  • trade_no 是否已经绑定其他本地订单。
  • trade_status 是否表示支付成功。

比较金额时不要使用 double 或 float,应使用 Java 的 BigDecimal 等定点小数类型:

BigDecimal notifiedAmount = new BigDecimal(totalAmount);
BigDecimal orderAmount = order.getAmount();

if (notifiedAmount.compareTo(orderAmount) != 0) {
    throw new IllegalStateException("支付金额不一致");
}

验签时必须使用支付宝公钥,不能使用应用私钥或应用公钥。如果应用采用支付宝公钥证书模式,应使用对应的证书验签方式,不能直接沿用普通公钥模式的配置。

必须保证幂等入账

异步通知可能重复发送。同一通知到达两次时,系统不能给用户增加两次余额。

可以在同一个数据库事务中完成订单更新和余额变更,并通过条件更新或唯一约束保证幂等:

UPDATE recharge_order
SET status = 'PAID',
    alipay_trade_no = :trade_no,
    paid_at = CURRENT_TIMESTAMP
WHERE out_trade_no = :out_trade_no
  AND status = 'PENDING';

只有受影响行数为 1 时,才执行余额增加:

UPDATE user_account
SET balance = balance + :amount
WHERE user_id = :user_id;

更稳妥的做法是把订单状态变更、账户流水写入和余额更新放在同一事务中,同时为支付流水设置唯一键,例如:

UNIQUE (alipay_trade_no)

如果订单已经是 PAID,说明此前处理成功,应直接返回 success,不要再次入账。

响应内容必须符合要求

成功处理通知后,应直接返回:

success

不要返回 HTML、JSON 或附带其他文字。例如,下面这些响应都不合适:

{"status":"success"}
<html>success</html>
success processed

还要检查框架是否自动包装响应,以及反向代理是否将请求重定向到其他页面。常见问题包括:

  • 返回 302,跳转到登录页。
  • Spring 接口没有添加 @ResponseBody,导致 success 被当作模板名称。
  • 程序已经完成处理,但随后抛出异常,最终返回 500。
  • 返回成功前执行邮件、消息推送等耗时操作,导致请求超时。
  • 数据库事务尚未提交,程序就提前返回成功。

应先完成验签、订单校验和持久化,再返回 success。邮件、消息推送等非关键任务可以交给消息队列或后台任务处理。

使用交易查询进行补偿

系统不能只依赖异步通知确认订单。对于长时间处于 PENDING 状态的订单,可以调用 alipay.trade.query 主动查询。

Java 示例:

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();

AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo("ORDER_202609080001");
request.setBizModel(model);

AlipayTradeQueryResponse response = alipayClient.execute(request);

if (response.isSuccess()) {
    String tradeStatus = response.getTradeStatus();

    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {
        // 核对订单号、金额和收款方后,按幂等方式补记支付结果
    }
}

交易查询适用于以下情况:

  • 用户支付后已经返回网站,但本地订单仍是待支付状态。
  • 异步通知因临时网络故障未能及时处理。
  • 通知已经到达,但当时数据库或应用发生异常。
  • 定时任务扫描到长期未完成的充值订单。

处理查询结果时,仍然要校验金额、订单号和收款方,并保证补记操作幂等。

推荐的排查顺序

按以下顺序检查,通常可以较快找到问题:

  1. 在支付请求日志中确认 notify_url 的实际值。
  2. 确认该地址能够从公网访问,并支持匿名 POST。
  3. 查看 Nginx、网关、CDN 或负载均衡日志,确认通知是否到达。
  4. 查看接口返回的 HTTP 状态码,确认是否发生重定向。
  5. 检查接口是否按表单方式读取参数。
  6. 检查支付宝公钥、字符集和签名类型是否与应用配置一致。
  7. 检查 app_id、seller_id、订单号和金额校验。
  8. 检查数据库事务、订单状态条件和账户流水。
  9. 确认处理成功后只返回纯文本 success。
  10. 使用 alipay.trade.query 查询问题订单,并按幂等方式补偿。

不要通过关闭验签、信任前端跳转参数或只凭 trade_status 入账来处理通知问题。这些做法会带来伪造支付和重复充值的风险。

备注:内容仅供参考。