Next.js 接入(App Router)
这套接入适合谁
用 Next.js 13+ App Router 做的电商站或 SaaS。它的 Route Handler 天然是服务端代码, 正好用来放"带 API KEY 去下单"这一步 —— 因此这是最贴合我们接口分工的框架。
前置准备
- 已拿到 API KEY(见立即接入)。
- 已配置收款地址并设为启用。
- 环境变量(只在服务端可用,不要加
NEXT_PUBLIC_前缀):
bash
# .env.local
API_BASE=https://api.web3sdk.com
PAY_API_KEY=your-api-key别用 NEXT_PUBLIC_ 前缀
NEXT_PUBLIC_ 开头的变量会被内联进浏览器包。KEY 只能用不带前缀的名字, 并且只在 Route Handler / Server Action 里读。
安装
Next.js 自带 Route Handler,无需额外依赖;链上交互按需:
bash
npm i viem最小可跑示例
1. Route Handler:服务端下单
ts
// app/api/checkout/route.ts
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const { orderNo, amount } = await req.json()
const r = await fetch(`${process.env.API_BASE}/api/pay/open/createOrder`, {
method: 'POST',
headers: {
'X-Pay-Api-Key': process.env.PAY_API_KEY!, // 只在服务端
'Content-Type': 'application/json',
},
body: JSON.stringify({
merchantOrderNo: orderNo, // 幂等键
chainId: 2,
tokenId: 1,
amount: String(amount), // 代币单位小数字符串
subject: `订单 ${orderNo}`,
notifyUrl: `${process.env.SITE_URL}/api/pay/notify`,
returnUrl: `${process.env.SITE_URL}/pay/done`,
}),
cache: 'no-store',
}).then((x) => x.json())
if (r.code !== 0) {
return NextResponse.json({ code: r.code, msg: r.msg }, { status: 400 })
}
const o = r.data.order
// 只回前端要展示的字段
return NextResponse.json({
code: 0,
orderNo: o.orderNo,
payAddress: o.payAddress,
payAmountDisplay: o.payAmountDisplay,
payAmountMinimal: o.payAmountMinimal,
identify: o.identify,
chainKey: o.chainKey,
idempotentReplay: r.data.idempotentReplay,
})
}2. 查状态(同样在服务端)
ts
// app/api/order-status/route.ts
import { NextResponse } from 'next/server'
export async function GET(req: Request) {
const orderNo = new URL(req.url).searchParams.get('orderNo')
const r = await fetch(
`${process.env.API_BASE}/api/pay/open/queryOrder?merchantOrderNo=${encodeURIComponent(orderNo!)}`,
{ headers: { 'X-Pay-Api-Key': process.env.PAY_API_KEY! }, cache: 'no-store' },
).then((x) => x.json())
if (r.code !== 0) return NextResponse.json({ code: r.code, msg: r.msg }, { status: 400 })
const o = r.data
return NextResponse.json({
code: 0,
status: o.status, // 1..6,只认 3/4 为已付款
statusText: o.statusText,
txHash: o.txHash,
receivedAmountMinimal: o.receivedAmountMinimal,
})
}3. 客户端组件展示收银台
tsx
// app/checkout/Checkout.tsx
'use client'
import { useEffect, useState } from 'react'
export default function Checkout({ orderNo, amount }: { orderNo: string; amount: string }) {
const [order, setOrder] = useState<any>(null)
const [status, setStatus] = useState(1)
useEffect(() => {
fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderNo, amount }),
})
.then((r) => r.json())
.then(setOrder)
}, [orderNo, amount])
useEffect(() => {
if (!order?.orderNo) return
const t = setInterval(async () => {
const s = await fetch(`/api/order-status?orderNo=${order.orderNo}`).then((r) => r.json())
setStatus(s.status)
if (s.status === 3 || s.status === 4) clearInterval(t)
}, 3000)
return () => clearInterval(t)
}, [order])
if (!order?.orderNo) return <p>正在创建订单…</p>
return (
<div className="cg-qr">
<div>
<p>请转账 <b>{order.payAmountDisplay}</b> USDT(链:{order.chainKey})</p>
<p>收款地址:<span className="cg-copy">{order.payAddress}</span></p>
<p>{status === 2 ? '已收到交易,等待链上确认…' : status >= 3 ? '支付已确认' : '等待付款…'}</p>
</div>
</div>
)
}回调处理
回调也用 Route Handler 接收。注意要拿到原始报文体 —— App Router 里用 await req.text(), 不要先 req.json()。
ts
// app/api/pay/notify/route.ts
import { NextResponse } from 'next/server'
import crypto from 'node:crypto'
function verify(rawBody: string, ts: string, sig: string) {
const want = crypto
.createHmac('sha256', process.env.PAY_API_KEY!)
.update(ts).update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(sig))
}
export async function POST(req: Request) {
const raw = await req.text() // 原始字符串,别用 req.json()
const ts = req.headers.get('x-pay-timestamp') ?? ''
const sig = req.headers.get('x-pay-signature') ?? ''
if (!verify(raw, ts, sig)) return new NextResponse('bad signature', { status: 401 })
const p = JSON.parse(raw)
if (await isProcessed(p.merchantOrderNo)) return new NextResponse(null, { status: 200 })
await markPaid(p.merchantOrderNo, p.txHash)
return new NextResponse(null, { status: 200 }) // 2xx 才会停止重试
}- 幂等:用
merchantOrderNo去重。 - 快返回:约 10 秒超时;耗时逻辑放队列或
after()。 - 以查询为准:发货等关键动作前用查询接口复核。
常见问题
| 问题 | 说明 |
|---|---|
| Route Handler 能放 KEY 吗 | 能,它只在服务端运行。这也是推荐的落点 |
| Server Action 行不行 | 行,同样跑在服务端,读环境变量即可 |
静态导出(output: export)怎么办 | 那就没有服务端了,下单必须放到独立的服务端(如一个小 Node 服务) |
| 回调拿不到原始正文 | 用 await req.text(),不要 req.json() 后再序列化 |
| 缓存导致状态不更新 | 查询与下单请求都加 cache: 'no-store' |