C#创建扫码支付订单后无法接收异步通知的原因
结论
同步支付结果正常,却一直收不到异步通知,通常有两个原因:当前使用的扫码支付接口不支持异步通知,或者实际发送给支付平台的请求中没有 notify_url。
先要分清两种扫码支付模式:
- 商户扫描用户付款码:支付结果通常由接口同步返回,部分支付渠道不会再发送异步通知。如果返回
USERPAYING或处理中状态,需要主动查询订单。 - 用户扫描商户二维码:通常先创建二维码订单,用户支付成功后,支付平台再向
notify_url发送异步通知。
调用 SetNotifyUrl 只是给 SDK 对象设置了一个地址,不代表当前接口一定会使用这个参数,也不会让原本不支持通知的接口自动发送回调。
首先确认支付模式
检查创建订单时实际调用的 API,不要仅根据 SDK 中是否有 SetNotifyUrl 来判断。
如果调用的是付款码支付、条码支付或类似 micropay 的接口,需要查阅对应支付渠道的接口说明,确认该接口是否支持支付结果通知。如果接口只返回同步结果,通常按以下方式处理:
- 同步结果明确成功时,更新订单状态。
- 同步结果明确失败时,关闭订单或将其标记为失败。
- 同步结果为处理中时,定时调用订单查询接口。
- 长时间无法得到确定结果时,按照渠道规则撤销或关闭订单。
如果调用的是 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。格式不正确时,平台可能会反复重试。
检查服务器日志的位置
回调请求可能已经到达入口层,却没有进入业务代码。可以按以下顺序检查:
- CDN、负载均衡或网关访问日志。
- Nginx、IIS、Apache 等 Web 服务器日志。
- WAF 和防火墙拦截日志。
- ASP.NET 路由及异常日志。
- 支付平台后台的通知记录或失败原因。
常见状态码通常表示:
404:通知路径或路由不正确。405:接口不允许支付平台使用的 HTTP 方法。401、403:请求被认证、鉴权或安全策略拦截。415:接口限制了不匹配的Content-Type。500:回调处理代码发生异常。502、504:反向代理无法连接应用,或者请求处理超时。
如果入口日志中完全没有请求,应先检查支付接口是否支持通知、请求是否真正上传了 notify_url、地址能否从公网访问,以及支付平台侧的通知记录。
异步通知处理注意事项
回调接口不能只根据订单号修改支付状态,还需要处理以下事项:
- 验证支付平台的签名。
- 校验商户号、应用编号等身份信息。
- 校验订单金额、币种和商户订单号。
- 只接受明确的支付成功状态。
- 使用数据库事务或唯一约束保证幂等。
- 对已经处理过的通知仍返回成功,避免平台持续重试。
- 完成必要的订单落库后,再返回平台要求的成功响应。
- 不要在回调中执行耗时较长的发货、短信或第三方调用。这些操作可以在落库后交给消息队列处理。
即使支付渠道支持异步通知,也不能将通知作为唯一的订单确认方式。网络超时、配置错误和服务故障都可能造成通知延迟或丢失。生产系统还应具备主动查询和定时对账能力。
建议的排查顺序
先确定调用的是“商户扫用户付款码”还是“用户扫商户二维码”接口,再核对该 API 是否支持异步通知。随后检查最终请求报文中是否确实包含 notify_url,并查看公网入口日志和回调路由。
如果当前接口不支持异步通知,修改 NotifyUrl 也无法解决问题。此时应根据同步结果处理订单,并主动查询仍处于处理中的订单。如果接口明确支持通知,则应重点检查请求参数是否上传、地址能否从公网访问、路由方法是否匹配,以及请求是否被网关拦截。
备注:内容仅供参考。