Vue 3 接入
这套接入适合谁
用 Vue 3 + Vite 做的商城、管理系统收银页或活动页,需要展示应付金额与收款信息, 并在付款成功后刷新状态。
前置准备
- 已拿到 API KEY(见立即接入)。
- 已配置收款地址并设为启用。
- 有你自己的服务端接口承接下单请求 —— API KEY 只放那里。
下单一定在你的服务端
下面示例里前端只调你自己的 /api/checkout。VITE_ 开头的前端环境变量会被打进构建产物, 等同于公开发布 KEY。
安装
bash
npm create vite@latest my-shop -- --template vue
# 链上交互按需:
npm i viem最小可跑示例
1. 你的服务端下单
js
// server/checkout.mjs
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',
}),
}).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,
chainKey: o.chainKey,
})
})
app.listen(8787)2. Vue 3 前端(<script setup>)
vue
<!-- Checkout.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
orderNo: { type: String, required: true },
amount: { type: [String, Number], required: true },
})
const order = ref(null)
const status = ref(1)
const err = ref('')
let timer = null
async function createOrder() {
const r = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderNo: props.orderNo, amount: props.amount }),
})
const d = await r.json()
if (d.code !== 0) {
err.value = d.msg || '下单失败'
return
}
order.value = d
}
async function pollStatus() {
const s = await fetch(`/api/order-status?orderNo=${order.value.orderNo}`).then((r) => r.json())
status.value = s.status
if (s.status === 3 || s.status === 4) {
clearInterval(timer) // 已确认 / 已完成,停止轮询
}
}
onMounted(async () => {
await createOrder()
if (order.value) timer = setInterval(pollStatus, 3000)
})
onUnmounted(() => timer && clearInterval(timer))
</script>
<template>
<div class="cg-qr">
<p v-if="err" class="cg-err">下单失败:{{ err }}</p>
<template v-else-if="order">
<div>
<p>请转账 <b>{{ order.payAmountDisplay }}</b> USDT(链:{{ order.chainKey }})</p>
<p>收款地址:<span class="cg-copy">{{ order.payAddress }}</span></p>
<p v-if="status === 1">等待付款…</p>
<p v-else-if="status === 2">已收到交易,等待链上确认…</p>
<p v-else-if="status === 3 || status === 4" class="cg-ok">支付已确认</p>
</div>
</template>
<p v-else>正在创建订单…</p>
</div>
</template>回调处理
回调由你的服务端接收,与 Vue 无关:
js
import crypto from 'node:crypto'
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))
}
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 放 import.meta.env | 不能,会被打进产物 |
| 钱包地址与收款地址要一致吗 | 不需要。买家付到订单里的 payAddress,那由你在商户后台配置 |
| 轮询间隔 | 3–5 秒即可。也可只用回调,但建议保留查询核对 |
| 状态怎么判断"付了没" | 只认 3 / 4,别用数值大小比较(已过期是 5、待认领是 6) |
相关
- 创建订单 · 查询订单 · 回调通知与验签
- React 接入 · Next.js 接入