PAYATHON 2026

部署SSL并强制Https后支付成功但商品页面未更新

支付阿杰

结论

部署 SSL 并强制使用 HTTPS 确实可能导致支付回调失败,但页面仍显示”未支付”,并不能直接证明问题出在 HTTPS。

通常,这种情况意味着:

  • 用户已经完成支付,支付渠道也成功扣款;
  • 浏览器正常跳转到了支付成功页;
  • 服务器没有收到异步支付通知,或者收到了通知却处理失败;
  • 订单状态因此没有从”待支付”更新为”已支付”。

这里需要区分两个地址:

  • 同步跳转地址:支付完成后将用户的浏览器带回网站,主要用于展示支付结果。
  • 异步回调地址:由支付平台的服务器主动请求,用来可靠地更新订单状态。

浏览器能打开支付成功页,不等于异步回调已经成功。订单状态应以异步回调的处理结果或主动查询到的支付结果为准,不能只看页面跳转。

HTTPS 为什么可能影响支付回调

从 HTTP 切换为强制 HTTPS 后,下面这些问题都可能导致支付平台无法完成回调。

回调地址发生重定向

支付平台请求的地址可能是:

http://example.com/payment/notify

服务器随后将它重定向到:

https://example.com/payment/notify

部分支付平台不会跟随 301、302 或 308 重定向。即使平台跟随了重定向,原来的 POST 请求也可能被改成 GET,或者丢失请求体,最终导致验签失败。

支付平台后台配置的回调地址应直接填写最终地址:

https://example.com/payment/notify

不要依赖 HTTP 自动跳转到 HTTPS。同时还要检查是否存在二次跳转,例如:

https://example.com/payment/notify
→ https://www.example.com/payment/notify
→ https://www.example.com/payment/notify/

SSL 证书链不完整

浏览器可能依靠缓存、中间证书或兼容机制正常打开网站,但支付平台的服务器仍有可能拒绝连接。

应重点检查:

  • 证书是否过期;
  • 域名是否与证书匹配;
  • 中间证书链是否完整;
  • 是否使用了支付平台不支持的 TLS 版本或加密套件;
  • 是否配置了只有特定客户端才能完成的双向 TLS;
  • IPv4 和 IPv6 是否都指向正确的服务器。

网关或反向代理没有正确转发

部署 SSL 后,请求一般会经过 Nginx、负载均衡器、CDN 或应用网关。这个过程中可能出现以下问题:

  • 回调路径没有转发到支付应用;
  • POST 请求体没有完整传递;
  • HTTPS 入口转发到了错误端口;
  • Host、X-Forwarded-Proto 等请求头设置不正确;
  • 应用误以为请求仍是 HTTP,于是再次重定向;
  • 网关的 WAF、CSRF 或访问控制拦截了回调;
  • 回调接口被登录鉴权中间件拦截。

验签使用的原始数据发生变化

许多支付平台要求使用原始请求体进行签名验证。如果框架提前解析 JSON、重新排列字段、修改字符编码,或者网关改动了请求体,验签就会失败。

切换域名、协议或回调 URL 后,如果签名内容包含 URL,还要确认验签使用的是 HTTPS 地址,而不是原来的 HTTP 地址。

回调处理成功,但响应不符合要求

支付平台通常要求回调接口返回指定的字符串或 JSON,具体内容要以对应支付平台的接口文档为准。

即使业务已经更新了订单,只要接口返回 500、发生超时、输出 HTML 页面,或者返回了错误的确认内容,支付平台仍会将这次回调视为失败,并继续重试。

数据库更新失败

如果服务器已经收到了回调,问题就未必与 SSL 有关,还可能出在订单处理阶段:

  • 回调中的订单号无法匹配本地订单;
  • 金额单位处理错误,例如混用了”10 元”和”1000 分”;
  • 事务发生回滚;
  • 数据库连接失败;
  • 订单状态条件写错;
  • 回调线程报错后仍然返回成功;
  • 多套环境的配置混乱,回调进入测试环境,而订单保存在生产环境;
  • 支付应用、商户号或密钥与创建订单时使用的配置不一致。

建议的排查顺序

1. 先核对支付平台侧的交易记录

先记录这些信息:

  • 商户订单号;
  • 支付平台交易号;
  • 实付金额;
  • 支付时间;
  • 商户号或应用编号;
  • 支付平台显示的交易状态;
  • 回调通知记录和重试记录。

如果支付平台提供回调日志,重点查看请求时间、HTTP 状态码和失败原因。

2. 检查服务器是否收到回调

在网关、Nginx 和应用日志中,根据订单号、交易号和支付时间查找对应请求。

例如:

grep '商户订单号' /var/log/nginx/access.log
grep '商户订单号' /path/to/application.log

通常会遇到三种情况:

  1. 完全没有回调请求
    优先检查回调地址、DNS、证书、网络访问、端口、防火墙和支付平台配置。

  2. 网关收到了请求,但应用没有收到
    优先检查 Nginx、CDN、应用网关、路由规则和安全策略。

  3. 应用收到了请求,但订单没有更新
    优先检查验签、订单匹配、金额校验、异常日志、事务和数据库更新结果。

不要在日志中记录私钥、完整支付密钥、银行卡信息或完整的敏感报文。排查结束后,应及时降低临时调试日志的级别。

3. 检查回调地址是否直接返回最终响应

可以从公网环境测试回调地址能否连接,以及是否发生跳转:

curl -v -I https://example.com/payment/notify

真实回调通常使用 POST,因此还应通过测试环境或脱敏后的测试数据验证:

curl -v \
  -X POST \
  -H 'Content-Type: application/json' \
  --data '{"test":true}' \
  https://example.com/payment/notify

重点查看:

  • 是否返回 301、302、307 或 308;
  • 是否返回 401、403、404、405 或 500;
  • 请求是否超时;
  • 是否被跳转到登录页;
  • 返回内容是否符合支付平台的要求。

如果测试请求没有合法签名,接口返回”验签失败”通常是正常的。此时主要是确认请求能否到达正确的应用,而不是在网关层就被拦截。

4. 检查 SSL 和 DNS

可以用下面的命令检查证书链:

openssl s_client \
  -connect example.com:443 \
  -servername example.com \
  -showcerts

还要检查:

  • 回调域名是否解析到当前服务器;
  • DNS 中是否残留旧 IP;
  • 是否存在错误的 AAAA 记录;
  • 证书是否覆盖回调域名;
  • 443 端口是否能从公网访问;
  • CDN 或网关是否已经部署最新证书。

5. 确保回调路径没有被业务中间件拦截

支付回调接口一般不能依赖用户登录状态,也不应要求浏览器提供 Cookie。如果项目启用了 CSRF 防护,应针对支付回调路径使用适合服务端通知的验证方式,不能直接依赖 CSRF Token。

例如,可以通过 Nginx 将 HTTPS 请求直接转发给应用:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /path/to/fullchain.pem;
    ssl_certificate_key /path/to/private.key;

    location = /payment/notify {
        proxy_pass http://payment_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;

        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }
}

HTTP 站点仍然可以全站跳转到 HTTPS:

server {
    listen 80;
    server_name example.com;

    return 301 https://$host$request_uri;
}

不过,支付平台后台登记的回调地址仍应直接填写 HTTPS 地址,避免平台先访问 HTTP 地址。

回调接口的正确处理方式

下面是一个与具体支付平台无关的结构示例。验签算法、请求字段和成功响应都必须按照实际支付平台的文档进行替换。

app.post(
  "/payment/notify",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    try {
      const rawBody = req.body;

      // verifySignature 必须按照支付平台文档实现。
      const verified = verifySignature(req.headers, rawBody);
      if (!verified) {
        return res.status(400).send("invalid signature");
      }

      const notification = JSON.parse(rawBody.toString("utf8"));

      if (notification.tradeStatus !== "SUCCESS") {
        // 是否需要返回成功确认,应以支付平台文档为准。
        return res.status(200).send("success");
      }

      await database.transaction(async (tx) => {
        const order = await tx.orders.findForUpdate(
          notification.merchantOrderNo
        );

        if (!order) {
          throw new Error("order not found");
        }

        if (order.status === "PAID") {
          // 支付平台可能重复通知,已经处理过的订单直接保持成功。
          return;
        }

        if (order.amountInCents !== notification.amountInCents) {
          throw new Error("amount mismatch");
        }

        if (order.merchantId !== notification.merchantId) {
          throw new Error("merchant mismatch");
        }

        await tx.orders.markPaid(order.id, {
          paymentTransactionId: notification.transactionId,
          paidAt: notification.paidAt
        });

        await tx.entitlements.grant(order.userId, order.productId);
      });

      // 返回值必须按支付平台要求调整。
      return res.status(200).send("success");
    } catch (error) {
      console.error("payment callback failed", error);

      // 处理失败时不要伪装成成功,让支付平台按其机制重试。
      return res.status(500).send("failed");
    }
  }
);

回调处理至少应做到:

  • 先验签,再处理订单;
  • 同时校验商户号、应用编号、订单号、币种和金额;
  • 使用事务保证订单更新与商品权益发放的一致性;
  • 根据交易号或订单号实现幂等,允许支付平台重复回调;
  • 只有业务处理真正成功后,才返回支付平台要求的成功响应;
  • 尽快完成接口处理,耗时操作可以在可靠落库后交给消息队列;
  • 记录可追踪的订单号、交易号、状态和错误原因。

增加主动查询作为补偿机制

异步回调可能因为网络抖动、证书故障或服务重启而暂时失败,因此系统还应提供主动查单能力。

用户返回网站后,前端不要直接把订单标记为成功,而应向自己的后端查询订单状态:

async function waitForPayment(orderId) {
  for (let attempt = 0; attempt < 10; attempt += 1) {
    const response = await fetch(`/api/orders/${orderId}/status`);
    const result = await response.json();

    if (result.status === "PAID") {
      window.location.reload();
      return;
    }

    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  // 提示用户支付结果正在确认,避免直接显示“支付失败”。
}

如果后端发现本地订单仍处于待支付状态,可以调用支付平台的订单查询接口。平台确认订单已经支付后,后端应先完成验签或可信响应校验,再以幂等方式补记订单状态。

同步跳转、异步回调和主动查单分别承担以下职责:

同步跳转:改善用户体验
异步回调:更新订单的主要依据
主动查单:异常情况下的补偿手段

已付款订单的处理

对于已经到账但尚未发货或开通权益的订单,应根据支付平台的交易记录进行对账,不能要求顾客再次支付。

建议按以下步骤处理:

  1. 根据商户订单号和支付平台交易号确认付款状态。
  2. 核对金额、商户号、商品和用户归属。
  3. 通过后台补单程序更新订单,不要直接手工修改多张业务表。
  4. 补单时同样执行幂等校验,防止重复发货或重复开通权益。
  5. 保存补单原因、操作人、交易号和处理时间,方便后续审计。

最容易忽略的几个问题

  • 只修改了前端跳转地址,没有修改支付平台后台登记的异步通知地址;
  • 修改配置后,生产服务没有重新加载;
  • 支付平台仍在缓存或使用创建订单时传入的旧回调地址;
  • 新地址包含多余的路径前缀或结尾斜杠;
  • HTTP 跳转到 HTTPS 时丢失了 POST 请求体;
  • 回调接口被登录校验、CSRF、WAF 或 IP 白名单拦截;
  • 经过反向代理后,应用再次把 HTTPS 请求重定向到 HTTPS,造成循环;
  • 验签时读取的不是原始请求体;
  • 应用返回了 200,但响应内容不是支付平台认可的成功确认;
  • 回调处理发生异常,却在 finally 中统一返回成功;
  • 测试环境和生产环境使用了不同的商户号、密钥或数据库;
  • 页面只在加载时读取一次订单状态,回调成功后没有轮询或刷新。

最有效的排查方式,是沿着”支付平台通知记录 → 公网入口日志 → 网关日志 → 应用日志 → 验签结果 → 数据库事务”逐层确认。找到回调在哪一层中断后,就能判断问题来自 HTTPS、网关配置,还是订单处理逻辑。

备注:内容仅供参考。