PAYATHON 2026

扫码支付成功后 notify_url 无法收到回调通知

支付老李

结论

浏览器能打开 notify_url,不等于支付平台能正常向它发送异步通知。

排查时主要确认四件事:回调接口是否支持平台使用的请求方法和数据格式,公网服务器能否直接访问,接口能否在规定时间内返回指定的成功响应,以及订单号 9107fe7c018242078409c7618462910c 对应的交易是否确实支付成功。

目前没有明确具体的支付平台,因此无法直接判断通知参数、签名算法和成功响应格式。这些内容应以实际接入平台的接口文档为准。

常见原因

请求方法或数据格式不匹配

浏览器访问通常发出 GET 请求,而支付通知大多使用 POST。请求体可能采用以下格式:

  • application/x-www-form-urlencoded
  • application/json
  • text/xml
  • application/xml

如果接口只支持 GET、只解析 JSON,或者请求在到达业务代码前就被框架的参数校验拦截,就会出现”浏览器测试正常,实际通知收不到”的情况。

接口返回内容不符合要求

支付平台通常要求回调接口返回固定内容,例如:

success

或者:

<xml>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <return_msg><![CDATA[OK]]></return_msg>
</xml>

有些平台则要求返回特定的 JSON。即使业务已经处理完成,只要 HTTP 状态码或响应正文不符合协议,平台仍可能把这次通知判定为失败并继续重试。

这些只是常见形式,不能混用。接口必须按照实际支付渠道的协议返回结果。

HTTPS、网络或安全策略拦截

需要逐项检查:

  • 域名能否正常解析;
  • HTTPS 证书是否有效,证书链是否完整;
  • 请求是否经过 HTTP 301、302 或 307 跳转;
  • 防火墙、安全组、WAF 或 CDN 是否拦截了平台请求;
  • 是否设置了 IP 白名单;
  • 是否限制了 User-Agent、请求头或请求体大小;
  • 是否启用了登录认证、Token 校验或验证码;
  • 请求是否被 CSRF 防护拦截。

回调地址必须允许支付平台直接访问,不能依赖 Cookie、登录状态或前端页面跳转。

回调处理超时或发生异常

如果通知接口同步执行大量业务逻辑,例如查询数据库、调用第三方服务或发送短信,支付平台可能在处理完成前就等待超时。

接口内部发生异常时,也可能返回 500、框架错误页面或网关错误。浏览器手工访问使用的请求方式和数据不同,因此未必能复现这些问题。

支付请求中的 notify_url 未实际生效

先确认创建订单时提交给支付平台的参数中确实包含:

https://elderly-api.house-keeper.cn/api/v1/pay/notify1.json

同时排查:

  • 参数名称或层级是否填写错误;
  • 参数是否未参与签名,或者签名是否失败;
  • SDK 是否使用了另一个默认回调地址;
  • 商户后台配置是否覆盖了请求参数;
  • 测试环境和生产环境的配置是否混用;
  • 支付订单是否在修改配置前就已创建。

create_time 是 2021-12-31 13:56:33。如果排查的是这笔历史交易,还要确认支付平台是否仍保留当时的通知日志,以及是否支持重新发送该订单的通知。

建议的排查步骤

1. 查询支付平台侧的订单状态

使用支付平台的订单查询接口,按以下商户订单号查询:

9107fe7c018242078409c7618462910c

需要确认:

  • 平台是否已受理该订单;
  • 订单是否真正支付成功;
  • 商户号、应用 ID 和支付环境是否正确;
  • 平台记录的 notify_url 是什么;
  • 是否有通知发送记录;
  • 最近一次通知的 HTTP 状态码和响应内容是什么。

如果查询结果不是支付成功,平台自然不会发送成功通知。

2. 查看入口层访问日志

不要只看应用日志,还要检查 Nginx、网关、负载均衡、CDN 或 WAF 的日志。

以 Nginx 为例:

grep 'notify1.json' /var/log/nginx/access.log
grep 'notify1.json' /var/log/nginx/error.log

可以根据日志结果判断问题大致发生在哪一层:

  • 没有任何请求记录:检查平台配置、DNS、证书、防火墙和订单状态;
  • 出现 301 或 302:取消跳转,让回调地址直接处理请求;
  • 出现 401 或 403:检查认证、CSRF、WAF 和访问控制;
  • 出现 404:检查路由、上下文路径和反向代理配置;
  • 出现 405:接口不支持平台使用的 HTTP 方法;
  • 出现 415:接口不支持平台发送的 Content-Type;
  • 出现 500 或 502:查看应用异常和上游服务状态;
  • 返回 200,但平台仍判定失败:核对响应正文和接口处理耗时。

3. 记录原始请求

排查期间应记录:

  • 请求时间;
  • HTTP 方法;
  • Content-Type;
  • 请求头;
  • 原始请求体;
  • 请求来源 IP;
  • HTTP 状态码;
  • 返回内容;
  • 接口处理耗时。

写日志时要隐藏密钥、银行卡号、证件号等敏感信息。解析参数前不要丢弃原始请求体,否则验签失败时很难判断问题出在参数解析还是报文传输。

4. 按支付协议验签

收到通知后,必须使用支付平台规定的算法验证签名。不能只凭 out_trade_no 或”支付成功”字段更新订单状态。

验签时通常要注意:

  • 平台要求使用原始请求数据,还是解析后的参数集合;
  • 参数排序规则;
  • 是否需要排除 sign 字段;
  • 字符编码是否为 UTF-8;
  • URL 解码次数是否正确;
  • 公钥、证书或密钥是否属于当前商户和当前环境;
  • 处理过程中是否错误修改了空格、换行或特殊字符。

验签失败时,可以记录失败原因和平台交易号,但不能输出私钥或完整密钥。

5. 快速返回成功响应

推荐按以下顺序处理:

  1. 接收原始报文;
  2. 验证签名;
  3. 校验商户号、订单号、金额和币种;
  4. 通过数据库事务幂等更新订单;
  5. 返回支付平台规定的成功响应;
  6. 将耗时的后续业务放入队列异步处理。

在返回成功响应前,不要发送短信、生成报表或调用不稳定的第三方接口。

Spring Boot 接收示例

如果支付平台发送 JSON,可以先用原始字符串接收:

@RestController
@RequestMapping("/api/v1/pay")
public class PayNotifyController {

    private static final Logger log =
            LoggerFactory.getLogger(PayNotifyController.class);

    @PostMapping(
        value = "/notify1.json",
        consumes = MediaType.APPLICATION_JSON_VALUE
    )
    public ResponseEntity<String> notify(
            @RequestBody String rawBody,
            HttpServletRequest request) {

        log.info("pay notify received, contentType={}, body={}",
                request.getContentType(), maskSensitiveData(rawBody));

        // 1. 按支付平台文档验证签名
        // boolean valid = signatureService.verify(rawBody, request);
        // if (!valid) {
        //     return ResponseEntity.badRequest().body("fail");
        // }

        // 2. 解析通知并校验订单号、金额、币种、商户号和交易状态
        // PayNotify notify = parse(rawBody);

        // 3. 幂等更新订单
        // orderService.markPaidIfNecessary(notify);

        // 4. 返回值必须替换成支付平台规定的成功响应
        return ResponseEntity.ok("success");
    }

    private String maskSensitiveData(String value) {
        return value;
    }
}

如果平台使用表单格式,应采用对应的接收方式:

@PostMapping(
    value = "/notify1.json",
    consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public ResponseEntity<String> notify(
        @RequestParam Map<String, String> params) {

    // 验签、订单校验和幂等处理
    return ResponseEntity.ok("success");
}

如果还不确定实际的 Content-Type,应先查看平台文档或入口日志。不要在没有确认协议的情况下,盲目声明多个格式后直接上线。

Nginx 配置检查

确认回调路径已正确转发,并且没有被重写到登录页:

location = /api/v1/pay/notify1.json {
    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 $scheme;

    proxy_connect_timeout 5s;
    proxy_read_timeout 15s;
}

还要仔细检查 proxy_pass 的路径拼接规则。配置错误可能导致后端实际收到的 URI 与回调地址不同。

本地模拟请求

确认平台的报文格式后,可以用 curl 测试 POST 请求,不要只在浏览器中打开回调地址。

JSON 示例:

curl -i -X POST \
  'https://elderly-api.house-keeper.cn/api/v1/pay/notify1.json' \
  -H 'Content-Type: application/json' \
  --data '{"out_trade_no":"9107fe7c018242078409c7618462910c","trade_status":"SUCCESS"}'

表单示例:

curl -i -X POST \
  'https://elderly-api.house-keeper.cn/api/v1/pay/notify1.json' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'out_trade_no=9107fe7c018242078409c7618462910c' \
  --data-urlencode 'trade_status=SUCCESS'

这些命令只能测试网络、路由和参数接收能力,不能证明真实支付通知的签名及业务处理没有问题。模拟数据也不能直接用于把生产订单更新为已支付。

必须做好幂等处理

支付平台可能因为等待超时或没有收到正确响应而重复发送通知。回调接口不能假设每条通知只会到达一次。

可以使用条件更新保证幂等:

UPDATE payment_order
SET pay_status = 'PAID',
    paid_at = CURRENT_TIMESTAMP
WHERE out_trade_no = '9107fe7c018242078409c7618462910c'
  AND pay_status = 'UNPAID';

如果受影响行数为 0,需要查询订单当前状态。订单已经是 PAID 时,可以按照平台协议再次返回成功,避免平台继续进行无意义的重试。

注意事项

  • 回调来自公网不代表可以跳过验签。
  • 除订单号外,还要核对金额、币种、商户号和应用 ID。
  • 不要把回调作为确认支付结果的唯一方式。订单长时间没有更新时,可以通过订单查询接口主动补偿。
  • 回调地址不能使用临时域名、过期证书或需要登录的路径。
  • 不要将支付回调永久重定向到其他地址。
  • 不要手工伪造回调后直接更新生产订单。
  • 修改回调配置通常只影响新创建的订单。历史订单是否采用新配置,取决于支付平台的规则。

最快的排查方法是先查看支付平台的通知记录,再对照 Nginx 访问日志。如果平台显示通知已经发出,而入口层完全没有记录,应优先检查网络和安全策略。如果入口层有记录,则结合 HTTP 状态码、响应正文和应用日志继续定位。

备注:内容仅供参考。