PAYATHON 2026

扫码支付通知与WCF每10秒查询订单如何处理?

支付老李

结论

不要把“WCF 每 10 秒查询一次订单”当作判断支付成功的唯一依据。更稳妥的处理方式是:

  1. 主要通过支付平台的异步通知(Webhook)更新订单;
  2. 对通知进行验签、金额校验和幂等处理;
  3. 主动查询只用于支付页面短轮询、通知失败后的补偿以及定时对账;
  4. 订单状态必须由服务端确认,不能根据前端的“支付完成”提示进行修改;
  5. 对长时间处于“待支付”状态的订单安排补偿查询。

用户已经付款,但系统没有显示成功,原因通常不在于查询间隔,而在于通知丢失、查询任务中断、状态更新失败或并发处理有误。

为什么固定轮询容易出问题

WCF 服务中的定时器或后台线程不适合承担可靠的调度任务。应用池回收、服务重启、线程异常和请求超时,都可能导致轮询中断。

每 10 秒扫描一次订单还会带来一些问题:

  • 待支付订单增多后,请求量会迅速上升;
  • 支付平台可能限制请求频率;
  • 查询超时后缺少重试或补偿;
  • 多个服务实例可能重复查询和更新同一订单;
  • 数据库事务失败后,程序可能误以为状态已经更新;
  • 支付时间接近订单过期时间时,可能出现状态竞争;
  • 查询逻辑只处理一种成功状态,没有覆盖平台实际返回的其他状态值。

因此,单纯缩短轮询间隔解决不了这些问题。

推荐处理流程

1. 创建支付订单

系统先创建本地订单,将初始状态设为 Pending,再调用支付平台的下单接口生成二维码。

至少应保存以下信息:

  • 本地订单号;
  • 支付平台订单号;
  • 应付金额和币种;
  • 当前支付状态;
  • 创建时间和过期时间;
  • 最后查询时间;
  • 查询次数;
  • 支付成功时间;
  • 支付平台的原始响应或其中的必要字段。

2. 接收支付平台异步通知

支付平台确认付款后,会向商户配置的回调地址发送通知。回调接口应依次完成以下处理:

  1. 读取原始请求内容;
  2. 验证签名;
  3. 校验商户号、订单号、金额和币种;
  4. 确认平台返回的支付状态确实为成功;
  5. 在数据库事务中更新订单;
  6. 记录支付流水;
  7. 返回平台规定的成功响应。

请求头、验签算法和成功响应格式由支付平台决定,应以接入平台的接口文档为准。

下面是一个简化的 C# 示例:

[HttpPost]
[Route("api/payment/notify")]
public async Task<IHttpActionResult> Notify()
{
    string body = await Request.Content.ReadAsStringAsync();
    string signature = Request.Headers.Contains("X-Signature")
        ? Request.Headers.GetValues("X-Signature").FirstOrDefault()
        : null;

    if (!paymentGateway.VerifySignature(body, signature))
    {
        return BadRequest("invalid signature");
    }

    PaymentNotification notification =
        paymentGateway.ParseNotification(body);

    if (notification.Status != "SUCCESS")
    {
        return Ok();
    }

    Order order = await orderRepository.FindByOrderNoAsync(
        notification.OrderNo);

    if (order == null)
    {
        return BadRequest("order not found");
    }

    if (order.Amount != notification.Amount ||
        order.Currency != notification.Currency)
    {
        return BadRequest("amount mismatch");
    }

    // 已处理过的通知直接返回成功,避免平台重复通知造成重复入账。
    if (order.Status == OrderStatus.Paid)
    {
        return Ok();
    }

    await orderService.MarkAsPaidAsync(
        order.OrderNo,
        notification.TransactionId,
        notification.PaidAt);

    return Ok();
}

其中的 X-Signature、SUCCESS 和响应内容仅用于示意,不能直接视为某个支付平台的固定协议。

3. 保证状态更新的幂等性

支付平台可能重复发送通知,主动查询也可能与异步通知同时返回成功。缺少幂等控制时,系统可能重复发货、充值或记账。

数据库更新应带上状态条件:

UPDATE payment_order
SET
    status = 'PAID',
    transaction_id = @TransactionId,
    paid_at = @PaidAt,
    updated_at = CURRENT_TIMESTAMP
WHERE order_no = @OrderNo
  AND status = 'PENDING';

更新后再检查受影响的行数:

int affectedRows = await orderRepository.MarkAsPaidAsync(
    orderNo,
    transactionId,
    paidAt);

if (affectedRows == 1)
{
    await businessService.ProcessPaidOrderAsync(orderNo);
}

只有订单状态确实从 PENDING 变为 PAID,才能触发后续业务。支付流水的 transaction_id 最好也设置唯一约束。

如果支付成功后还需要发货、充值或通知其他系统,可以使用本地消息表或 Outbox 模式,防止出现“订单已经更新,后续业务消息却没有发出”的情况。

主动查询应该如何使用

主动查询仍有必要,但它应当用于补偿,不能作为唯一机制。

以下订单可以纳入主动查询:

  • 创建一段时间后仍为 Pending 的订单;
  • 收到异常通知但无法确认状态的订单;
  • 用户点击“我已完成支付”后的订单;
  • 即将过期或刚刚过期的订单;
  • 对账时发现平台状态与本地状态不一致的订单。

不要对所有订单永久执行固定的 10 秒轮询。可以逐步降低查询频率,例如:

  • 支付页面打开期间,每 2~5 秒查询一次本地订单状态;
  • 服务端创建订单后的短时间内,以较高频率查询支付平台;
  • 随后逐步降低查询频率;
  • 超过合理期限后停止实时查询,改为低频补偿和对账。

这些时间只是设计示例。实际查询频率需要根据支付平台的限流要求、订单量和业务时效来确定。

补偿任务的主要逻辑可以写成:

public async Task ReconcilePendingOrderAsync(PaymentOrder order)
{
    GatewayOrderResult result =
        await paymentGateway.QueryOrderAsync(order.OrderNo);

    if (result.Status != GatewayPaymentStatus.Paid)
    {
        await orderRepository.RecordQueryResultAsync(
            order.OrderNo,
            result.Status,
            DateTime.UtcNow);

        return;
    }

    if (result.Amount != order.Amount ||
        result.Currency != order.Currency)
    {
        await alertService.ReportPaymentMismatchAsync(
            order.OrderNo,
            result.TransactionId);

        return;
    }

    await orderService.MarkAsPaidAsync(
        order.OrderNo,
        result.TransactionId,
        result.PaidAt);
}

补偿任务最好运行在独立的任务调度系统或常驻 Worker 中,并支持:

  • 失败重试;
  • 超时控制;
  • 分布式锁或任务抢占;
  • 查询次数限制;
  • 限流;
  • 异常告警;
  • 可追踪的执行日志。

如果业务接口仍由 WCF 承载,不建议把重要的定时任务只放在 WCF 服务进程中。查询任务可以拆分到 Windows Service、Worker Service、Quartz.NET、Hangfire,或者现有的可靠任务调度平台中。具体采用哪种方式,取决于部署环境和已有基础设施。

前端应该展示什么状态

完成扫码不代表支付平台已经确认到账。前端应查询本地订单状态,不能直接调用支付平台,也不能自行把订单改为成功。

页面可以展示三种结果:

  • Pending:正在确认支付结果;
  • Paid:服务端已确认支付成功;
  • Closed 或 Failed:订单已关闭或支付失败。

如果用户确认已经付款,但本地状态仍为 Pending,可以提供“刷新支付结果”按钮。按钮触发服务端向支付平台查询一次,同时要限制操作频率,避免用户连续点击给接口造成压力。

不要过早提示“支付失败”。支付通知和主动查询都可能出现短暂延迟,此时显示“支付结果确认中,请稍候”更合适。

排查现有问题

排查“用户已付款但系统未成功”的订单时,需要同时核对四类记录:

  1. 支付平台后台是否有对应的成功交易;
  2. 平台是否发送过异步通知,系统是否成功响应;
  3. WCF 查询任务是否实际运行,请求和响应记录是否完整;
  4. 数据库更新和后续业务处理是否成功。

日志至少应包含:

order_no
transaction_id
payment_status
notification_id
request_time
response_time
retry_count
amount
currency
trace_id
error_code

日志中不要记录私钥、完整签名密钥、银行卡信息或其他敏感数据。

如果支付平台显示交易成功,但系统中完全没有通知记录,应重点检查:

  • 回调地址能否通过公网访问;
  • HTTPS 证书是否有效;
  • 防火墙、网关或反向代理是否拦截了请求;
  • 回调接口是否超时;
  • 返回内容是否符合平台要求;
  • 验签过程中是否错误处理了编码、字段排序或原始请求体。

如果查询接口已经返回成功,但数据库中的订单仍为待支付,应检查事务回滚、连接异常、状态条件、订单号映射和并发更新。

还要增加每日对账

即使异步通知和补偿查询已经完善,也应定期下载或查询支付平台账单,并与本地支付流水核对。

对账时应识别以下情况:

  • 平台显示成功,本地未成功;
  • 本地显示成功,平台不存在对应交易;
  • 金额或币种不一致;
  • 同一支付流水关联了多个订单;
  • 平台已经退款,本地却没有更新。

发现“平台成功、本地未成功”时,不能直接无条件修改订单状态。应先校验订单、金额、商户号和交易流水,再通过可审计的补单流程处理。

整个系统应由三层机制组成:异步通知负责实时更新,主动查询负责补偿,定期对账负责兜底。这样即使某次通知丢失、服务重启或查询超时,已付款订单也不会长期停留在待支付状态。

备注:内容仅供参考。