Skip to content

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 幂等复用;换号才会新开
怎么知道付款成功回调 + 查询双轨。轮询你自己的后端,后端去查平台查询接口

相关 ​

面向全球中小商家的 Web3 支付基础设施