Next.js App Router 如何获取 callback 中的 trans_id?
明确结论
如果在浏览器开发者工具的 Network -> callback -> Payload 中看到了 trans_id,银行很可能是通过 POST 请求体发送回调数据的。Next.js 页面组件无法直接读取这次 POST 的 payload,需要使用 App Router 的 Route Handler 接收回调,再根据数据格式调用 request.formData()、request.json() 或 request.text() 解析 trans_id。
处理完回调后,Route Handler 可以将浏览器重定向到支付结果页。交易状态查询和签名校验应尽量放在服务端完成,不能只依赖客户端页面。
推荐的路由结构
将银行后台配置的 callback URL 设为:
/transaction/callback
对应的 Route Handler 目录结构如下:
app/
├── transaction/
│ ├── callback/
│ │ └── route.ts
│ └── result/
│ └── page.tsx
同一个路由段不能同时使用 page.tsx 和 route.ts 处理相同 URL。因此,/transaction/callback 负责接收 POST 回调,展示结果的页面应放在 /transaction/result 等其他地址。
接收表单形式的 callback
支付平台常见的回调格式是:
Content-Type: application/x-www-form-urlencoded
这类请求可以通过 request.formData() 读取:
// app/transaction/callback/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const formData = await request.formData();
const transIdValue = formData.get("trans_id");
if (typeof transIdValue !== "string" || !transIdValue) {
return NextResponse.json(
{ error: "Missing trans_id" },
{ status: 400 }
);
}
const transId = transIdValue;
// 在这里调用服务端 API,查询并验证交易状态
// const transaction = await checkTransactionStatus(transId);
const resultUrl = new URL("/transaction/result", request.url);
resultUrl.searchParams.set("trans_id", transId);
return NextResponse.redirect(resultUrl, 303);
}
状态码 303 会让浏览器接下来使用 GET 请求打开结果页,从而避免用户刷新页面时再次提交 POST。
如果 callback 发送 JSON
如果开发者工具中的请求头是:
Content-Type: application/json
则使用 request.json():
// app/transaction/callback/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body: unknown = await request.json();
if (
typeof body !== "object" ||
body === null ||
!("trans_id" in body) ||
typeof body.trans_id !== "string"
) {
return NextResponse.json(
{ error: "Invalid callback payload" },
{ status: 400 }
);
}
const transId = body.trans_id;
// 查询交易状态,并验证支付平台提供的签名
// const transaction = await checkTransactionStatus(transId);
const resultUrl = new URL("/transaction/result", request.url);
resultUrl.searchParams.set("trans_id", transId);
return NextResponse.redirect(resultUrl, 303);
}
应该使用 formData() 还是 json(),要看支付平台实际发送的 Content-Type,不能只根据 Payload 面板中的显示形式判断。
在结果页面读取 trans_id
重定向后的地址类似:
/transaction/result?trans_id=abc123
结果页可以继续使用服务端组件,通过 searchParams 读取参数:
// app/transaction/result/page.tsx
type PageProps = {
searchParams: {
trans_id?: string;
};
};
export default function Page({ searchParams }: PageProps) {
const transId = searchParams.trans_id;
return (
<main>
<h1>The order has been taken</h1>
<p>
All information regarding your order has been sent to the indicated
email.
</p>
{transId && <p>Transaction ID: {transId}</p>}
</main>
);
}
这个页面不需要 "use client"。如果确实要在客户端读取查询参数,可以改用 useSearchParams():
"use client";
import { useSearchParams } from "next/navigation";
export default function Page() {
const searchParams = useSearchParams();
const transId = searchParams.get("trans_id");
return <p>Transaction ID: {transId ?? "Unavailable"}</p>;
}
如果银行通过 URL 查询参数回调
有些支付平台不会发送 POST 请求,而是直接访问:
/transaction/callback?trans_id=abc123
这种请求没有请求体,可以在 Route Handler 的 GET 方法中读取查询参数:
// app/transaction/callback/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const url = new URL(request.url);
const transId = url.searchParams.get("trans_id");
if (!transId) {
return NextResponse.json(
{ error: "Missing trans_id" },
{ status: 400 }
);
}
// 在服务端查询交易状态
// const transaction = await checkTransactionStatus(transId);
const resultUrl = new URL("/transaction/result", request.url);
resultUrl.searchParams.set("trans_id", transId);
return NextResponse.redirect(resultUrl, 303);
}
如果支付平台确实可能使用两种回调方式,也可以同时导出 GET 和 POST。
支付回调的安全注意事项
trans_id 只是交易标识符,不能单独用来证明支付成功。实际处理回调时,还需要:
- 按照支付平台文档验证 callback 的签名、MAC 或其他认证字段。
- 在服务端调用支付平台 API,查询最终交易状态。
- 核对交易金额、币种、商户订单号与当前订单是否一致。
- 做好幂等处理,因为支付平台可能重复发送同一个 callback。
- 收到 callback 后,不要直接将订单标记为支付成功。
- 不要将支付平台密钥或查询接口密钥放入客户端组件。
- 用户可以修改查询参数中的
trans_id,所以结果页不能将其视为可信的支付凭证。
更稳妥的处理流程是由 Route Handler 在服务端验证交易并更新订单,然后只把内部订单号或一次性结果标识传给结果页。这样既能正确取得 trans_id,也能避免将支付确认逻辑暴露在浏览器端。
备注:内容仅供参考。
