WebPaymentsSkill(Stripe) web-payments

集成Stripe支付,支持一次性支付、订阅服务、Webhooks和客户门户管理。

支付系统 0 次安装 0 次浏览 更新于 3/5/2026

name: web-payments description: Stripe Checkout, subscriptions, webhooks, customer portal

网络支付技能(Stripe)

与:base.md + [framework].md 一起加载

用于将Stripe支付集成到Web应用程序中 - 一次性支付、订阅和结账流程。

来源: Stripe Checkout | Payment Element 最佳实践 | 构建可靠的Stripe集成 | Subscriptions


设置

1. 创建Stripe账户

  1. 访问 https://dashboard.stripe.com/register
  2. 完成商业验证
  3. https://dashboard.stripe.com/apikeys 获取API密钥

2. 环境变量

# .env
STRIPE_SECRET_KEY=sk_test_xxx          # 仅限服务器端
STRIPE_PUBLISHABLE_KEY=pk_test_xxx     # 客户端安全
STRIPE_WEBHOOK_SECRET=whsec_xxx        # 用于Webhook验证

# 生产环境
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_PUBLISHABLE_KEY=pk_live_xxx

3. 安装SDK

# Node.js
npm install stripe @stripe/stripe-js

# Python
pip install stripe

集成选项

方法 最适合 复杂度
Checkout (Hosted) 快速设置,Stripe托管页面
Checkout (Embedded) 自定义站点,嵌入式表单
Payment Element 全定制,复杂流程 中等
Custom Form 完全控制(罕见)

推荐:从Checkout开始,如有需要迁移到Payment Element。


Stripe Checkout(推荐)

服务器:创建结账会话

Node.js / Next.js

// 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:", error);
    return NextResponse.json({ error: "Failed to create session" }, { status: 500 });
  }
}

Python / FastAPI

# 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))

客户端:重定向到结账

// 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结账
    window.location.href = url;
  };

  return (
    <button onClick={handleCheckout}>
      立即订阅
    </button>
  );
}

嵌入式结账

为了保持用户在您的网站上:

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

服务器端嵌入式结账:

// 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(关键)

永远不要信任客户端数据。始终通过Webhooks验证支付。

Webhook端点

// 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("Invalid signature", { 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(`Unhandled event type: ${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

# 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")

    # 处理事件
    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"}

关键Webhook事件

事件 何时 操作
checkout.session.completed 支付成功 提供访问权限
customer.subscription.created 新订阅 存储订阅ID
customer.subscription.updated 计划更改 更新数据库中的计划
customer.subscription.deleted 已取消 撤销访问权限
invoice.payment_failed 支付失败 通知用户,重试
invoice.paid 续订成功 延长访问权限

产品和价格

通过仪表板创建(推荐)

  1. 访问 https://dashboard.stripe.com/products
  2. 创建产品,包括名称、描述
  3. 添加价格 - 一次性或定期
  4. 复制价格ID(price_xxx

通过API创建

// 一次性产品
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",
});

// 订阅产品
const subscriptionPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 999, // $9.99/month
  currency: "usd",
  recurring: {
    interval: "month",
  },
});

客户门户

让用户管理他们的订阅:

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

配置门户:https://dashboard.stripe.com/settings/billing/portal


订阅

创建带有试用期的订阅

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

检查订阅状态

// 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,
  };
}

测试

测试卡

卡号 场景
4242424242424242 成功
4000000000000002 被拒绝
4000002500003155 需要3D Secure
4000000000009995 资金不足

Stripe CLI用于Webhooks

# 安装CLI
brew install stripe/stripe-cli/stripe

# 登录
stripe login

# 将Webhooks转发到本地服务器
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# 触发测试事件
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted

项目结构

project/
├── 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.local

安全最佳实践

不可协商规则

  1. 仅限服务器端密钥 - 永远不要暴露STRIPE_SECRET_KEY
  2. 始终验证Webhooks - 在处理之前检查签名
  3. 幂等性 - 存储Webhook事件ID,跳过重复项
  4. 使用元数据 - 跟踪用户ID、来源以便于调试
  5. 处理所有状态 - 成功、失败、待处理、已取消

幂等Webhook处理器

const processedEvents = new Set<string>(); // 在生产中使用Redis

export async function POST(request: Request) {
  // ...验证签名...

  // 跳过重复事件
  if (processedEvents.has(event.id)) {
    return new Response("Already processed", { status: 200 });
  }
  processedEvents.add(event.id);

  // 处理事件...
}

金额处理

// 始终使用美分(最小货币单位)
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));

常见模式

定价页面

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

按订阅保护路由

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

反模式

  • 硬编码API密钥 - 使用环境变量
  • 客户端支付创建 - 始终在服务器端创建PaymentIntent/Session
  • 跳过Webhook验证 - 始终验证签名
  • 处理重复的Webhooks - 实施幂等性
  • 浮点货币数学 - 使用整数(美分)
  • 信任客户端数据 - 验证服务器端的所有内容
  • 忽略失败的支付 - 处理invoice.payment_failed
  • 没有错误处理 - 捕获和处理Stripe错误

快速参考

# 安装
npm install stripe @stripe/stripe-js @stripe/react-stripe-js

# Stripe CLI
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed

# 测试模式前缀
sk_test_xxx  # 密钥
pk_test_xxx  # 发布密钥

# 现场模式前缀
sk_live_xxx
pk_live_xxx

关键端点

端点 目的
POST /api/checkout 创建结账会话
POST /api/portal 客户账单门户
POST /api/webhooks/stripe 处理Stripe事件

环境变量

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