移动端支付与二维码扫码支付如何获取支付结果?
结论
无论用户在移动端直接支付,还是用另一部手机扫描二维码支付,最终结果都要以服务端确认的订单状态为准。
- 客户端 SDK 返回的结果只适合用来更新页面提示,不能作为款项到账的最终依据。
- 服务端应接收支付平台发送的异步通知,完成验签后更新订单状态。
- 移动端或二维码展示端应向业务服务端查询支付状态。业务服务端也可以通过 WebSocket、SSE 等方式主动推送结果。
- 如果异步通知迟迟没有到达,服务端可以按需调用支付平台的订单查询接口补查。
- 客户端不应直接查询支付平台,也不能只根据客户端回调执行发货、充值或开通服务等操作。
“SDK 自动轮询服务端”并不是所有支付 SDK 都具备的机制,具体要看所使用的 SDK。即使 SDK 返回支付成功,业务系统仍需以服务端记录的订单状态为准。
为什么不能直接相信客户端结果
客户端支付回调只能说明支付流程在客户端的执行情况,实际使用中可能遇到这些问题:
- 用户已经支付成功,但客户端被关闭、断网,或者没有收到回调。
- 客户端仍显示支付处理中,服务端稍后才收到成功通知。
- SDK 返回成功时,业务服务端还没有核对订单金额、商户号等信息。
- 客户端环境并不可信,回调参数可能被篡改或伪造。
- 支付平台的异步通知可能发送失败,也可能晚于客户端查询到达。
因此,客户端回调可以用来更新交互状态,例如显示”正在确认支付结果”。发货、充值、开通服务等业务处理则应由服务端执行。
移动端支付结果的推荐处理流程
常见的处理流程是:
- 移动端请求业务服务端创建订单。
- 服务端调用支付平台的下单接口,保存商户订单号和初始状态。
- 服务端将支付参数返回给移动端。
- 移动端调用支付 SDK。
- SDK 回调后,移动端向业务服务端查询订单状态。
- 支付平台向服务端发送异步通知。
- 服务端完成验签、金额校验和幂等处理,再更新订单状态。
- 如果订单长时间停留在处理中,服务端可以调用支付平台的订单查询接口补查。
客户端查询的是业务系统自己的接口,例如:
GET /api/payment/orders/{orderNo}/status
服务端可以返回统一的状态数据:
{
"orderNo": "ORDER_202609130001",
"status": "PAID",
"paidAt": "2026-09-13T14:30:00+08:00"
}
订单状态至少可以区分为:
PENDING 待支付
PROCESSING 支付处理中
PAID 支付成功
CLOSED 订单已关闭
FAILED 支付失败
REFUNDED 已退款
收到 SDK 回调后,客户端可以在短时间内轮询业务服务端:
async function waitForPayment(orderNo) {
const intervals = [1000, 1500, 2000, 3000, 5000];
for (const delay of intervals) {
const response = await fetch(
`/api/payment/orders/${encodeURIComponent(orderNo)}/status`
);
const result = await response.json();
if (result.status === "PAID") {
return result;
}
if (["CLOSED", "FAILED", "REFUNDED"].includes(result.status)) {
throw new Error(`Payment ended with status: ${result.status}`);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
return { orderNo, status: "PROCESSING" };
}
轮询超时并不等于支付失败。页面可以提示”支付结果确认中”,并允许用户稍后刷新订单状态。
二维码展示端如何获取结果
二维码支付与移动端支付遵循同一原则。区别在于,发起支付和完成支付的设备不是同一台。
可以按以下流程处理:
- 二维码展示端向业务服务端创建支付订单。
- 服务端调用支付平台接口,获取二维码内容。
- 展示端显示二维码并记录业务订单号。
- 用户使用另一台手机扫码支付。
- 支付平台向业务服务端发送异步通知。
- 服务端验签并更新订单状态。
- 二维码展示端从业务服务端获取最新状态。
- 确认支付成功后,展示端停止轮询、关闭二维码或跳转到成功页面。
展示端可以轮询业务服务端,但不应直接轮询支付平台:
function startQrPaymentPolling(orderNo) {
const timer = setInterval(async () => {
try {
const response = await fetch(
`/api/payment/orders/${encodeURIComponent(orderNo)}/status`
);
const result = await response.json();
if (result.status === "PAID") {
clearInterval(timer);
showPaymentSuccess(result);
} else if (["CLOSED", "FAILED"].includes(result.status)) {
clearInterval(timer);
showPaymentClosed(result);
}
} catch (error) {
// 短暂网络异常不应立即判定支付失败
console.error("Failed to query payment status", error);
}
}, 2000);
return () => clearInterval(timer);
}
如果需要减少轮询请求,可以改用 WebSocket 或 SSE。展示端订阅订单状态,服务端收到支付通知后主动推送结果:
const socket = new WebSocket(
`wss://example.com/ws/payments?orderNo=${encodeURIComponent(orderNo)}`
);
socket.onmessage = event => {
const result = JSON.parse(event.data);
if (result.status === "PAID") {
showPaymentSuccess(result);
socket.close();
}
};
使用 WebSocket 后,页面在重新连接或刷新时仍应调用订单查询接口。连接中断可能导致推送消息丢失。
服务端异步通知的处理要点
服务端收到异步通知后,至少要完成以下检查:
- 验证通知签名和证书。
- 检查商户订单号是否存在。
- 检查商户号、应用标识等是否属于当前系统。
- 核对实付金额、币种与原订单是否一致。
- 确认通知中的支付状态确实为成功。
- 通过事务或唯一约束保证幂等。
- 可靠保存支付结果后,再向支付平台返回成功响应。
伪代码示例:
@Transactional
public void handlePaymentNotification(PaymentNotification notification) {
verifySignature(notification);
PaymentOrder order = paymentOrderRepository
.findByOrderNoForUpdate(notification.getOrderNo())
.orElseThrow(() -> new IllegalArgumentException("Order not found"));
verifyMerchant(notification, order);
verifyAmountAndCurrency(notification, order);
if (order.getStatus() == PaymentStatus.PAID) {
return;
}
if (!notification.isPaymentSuccessful()) {
return;
}
order.markPaid(
notification.getTransactionId(),
notification.getPaidAt()
);
paymentOrderRepository.save(order);
publishPaymentSucceededEvent(order);
}
主动查询支付平台的时机
异步通知通常是确认支付结果的主要渠道,但通知不一定能按时到达。遇到以下情况时,服务端可以主动查询支付平台:
- 客户端报告 SDK 支付成功,但本地订单仍是
PENDING。 - 二维码展示端等待较长时间后仍未收到结果。
- 异步通知验签失败或处理异常,需要通过对账确认。
- 定时任务发现订单长时间停留在中间状态。
- 日终对账发现业务订单与支付平台记录不一致。
主动查询需要设置频率限制和超时时间,不能把每次客户端轮询都直接转成对支付平台的查询。服务端应先查询本地数据库,只有满足补查条件时才访问支付平台。
注意事项
- 客户端显示”取消支付”,通常只说明用户退出了当前支付流程,不一定表示订单最终失败。
- 二维码需要设置有效期。二维码过期后应停止轮询并关闭订单,具体能否关闭取决于支付平台。
- 订单号必须唯一。刷新二维码时,要明确复用原订单还是创建新订单。
- 支付成功后的发货、充值和记账等操作必须支持幂等处理。
- 支付平台的密钥、签名私钥和敏感查询凭证不能放在客户端。
- 轮询可以使用固定间隔或指数退避,同时设置总时长。页面进入后台时可以暂停轮询,恢复后立即查询一次。
- 推送可以改善结果更新的实时性,但不能替代订单查询、异步通知和对账机制。
- 不同支付平台使用的回调字段、验签方式和订单查询接口并不相同,具体实现应以所接入平台的官方接口定义为准。
备注:内容仅供参考。