PAYATHON 2026

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,也能避免将支付确认逻辑暴露在浏览器端。

备注:内容仅供参考。