Skip to content

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)

相关 ​

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