如何在Next.js网站中获取任意UPI应用的支付状态(成功/失败)
解决Next.js中UPI支付状态回调的实现方案
核心逻辑说明
UPI支付的状态校验必须依赖商户后台的Webhook回调,前端深度链接仅用于同步页面状态,不能作为最终支付结果的判断依据(前端状态可被篡改)。以下是具体实现步骤:
1. 配置UPI商户Webhook回调接口
在你的Next.js项目中创建API路由(替代PHP后端),用于接收UPI网关发送的支付状态通知:
- 路径:
pages/api/upi-webhook.js(App Router则用app/api/upi-webhook/route.js) - 核心功能:
- 验证UPI网关传递的签名(防止伪造请求,具体签名规则参考你的商户服务商文档)
- 根据回调中的订单ID(
tn参数)更新数据库内的订单状态(成功/失败/超时) - 返回UPI网关要求的响应格式(一般是
{"status":"SUCCESS"})
示例代码(App Router):
export async function POST(request) { const body = await request.json(); // 1. 验证签名(示例逻辑,需替换为服务商实际规则) const isValid = verifySignature(body, process.env.UPI_MERCHANT_SECRET); if (!isValid) return new Response(JSON.stringify({status: "FAILED"}), {status: 403}); // 2. 更新数据库订单状态 const orderId = body.tn; // 对应支付时传递的tn参数 await updateOrderStatus(orderId, body.status); // 3. 返回成功响应 return new Response(JSON.stringify({status: "SUCCESS"}), {status: 200}); }
2. 发起支付时关联唯一订单ID
调用upi://pay链接时,必须通过tn参数传递你的业务订单ID,确保UPI网关回调时能关联到具体订单:
const handleInitiatePayment = (orderId) => { const upiUrl = `upi://pay?pa=你的商户UPI ID&pn=商户名称&am=100.00&tn=${orderId}&cu=INR`; window.location.href = upiUrl; // 启动轮询兜底逻辑(见步骤4) startPaymentPolling(orderId); };
3. 深度链接实现前端状态同步
配置UPI商户后台的支付完成跳转地址,让用户支付后回到你的网站并触发状态更新:
- 跳转地址可以是HTTPS链接(兼容性更好):
https://你的域名.com/payment-callback?orderId=ORDER_12345 - 在Next.js中创建
/payment-callback页面,逻辑如下:'use client'; import { useEffect, useState } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; export default function PaymentCallback() { const searchParams = useSearchParams(); const router = useRouter(); const [status, setStatus] = useState('LOADING'); useEffect(() => { const orderId = searchParams.get('orderId'); if (!orderId) return; // 调用后端接口查询最终支付状态 fetch(`/api/check-payment?orderId=${orderId}`) .then(res => res.json()) .then(data => { if (data.status === 'SUCCESS') { router.push('/payment-success'); } else { router.push('/payment-failed'); } }); }, [searchParams, router]); return <div>正在查询支付状态...</div>; }
4. 轮询兜底(必加)
部分UPI APP可能无法触发跳转,或用户手动返回网站,此时需要前端轮询后端接口获取状态:
const startPaymentPolling = (orderId) => { let attempt = 0; const maxAttempts = 10; // 最多轮询10次(约30秒) const poll = async () => { attempt++; const res = await fetch(`/api/check-payment?orderId=${orderId}`); const data = await res.json(); if (data.status === 'SUCCESS' || data.status === 'FAILED' || attempt >= maxAttempts) { // 停止轮询,跳转对应页面 router.push(data.status === 'SUCCESS' ? '/payment-success' : '/payment-failed'); return; } // 3秒后继续轮询 setTimeout(poll, 3000); }; poll(); };
关键注意事项
- 签名验证不可省略:必须校验UPI网关回调的签名,防止恶意篡改订单状态
- 最终状态以Webhook为准:前端轮询和深度链接仅用于用户体验优化,订单状态的最终判断必须基于后端数据库
- 超时处理:后端需设置订单超时时间(如15分钟),超时后自动标记为无效订单
内容的提问来源于stack exchange,提问作者user23470414
相关产品推荐
相关产品推荐

