网页支付指在没有原生 App(或 App 内嵌 H5)场景下,通过浏览器收银台完成收款。本文档覆盖四种主流网页支付方式:
| 方式 | 接口/产品 | 适用浏览器 | 用户交互 |
|---|---|---|---|
| 微信 Native(扫码) | /v3/pay/transactions/native |
任意(PC/移动) | 网页展示二维码,微信 App 扫码支付 |
| 支付宝 PC / 手机网站 | alipay.trade.page.pay / alipay.trade.wap.pay |
任意 | 跳转支付宝收银台(扫码或登录支付) |
| Stripe Checkout | checkout.sessions.create |
任意 | 跳转 Stripe 托管收银台(多卡种/Apple Pay/Google Pay) |
| 浏览器原生 PaymentRequest | Web Payments API | Chrome/Safari | 系统级支付面板 |
建议后端统一实现一个聚合收银台:
POST /v1/checkout/session根据channel参数分发到对应渠道。
| 场景 | 推荐方案 |
|---|---|
| 中国大陆用户 + 微信 | 微信 Native 扫码 |
| 中国大陆用户 + 支付宝 | 支付宝 PC/WAP 支付 |
| 海外用户 / 多卡种 | Stripe Checkout(或 PaymentRequest) |
| 无法接入微信/支付宝直连 | 聚合支付服务商(如 PayerMax、Adyen)或 Stripe(收支付宝/微信) |
| 通用免卡支付 | Web Payment Request API |
flowchart LR
U[用户浏览器] -->|访问收银台页面| C[Web 收银台前端]
C -->|POST /v1/checkout/session| B[业务后端]
B -->|分发| W[微信Native: code_url→二维码]
B -->|分发| A[支付宝PC: 跳转form/URL]
B -->|分发| S[Stripe Checkout: 跳转URL]
B -->|分发| P[PaymentRequest: 返回支持性]
W -->|异步通知| B
A -->|异步通知| B
S -->|Webhook| B
B -->|轮询/回调| C[收银台展示结果]
// src/controllers/checkout_controller.js
const express = require('express');
const router = express.Router();
const { createWechatNative, createAlipayPage, createStripeCheckout } =
require('../services/pay_gateway_service');
/**
* 创建收银台会话(聚合分发)
* body: { channel: 'wechat'|'alipay'|'stripe', product_id, return_url }
*/
router.post('/v1/checkout/session', async (req, res) => {
const { channel, product_id, return_url } = req.body;
const order = await orderService.createOrder(product_id, { source: 'web' });
let payload;
switch (channel) {
case 'wechat':
// 微信 Native:返回 code_url 用于渲染二维码
payload = await createWechatNative(order);
break;
case 'alipay':
// 支付宝 PC/WAP:返回跳转 form 或 URL
payload = await createAlipayPage(order, return_url);
break;
case 'stripe':
// Stripe Checkout:返回托管收银台 URL
payload = await createStripeCheckout(order, return_url);
break;
default:
return res.status(400).json({ error: 'unsupported channel' });
}
res.json({
channel,
payment_id: order.id,
...payload,
});
});
// 查询收银台支付状态
router.get('/v1/checkout/session/:id', async (req, res) => {
const status = await orderService.getStatus(req.params.id);
res.json({ payment_id: req.params.id, success: status === 'PAID', note: status });
});
// src/services/pay_gateway_service.js
// 复用 wechatpay-node-v3(详见 wechat_pay.md 4.2)
async function createWechatNative(order) {
const result = await pay.transactions_native({
description: order.name,
out_trade_no: order.id,
notify_url: process.env.WX_NOTIFY_URL,
amount: { total: order.amount }, // 分
scene_info: { payer_client_ip: order.clientIp },
});
return { code_url: result.code_url }; // weixin://wxpay/bizpayurl?pr=...
}
// 复用 alipay-sdk(详见 alipay.md 4.2)
async function createAlipayPage(order, returnUrl) {
if (isMobile) {
// 手机网站支付
const url = await alipaySdk.pageExecute('alipay.trade.wap.pay', {
returnUrl,
notifyUrl: process.env.ALIPAY_NOTIFY_URL,
bizContent: {
out_trade_no: order.id,
total_amount: (order.amount / 100).toFixed(2),
subject: order.name,
product_code: 'QUICK_WAP_WAY',
},
});
return { redirect_url: url }; // 302 跳转
}
// PC 支付:返回自动提交的 form
const form = await alipaySdk.pageExecute('alipay.trade.page.pay', {
returnUrl,
notifyUrl: process.env.ALIPAY_NOTIFY_URL,
bizContent: {
out_trade_no: order.id,
total_amount: (order.amount / 100).toFixed(2),
subject: order.name,
product_code: 'FAST_INSTANT_TRADE_PAY',
},
});
return { html_form: form };
}
// Node.js + stripe SDK
async function createStripeCheckout(order, returnUrl) {
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{
price_data: {
currency: order.currency,
product_data: { name: order.name },
unit_amount: order.amount, // 分
},
quantity: 1,
}],
success_url: `${returnUrl}?result=success&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${returnUrl}?result=cancel`,
metadata: { out_trade_no: order.id },
});
return { redirect_url: session.url };
}
支付结果通过 Webhook
checkout.session.completed通知(验签方式见 stripe.md)。
当浏览器不支持跳转收银台时,可用 Web Payments API。后端需额外提供两个端点:
// 1. 支付意向确认(校验金额后创建订单)
router.post('/v1/payment/paymentrequest', async (req, res) => {
const { amount, currency, methodData } = req.body; // methodData 含令牌
// 走 Stripe/收单渠道验证令牌并扣款
const result = await acquiringGateway.charge({
orderId: req.body.orderId, amount, currency, token: methodData.token,
});
res.json({ success: result.success, paymentId: req.body.orderId });
});
<!-- web/checkout.html 示意(Vue3 实现见项目 src/pages/) -->
<div id="app">
<h2>选择支付方式</h2>
<button @click="createSession('wechat')">微信扫码</button>
<button @click="createSession('alipay')">支付宝</button>
<button @click="createSession('stripe')">Stripe</button>
<!-- 微信扫码:展示二维码 -->
<div v-if="channel==='wechat' && codeUrl">
<img :src="qrcode(codeUrl)" />
<p>请使用微信扫码支付</p>
<button @click="pollStatus">我已完成支付</button>
</div>
<!-- 支付宝 / Stripe:自动跳转 -->
<div v-if="htmlForm" v-html="htmlForm"></div>
<a v-if="redirectUrl" :href="redirectUrl">前往收银台</a>
</div>
Flutter 项目的 web/ 目录可直接托管上述收银台页面,或在 App 内用 url_launcher 跳转:
// Flutter App 内跳转网页收银台
import 'package:url_launcher/url_launcher.dart';
Future<void> launchWebCheckout(String channel, String productId) async {
final uri = Uri.parse(
'${APIServer.baseUrl}/web/checkout?channel=$channel&product_id=$productId',
);
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
// 收银台前端:二维码支付后轮询(微信 Native 无前端回调)
export function pollStatus(paymentId, onDone) {
const timer = setInterval(async () => {
const res = await fetch(`/v1/checkout/session/${paymentId}`);
const data = await res.json();
if (data.success) { clearInterval(timer); onDone(true); }
}, 3000);
}
建议同时配合后端 WebSocket/SSE 推送,减少轮询压力(小额场景轮询即可)。
POST /v1/checkout/session
请求:
{ "channel": "wechat", "product_id": "p001", "return_url": "https://shop.example.com/pay/result" }
响应:
{
"channel": "wechat",
"payment_id": "pay_8f3a2b",
"code_url": "weixin://wxpay/bizpayurl?pr=9xFPmlUzz"
}
各 channel 响应字段:
| channel | 返回字段 | 前端处理 |
|---|---|---|
wechat |
code_url |
渲染二维码 |
alipay |
html_form 或 redirect_url |
注入 form 自动提交 / 302 跳转 |
stripe |
redirect_url |
302 跳转 |
GET /v1/checkout/session/{payment_id}
{ "payment_id": "pay_8f3a2b", "success": true, "note": "PAID" }
各渠道回调地址(统一前缀):
| 渠道 | 回调 | 文档 |
|---|---|---|
| 微信 | /v1/payment/notify/wechat |
wechat_pay.md |
| 支付宝 | /v1/payment/notify/alipay |
alipay.md |
| Stripe | /v1/payment/notify/stripe |
stripe.md |
code_url 绑定订单号,不要暴露可替换参数的接口。success_url 白名单校验,防止钓鱼。| 渠道 | 测试方式 |
|---|---|
| 微信 | 小额真实测试(0.01 元)+ 主动查询兜底 |
| 支付宝 | 沙箱网关 + 沙箱账号扫码支付 |
| Stripe | 测试密钥 + stripe listen 本地收 Webhook + 测试卡 4242 4242 4242 4242 |
| PaymentRequest | Chrome DevTools 模拟支付方式 |
| 问题 | 原因/解决 |
|---|---|
| 二维码无法支付 | code_url 过期(约 2 小时);或订单已关闭 |
| 支付宝跳转空白 | html_form 需在页面加载完成后注入;或 return_url 非法 |
| Stripe 收银台 403 | Checkout 未开通相应支付方式,或币种/国家不匹配 |
| 微信扫码后无回调 | 回调地址公网可达;或应答未返回成功约定 |
| 轮询与回调结果不一致 | 以服务端订单状态机为准,轮询仅作展示 |
| 支付成功未发货 | 检查回调幂等标记是否已置位、Webhook 事件类型是否完整 |