PAYATHON 2026

C#创建扫码支付订单后无法接收异步通知的原因

支付阿杰

结论

同步支付结果正常,却一直收不到异步通知,通常有两个原因:当前使用的扫码支付接口不支持异步通知,或者实际发送给支付平台的请求中没有 notify_url。

先要分清两种扫码支付模式:

  • 商户扫描用户付款码:支付结果通常由接口同步返回,部分支付渠道不会再发送异步通知。如果返回 USERPAYING 或处理中状态,需要主动查询订单。
  • 用户扫描商户二维码:通常先创建二维码订单,用户支付成功后,支付平台再向 notify_url 发送异步通知。

调用 SetNotifyUrl 只是给 SDK 对象设置了一个地址,不代表当前接口一定会使用这个参数,也不会让原本不支持通知的接口自动发送回调。

首先确认支付模式

检查创建订单时实际调用的 API,不要仅根据 SDK 中是否有 SetNotifyUrl 来判断。

如果调用的是付款码支付、条码支付或类似 micropay 的接口,需要查阅对应支付渠道的接口说明,确认该接口是否支持支付结果通知。如果接口只返回同步结果,通常按以下方式处理:

  1. 同步结果明确成功时,更新订单状态。
  2. 同步结果明确失败时,关闭订单或将其标记为失败。
  3. 同步结果为处理中时,定时调用订单查询接口。
  4. 长时间无法得到确定结果时,按照渠道规则撤销或关闭订单。

如果调用的是 Native、预下单或二维码下单接口,支付成功后通常会有异步通知。这时需要继续检查请求参数和回调环境。

检查实际发送的请求参数

除了查看 SetNotifyUrl 的调用代码,还要记录最终发往支付平台的请求内容,确认请求中确实包含正确的通知地址,例如:

notify_url=https://pay.example.com/api/payment/notify

重点检查以下几项:

  • SetNotifyUrl 是否在发送请求前调用。
  • 后续代码是否重新创建了请求对象。
  • SDK 序列化时是否忽略了该属性。
  • 当前接口使用的参数是否确实是 notify_url。
  • 配置文件中的地址是否覆盖了代码设置的地址。
  • 测试环境和生产环境是否使用了不同配置。

日志可以记录通知地址和订单号,但不要输出商户密钥、私钥、完整签名材料或用户敏感信息。

request.SetNotifyUrl(notifyUrl);

// 在执行下单请求前记录非敏感参数
logger.LogInformation(
    "Creating payment order. OrderNo={OrderNo}, NotifyUrl={NotifyUrl}",
    orderNo,
    notifyUrl
);

var result = paymentClient.CreateOrder(request);

具体方法名以实际使用的 SDK 为准。有些 SDK 的 SetNotifyUrl 只是本地封装方法,参数最终是否会上传,仍由所调用的支付 API 决定。

确认回调地址能被公网访问

异步通知由支付平台的服务器主动请求商户服务器,与浏览器的同步跳转无关。同步消息正常,不代表异步回调地址一定可用。

notify_url 通常需要满足以下条件:

  • 可以从公网访问,不能使用 localhost、127.0.0.1 或局域网地址。
  • 域名能够正确解析。
  • HTTPS 证书有效,证书链完整。
  • 不依赖登录、Cookie 或人工认证。
  • 不会被防火墙、WAF、反向代理或 IP 白名单拦截。
  • 路由允许支付平台要求的 HTTP 方法,通常是 POST。
  • 不产生不必要的 301 或 302 跳转。
  • 回调接口能在规定时间内返回成功响应。

可以从另一台公网服务器执行测试:

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

这个测试只能验证网络和路由基本可达,不能证明支付平台一定会发送通知。

正确接收原始请求内容

不同支付渠道可能发送 JSON、XML、表单或加密报文。如果回调接口只读取查询字符串,或者绑定了不匹配的 DTO,看起来就会像是“没有收到数据”。

ASP.NET Core 可以先读取原始请求体,再根据支付渠道的协议验签和解析:

using System.Text;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/payment")]
public class PaymentController : ControllerBase
{
    private readonly ILogger<PaymentController> _logger;

    public PaymentController(ILogger<PaymentController> logger)
    {
        _logger = logger;
    }

    [HttpPost("notify")]
    public async Task<IActionResult> Notify()
    {
        using var reader = new StreamReader(
            Request.Body,
            Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false,
            leaveOpen: true
        );

        var body = await reader.ReadToEndAsync();

        _logger.LogInformation(
            "Payment notification received. ContentType={ContentType}, Length={Length}",
            Request.ContentType,
            body.Length
        );

        // 1. 按支付渠道要求验证签名
        // 2. 解密或解析通知报文
        // 3. 校验商户号、订单号、金额和币种
        // 4. 幂等更新订单状态
        // 5. 返回渠道规定的成功响应

        return Content("success", "text/plain", Encoding.UTF8);
    }
}

这里的 "success" 仅用于示意。实际响应内容、大小写、状态码和数据格式必须符合支付平台的协议。有些平台要求返回 XML,有些要求返回 JSON。格式不正确时,平台可能会反复重试。

检查服务器日志的位置

回调请求可能已经到达入口层,却没有进入业务代码。可以按以下顺序检查:

  1. CDN、负载均衡或网关访问日志。
  2. Nginx、IIS、Apache 等 Web 服务器日志。
  3. WAF 和防火墙拦截日志。
  4. ASP.NET 路由及异常日志。
  5. 支付平台后台的通知记录或失败原因。

常见状态码通常表示:

  • 404:通知路径或路由不正确。
  • 405:接口不允许支付平台使用的 HTTP 方法。
  • 401、403:请求被认证、鉴权或安全策略拦截。
  • 415:接口限制了不匹配的 Content-Type。
  • 500:回调处理代码发生异常。
  • 502、504:反向代理无法连接应用,或者请求处理超时。

如果入口日志中完全没有请求,应先检查支付接口是否支持通知、请求是否真正上传了 notify_url、地址能否从公网访问,以及支付平台侧的通知记录。

异步通知处理注意事项

回调接口不能只根据订单号修改支付状态,还需要处理以下事项:

  • 验证支付平台的签名。
  • 校验商户号、应用编号等身份信息。
  • 校验订单金额、币种和商户订单号。
  • 只接受明确的支付成功状态。
  • 使用数据库事务或唯一约束保证幂等。
  • 对已经处理过的通知仍返回成功,避免平台持续重试。
  • 完成必要的订单落库后,再返回平台要求的成功响应。
  • 不要在回调中执行耗时较长的发货、短信或第三方调用。这些操作可以在落库后交给消息队列处理。

即使支付渠道支持异步通知,也不能将通知作为唯一的订单确认方式。网络超时、配置错误和服务故障都可能造成通知延迟或丢失。生产系统还应具备主动查询和定时对账能力。

建议的排查顺序

先确定调用的是“商户扫用户付款码”还是“用户扫商户二维码”接口,再核对该 API 是否支持异步通知。随后检查最终请求报文中是否确实包含 notify_url,并查看公网入口日志和回调路由。

如果当前接口不支持异步通知,修改 NotifyUrl 也无法解决问题。此时应根据同步结果处理订单,并主动查询仍处于处理中的订单。如果接口明确支持通知,则应重点检查请求参数是否上传、地址能否从公网访问、路由方法是否匹配,以及请求是否被网关拦截。

备注:内容仅供参考。