# 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 托管收银台(见 [网页支付](web_payment.md)) | 本项目对应前端模型 `StripePaymentCreatedResponse`(customer / payment_intent / ephemeral_key / publishable_key / proxy_url),为 PaymentIntent + Customer + EphemeralKey 组合。 ## 2. 申请与配置 | 项目 | 说明 | | --- | --- | | Stripe 账户 | [stripe.com](https://stripe.com) 注册,中国大陆可收款(需资料审核) | | 测试密钥 | `pk_test_...`(发布) + `sk_test_...`(密钥) | | 生产密钥 | `pk_live_...` + `sk_live_...` | | Webhook 密钥 | `whsec_...`(在 Dashboard → Developers → Webhooks 配置端点后获取) | > **密钥管理**:`sk_*` 密钥只能存后端,**严禁**出现在客户端/前端代码。前端只使用 `pk_*` 发布密钥与 `client_secret`。 ## 3. 支付流程(PaymentIntent + PaymentSheet) ```mermaid 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 安装与初始化 ```bash npm install stripe ``` ```javascript // 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 ```javascript // 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 主动查询 / 确认状态 ```javascript // 客户端回前端后兜底确认 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 回调(验签) ```javascript // 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 退款 ```javascript async function refund(paymentIntentId, { amount } = {}) { const refund = await stripe.refunds.create({ payment_intent: paymentIntentId, amount, // 部分退款传金额(分);不传为全额 }); return refund; } ``` ## 5. 前端集成(Flutter) ### 5.1 依赖与初始化 ```yaml # pubspec.yaml dependencies: flutter_stripe: ^10.0.0 ``` ```dart // lib/main.dart import 'package:flutter_stripe/flutter_stripe.dart'; void main() { // 发布密钥可以放在客户端;密钥 sk_ 只能在后端 Stripe.publishableKey = 'pk_test_...'; runApp(const MyApp()); } ``` ### 5.2 拉起 PaymentSheet(标准支付) ```dart // lib/service/pay_service.dart import 'package:flutter_stripe/flutter_stripe.dart'; Future 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 模式(可选) 若需要自己渲染卡片输入框: ```dart // 收集卡信息 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/` 请求: ```json { "product_id": "p001", "currency": "usd", "source": "app" } ``` 响应(对应模型 `StripePaymentCreatedResponse`): ```json { "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 请求的场景(如无法直连海外)。 ### 6.2 查询支付状态 `GET /v1/payment/{payment_id}` ```json { "success": true, "note": "支付成功" } ``` ### 6.3 Webhook `POST /v1/payment/notify/stripe`(`application/json` 原始 body) 请求头:`Stripe-Signature: t=...,v1=...` ```json { "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 打不开 | `customer` 与 `ephemeralKey` 不匹配,或未 `initPaymentSheet` | | Webhook 验签失败 | 用了 `express.json()`;改用 `express.raw()`;或 `whsec` 与端点不匹配 | | 3DS 无法弹出 | `payment_method_types` 未包含 `card`;或后端未允许重定向 | | 扣款成功但订单未更新 | 检查 Webhook 事件类型与 `metadata.out_trade_no` 是否回传 | | 中国大陆收款受限 | Stripe 需开通相应地区;或使用 [网页支付](web_payment.md) 的收银台路由到本地支付渠道 |