扫码支付通知与WCF每10秒查询订单如何处理?
结论
不要把“WCF 每 10 秒查询一次订单”当作判断支付成功的唯一依据。更稳妥的处理方式是:
- 主要通过支付平台的异步通知(Webhook)更新订单;
- 对通知进行验签、金额校验和幂等处理;
- 主动查询只用于支付页面短轮询、通知失败后的补偿以及定时对账;
- 订单状态必须由服务端确认,不能根据前端的“支付完成”提示进行修改;
- 对长时间处于“待支付”状态的订单安排补偿查询。
用户已经付款,但系统没有显示成功,原因通常不在于查询间隔,而在于通知丢失、查询任务中断、状态更新失败或并发处理有误。
为什么固定轮询容易出问题
WCF 服务中的定时器或后台线程不适合承担可靠的调度任务。应用池回收、服务重启、线程异常和请求超时,都可能导致轮询中断。
每 10 秒扫描一次订单还会带来一些问题:
- 待支付订单增多后,请求量会迅速上升;
- 支付平台可能限制请求频率;
- 查询超时后缺少重试或补偿;
- 多个服务实例可能重复查询和更新同一订单;
- 数据库事务失败后,程序可能误以为状态已经更新;
- 支付时间接近订单过期时间时,可能出现状态竞争;
- 查询逻辑只处理一种成功状态,没有覆盖平台实际返回的其他状态值。
因此,单纯缩短轮询间隔解决不了这些问题。
推荐处理流程
1. 创建支付订单
系统先创建本地订单,将初始状态设为 Pending,再调用支付平台的下单接口生成二维码。
至少应保存以下信息:
- 本地订单号;
- 支付平台订单号;
- 应付金额和币种;
- 当前支付状态;
- 创建时间和过期时间;
- 最后查询时间;
- 查询次数;
- 支付成功时间;
- 支付平台的原始响应或其中的必要字段。
2. 接收支付平台异步通知
支付平台确认付款后,会向商户配置的回调地址发送通知。回调接口应依次完成以下处理:
- 读取原始请求内容;
- 验证签名;
- 校验商户号、订单号、金额和币种;
- 确认平台返回的支付状态确实为成功;
- 在数据库事务中更新订单;
- 记录支付流水;
- 返回平台规定的成功响应。
请求头、验签算法和成功响应格式由支付平台决定,应以接入平台的接口文档为准。
下面是一个简化的 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,可以提供“刷新支付结果”按钮。按钮触发服务端向支付平台查询一次,同时要限制操作频率,避免用户连续点击给接口造成压力。
不要过早提示“支付失败”。支付通知和主动查询都可能出现短暂延迟,此时显示“支付结果确认中,请稍候”更合适。
排查现有问题
排查“用户已付款但系统未成功”的订单时,需要同时核对四类记录:
- 支付平台后台是否有对应的成功交易;
- 平台是否发送过异步通知,系统是否成功响应;
- WCF 查询任务是否实际运行,请求和响应记录是否完整;
- 数据库更新和后续业务处理是否成功。
日志至少应包含:
order_no
transaction_id
payment_status
notification_id
request_time
response_time
retry_count
amount
currency
trace_id
error_code
日志中不要记录私钥、完整签名密钥、银行卡信息或其他敏感数据。
如果支付平台显示交易成功,但系统中完全没有通知记录,应重点检查:
- 回调地址能否通过公网访问;
- HTTPS 证书是否有效;
- 防火墙、网关或反向代理是否拦截了请求;
- 回调接口是否超时;
- 返回内容是否符合平台要求;
- 验签过程中是否错误处理了编码、字段排序或原始请求体。
如果查询接口已经返回成功,但数据库中的订单仍为待支付,应检查事务回滚、连接异常、状态条件、订单号映射和并发更新。
还要增加每日对账
即使异步通知和补偿查询已经完善,也应定期下载或查询支付平台账单,并与本地支付流水核对。
对账时应识别以下情况:
- 平台显示成功,本地未成功;
- 本地显示成功,平台不存在对应交易;
- 金额或币种不一致;
- 同一支付流水关联了多个订单;
- 平台已经退款,本地却没有更新。
发现“平台成功、本地未成功”时,不能直接无条件修改订单状态。应先校验订单、金额、商户号和交易流水,再通过可审计的补单流程处理。
整个系统应由三层机制组成:异步通知负责实时更新,主动查询负责补偿,定期对账负责兜底。这样即使某次通知丢失、服务重启或查询超时,已付款订单也不会长期停留在待支付状态。
备注:内容仅供参考。