当面付扫码支付为何触发 WAIT_BUYER_PAY 异步回调
结论
对于已经通过验签,并确认属于本商户订单的 WAIT_BUYER_PAY 通知,可以直接返回纯文本 success,但不能执行付款成功后的业务。这个响应只代表当前通知已经收到,不会影响订单之后变为 TRADE_SUCCESS 时触发新的异步通知。
异步通知也不能作为确认订单状态的唯一方式。即使 WAIT_BUYER_PAY 已正确处理,后续的成功通知仍可能因网络、配置或程序异常而延迟或遗漏。系统还需要通过主动查询和定时补偿来确认最终状态。
如果相关功能默认关闭,却仍收到 WAIT_BUYER_PAY,应检查商户配置、实际调用身份、通知来源和订单归属。仅凭通知中的状态,不能直接认定支付宝出现异常。
返回 success 为什么不影响后续成功通知
支付宝异步通知确认的是当前这一次通知,并不表示整个订单以后不再发送通知。
订单状态可能发生以下变化:
WAIT_BUYER_PAY -> TRADE_SUCCESS
服务端对 WAIT_BUYER_PAY 返回 success,只表示:
这次 WAIT_BUYER_PAY 通知已收到,无须重试
订单之后支付成功时,TRADE_SUCCESS 属于新的状态变化。正常情况下,支付宝仍会发送相应的异步通知。
如果服务端收到 WAIT_BUYER_PAY 后直接抛出异常,通常就无法返回支付宝要求的 success。支付宝可能认为通知处理失败并继续重试,进而造成:
- 同一状态被重复推送;
- 日志和异常告警持续增加;
- 无效重试占用服务端资源;
- 大量异常日志干扰真正的支付成功通知。
因此,合法但暂时不需要处理的业务状态,不应被当成系统异常。
推荐的处理方式
异步通知入口应先完成安全校验,再根据状态分别处理。
1. 验证通知签名
不能在验签前返回 success。验签失败的请求不是可信的支付结果,也不能用来更新订单状态。
使用 EasySDK 时,可以先把通知参数整理为 Map<String, String>,再调用通知验签接口。具体方法签名以项目实际使用的 EasySDK 版本为准。
2. 确认通知属于当前应用和订单
验签通过后,至少需要检查:
app_id是否为当前应用;out_trade_no是否存在;- 订单是否属于当前商户;
- 通知金额是否与本地订单金额一致;
seller_id等商户身份信息是否符合预期;- 使用服务商模式时,是否与实际被授权商户匹配。
金额校验应使用精确的十进制类型,例如 Java 的 BigDecimal,不要用 double 直接比较。
3. 根据 trade_status 分别处理
建议至少区分以下状态:
WAIT_BUYER_PAY:记录或忽略,不发货、不记账,返回success;TRADE_SUCCESS:以幂等方式完成支付成功处理;TRADE_FINISHED:按照业务规则更新最终状态;TRADE_CLOSED:更新订单关闭状态;- 未识别状态:记录完整上下文并告警,不能直接执行付款成功逻辑。
4. 支付成功处理必须幂等
支付宝可能重复发送同一通知,因此,收到回调后不能无条件重复执行业务。
可以通过订单状态的条件更新实现幂等:
UPDATE payment_order
SET payment_status = 'PAID',
alipay_trade_no = ?,
paid_at = ?
WHERE out_trade_no = ?
AND payment_status = 'PENDING';
只有更新行数为 1 时,才继续执行首次支付成功后的业务。更新行数为 0 通常说明订单已经处理过,此时直接返回 success 即可。
Java 处理示例
下面的代码展示了基本处理结构。参数获取方式和 EasySDK 初始化方式,需要根据实际使用的 Web 框架和 SDK 版本调整。
@PostMapping(
value = "/alipay/notify",
produces = "text/plain;charset=UTF-8"
)
public String handleAlipayNotify(HttpServletRequest request) {
Map<String, String> parameters = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
parameters.put(key, String.join(",", values));
}
});
boolean verified;
try {
verified = Factory.Payment.Common().verifyNotify(parameters);
} catch (Exception e) {
log.error("Alipay notify signature verification failed", e);
return "failure";
}
if (!verified) {
log.warn("Rejected Alipay notify because signature is invalid");
return "failure";
}
String appId = parameters.get("app_id");
String outTradeNo = parameters.get("out_trade_no");
String tradeNo = parameters.get("trade_no");
String tradeStatus = parameters.get("trade_status");
String totalAmount = parameters.get("total_amount");
PaymentOrder order = paymentOrderService.findByOutTradeNo(outTradeNo);
if (order == null) {
log.error("Alipay notify references an unknown order: {}", outTradeNo);
return "failure";
}
if (!expectedAppId.equals(appId)) {
log.error("Unexpected app_id for order {}: {}", outTradeNo, appId);
return "failure";
}
BigDecimal notifiedAmount = new BigDecimal(totalAmount);
if (order.getAmount().compareTo(notifiedAmount) != 0) {
log.error(
"Amount mismatch for order {}, expected {}, actual {}",
outTradeNo,
order.getAmount(),
notifiedAmount
);
return "failure";
}
switch (tradeStatus) {
case "WAIT_BUYER_PAY":
log.info(
"Order is waiting for payment, outTradeNo={}, tradeNo={}",
outTradeNo,
tradeNo
);
return "success";
case "TRADE_SUCCESS":
paymentOrderService.markPaidIdempotently(
outTradeNo,
tradeNo,
parameters
);
return "success";
case "TRADE_FINISHED":
paymentOrderService.markFinishedIdempotently(
outTradeNo,
tradeNo,
parameters
);
return "success";
case "TRADE_CLOSED":
paymentOrderService.markClosedIdempotently(
outTradeNo,
tradeNo,
parameters
);
return "success";
default:
log.warn(
"Unhandled Alipay trade status, outTradeNo={}, status={}",
outTradeNo,
tradeStatus
);
return "success";
}
}
响应内容应是支付宝接口要求的纯文本,例如:
success
不要返回 JSON:
{"result":"success"}
响应中也不要包含 HTML 标签、额外空格或其他业务说明。
未知状态是否返回 success,取决于系统的告警和补偿能力。通常,当验签、应用、订单和金额都已确认无误时,可以保存原始通知并返回 success,避免无意义的重试,再通过主动查询核实订单状态。
默认关闭时为什么仍可能收到 WAIT_BUYER_PAY
商户或应用实际开启了通知能力
“默认关闭”并不等于当前商户一定处于关闭状态。商户、服务商、支付宝运营人员或历史接入流程都可能开启过相关能力。
需要确认配置是否对应实际发起交易的:
app_id;- 商户 PID;
- 服务商代调用关系;
- 应用授权令牌所指向的被授权商户。
如果使用服务商模式,服务商自己的配置不能代表实际收款商户的配置。
生产环境使用了另一套应用或商户身份
代码中的应用配置、证书、授权令牌,可能与运营人员在开放平台查看的应用不属于同一套配置。应从预创建请求和回调中直接核对 app_id、seller_id、out_trade_no 等字段,不能只根据部署环境名称判断。
多个应用或支付产品共用通知地址
同一个通知地址如果同时接收多个应用、多个商户或不同支付产品的通知,其他业务产生的 WAIT_BUYER_PAY 可能会被误认为当前当面付订单的回调。
建议在日志中记录以下经过脱敏的信息:
app_id
seller_id
out_trade_no
trade_no
trade_status
notify_id
notify_time
再通过 out_trade_no 关联预创建请求日志,确认二者是否属于同一笔交易。
请求并非可信的支付宝通知
公网通知地址可能收到扫描请求、伪造请求或内部测试请求。只有签名验证通过,并且应用、订单、金额和商户身份全部匹配,才能将请求认定为支付宝通知。
文档范围或能力配置有差异
文档描述的默认行为,可能因产品能力、商户配置、服务商模式或接入时间而不同。如果验签已经通过,也确认通知属于同一应用和同一订单,但后台仍找不到相关开关,可以携带以下信息联系支付宝技术支持核查:
app_id;- 商户 PID;
out_trade_no;trade_no;notify_id;- 通知发生时间;
- 已脱敏的预创建请求和异步通知参数。
不要提交应用私钥、完整证书私钥或其他敏感凭据。
增加主动查询补偿
扫码页面可以轮询 alipay.trade.query。服务端也应定时查询长时间停留在待支付状态的订单。查询到支付成功后,仍需复用异步通知使用的幂等支付处理逻辑。
建议按以下流程确认状态:
异步通知优先处理
↓
未收到或处理异常
↓
主动调用 alipay.trade.query
↓
幂等更新本地订单
主动查询用于弥补单次网络请求可能出现的问题,并不替代异步通知。
注意事项
WAIT_BUYER_PAY只表示交易正在等待买家付款,不能据此发货、充值或记账。- 即使已经收到
WAIT_BUYER_PAY,也不能拒绝同一订单后续发送的TRADE_SUCCESS。 - 对合法但暂时不关心的状态,不要抛出异常,以免触发重复通知。
- 返回
success前,必须完成验签和订单关键字段校验。 - 支付成功应以支付宝的可信状态为依据,同时校验金额、商户身份和本地订单。
- 异步通知和主动查询应共用一套幂等状态机,防止重复处理。
- 建议保留原始通知,但个人信息和敏感字段需要脱敏。
备注:内容仅供参考。
