stripe.md 11 KB

Stripe 技术文档

1. 概述

Stripe 是全球主流支付服务商,一次接入可覆盖多币种、多支付方式(卡 / Apple Pay / Google Pay / SEPA / 支付宝 / 微信等,取决于地区)。本文覆盖三种集成模式:

模式 适用端 说明
PaymentIntent + PaymentSheet iOS / Android App 原生支付界面,支持 Apple Pay / Google Pay / 3DS
PaymentIntent(自定义 UI) 全端 自己收集卡信息,用 confirmPayment
Checkout Session Web 跳转 Stripe 托管收银台(见 网页支付

本项目对应前端模型 StripePaymentCreatedResponse(customer / payment_intent / ephemeral_key / publishable_key / proxy_url),为 PaymentIntent + Customer + EphemeralKey 组合。

2. 申请与配置

项目 说明
Stripe 账户 stripe.com 注册,中国大陆可收款(需资料审核)
测试密钥 pk_test_...(发布) + sk_test_...(密钥)
生产密钥 pk_live_... + sk_live_...
Webhook 密钥 whsec_...(在 Dashboard → Developers → Webhooks 配置端点后获取)

密钥管理sk_* 密钥只能存后端,严禁出现在客户端/前端代码。前端只使用 pk_* 发布密钥与 client_secret

3. 支付流程(PaymentIntent + PaymentSheet)

sequenceDiagram
    participant App as Flutter App
    participant Backend as 业务后端
    participant Stripe as Stripe API

    App->>Backend: 1. POST /v1/payment/stripe/ {product_id}
    Backend->>Stripe: 2a. 创建 Customer(可选)
    Backend->>Stripe: 2b. 创建 PaymentIntent + 绑定Customer
    Backend->>Stripe: 2c. 生成 EphemeralKey
    Backend-->>App: { payment_intent, customer, ephemeral_key, publishable_key }
    App->>Stripe: 3. initPaymentSheet(client_secret)
    App->>Stripe: 4. presentPaymentSheet() 用户输入卡/Apple Pay
    Stripe->>Stripe: 扣款 + 3DS 验证
    Stripe->>Backend: 5. Webhook payment_intent.succeeded
    Backend->>Backend: 验签 + 幂等更新订单 + 发货
    App->>Backend: 6. 查询最终状态(与Webhook双保险)

4. 后端集成(Node.js/Express)

4.1 安装与初始化

npm install stripe
// src/services/stripe_service.js
const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-06-20', // 使用固定版本,避免随升级变化
});

4.2 创建 PaymentIntent + Customer + EphemeralKey

// src/services/stripe_service.js
const { v4: uuidv4 } = require('uuid');

/**
 * 创建 Stripe 支付所需全部参数
 * @param {string} outTradeNo 商户订单号
 * @param {number} amount 金额,单位:分
 * @param {string} currency 币种,如 usd
 * @returns {{ paymentId, customer, paymentIntent, ephemeralKey }}
 */
async function createPayment({ outTradeNo, amount, currency }) {
  // 1. 可选:创建/复用 Customer(用于绑定支付方式、生成 EphemeralKey)
  const customer = await stripe.customers.create({
    metadata: { out_trade_no: outTradeNo },
  });

  // 2. 创建 PaymentIntent(自动抓取支付方式,含 Apple/Google Pay 需传 card)
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency,
    customer: customer.id,
    payment_method_types: ['card'],
    metadata: { out_trade_no: outTradeNo },
    // 若需自动捕获:automatic_payment_methods: { enabled: true }
  });

  // 3. EphemeralKey:客户端 initPaymentSheet 绑定 Customer 用,有效 24h
  const ephemeralKey = await stripe.ephemeralKeys.create(
    { customer: customer.id },
    { apiVersion: '2024-06-20' }
  );

  return {
    paymentId: paymentIntent.id,
    customer: customer.id,
    paymentIntent: paymentIntent.client_secret,
    ephemeralKey: ephemeralKey.secret,
  };
}

4.3 主动查询 / 确认状态

// 客户端回前端后兜底确认
async function queryPaymentIntent(paymentIntentId) {
  const pi = await stripe.paymentIntents.retrieve(paymentIntentId);
  return {
    success: pi.status === 'succeeded',
    status: pi.status, // requires_payment_method | requires_confirmation | requires_action | succeeded
    note: pi.status,
  };
}

4.4 Webhook 回调(验签)

// src/controllers/payment_controller.js
const express = require('express');
const router = express.Router();
const stripe = require('../services/stripe_service');

/**
 * Stripe Webhook:必须用 express.raw 保留原始 body,否则验签失败
 */
router.post('/v1/payment/notify/stripe',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['stripe-signature'];
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,                                  // 原始 body(Buffer)
        signature,
        process.env.STRIPE_WEBHOOK_SECRET,         // whsec_...
      );
    } catch (err) {
      console.warn('[stripe] Webhook 验签失败:', err.message);
      return res.status(400).json({ received: false });
    }

    switch (event.type) {
      case 'payment_intent.succeeded': {
        const pi = event.data.object;
        // 幂等:按 metadata.out_trade_no 更新订单
        orderService
          .markPaid(pi.metadata.out_trade_no, { channel: 'stripe', channelTradeNo: pi.id })
          .then(() => fulfillmentService.deliver(pi.metadata.out_trade_no));
        break;
      }
      case 'payment_intent.payment_failed':
        // 记录失败(不发货)
        console.warn('[stripe] payment failed', event.data.object.id);
        break;
      default:
        console.log(`[stripe] unhandled event ${event.type}`);
    }

    // 立即应答,Stripe 未收到 2xx 会重试
    res.json({ received: true });
  });

Webhook 要点

  • express.raw() 必须在业务中间件之前使用,否则 body 已被 express.json() 消费。
  • constructEvent 返回前必须成功校验签名,失败直接 4xx。
  • 处理逻辑异步执行,先 200 应答再处理,避免超时重试。

4.5 退款

async function refund(paymentIntentId, { amount } = {}) {
  const refund = await stripe.refunds.create({
    payment_intent: paymentIntentId,
    amount, // 部分退款传金额(分);不传为全额
  });
  return refund;
}

5. 前端集成(Flutter)

5.1 依赖与初始化

# pubspec.yaml
dependencies:
  flutter_stripe: ^10.0.0
// lib/main.dart
import 'package:flutter_stripe/flutter_stripe.dart';

void main() {
  // 发布密钥可以放在客户端;密钥 sk_ 只能在后端
  Stripe.publishableKey = 'pk_test_...';
  runApp(const MyApp());
}

5.2 拉起 PaymentSheet(标准支付)

// lib/service/pay_service.dart
import 'package:flutter_stripe/flutter_stripe.dart';

Future<void> stripePay() async {
  try {
    // 1. 后端创建 PaymentIntent/Customer/EphemeralKey
    final created = await APIServer().createStripePayment(
      productId: product.id,
      source: paymentSource(),
    );

    // 2. 初始化 PaymentSheet
    await Stripe.instance.initPaymentSheet(
      paymentSheetParameters: SetupPaymentSheetParameters(
        merchantDisplayName: 'Example Store',
        customerId: created.customer,
        paymentIntentClientSecret: created.paymentIntent,
        customerEphemeralKeySecret: created.ephemeralKey,
        // 可选:启用 Apple Pay / Google Pay
        // applePay: PaymentSheetApplePay(merchantCountryCode: 'US'),
        // googlePay: PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true),
      ),
    );

    // 3. 展示支付界面
    await Stripe.instance.presentPaymentSheet();

    // 4. 弹窗关闭即支付成功,仍以后端查询为准
    final resp = await APIServer().queryPaymentStatus(created.paymentId);
    if (resp.success) showSuccessMessage(resp.note ?? '支付成功');
  } on StripeException catch (e) {
    // 用户取消或失败
    showErrorMessage(e.error.localizedMessage ?? '支付失败');
  } on Exception catch (e) {
    showErrorMessageEnhanced(context, e);
  }
}

5.3 自定义 UI 模式(可选)

若需要自己渲染卡片输入框:

// 收集卡信息
final card = CardNumberInput();
final params = PaymentMethodParams.card(
  paymentMethodData: PaymentMethodData(
    billingDetails: const BillingDetails(email: 'user@example.com'),
  ),
);

// 确认支付(触发 3DS 等)
final pi = await Stripe.instance.confirmPayment(
  paymentIntentClientSecret: created.paymentIntent,
  data: params,
);
if (pi.status == PaymentIntentsStatus.Succeeded) {
  // 支付成功
}

6. 接口定义

6.1 创建 Stripe 支付

POST /v1/payment/stripe/

请求:

{ "product_id": "p001", "currency": "usd", "source": "app" }

响应(对应模型 StripePaymentCreatedResponse):

{
  "payment_id": "pi_3Qx...",
  "customer": "cus_...",
  "payment_intent": "pi_3Qx..._secret_xxx",
  "ephemeral_key": "ek_test_...",
  "publishable_key": "pk_test_...",
  "proxy_url": "https://example.com/stripe"
}

说明:payment_intentclient_secretproxy_url 可选,用于中转 Stripe 请求的场景(如无法直连海外)。

6.2 查询支付状态

GET /v1/payment/{payment_id}

{ "success": true, "note": "支付成功" }

6.3 Webhook

POST /v1/payment/notify/stripeapplication/json 原始 body)

请求头:Stripe-Signature: t=...,v1=...

{
  "id": "evt_...",
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_3Qx...",
      "status": "succeeded",
      "metadata": { "out_trade_no": "pay_8f3a2b" }
    }
  }
}

7. 安全注意事项

  1. sk_ 密钥只在后端,前端用 pk_client_secret 即可。
  2. Webhook 必须验签whsec_ + 原始 body + constructEvent;验签失败拒绝并告警。
  3. 幂等payment_intent.succeeded 可能重复投递,按 out_trade_no 幂等处理。
  4. 金额服务端为准amount(分)由后端计算,metadata 携带订单号用于回调关联。
  5. 不使用 client_secret 之外的敏感数据ephemeral_key 24h 有效,泄露仅影响单一客户。
  6. 固定 apiVersion:SDK 升级不会导致行为漂移。

8. 沙箱与测试

说明
测试密钥 Dashboard → Developers → API keys,pk_test_/sk_test_
测试卡 4242 4242 4242 4242(成功)、4000 0000 0000 0002(拒付)、4000 0025 0000 3155(3DS 必用)
本地 Webhook stripe listen --forward-to localhost:3000/v1/payment/notify/stripe
CLI 触发 stripe trigger payment_intent.succeeded

9. 常见问题

问题 原因/解决
No such payment_intent client_secret 无效/过期,重新创建 PaymentIntent
PaymentSheet 打不开 customerephemeralKey 不匹配,或未 initPaymentSheet
Webhook 验签失败 用了 express.json();改用 express.raw();或 whsec 与端点不匹配
3DS 无法弹出 payment_method_types 未包含 card;或后端未允许重定向
扣款成功但订单未更新 检查 Webhook 事件类型与 metadata.out_trade_no 是否回传
中国大陆收款受限 Stripe 需开通相应地区;或使用 网页支付 的收银台路由到本地支付渠道