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 组合。
| 项目 | 说明 |
|---|---|
| Stripe 账户 | stripe.com 注册,中国大陆可收款(需资料审核) |
| 测试密钥 | pk_test_...(发布) + sk_test_...(密钥) |
| 生产密钥 | pk_live_... + sk_live_... |
| Webhook 密钥 | whsec_...(在 Dashboard → Developers → Webhooks 配置端点后获取) |
密钥管理:
sk_*密钥只能存后端,严禁出现在客户端/前端代码。前端只使用pk_*发布密钥与client_secret。
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双保险)
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', // 使用固定版本,避免随升级变化
});
// 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,
};
}
// 客户端回前端后兜底确认
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,
};
}
// 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应答再处理,避免超时重试。
async function refund(paymentIntentId, { amount } = {}) {
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount, // 部分退款传金额(分);不传为全额
});
return refund;
}
# 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());
}
// 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);
}
}
若需要自己渲染卡片输入框:
// 收集卡信息
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) {
// 支付成功
}
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_intent是client_secret;proxy_url可选,用于中转 Stripe 请求的场景(如无法直连海外)。
GET /v1/payment/{payment_id}
{ "success": true, "note": "支付成功" }
POST /v1/payment/notify/stripe(application/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" }
}
}
}
sk_ 密钥只在后端,前端用 pk_ 与 client_secret 即可。whsec_ + 原始 body + constructEvent;验签失败拒绝并告警。payment_intent.succeeded 可能重复投递,按 out_trade_no 幂等处理。amount(分)由后端计算,metadata 携带订单号用于回调关联。client_secret 之外的敏感数据:ephemeral_key 24h 有效,泄露仅影响单一客户。apiVersion:SDK 升级不会导致行为漂移。| 项 | 说明 |
|---|---|
| 测试密钥 | 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 |
| 问题 | 原因/解决 |
|---|---|
No such payment_intent |
client_secret 无效/过期,重新创建 PaymentIntent |
| PaymentSheet 打不开 | customer 与 ephemeralKey 不匹配,或未 initPaymentSheet |
| Webhook 验签失败 | 用了 express.json();改用 express.raw();或 whsec 与端点不匹配 |
| 3DS 无法弹出 | payment_method_types 未包含 card;或后端未允许重定向 |
| 扣款成功但订单未更新 | 检查 Webhook 事件类型与 metadata.out_trade_no 是否回传 |
| 中国大陆收款受限 | Stripe 需开通相应地区;或使用 网页支付 的收银台路由到本地支付渠道 |