web_payment.md 10 KB

网页支付技术文档

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. 聚合收银台架构

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 统一收银台接口

// 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(扫码)

// 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 / 手机网站

// 复用 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(托管收银台)

// 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)。

4.5 浏览器原生 PaymentRequest(可选)

当浏览器不支持跳转收银台时,可用 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 });
});

5. 前端集成

5.1 收银台页面(Web)

<!-- 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>

5.2 Flutter Web / WebView 复用

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);
}

5.3 前端轮询支付结果

// 收银台前端:二维码支付后轮询(微信 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

请求:

{ "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_formredirect_url 注入 form 自动提交 / 302 跳转
stripe redirect_url 302 跳转

6.2 查询会话状态

GET /v1/checkout/session/{payment_id}

{ "payment_id": "pay_8f3a2b", "success": true, "note": "PAID" }

6.3 异步通知

各渠道回调地址(统一前缀):

渠道 回调 文档
微信 /v1/payment/notify/wechat wechat_pay.md
支付宝 /v1/payment/notify/alipay alipay.md
Stripe /v1/payment/notify/stripe 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 事件类型是否完整