Vue H5 页面如何实现支付宝扫码跳转及支付前用户判断?
明确结论
这套流程可以拆成四个环节:
- 二维码携带业务场景参数,并打开 H5 页面。
- H5 页面打开后,通过支付宝 OAuth 或已有登录态识别用户。
- 用户资格、订单状态和支付金额等判断都由服务端完成。
- 服务端创建支付宝交易,Vue 页面调用支付宝支付能力。最终支付结果以服务端收到的异步通知为准。
不要把支付链接直接放进二维码,也不要在 Vue 前端保存支付宝私钥、计算签名或决定支付金额。前端传来的用户信息和金额都不能直接采信。
推荐流程
假设二维码内容是:
https://m.example.com/pay-entry?scene=STORE_1001
scene 用于标识门店、设备、活动或其他业务场景。实际项目中,最好改用随机且可校验的短期令牌,避免在二维码中直接暴露内部编号和敏感信息。
完整流程如下:
支付宝扫码
→ 打开 Vue H5 页面
→ 服务端校验二维码场景
→ 判断是否已有可信用户身份
→ 必要时进行支付宝 OAuth 授权
→ 服务端判断用户是否允许支付
→ 服务端生成业务订单
→ 创建支付宝交易
→ H5 调起支付宝支付
→ 服务端接收异步支付通知
→ H5 查询订单最终状态
第一步:进入页面后获取业务场景
Vue 页面只读取二维码携带的场景参数,再将参数交给服务端校验。
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute()
const loading = ref(true)
const message = ref('')
onMounted(async () => {
const scene = String(route.query.scene || '')
if (!scene) {
message.value = '二维码参数无效'
loading.value = false
return
}
await initialize(scene)
})
async function initialize(scene) {
// 后续调用服务端初始化接口
}
return {
loading,
message
}
}
}
服务端收到 scene 后,需要检查:
- 场景是否存在并已启用;
- 二维码是否过期;
- 当前场景是否允许创建订单;
- 参数是否被伪造或重复使用;
- 是否需要识别支付宝用户。
如果二维码有使用次数或有效期限制,可以把二维码内容设计成由服务端签发的随机令牌,例如:
https://m.example.com/pay-entry?token=随机不可预测的短期令牌
服务端通过令牌查询真实的业务场景,不要让前端自行解析或信任其中的业务数据。
第二步:识别支付宝用户
如果业务需要确认当前支付宝用户的身份,通常要通过支付宝 OAuth 获取用户标识。仅凭 User-Agent 判断用户是否来自支付宝并不可靠,因为 User-Agent 可以伪造,也无法代表具体的用户身份。
常见的授权入口形式如下:
https://openauth.alipay.com/oauth2/publicAppAuthorize.htm
?app_id=你的应用ID
&scope=auth_user
&redirect_uri=URL编码后的回调地址
&state=一次性随机值
具体可用的授权范围、应用类型和配置要求,需要以项目接入时的支付宝开放平台文档及应用权限为准。
授权地址建议由服务端生成:
// Node.js / Express 示例,省略数据库和异常处理中间件
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.get('/api/alipay/authorize-url', async (req, res) => {
const scene = String(req.query.scene || '')
if (!scene) {
return res.status(400).json({ message: '缺少 scene' })
}
const state = crypto.randomBytes(24).toString('hex')
// 应将 state、scene、过期时间绑定保存到 Session、Redis 或数据库
await saveOAuthState({
state,
scene,
expiresAt: Date.now() + 5 * 60 * 1000
})
const callbackUrl =
`https://m.example.com/api/alipay/oauth/callback`
const authorizeUrl =
'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm' +
`?app_id=${encodeURIComponent(process.env.ALIPAY_APP_ID)}` +
'&scope=auth_user' +
`&redirect_uri=${encodeURIComponent(callbackUrl)}` +
`&state=${encodeURIComponent(state)}`
res.json({ authorizeUrl })
})
Vue 页面收到授权地址后跳转:
async function startAuthorization(scene) {
const response = await fetch(
`/api/alipay/authorize-url?scene=${encodeURIComponent(scene)}`,
{
credentials: 'include'
}
)
if (!response.ok) {
throw new Error('获取授权地址失败')
}
const data = await response.json()
window.location.replace(data.authorizeUrl)
}
支付宝回调服务端时,一般会携带 auth_code 和 state。服务端先验证 state,再用 auth_code 调用 OAuth 换取接口,取得支付宝用户标识。
app.get('/api/alipay/oauth/callback', async (req, res) => {
const authCode = String(req.query.auth_code || '')
const state = String(req.query.state || '')
const stateRecord = await consumeOAuthState(state)
if (!stateRecord || stateRecord.expiresAt < Date.now()) {
return res.status(400).send('授权状态无效或已过期')
}
if (!authCode) {
return res.status(400).send('未取得授权码')
}
// 使用支付宝服务端 SDK 调用 alipay.system.oauth.token
const tokenResult = await exchangeAlipayAuthCode(authCode)
// 将支付宝 user_id 与站内用户或临时会话建立绑定
await saveUserSession({
alipayUserId: tokenResult.user_id
})
const target =
`https://m.example.com/pay-entry?scene=${encodeURIComponent(stateRecord.scene)}`
res.redirect(target)
})
auth_code 必须由服务端兑换,不要通过浏览器调用支付宝开放接口。应用私钥也只能存放在服务端。
第三步:在服务端完成用户判断
取得可信的用户身份后,Vue 页面调用初始化接口:
async function initialize(scene) {
const response = await fetch('/api/payment/context', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ scene })
})
const result = await response.json()
if (result.action === 'AUTHORIZE') {
await startAuthorization(scene)
return
}
if (result.action === 'REJECT') {
message.value = result.message || '当前用户不能参与'
loading.value = false
return
}
if (result.action === 'PAY') {
loading.value = false
// 展示由服务端返回的商品和金额,等待用户确认
}
}
服务端可以在这个接口中判断:
- 用户是否已经注册或绑定;
- 用户是否在允许参与的范围内;
- 用户是否已经购买或支付;
- 用户是否达到限购次数;
- 订单是否已经存在;
- 活动是否已经开始或结束;
- 场景、商品和库存是否有效;
- 用户是否受到风控限制。
这些判断必须由服务端完成。前端隐藏支付按钮只能改善页面交互,不能作为安全措施。
第四步:创建业务订单和支付宝交易
用户确认付款后,前端只提交场景或商品标识:
async function createPayment(scene) {
const response = await fetch('/api/payment/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ scene })
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.message || '创建订单失败')
}
return result
}
服务端需要重新校验用户资格,并按照数据库中的商品配置计算金额:
app.post('/api/payment/orders', async (req, res) => {
const user = await requireCurrentUser(req)
const scene = String(req.body.scene || '')
const context = await loadPaymentContext(scene)
if (!context || !context.enabled) {
return res.status(400).json({ message: '支付场景无效' })
}
const eligibility = await checkUserEligibility(user, context)
if (!eligibility.allowed) {
return res.status(403).json({ message: eligibility.reason })
}
// 金额必须来自服务端配置,不能采用前端传入的金额
const amount = context.price
// outTradeNo 必须全局唯一;重复请求应返回同一未支付订单
const order = await findOrCreatePendingOrder({
userId: user.id,
scene,
amount
})
// 通过支付宝服务端 SDK 调用 alipay.trade.create
const alipayTrade = await createAlipayTrade({
outTradeNo: order.outTradeNo,
totalAmount: amount,
subject: context.subject,
buyerId: user.alipayUserId
})
await saveAlipayTradeNo(order.id, alipayTrade.trade_no)
res.json({
orderNo: order.outTradeNo,
tradeNo: alipayTrade.trade_no
})
})
订单幂等性需要特别处理。即使用户连续点击按钮、刷新页面或因网络问题重试,服务端也不能生成多笔有效订单。
第五步:在支付宝客户端内调起支付
如果使用支付宝客户端内的 JSAPI 支付,可以等支付宝 JSBridge 准备完成后调用 tradePay。其中的 tradeNO 必须来自服务端创建交易后的返回结果。
function readyAlipayBridge() {
return new Promise((resolve, reject) => {
if (window.AlipayJSBridge) {
resolve(window.AlipayJSBridge)
return
}
const timer = window.setTimeout(() => {
reject(new Error('支付宝支付组件加载超时'))
}, 10000)
document.addEventListener(
'AlipayJSBridgeReady',
() => {
window.clearTimeout(timer)
resolve(window.AlipayJSBridge)
},
{ once: true }
)
})
}
async function pay(scene) {
const payment = await createPayment(scene)
const bridge = await readyAlipayBridge()
bridge.call(
'tradePay',
{
tradeNO: payment.tradeNo
},
async result => {
// resultStatus 只适合决定页面提示,不能作为最终入账依据
if (result.resultCode === '9000' || result.resultStatus === '9000') {
await pollOrderStatus(payment.orderNo)
} else {
message.value = '支付未完成,可稍后重新查询订单'
}
}
)
}
不同的支付宝接入方案可能返回不同字段。实际使用哪些字段,要以当前采用的 JSAPI、SDK 和开放平台文档为准,不能假定 resultCode 与 resultStatus 一定同时存在。
如果不使用支付宝客户端内的 JSAPI,也可以由服务端调用 alipay.trade.wap.pay,再把生成的支付表单或跳转内容返回给浏览器。两种方案选择一种即可:
- 已在支付宝客户端内并取得用户标识时,通常更适合使用交易创建接口配合
tradePay。 - 如果普通移动浏览器也需要付款,可以考虑手机网站支付
alipay.trade.wap.pay。
最终能使用哪种能力,取决于应用签约的产品、应用类型和已经开通的权限。
支付结果必须由服务端确认
支付控件返回成功或同步跳回成功页,都不等于付款已经成功。最终状态必须以支付宝异步通知或服务端主动查询到的结果为准。
处理异步通知时,至少需要完成以下工作:
- 使用支付宝公钥验签;
- 校验通知中的应用 ID;
- 校验商户订单号;
- 校验订单金额和币种;
- 校验收款方身份;
- 判断交易状态;
- 以幂等方式更新订单;
- 按支付宝要求返回通知响应。
前端可以轮询自己的订单查询接口:
async function pollOrderStatus(orderNo) {
for (let count = 0; count < 10; count += 1) {
const response = await fetch(
`/api/payment/orders/${encodeURIComponent(orderNo)}`,
{
credentials: 'include'
}
)
const order = await response.json()
if (order.status === 'PAID') {
window.location.replace(`/payment/success?orderNo=${encodeURIComponent(orderNo)}`)
return
}
if (order.status === 'CLOSED' || order.status === 'FAILED') {
message.value = '订单未支付成功'
return
}
await new Promise(resolve => window.setTimeout(resolve, 1500))
}
message.value = '支付结果确认中,请稍后查看订单'
}
订单查询接口还必须校验订单归属,不能让任何人仅凭订单号就查询订单。
容易忽略的事项
OAuth 回跳循环
页面不能每次加载都直接跳转到授权页。应先向服务端查询当前会话是否已经绑定支付宝用户,只有缺少用户身份时才进入 OAuth,否则很容易出现无限重定向。
Vue Router 模式
OAuth 回调地址通常要指向服务端的真实 URL。服务端建立会话后,再重定向到 Vue 页面。如果使用 History 模式,Web 服务器还要配置 SPA fallback,否则用户刷新页面时可能遇到 404。
二维码参数安全
二维码中的参数可以被复制和修改。服务端必须校验有效期、状态、签名或随机令牌,不能只根据 scene=STORE_1001 创建订单。
会话与跨域
如果 H5 和 API 使用不同域名,需要正确配置 Cookie 的 SameSite、Secure、CORS 和 credentials。让 H5 与 API 位于同一站点下,配置会更简单。
重复支付
创建订单接口、支付宝异步通知和业务发货逻辑都需要实现幂等。支付请求期间也应禁用支付按钮,但前端防止重复点击不能替代服务端幂等。
金额与资格二次校验
页面初始化时判断通过,不代表用户创建订单时仍然符合条件。用户点击支付后,服务端必须再次检查资格、库存、活动时间和订单状态。
整个流程中,Vue 只负责页面交互和调起支付。用户身份、业务资格、订单金额、签名、交易创建以及支付结果确认都应放在服务端。这样可以实现扫码进入后先判断用户再付款,也能避免前端参数被篡改后造成越权或金额错误。
备注:内容仅供参考。
