web-payments
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb Payments Skill (Stripe)
网页支付技能(Stripe)
Load with: base.md + [framework].md
For integrating Stripe payments into web applications - one-time payments, subscriptions, and checkout flows.
Sources: Stripe Checkout | Payment Element Best Practices | Building Solid Stripe Integrations | Subscriptions
加载方式:base.md + [framework].md
用于在网页应用中集成Stripe支付功能——涵盖一次性支付、订阅和结账流程。
Setup
设置步骤
1. Create Stripe Account
1. 创建Stripe账户
- Go to https://dashboard.stripe.com/register
- Complete business verification
- Get API keys from https://dashboard.stripe.com/apikeys
2. Environment Variables
2. 环境变量
bash
undefinedbash
undefined.env
.env
STRIPE_SECRET_KEY=sk_test_xxx # Server-side only
STRIPE_PUBLISHABLE_KEY=pk_test_xxx # Client-side safe
STRIPE_WEBHOOK_SECRET=whsec_xxx # For webhook verification
STRIPE_SECRET_KEY=sk_test_xxx # 仅在服务端使用
STRIPE_PUBLISHABLE_KEY=pk_test_xxx # 可在客户端安全使用
STRIPE_WEBHOOK_SECRET=whsec_xxx # 用于Webhook验证
Production
生产环境
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_PUBLISHABLE_KEY=pk_live_xxx
undefinedSTRIPE_SECRET_KEY=sk_live_xxx
STRIPE_PUBLISHABLE_KEY=pk_live_xxx
undefined3. Install SDK
3. 安装SDK
bash
undefinedbash
undefinedNode.js
Node.js
npm install stripe @stripe/stripe-js
npm install stripe @stripe/stripe-js
Python
Python
pip install stripe
---pip install stripe
---Integration Options
集成选项
| Method | Best For | Complexity |
|---|---|---|
| Checkout (Hosted) | Quick setup, Stripe-hosted page | Low |
| Checkout (Embedded) | Custom site, embedded form | Low |
| Payment Element | Full customization, complex flows | Medium |
| Custom Form | Complete control (rare) | High |
Recommendation: Start with Checkout, migrate to Payment Element if needed.
| 方法 | 适用场景 | 复杂度 |
|---|---|---|
| Checkout(托管式) | 快速搭建,Stripe托管页面 | 低 |
| Checkout(嵌入式) | 自定义网站,嵌入式表单 | 低 |
| Payment Element | 完全自定义,复杂流程 | 中 |
| 自定义表单 | 完全控制(罕见场景) | 高 |
推荐方案:从Checkout开始,如需自定义再迁移到Payment Element。
Stripe Checkout (Recommended)
Stripe Checkout(推荐方案)
Server: Create Checkout Session
服务端:创建结账会话
Node.js / Next.js
Node.js / Next.js
typescript
// app/api/checkout/route.ts (Next.js App Router)
import Stripe from "stripe";
import { NextResponse } from "next/server";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, mode = "payment" } = await request.json();
try {
const session = await stripe.checkout.sessions.create({
mode: mode as "payment" | "subscription",
payment_method_types: ["card"],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/canceled`,
// Optional: Link to existing customer
// customer: customerId,
// Optional: Collect shipping
// shipping_address_collection: { allowed_countries: ["US", "CA"] },
// Optional: Add metadata for tracking
metadata: {
userId: "user_123",
source: "pricing_page",
},
});
return NextResponse.json({ sessionId: session.id, url: session.url });
} catch (error) {
console.error("Stripe error:", error);
return NextResponse.json({ error: "Failed to create session" }, { status: 500 });
}
}typescript
// app/api/checkout/route.ts (Next.js App Router)
import Stripe from "stripe";
import { NextResponse } from "next/server";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, mode = "payment" } = await request.json();
try {
const session = await stripe.checkout.sessions.create({
mode: mode as "payment" | "subscription",
payment_method_types: ["card"],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/canceled`,
// 可选:关联已有客户
// customer: customerId,
// 可选:收集配送信息
// shipping_address_collection: { allowed_countries: ["US", "CA"] },
// 可选:添加元数据用于追踪
metadata: {
userId: "user_123",
source: "pricing_page",
},
});
return NextResponse.json({ sessionId: session.id, url: session.url });
} catch (error) {
console.error("Stripe错误:", error);
return NextResponse.json({ error: "创建会话失败" }, { status: 500 });
}
}Python / FastAPI
Python / FastAPI
python
undefinedpython
undefinedapp/api/checkout.py
app/api/checkout.py
import stripe
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
import os
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
router = APIRouter()
class CheckoutRequest(BaseModel):
price_id: str
mode: str = "payment" # or "subscription"
@router.post("/api/checkout")
async def create_checkout_session(request: CheckoutRequest):
try:
session = stripe.checkout.Session.create(
mode=request.mode,
payment_method_types=["card"],
line_items=[{
"price": request.price_id,
"quantity": 1,
}],
success_url=f"{os.environ['APP_URL']}/success?session_id={{CHECKOUT_SESSION_ID}}",
cancel_url=f"{os.environ['APP_URL']}/canceled",
metadata={
"user_id": "user_123",
},
)
return {"session_id": session.id, "url": session.url}
except stripe.error.StripeError as e:
raise HTTPException(status_code=400, detail=str(e))
undefinedimport stripe
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
import os
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
router = APIRouter()
class CheckoutRequest(BaseModel):
price_id: str
mode: str = "payment" # 或 "subscription"
@router.post("/api/checkout")
async def create_checkout_session(request: CheckoutRequest):
try:
session = stripe.checkout.Session.create(
mode=request.mode,
payment_method_types=["card"],
line_items=[{
"price": request.price_id,
"quantity": 1,
}],
success_url=f"{os.environ['APP_URL']}/success?session_id={{CHECKOUT_SESSION_ID}}",
cancel_url=f"{os.environ['APP_URL']}/canceled",
metadata={
"user_id": "user_123",
},
)
return {"session_id": session.id, "url": session.url}
except stripe.error.StripeError as e:
raise HTTPException(status_code=400, detail=str(e))
undefinedClient: Redirect to Checkout
客户端:跳转到结账页面
typescript
// components/CheckoutButton.tsx
"use client";
import { loadStripe } from "@stripe/stripe-js";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export function CheckoutButton({ priceId }: { priceId: string }) {
const handleCheckout = async () => {
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId }),
});
const { url } = await response.json();
// Redirect to Stripe Checkout
window.location.href = url;
};
return (
<button onClick={handleCheckout}>
Subscribe Now
</button>
);
}typescript
// components/CheckoutButton.tsx
"use client";
import { loadStripe } from "@stripe/stripe-js";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export function CheckoutButton({ priceId }: { priceId: string }) {
const handleCheckout = async () => {
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId }),
});
const { url } = await response.json();
// 跳转到Stripe Checkout
window.location.href = url;
};
return (
<button onClick={handleCheckout}>
立即订阅
</button>
);
}Embedded Checkout
嵌入式Checkout
For keeping users on your site:
typescript
// components/EmbeddedCheckout.tsx
"use client";
import { useEffect, useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import {
EmbeddedCheckoutProvider,
EmbeddedCheckout,
} from "@stripe/react-stripe-js";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export function EmbeddedCheckoutForm({ priceId }: { priceId: string }) {
const [clientSecret, setClientSecret] = useState("");
useEffect(() => {
fetch("/api/checkout/embedded", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId }),
})
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret));
}, [priceId]);
if (!clientSecret) return <div>Loading...</div>;
return (
<EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
);
}Server endpoint for embedded:
typescript
// app/api/checkout/embedded/route.ts
export async function POST(request: Request) {
const { priceId } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
ui_mode: "embedded",
return_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
});
return NextResponse.json({ clientSecret: session.client_secret });
}用于让用户停留在你的网站内完成支付:
typescript
// components/EmbeddedCheckout.tsx
"use client";
import { useEffect, useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import {
EmbeddedCheckoutProvider,
EmbeddedCheckout,
} from "@stripe/react-stripe-js";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export function EmbeddedCheckoutForm({ priceId }: { priceId: string }) {
const [clientSecret, setClientSecret] = useState("");
useEffect(() => {
fetch("/api/checkout/embedded", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId }),
})
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret));
}, [priceId]);
if (!clientSecret) return <div>加载中...</div>;
return (
<EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
);
}嵌入式支付的服务端端点:
typescript
// app/api/checkout/embedded/route.ts
export async function POST(request: Request) {
const { priceId } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
ui_mode: "embedded",
return_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
});
return NextResponse.json({ clientSecret: session.client_secret });
}Webhooks (Critical)
Webhook(关键功能)
Never trust client-side data. Always verify payments via webhooks.
绝不要信任客户端数据。始终通过Webhook验证支付状态。
Webhook Endpoint
Webhook端点
typescript
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { headers } from "next/headers";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get("stripe-signature")!;
let event: Stripe.Event;
// Verify webhook signature
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error("Webhook signature verification failed");
return new Response("Invalid signature", { status: 400 });
}
// Handle events
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case "customer.subscription.created":
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCanceled(subscription);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
// Return 200 quickly - process async if needed
return new Response("OK", { status: 200 });
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId;
const customerId = session.customer as string;
const subscriptionId = session.subscription as string;
// Update your database
await db.user.update({
where: { id: userId },
data: {
stripeCustomerId: customerId,
stripeSubscriptionId: subscriptionId,
subscriptionStatus: "active",
},
});
}typescript
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { headers } from "next/headers";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get("stripe-signature")!;
let event: Stripe.Event;
// 验证Webhook签名
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error("Webhook签名验证失败");
return new Response("无效签名", { status: 400 });
}
// 处理事件
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case "customer.subscription.created":
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCanceled(subscription);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
default:
console.log(`未处理的事件类型: ${event.type}`);
}
// 快速返回200状态码——如需复杂处理可异步执行
return new Response("OK", { status: 200 });
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId;
const customerId = session.customer as string;
const subscriptionId = session.subscription as string;
// 更新你的数据库
await db.user.update({
where: { id: userId },
data: {
stripeCustomerId: customerId,
stripeSubscriptionId: subscriptionId,
subscriptionStatus: "active",
},
});
}Python Webhook
Python Webhook实现
python
undefinedpython
undefinedapp/api/webhooks.py
app/api/webhooks.py
import stripe
from fastapi import APIRouter, Request, HTTPException
router = APIRouter()
@router.post("/api/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, os.environ["STRIPE_WEBHOOK_SECRET"]
)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid payload")
except stripe.error.SignatureVerificationError:
raise HTTPException(status_code=400, detail="Invalid signature")
# Handle events
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
await handle_checkout_complete(session)
elif event["type"] == "customer.subscription.deleted":
subscription = event["data"]["object"]
await handle_subscription_canceled(subscription)
return {"status": "success"}undefinedimport stripe
from fastapi import APIRouter, Request, HTTPException
router = APIRouter()
@router.post("/api/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, os.environ["STRIPE_WEBHOOK_SECRET"]
)
except ValueError:
raise HTTPException(status_code=400, detail="无效负载")
except stripe.error.SignatureVerificationError:
raise HTTPException(status_code=400, detail="无效签名")
# 处理事件
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
await handle_checkout_complete(session)
elif event["type"] == "customer.subscription.deleted":
subscription = event["data"]["object"]
await handle_subscription_canceled(subscription)
return {"status": "success"}undefinedKey Webhook Events
关键Webhook事件
| Event | When | Action |
|---|---|---|
| Payment successful | Provision access |
| New subscription | Store subscription ID |
| Plan change | Update plan in DB |
| Canceled | Revoke access |
| Payment failed | Notify user, retry |
| Renewal successful | Extend access |
| 事件 | 触发时机 | 处理动作 |
|---|---|---|
| 支付成功 | 开通用户权限 |
| 新订阅创建 | 存储订阅ID |
| 套餐变更 | 更新数据库中的套餐信息 |
| 订阅取消 | 收回用户权限 |
| 支付失败 | 通知用户,重试支付 |
| 续费成功 | 延长用户权限 |
Products & Prices
产品与价格
Create via Dashboard (Recommended)
通过仪表盘创建(推荐)
- Go to https://dashboard.stripe.com/products
- Create product with name, description
- Add price(s) - one-time or recurring
- Copy Price ID ()
price_xxx
- 访问https://dashboard.stripe.com/products
- 创建产品并填写名称、描述
- 添加价格——支持一次性或周期性
- 复制价格ID()
price_xxx
Create via API
通过API创建
typescript
// One-time product
const product = await stripe.products.create({
name: "Pro Plan",
description: "Full access to all features",
});
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2999, // $29.99 in cents
currency: "usd",
});
// Subscription product
const subscriptionPrice = await stripe.prices.create({
product: product.id,
unit_amount: 999, // $9.99/month
currency: "usd",
recurring: {
interval: "month",
},
});typescript
// 一次性支付产品
const product = await stripe.products.create({
name: "专业版套餐",
description: "全功能访问权限",
});
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2999, // 29.99美元,以美分为单位
currency: "usd",
});
// 订阅产品
const subscriptionPrice = await stripe.prices.create({
product: product.id,
unit_amount: 999, // 9.99美元/月
currency: "usd",
recurring: {
interval: "month",
},
});Customer Portal
客户门户
Let users manage their subscriptions:
typescript
// app/api/portal/route.ts
export async function POST(request: Request) {
const { customerId } = await request.json();
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/settings`,
});
return NextResponse.json({ url: session.url });
}Configure portal at: https://dashboard.stripe.com/settings/billing/portal
让用户自行管理订阅:
typescript
// app/api/portal/route.ts
export async function POST(request: Request) {
const { customerId } = await request.json();
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/settings`,
});
return NextResponse.json({ url: session.url });
}Subscriptions
订阅功能
Create Subscription with Trial
创建带试用期的订阅
typescript
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: {
trial_period_days: 14,
// Cancel if no payment method after trial
trial_settings: {
end_behavior: { missing_payment_method: "cancel" },
},
},
success_url: successUrl,
cancel_url: cancelUrl,
});typescript
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: {
trial_period_days: 14,
// 试用期结束后若无支付方式则取消订阅
trial_settings: {
end_behavior: { missing_payment_method: "cancel" },
},
},
success_url: successUrl,
cancel_url: cancelUrl,
});Check Subscription Status
检查订阅状态
typescript
// lib/subscription.ts
export async function getSubscriptionStatus(customerId: string) {
const subscriptions = await stripe.subscriptions.list({
customer: customerId,
status: "all",
limit: 1,
});
if (subscriptions.data.length === 0) {
return { status: "none", plan: null };
}
const subscription = subscriptions.data[0];
return {
status: subscription.status,
plan: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
};
}typescript
// lib/subscription.ts
export async function getSubscriptionStatus(customerId: string) {
const subscriptions = await stripe.subscriptions.list({
customer: customerId,
status: "all",
limit: 1,
});
if (subscriptions.data.length === 0) {
return { status: "none", plan: null };
}
const subscription = subscriptions.data[0];
return {
status: subscription.status,
plan: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
};
}Testing
测试
Test Cards
测试卡号
| Card Number | Scenario |
|---|---|
| Success |
| Declined |
| Requires 3D Secure |
| Insufficient funds |
| 卡号 | 测试场景 |
|---|---|
| 支付成功 |
| 支付被拒绝 |
| 需要3D安全验证 |
| 余额不足 |
Stripe CLI for Webhooks
使用Stripe CLI测试Webhook
bash
undefinedbash
undefinedInstall CLI
安装CLI
brew install stripe/stripe-cli/stripe
brew install stripe/stripe-cli/stripe
Login
登录
stripe login
stripe login
Forward webhooks to local server
将Webhook转发到本地服务器
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Trigger test events
触发测试事件
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
---stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
---Project Structure
项目结构
project/
├── app/
│ ├── api/
│ │ ├── checkout/
│ │ │ └── route.ts # Create checkout session
│ │ ├── portal/
│ │ │ └── route.ts # Customer portal
│ │ └── webhooks/
│ │ └── stripe/
│ │ └── route.ts # Webhook handler
│ ├── pricing/
│ │ └── page.tsx # Pricing page
│ ├── success/
│ │ └── page.tsx # Post-checkout success
│ └── settings/
│ └── page.tsx # Manage subscription
├── lib/
│ ├── stripe.ts # Stripe client
│ └── subscription.ts # Subscription helpers
└── .env.localproject/
├── app/
│ ├── api/
│ │ ├── checkout/
│ │ │ └── route.ts # 创建结账会话
│ │ ├── portal/
│ │ │ └── route.ts # 客户门户
│ │ └── webhooks/
│ │ └── stripe/
│ │ └── route.ts # Webhook处理器
│ ├── pricing/
│ │ └── page.tsx # 定价页面
│ ├── success/
│ │ └── page.tsx # 结账成功页
│ └── settings/
│ └── page.tsx # 订阅管理页
├── lib/
│ ├── stripe.ts # Stripe客户端
│ └── subscription.ts # 订阅辅助函数
└── .env.localSecurity Best Practices
安全最佳实践
Non-Negotiable Rules
必须遵守的规则
- Server-side only for secrets - Never expose
STRIPE_SECRET_KEY - Always verify webhooks - Check signature before processing
- Idempotency - Store webhook event IDs, skip duplicates
- Use metadata - Track user IDs, sources for debugging
- Handle all states - Success, failure, pending, canceled
- 密钥仅在服务端使用 - 绝不要暴露
STRIPE_SECRET_KEY - 始终验证Webhook - 处理前先检查签名
- 幂等性 - 存储Webhook事件ID,跳过重复事件
- 使用元数据 - 追踪用户ID、来源信息用于调试
- 处理所有状态 - 成功、失败、待处理、取消
Idempotent Webhook Handler
幂等Webhook处理器
typescript
const processedEvents = new Set<string>(); // Use Redis in production
export async function POST(request: Request) {
// ... verify signature ...
// Skip duplicate events
if (processedEvents.has(event.id)) {
return new Response("Already processed", { status: 200 });
}
processedEvents.add(event.id);
// Process event...
}typescript
const processedEvents = new Set<string>(); // 生产环境使用Redis
export async function POST(request: Request) {
// ... 验证签名 ...
// 跳过重复事件
if (processedEvents.has(event.id)) {
return new Response("已处理", { status: 200 });
}
processedEvents.add(event.id);
// 处理事件...
}Amount Handling
金额处理
typescript
// Always use cents (smallest currency unit)
const priceInCents = 2999; // $29.99
// Helper functions
const toCents = (dollars: number) => Math.round(dollars * 100);
const toDollars = (cents: number) => cents / 100;
// Display
const displayPrice = (cents: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(toDollars(cents));typescript
// 始终使用美分(最小货币单位)
const priceInCents = 2999; // 29.99美元
// 辅助函数
const toCents = (dollars: number) => Math.round(dollars * 100);
const toDollars = (cents: number) => cents / 100;
// 显示金额
const displayPrice = (cents: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(toDollars(cents));Common Patterns
常见实现模式
Pricing Page
定价页面
typescript
// app/pricing/page.tsx
const plans = [
{
name: "Starter",
price: "$9/mo",
priceId: "price_starter_monthly",
features: ["Feature 1", "Feature 2"],
},
{
name: "Pro",
price: "$29/mo",
priceId: "price_pro_monthly",
features: ["Everything in Starter", "Feature 3", "Feature 4"],
popular: true,
},
];
export default function PricingPage() {
return (
<div className="grid md:grid-cols-2 gap-8">
{plans.map((plan) => (
<div key={plan.name} className={plan.popular ? "border-blue-500" : ""}>
<h3>{plan.name}</h3>
<p>{plan.price}</p>
<ul>
{plan.features.map((f) => <li key={f}>{f}</li>)}
</ul>
<CheckoutButton priceId={plan.priceId} />
</div>
))}
</div>
);
}typescript
// app/pricing/page.tsx
const plans = [
{
name: "入门版",
price: "$9/月",
priceId: "price_starter_monthly",
features: ["功能1", "功能2"],
},
{
name: "专业版",
price: "$29/月",
priceId: "price_pro_monthly",
features: ["包含入门版所有功能", "功能3", "功能4"],
popular: true,
},
];
export default function PricingPage() {
return (
<div className="grid md:grid-cols-2 gap-8">
{plans.map((plan) => (
<div key={plan.name} className={plan.popular ? "border-blue-500" : ""}>
<h3>{plan.name}</h3>
<p>{plan.price}</p>
<ul>
{plan.features.map((f) => <li key={f}>{f}</li>)}
</ul>
<CheckoutButton priceId={plan.priceId} />
</div>
))}
</div>
);
}Protect Routes by Subscription
根据订阅状态保护路由
typescript
// middleware.ts
import { getSubscriptionStatus } from "@/lib/subscription";
export async function middleware(request: NextRequest) {
const session = await getSession();
if (request.nextUrl.pathname.startsWith("/pro")) {
const { status } = await getSubscriptionStatus(session.stripeCustomerId);
if (status !== "active" && status !== "trialing") {
return NextResponse.redirect(new URL("/pricing", request.url));
}
}
}typescript
// middleware.ts
import { getSubscriptionStatus } from "@/lib/subscription";
export async function middleware(request: NextRequest) {
const session = await getSession();
if (request.nextUrl.pathname.startsWith("/pro")) {
const { status } = await getSubscriptionStatus(session.stripeCustomerId);
if (status !== "active" && status !== "trialing") {
return NextResponse.redirect(new URL("/pricing", request.url));
}
}
}Anti-Patterns
反模式
- Hardcoding API keys - Use environment variables
- Client-side payment creation - Always create PaymentIntent/Session server-side
- Skipping webhook verification - Always verify signatures
- Processing duplicate webhooks - Implement idempotency
- Floating-point currency math - Use integers (cents)
- Trusting client data - Verify everything server-side
- Ignoring failed payments - Handle
invoice.payment_failed - No error handling - Catch and handle Stripe errors
- 硬编码API密钥 - 使用环境变量
- 客户端创建支付 - 始终在服务端创建PaymentIntent/会话
- 跳过Webhook验证 - 始终验证签名
- 处理重复Webhook - 实现幂等性
- 使用浮点数处理货币 - 使用整数(美分)
- 信任客户端数据 - 所有验证在服务端完成
- 忽略支付失败 - 处理事件
invoice.payment_failed - 无错误处理 - 捕获并处理Stripe错误
Quick Reference
快速参考
bash
undefinedbash
undefinedInstall
安装依赖
npm install stripe @stripe/stripe-js @stripe/react-stripe-js
npm install stripe @stripe/stripe-js @stripe/react-stripe-js
Stripe CLI
Stripe CLI命令
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
Test mode prefix
测试环境密钥前缀
sk_test_xxx # Secret key
pk_test_xxx # Publishable key
sk_test_xxx # 密钥
pk_test_xxx # 可发布密钥
Live mode prefix
生产环境密钥前缀
sk_live_xxx
pk_live_xxx
undefinedsk_live_xxx
pk_live_xxx
undefinedKey Endpoints
关键端点
| Endpoint | Purpose |
|---|---|
| Create checkout session |
| Customer billing portal |
| Handle Stripe events |
| 端点 | 用途 |
|---|---|
| 创建结账会话 |
| 客户账单门户 |
| 处理Stripe事件 |
Environment Variables
环境变量
bash
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxbash
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx