React 接入
这套接入适合谁
用 React 做的电商前台、SaaS 控制台或自建收银页,需要在自己的界面上展示"应付金额 + 二维码 / 唤起钱包", 并且在付款成功后刷新订单状态。
前置准备
- 已拿到 API KEY(见立即接入)。
- 已配置收款地址并设为启用。
- 已准备一个服务端接口,用来接收买家的下单请求。API KEY 只能放在那里。
下单一定在你的服务端
下面示例里,前端只调你自己的 /api/checkout,由你的服务端带着 KEY 去调平台的开放接口。 把 KEY 写进 React 代码(哪怕是环境变量 VITE_*)都会被打进产物,任何访客都能读走。
安装
前端不需要额外依赖;链上交互按需引入钱包库:
bash
npm i viem # EVM 链(ERC20 / BEP20)交互最小可跑示例
1. 你的服务端下单
js
// server/checkout.js —— Node/Express
import express from 'express'
const app = express()
app.use(express.json())
app.post('/api/checkout', async (req, res) => {
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: req.body.orderNo, // 幂等键,重试要一样
chainId: 2,
tokenId: 1,
amount: String(req.body.amount), // 代币单位小数字符串
subject: `订单 ${req.body.orderNo}`,
notifyUrl: 'https://your-shop.com/pay/notify',
returnUrl: 'https://your-shop.com/pay/done',
}),
}).then((x) => x.json())
if (r.code !== 0) return res.status(400).json(r)
// 只把前端需要展示的字段挑出来,不要把整包原样吐给浏览器
const o = r.data.order
res.json({
orderNo: o.orderNo,
payAddress: o.payAddress,
payAmountDisplay: o.payAmountDisplay, // 展示用;实际该转的金额
payAmountMinimal: o.payAmountMinimal,
identify: o.identify,
tokenContract: o.tokenContract,
tokenDecimals: o.tokenDecimals,
chainKey: o.chainKey,
idempotentReplay: r.data.idempotentReplay,
})
})
app.listen(8787)2. React 前端
jsx
// Checkout.jsx
import { useEffect, useState } from 'react'
export default function Checkout({ orderNo, amount }) {
const [order, setOrder] = useState(null)
const [err, setErr] = useState('')
useEffect(() => {
fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderNo, amount }),
})
.then((r) => r.json())
.then((d) => (d.code === 0 ? setErr('下单失败') : setOrder(d)))
.catch((e) => setErr(String(e)))
}, [orderNo, amount])
// 轮询自己的后端核对状态;后端再去查平台的查询接口
useEffect(() => {
if (!order) return
const t = setInterval(async () => {
const s = await fetch(`/api/order-status?orderNo=${order.orderNo}`).then((r) => r.json())
if (s.status === 3 || s.status === 4) clearInterval(t)
}, 3000)
return () => clearInterval(t)
}, [order])
if (err) return <p>下单失败:{err}</p>
if (!order) return <p>正在创建订单…</p>
return (
<div className="cg-qr">
<div>
<p>请转账 <b>{order.payAmountDisplay}</b> USDT</p>
<p>收款地址:<span className="cg-copy">{order.payAddress}</span></p>
<p>链:{order.chainKey}</p>
<p>请务必按此金额转账,识别码随交易一起上链。</p>
</div>
</div>
)
}也可以不自己画收银页,直接让买家跳平台收银台:
{cashierBaseUrl}/pay/cashier/m/{merchantNo}。 这两个值从商户侧接口GET /api/pay/getMyProfile取。
回调处理
回调的接收端同样是你的服务端,与 React 无关。要点:
js
// 验签(用原始报文!)
import crypto from 'node:crypto'
export function verify(rawBody, ts, sig) {
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))
}js
app.post('/pay/notify', express.raw({ type: '*/*' }), (req, res) => {
const raw = req.body.toString('utf8')
if (!verify(raw, req.get('X-Pay-Timestamp'), req.get('X-Pay-Signature'))) {
return res.sendStatus(401)
}
const p = JSON.parse(raw)
if (isProcessed(p.merchantOrderNo)) return res.sendStatus(200) // 幂等
markPaid(p.merchantOrderNo, p.txHash)
res.sendStatus(200) // 2xx 才会停止重试
})- 幂等:回调会重试,用
merchantOrderNo去重,别重复发货。 - 快返回:约 10 秒超时,重活放队列。
- 以查询为准:关键动作前用查询订单复核。
常见问题
| 问题 | 说明 |
|---|---|
| KEY 能不能放前端 | 不能。前端只调你自己的后端,后端带 KEY 调平台 |
amount 传什么 | 开放接口要代币单位小数字符串("100.5") |
用 payAmountMinimal 还是 amountMinimal | 展示与转账金额都用 payAmountMinimal 对应的显示值 |
| 重复点击下单会不会多开单 | 不会,同一个 merchantOrderNo 幂等复用;换号才会新开 |
| 怎么知道付款成功 | 回调 + 查询双轨。轮询你自己的后端,后端去查平台查询接口 |
相关
- 创建订单 · 查询订单 · 回调通知与验签
- Vue 3 接入 · Next.js 接入