# 网页支付技术文档 ## 1. 概述 网页支付指在没有原生 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` 参数分发到对应渠道。 ## 2. 方案选择 | 场景 | 推荐方案 | | --- | --- | | 中国大陆用户 + 微信 | 微信 Native 扫码 | | 中国大陆用户 + 支付宝 | 支付宝 PC/WAP 支付 | | 海外用户 / 多卡种 | Stripe Checkout(或 PaymentRequest) | | 无法接入微信/支付宝直连 | 聚合支付服务商(如 PayerMax、Adyen)或 Stripe(收支付宝/微信) | | 通用免卡支付 | Web Payment Request API | ## 3. 聚合收银台架构 ```mermaid 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[收银台展示结果] ``` ## 4. 后端集成 ### 4.1 统一收银台接口 ```javascript // 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 }); }); ``` ### 4.2 微信 Native(扫码) ```javascript // 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=... } ``` ### 4.3 支付宝 PC / 手机网站 ```javascript // 复用 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 }; } ``` ### 4.4 Stripe Checkout(托管收银台) ```javascript // 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](stripe.md))。 ### 4.5 浏览器原生 PaymentRequest(可选) 当浏览器不支持跳转收银台时,可用 Web Payments API。后端需额外提供两个端点: ```javascript // 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 }); }); ``` ## 5. 前端集成 ### 5.1 收银台页面(Web) ```html

选择支付方式

请使用微信扫码支付

前往收银台
``` ### 5.2 Flutter Web / WebView 复用 Flutter 项目的 `web/` 目录可直接托管上述收银台页面,或在 App 内用 `url_launcher` 跳转: ```dart // Flutter App 内跳转网页收银台 import 'package:url_launcher/url_launcher.dart'; Future 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); } ``` ### 5.3 前端轮询支付结果 ```javascript // 收银台前端:二维码支付后轮询(微信 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 推送,减少轮询压力(小额场景轮询即可)。 ## 6. 接口定义汇总 ### 6.1 创建收银台会话 `POST /v1/checkout/session` 请求: ```json { "channel": "wechat", "product_id": "p001", "return_url": "https://shop.example.com/pay/result" } ``` 响应: ```json { "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 跳转 | ### 6.2 查询会话状态 `GET /v1/checkout/session/{payment_id}` ```json { "payment_id": "pay_8f3a2b", "success": true, "note": "PAID" } ``` ### 6.3 异步通知 各渠道回调地址(统一前缀): | 渠道 | 回调 | 文档 | | --- | --- | --- | | 微信 | `/v1/payment/notify/wechat` | [wechat_pay.md](wechat_pay.md) | | 支付宝 | `/v1/payment/notify/alipay` | [alipay.md](alipay.md) | | Stripe | `/v1/payment/notify/stripe` | [stripe.md](stripe.md) | ## 7. 安全注意事项 1. **回调验签**:所有渠道回调先验签再处理(签名机制见各渠道文档)。 2. **金额服务端校验**:收银台创建订单与回调校验金额均以服务端为准。 3. **防重复发货**:回调幂等 + 前端轮询不作为发货依据。 4. **二维码防替换**:微信 `code_url` 绑定订单号,不要暴露可替换参数的接口。 5. **return_url 防开放重定向**:`success_url` 白名单校验,防止钓鱼。 6. **HTTPS 必须**:收银台页面与回调必须 HTTPS。 ## 8. 沙箱与测试 | 渠道 | 测试方式 | | --- | --- | | 微信 | 小额真实测试(0.01 元)+ 主动查询兜底 | | 支付宝 | 沙箱网关 + 沙箱账号扫码支付 | | Stripe | 测试密钥 + `stripe listen` 本地收 Webhook + 测试卡 `4242 4242 4242 4242` | | PaymentRequest | Chrome DevTools 模拟支付方式 | ## 9. 常见问题 | 问题 | 原因/解决 | | --- | --- | | 二维码无法支付 | `code_url` 过期(约 2 小时);或订单已关闭 | | 支付宝跳转空白 | `html_form` 需在页面加载完成后注入;或 return_url 非法 | | Stripe 收银台 403 | Checkout 未开通相应支付方式,或币种/国家不匹配 | | 微信扫码后无回调 | 回调地址公网可达;或应答未返回成功约定 | | 轮询与回调结果不一致 | 以服务端订单状态机为准,轮询仅作展示 | | 支付成功未发货 | 检查回调幂等标记是否已置位、Webhook 事件类型是否完整 |