payment-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseResources
资源
scripts/
validate-payments.sh
references/
payment-patterns.mdscripts/
validate-payments.sh
references/
payment-patterns.mdPayment Integration Implementation
支付集成实现
This skill guides you through implementing payment processing in applications, from provider selection to webhook handling. It leverages GoodVibes precision tools for secure, production-ready payment integrations.
本技能将引导你在应用中实现支付处理功能,从支付服务商选择到Webhook处理的全流程。它借助GoodVibes精准工具,帮助你构建安全、可用于生产环境的支付集成方案。
When to Use This Skill
何时使用此技能
Use this skill when you need to:
- Set up payment processing (one-time or recurring)
- Implement checkout flows and payment forms
- Handle subscription billing and management
- Process payment webhooks securely
- Test payment flows in development
- Ensure PCI compliance and security
- Migrate between payment providers
当你需要完成以下操作时,可使用本技能:
- 设置一次性或定期(recurring)支付处理
- 实现结账流程和支付表单
- 处理订阅账单与管理
- 安全处理支付Webhook
- 在开发环境中测试支付流程
- 确保PCI合规性与安全性
- 在不同支付服务商之间迁移
Workflow
工作流程
Follow this sequence for payment integration:
按照以下步骤完成支付集成:
1. Discover Existing Payment Infrastructure
1. 调研现有支付基础设施
Before implementing payment features, understand the current state:
yaml
precision_grep:
queries:
- id: payment-libs
pattern: "stripe|lemonsqueezy|paddle"
glob: "package.json"
- id: webhook-routes
pattern: "(webhook|payment|checkout)"
glob: "src/**/*.{ts,js,tsx,jsx}"
output:
format: files_onlyCheck for:
- Existing payment libraries
- Webhook endpoints
- Environment variables for API keys
- Payment-related database models
Check project memory for payment decisions:
yaml
precision_read:
files:
- path: ".goodvibes/memory/decisions.md"
extract: content
output:
format: minimalSearch for payment provider choices, checkout flow patterns, and subscription models.
在实现支付功能前,先了解当前项目的状态:
yaml
precision_grep:
queries:
- id: payment-libs
pattern: "stripe|lemonsqueezy|paddle"
glob: "package.json"
- id: webhook-routes
pattern: "(webhook|payment|checkout)"
glob: "src/**/*.{ts,js,tsx,jsx}"
output:
format: files_only检查以下内容:
- 已有的支付库
- Webhook端点
- 用于API密钥的环境变量
- 支付相关的数据库模型
查看项目记忆中的支付决策:
yaml
precision_read:
files:
- path: ".goodvibes/memory/decisions.md"
extract: content
output:
format: minimal搜索支付服务商选择、结账流程模式以及订阅模型相关内容。
2. Select Payment Provider
2. 选择支付服务商
Choose based on your requirements:
Use Stripe when:
- You need maximum flexibility and control
- Supporting complex subscription models
- Requiring advanced features (invoicing, tax, fraud detection)
- Building custom payment flows
- Need extensive API documentation
Use LemonSqueezy when:
- You're selling digital products/SaaS
- Want merchant of record (handles taxes, VAT, compliance)
- Need simple subscription management
- Prefer minimal compliance burden
- Operating globally with automatic tax handling
Use Paddle when:
- Selling software/SaaS products
- Need merchant of record services
- Want subscription and license management
- Prefer revenue recovery features
- Operating in B2B/enterprise markets
Document the decision:
yaml
precision_write:
files:
- path: ".goodvibes/memory/decisions.md"
mode: overwrite
content: |
## Payment Provider Selection
**Decision**: Using [Provider] for payment processing
**Rationale**: [Why this provider fits requirements]
**Date**: YYYY-MM-DD
Key factors:
- [Factor 1]
- [Factor 2]
- [Factor 3]根据你的需求选择合适的服务商:
当以下情况时选择Stripe:
- 你需要最大的灵活性和控制权
- 支持复杂的订阅模型
- 需要高级功能(发票、税务、欺诈检测)
- 构建自定义支付流程
- 需要详尽的API文档
当以下情况时选择LemonSqueezy:
- 你销售数字产品/SaaS服务
- 希望使用商户代运营服务(处理税务、VAT、合规性)
- 需要简单的订阅管理
- 偏好最小化合规负担
- 面向全球运营,需要自动税务处理
当以下情况时选择Paddle:
- 销售软件/SaaS产品
- 需要商户代运营服务
- 希望进行订阅和许可证管理
- 偏好营收恢复功能
- 面向B2B/企业市场运营
记录决策:
yaml
precision_write:
files:
- path: ".goodvibes/memory/decisions.md"
mode: overwrite
content: |
## 支付服务商选择
**决策**: 使用[服务商名称]进行支付处理
**理由**: [该服务商符合需求的原因]
**日期**: YYYY-MM-DD
关键因素:
- [因素1]
- [因素2]
- [因素3]3. Setup and Installation
3. 安装与配置
Install the appropriate SDK:
Stripe:
yaml
precision_exec:
commands:
- cmd: "npm install stripe @stripe/stripe-js"
# @types/stripe is not needed - Stripe ships with built-in types
verbosity: minimalLemonSqueezy:
yaml
precision_exec:
commands:
- cmd: "npm install @lemonsqueezy/lemonsqueezy.js"
verbosity: minimalPaddle:
yaml
precision_exec:
commands:
- cmd: "npm install @paddle/paddle-node-sdk"
verbosity: minimalConfigure environment variables:
Create template:
.env.exampleyaml
precision_write:
files:
- path: ".env.example"
mode: overwrite
content: |
# Payment Provider Configuration
# Stripe (if using Stripe)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# LemonSqueezy (if using LemonSqueezy)
LEMONSQUEEZY_API_KEY=...
LEMONSQUEEZY_STORE_ID=...
LEMONSQUEEZY_WEBHOOK_SECRET=...
# Paddle (if using Paddle)
PADDLE_API_KEY=...
PADDLE_WEBHOOK_SECRET=...
PADDLE_ENVIRONMENT=sandboxVerify secrets are gitignored:
yaml
precision_grep:
queries:
- id: check-gitignore
pattern: "^\\.env$"
path: ".gitignore"
output:
format: files_onlyIf not found, add to .
.env.gitignore安装对应的SDK:
Stripe:
yaml
precision_exec:
commands:
- cmd: "npm install stripe @stripe/stripe-js"
# @types/stripe 无需安装 - Stripe自带内置类型
verbosity: minimalLemonSqueezy:
yaml
precision_exec:
commands:
- cmd: "npm install @lemonsqueezy/lemonsqueezy.js"
verbosity: minimalPaddle:
yaml
precision_exec:
commands:
- cmd: "npm install @paddle/paddle-node-sdk"
verbosity: minimal配置环境变量:
创建模板:
.env.exampleyaml
precision_write:
files:
- path: ".env.example"
mode: overwrite
content: |
# 支付服务商配置
# Stripe(如果使用Stripe)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# LemonSqueezy(如果使用LemonSqueezy)
LEMONSQUEEZY_API_KEY=...
LEMONSQUEEZY_STORE_ID=...
LEMONSQUEEZY_WEBHOOK_SECRET=...
# Paddle(如果使用Paddle)
PADDLE_API_KEY=...
PADDLE_WEBHOOK_SECRET=...
PADDLE_ENVIRONMENT=sandbox验证密钥已被git忽略:
yaml
precision_grep:
queries:
- id: check-gitignore
pattern: "^\\.env$"
path: ".gitignore"
output:
format: files_only如果未找到,将添加到中。
.env.gitignore4. Implement Checkout Flow
4. 实现结账流程
One-Time Payments (Stripe)
一次性支付(Stripe)
Create checkout session endpoint:
yaml
precision_write:
files:
- path: "src/app/api/checkout/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { z } from 'zod';
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
throw new Error('STRIPE_SECRET_KEY is required');
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: '2024-11-20.acacia',
});
export async function POST(request: NextRequest) {
try {
// Check authentication
const authSession = await getServerSession();
if (!authSession?.user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Validate input with Zod
const schema = z.object({
priceId: z.string().min(1),
successUrl: z.string().url(),
cancelUrl: z.string().url(),
});
const body = await request.json();
const result = schema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid input', details: result.error.flatten() },
{ status: 400 }
);
}
const { priceId, successUrl, cancelUrl } = result.data;
// Validate URLs against allowed origins
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
if (!appUrl) throw new Error('NEXT_PUBLIC_APP_URL is required');
const allowedOrigins = [appUrl];
const successOrigin = new URL(successUrl).origin;
const cancelOrigin = new URL(cancelUrl).origin;
if (!allowedOrigins.includes(successOrigin) || !allowedOrigins.includes(cancelOrigin)) {
return NextResponse.json(
{ error: 'Invalid redirect URLs' },
{ status: 400 }
);
}
const stripeSession = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: successUrl,
cancel_url: cancelUrl,
metadata: {
userId: authSession.user.id,
},
});
return NextResponse.json({ sessionId: stripeSession.id });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Checkout error:', message);
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
);
}
}创建结账会话端点:
yaml
precision_write:
files:
- path: "src/app/api/checkout/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { z } from 'zod';
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
throw new Error('STRIPE_SECRET_KEY is required');
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: '2024-11-20.acacia',
});
export async function POST(request: NextRequest) {
try {
// 检查身份验证
const authSession = await getServerSession();
if (!authSession?.user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 使用Zod验证输入
const schema = z.object({
priceId: z.string().min(1),
successUrl: z.string().url(),
cancelUrl: z.string().url(),
});
const body = await request.json();
const result = schema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid input', details: result.error.flatten() },
{ status: 400 }
);
}
const { priceId, successUrl, cancelUrl } = result.data;
// 验证URL是否符合允许的源
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
if (!appUrl) throw new Error('NEXT_PUBLIC_APP_URL is required');
const allowedOrigins = [appUrl];
const successOrigin = new URL(successUrl).origin;
const cancelOrigin = new URL(cancelUrl).origin;
if (!allowedOrigins.includes(successOrigin) || !allowedOrigins.includes(cancelOrigin)) {
return NextResponse.json(
{ error: 'Invalid redirect URLs' },
{ status: 400 }
);
}
const stripeSession = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: successUrl,
cancel_url: cancelUrl,
metadata: {
userId: authSession.user.id,
},
});
return NextResponse.json({ sessionId: stripeSession.id });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Checkout error:', message);
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
);
}
}One-Time Payments (LemonSqueezy)
一次性支付(LemonSqueezy)
yaml
precision_write:
files:
- path: "src/app/api/checkout/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import { lemonSqueezySetup, createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
const lemonSqueezyApiKey = process.env.LEMONSQUEEZY_API_KEY;
if (!lemonSqueezyApiKey) {
throw new Error('LEMONSQUEEZY_API_KEY is required');
}
lemonSqueezySetup({ apiKey: lemonSqueezyApiKey });
export async function POST(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { variantId, email } = await request.json();
const lemonSqueezyStoreId = process.env.LEMONSQUEEZY_STORE_ID;
if (!lemonSqueezyStoreId) {
throw new Error('LEMONSQUEEZY_STORE_ID is required');
}
const { data, error } = await createCheckout(
lemonSqueezyStoreId,
variantId,
{
checkoutData: {
email,
custom: {
user_id: authSession.user.id,
},
},
}
);
if (error) {
throw new Error(error.message);
}
return NextResponse.json({ checkoutUrl: data.attributes.url });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Checkout error:', message);
return NextResponse.json(
{ error: 'Failed to create checkout' },
{ status: 500 }
);
}
}yaml
precision_write:
files:
- path: "src/app/api/checkout/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import { lemonSqueezySetup, createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
const lemonSqueezyApiKey = process.env.LEMONSQUEEZY_API_KEY;
if (!lemonSqueezyApiKey) {
throw new Error('LEMONSQUEEZY_API_KEY is required');
}
lemonSqueezySetup({ apiKey: lemonSqueezyApiKey });
export async function POST(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { variantId, email } = await request.json();
const lemonSqueezyStoreId = process.env.LEMONSQUEEZY_STORE_ID;
if (!lemonSqueezyStoreId) {
throw new Error('LEMONSQUEEZY_STORE_ID is required');
}
const { data, error } = await createCheckout(
lemonSqueezyStoreId,
variantId,
{
checkoutData: {
email,
custom: {
user_id: authSession.user.id,
},
},
}
);
if (error) {
throw new Error(error.message);
}
return NextResponse.json({ checkoutUrl: data.attributes.url });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Checkout error:', message);
return NextResponse.json(
{ error: 'Failed to create checkout' },
{ status: 500 }
);
}
}5. Subscription Billing
5. 订阅账单管理
Create Subscription Plans (Stripe)
创建订阅计划(Stripe)
yaml
precision_write:
files:
- path: "src/lib/stripe/plans.ts"
content: |
export const SUBSCRIPTION_PLANS = {
starter: {
name: 'Starter',
priceId: (() => {
const priceId = process.env.STRIPE_PRICE_STARTER;
if (!priceId) throw new Error('STRIPE_PRICE_STARTER is required');
return priceId;
})(),
price: 9,
interval: 'month' as const,
features: ['Feature 1', 'Feature 2'],
},
pro: {
name: 'Pro',
priceId: (() => {
const priceId = process.env.STRIPE_PRICE_PRO;
if (!priceId) throw new Error('STRIPE_PRICE_PRO is required');
return priceId;
})(),
price: 29,
interval: 'month' as const,
features: ['All Starter features', 'Feature 3', 'Feature 4'],
},
} as const;
export type PlanId = keyof typeof SUBSCRIPTION_PLANS;yaml
precision_write:
files:
- path: "src/lib/stripe/plans.ts"
content: |
export const SUBSCRIPTION_PLANS = {
starter: {
name: 'Starter',
priceId: (() => {
const priceId = process.env.STRIPE_PRICE_STARTER;
if (!priceId) throw new Error('STRIPE_PRICE_STARTER is required');
return priceId;
})(),
price: 9,
interval: 'month' as const,
features: ['Feature 1', 'Feature 2'],
},
pro: {
name: 'Pro',
priceId: (() => {
const priceId = process.env.STRIPE_PRICE_PRO;
if (!priceId) throw new Error('STRIPE_PRICE_PRO is required');
return priceId;
})(),
price: 29,
interval: 'month' as const,
features: ['All Starter features', 'Feature 3', 'Feature 4'],
},
} as const;
export type PlanId = keyof typeof SUBSCRIPTION_PLANS;Subscription Checkout
订阅结账
yaml
precision_write:
files:
- path: "src/app/api/subscribe/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { z } from 'zod';
import { SUBSCRIPTION_PLANS } from '@/lib/stripe/plans';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
// Validate required environment variables
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
const stripe = new Stripe(secretKey, {
apiVersion: '2024-11-20.acacia',
});
const subscribeSchema = z.object({
planId: z.string().min(1),
customerId: z.string().min(1),
successUrl: z.string().url(),
cancelUrl: z.string().url(),
});
export async function POST(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const result = subscribeSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.flatten() },
{ status: 400 }
);
}
const { planId, customerId, successUrl, cancelUrl } = result.data;
const plan = SUBSCRIPTION_PLANS[planId as keyof typeof SUBSCRIPTION_PLANS];
if (!plan) {
return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
}
const stripeSession = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customerId,
line_items: [
{
price: plan.priceId,
quantity: 1,
},
],
success_url: successUrl,
cancel_url: cancelUrl,
subscription_data: {
trial_period_days: 14,
},
});
return NextResponse.json({ sessionId: stripeSession.id });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Subscription error:', message);
return NextResponse.json(
{ error: 'Failed to create subscription' },
{ status: 500 }
);
}
}yaml
precision_write:
files:
- path: "src/app/api/subscribe/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { z } from 'zod';
import { SUBSCRIPTION_PLANS } from '@/lib/stripe/plans';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
// 验证所需环境变量
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
const stripe = new Stripe(secretKey, {
apiVersion: '2024-11-20.acacia',
});
const subscribeSchema = z.object({
planId: z.string().min(1),
customerId: z.string().min(1),
successUrl: z.string().url(),
cancelUrl: z.string().url(),
});
export async function POST(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const result = subscribeSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.flatten() },
{ status: 400 }
);
}
const { planId, customerId, successUrl, cancelUrl } = result.data;
const plan = SUBSCRIPTION_PLANS[planId as keyof typeof SUBSCRIPTION_PLANS];
if (!plan) {
return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
}
const stripeSession = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customerId,
line_items: [
{
price: plan.priceId,
quantity: 1,
},
],
success_url: successUrl,
cancel_url: cancelUrl,
subscription_data: {
trial_period_days: 14,
},
});
return NextResponse.json({ sessionId: stripeSession.id });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Subscription error:', message);
return NextResponse.json(
{ error: 'Failed to create subscription' },
{ status: 500 }
);
}
}Manage Subscriptions
管理订阅
yaml
precision_write:
files:
- path: "src/app/api/subscription/manage/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
const stripe = new Stripe(secretKey, {
apiVersion: '2024-11-20.acacia',
});
// Cancel subscription
export async function DELETE(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { subscriptionId } = await request.json();
const subscription = await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
return NextResponse.json({ subscription });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Cancel error:', message);
return NextResponse.json(
{ error: 'Failed to cancel subscription' },
{ status: 500 }
);
}
}
// Update subscription
export async function PATCH(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { subscriptionId, newPriceId } = await request.json();
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const updated = await stripe.subscriptions.update(subscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
},
],
proration_behavior: 'create_prorations',
});
return NextResponse.json({ subscription: updated });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Update error:', message);
return NextResponse.json(
{ error: 'Failed to update subscription' },
{ status: 500 }
);
}
}yaml
precision_write:
files:
- path: "src/app/api/subscription/manage/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
const stripe = new Stripe(secretKey, {
apiVersion: '2024-11-20.acacia',
});
// 取消订阅
export async function DELETE(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { subscriptionId } = await request.json();
const subscription = await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
return NextResponse.json({ subscription });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Cancel error:', message);
return NextResponse.json(
{ error: 'Failed to cancel subscription' },
{ status: 500 }
);
}
}
// 更新订阅
export async function PATCH(request: NextRequest) {
try {
const authSession = await getServerSession(authOptions);
if (!authSession?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { subscriptionId, newPriceId } = await request.json();
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const updated = await stripe.subscriptions.update(subscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
},
],
proration_behavior: 'create_prorations',
});
return NextResponse.json({ subscription: updated });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Update error:', message);
return NextResponse.json(
{ error: 'Failed to update subscription' },
{ status: 500 }
);
}
}6. Webhook Handling
6. Webhook处理
Webhooks are critical for payment processing. They notify your application of payment events.
Webhook是支付处理的关键组件,用于通知应用支付事件的发生。
Stripe Webhook Handler
Stripe Webhook处理器
yaml
precision_write:
files:
- path: "src/app/api/webhooks/stripe/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { headers } from 'next/headers';
import { db } from '@/lib/db';
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
const stripe = new Stripe(secretKey, {
apiVersion: '2024-11-20.acacia',
});
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
throw new Error('STRIPE_WEBHOOK_SECRET is required');
}
export async function POST(request: NextRequest) {
const body = await request.text();
const headersList = await headers();
const signature = headersList.get('stripe-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing stripe-signature header' },
{ status: 400 }
);
}
let event: Stripe.Event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook signature verification failed:', message);
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
// Handle the event
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.created': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCreated(subscription);
break;
}
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object as Stripe.Subscription);
break;
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(event.data.object as Stripe.Invoice);
break;
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object as Stripe.Invoice);
break;
default:
// Replace with structured logger in production
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook handler error:', message);
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
);
}
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
// Update database with payment details
await db.payment.create({
data: {
id: session.payment_intent as string,
userId: session.metadata?.userId,
amount: session.amount_total ?? 0,
currency: session.currency ?? 'usd',
status: 'succeeded',
},
});
// Grant access to product/service based on metadata
}
async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
// Create subscription record in database
await db.subscription.create({
data: {
id: subscription.id,
userId: subscription.metadata?.userId,
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
// Update subscription status in database
await db.subscription.update({
where: { id: subscription.id },
data: {
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
// Mark subscription as canceled in database
await db.subscription.update({
where: { id: subscription.id },
data: {
status: 'canceled',
canceledAt: new Date(),
},
});
// Revoke access at period end
}
async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
// Record successful payment
await db.payment.create({
data: {
id: invoice.payment_intent as string,
userId: invoice.subscription_details?.metadata?.userId,
amount: invoice.amount_paid,
currency: invoice.currency,
status: 'succeeded',
},
});
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
// Notify user of failed payment
await db.payment.create({
data: {
id: invoice.payment_intent as string,
userId: invoice.subscription_details?.metadata?.userId,
amount: invoice.amount_due,
currency: invoice.currency,
status: 'failed',
},
});
// Send notification email
}yaml
precision_write:
files:
- path: "src/app/api/webhooks/stripe/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { headers } from 'next/headers';
import { db } from '@/lib/db';
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
const stripe = new Stripe(secretKey, {
apiVersion: '2024-11-20.acacia',
});
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
throw new Error('STRIPE_WEBHOOK_SECRET is required');
}
export async function POST(request: NextRequest) {
const body = await request.text();
const headersList = await headers();
const signature = headersList.get('stripe-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing stripe-signature header' },
{ status: 400 }
);
}
let event: Stripe.Event;
try {
// 验证Webhook签名
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook signature verification failed:', message);
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
// 处理事件
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.created': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCreated(subscription);
break;
}
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object as Stripe.Subscription);
break;
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(event.data.object as Stripe.Invoice);
break;
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object as Stripe.Invoice);
break;
default:
// 生产环境中替换为结构化日志
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook handler error:', message);
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
);
}
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
// 在数据库中更新支付详情
await db.payment.create({
data: {
id: session.payment_intent as string,
userId: session.metadata?.userId,
amount: session.amount_total ?? 0,
currency: session.currency ?? 'usd',
status: 'succeeded',
},
});
// 根据元数据授予产品/服务访问权限
}
async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
// 在数据库中创建订阅记录
await db.subscription.create({
data: {
id: subscription.id,
userId: subscription.metadata?.userId,
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
// 在数据库中更新订阅状态
await db.subscription.update({
where: { id: subscription.id },
data: {
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
// 在数据库中将订阅标记为已取消
await db.subscription.update({
where: { id: subscription.id },
data: {
status: 'canceled',
canceledAt: new Date(),
},
});
// 在周期结束时撤销访问权限
}
async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
// 记录成功的支付
await db.payment.create({
data: {
id: invoice.payment_intent as string,
userId: invoice.subscription_details?.metadata?.userId,
amount: invoice.amount_paid,
currency: invoice.currency,
status: 'succeeded',
},
});
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
// 通知用户支付失败
await db.payment.create({
data: {
id: invoice.payment_intent as string,
userId: invoice.subscription_details?.metadata?.userId,
amount: invoice.amount_due,
currency: invoice.currency,
status: 'failed',
},
});
// 发送通知邮件
}LemonSqueezy Webhook Handler
LemonSqueezy Webhook处理器
yaml
precision_write:
files:
- path: "src/app/api/webhooks/lemonsqueezy/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
import { db } from '@/lib/db';
const webhookSecret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
if (!webhookSecret) {
throw new Error('LEMONSQUEEZY_WEBHOOK_SECRET is required');
}
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('x-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
}
// Verify webhook signature
const hmac = crypto.createHmac('sha256', webhookSecret);
const digest = hmac.update(body).digest('hex');
const signatureBuffer = Buffer.from(signature, 'utf8');
const digestBuffer = Buffer.from(digest, 'utf8');
if (signatureBuffer.length !== digestBuffer.length || !crypto.timingSafeEqual(signatureBuffer, digestBuffer)) {
console.error('Webhook signature verification failed');
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
const event = JSON.parse(body);
try {
switch (event.meta.event_name) {
case 'order_created':
await handleOrderCreated(event.data);
break;
case 'subscription_created':
await handleSubscriptionCreated(event.data);
break;
case 'subscription_updated':
await handleSubscriptionUpdated(event.data);
break;
case 'subscription_cancelled':
await handleSubscriptionCancelled(event.data);
break;
case 'subscription_payment_success':
await handlePaymentSuccess(event.data);
break;
default:
// Replace with structured logger in production
console.log(`Unhandled event: ${event.meta.event_name}`);
}
return NextResponse.json({ received: true });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook handler error:', message);
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
);
}
}
interface LemonSqueezyWebhookData {
id: string;
attributes: Record<string, unknown>;
}
async function handleOrderCreated(data: LemonSqueezyWebhookData) {
await db.order.create({
data: {
id: data.id,
status: 'completed',
attributes: data.attributes,
},
});
}
async function handleSubscriptionCreated(data: LemonSqueezyWebhookData) {
await db.subscription.create({
data: {
id: data.id,
status: 'active',
userId: data.attributes.user_id as string,
attributes: data.attributes,
},
});
}
async function handleSubscriptionUpdated(data: LemonSqueezyWebhookData) {
await db.subscription.update({
where: { id: data.id },
data: {
status: data.attributes.status as string,
attributes: data.attributes,
},
});
}
async function handleSubscriptionCancelled(data: LemonSqueezyWebhookData) {
await db.subscription.update({
where: { id: data.id },
data: {
status: 'cancelled',
cancelledAt: new Date(),
},
});
}
async function handlePaymentSuccess(data: LemonSqueezyWebhookData) {
await db.payment.create({
data: {
id: data.id,
status: 'succeeded',
amount: data.attributes.total as number,
attributes: data.attributes,
},
});
}yaml
precision_write:
files:
- path: "src/app/api/webhooks/lemonsqueezy/route.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
import { db } from '@/lib/db';
const webhookSecret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
if (!webhookSecret) {
throw new Error('LEMONSQUEEZY_WEBHOOK_SECRET is required');
}
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('x-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
}
// 验证Webhook签名
const hmac = crypto.createHmac('sha256', webhookSecret);
const digest = hmac.update(body).digest('hex');
const signatureBuffer = Buffer.from(signature, 'utf8');
const digestBuffer = Buffer.from(digest, 'utf8');
if (signatureBuffer.length !== digestBuffer.length || !crypto.timingSafeEqual(signatureBuffer, digestBuffer)) {
console.error('Webhook signature verification failed');
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
const event = JSON.parse(body);
try {
switch (event.meta.event_name) {
case 'order_created':
await handleOrderCreated(event.data);
break;
case 'subscription_created':
await handleSubscriptionCreated(event.data);
break;
case 'subscription_updated':
await handleSubscriptionUpdated(event.data);
break;
case 'subscription_cancelled':
await handleSubscriptionCancelled(event.data);
break;
case 'subscription_payment_success':
await handlePaymentSuccess(event.data);
break;
default:
// 生产环境中替换为结构化日志
console.log(`Unhandled event: ${event.meta.event_name}`);
}
return NextResponse.json({ received: true });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook handler error:', message);
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
);
}
}
interface LemonSqueezyWebhookData {
id: string;
attributes: Record<string, unknown>;
}
async function handleOrderCreated(data: LemonSqueezyWebhookData) {
await db.order.create({
data: {
id: data.id,
status: 'completed',
attributes: data.attributes,
},
});
}
async function handleSubscriptionCreated(data: LemonSqueezyWebhookData) {
await db.subscription.create({
data: {
id: data.id,
status: 'active',
userId: data.attributes.user_id as string,
attributes: data.attributes,
},
});
}
async function handleSubscriptionUpdated(data: LemonSqueezyWebhookData) {
await db.subscription.update({
where: { id: data.id },
data: {
status: data.attributes.status as string,
attributes: data.attributes,
},
});
}
async function handleSubscriptionCancelled(data: LemonSqueezyWebhookData) {
await db.subscription.update({
where: { id: data.id },
data: {
status: 'cancelled',
cancelledAt: new Date(),
},
});
}
async function handlePaymentSuccess(data: LemonSqueezyWebhookData) {
await db.payment.create({
data: {
id: data.id,
status: 'succeeded',
amount: data.attributes.total as number,
attributes: data.attributes,
},
});
}Webhook Idempotency
Webhook幂等性
Always implement idempotency to handle duplicate webhook deliveries:
yaml
precision_write:
files:
- path: "src/lib/webhooks/idempotency.ts"
content: |
import { db } from '@/lib/db';
export async function isProcessed(eventId: string): Promise<boolean> {
const existing = await db.webhookEvent.findUnique({
where: { id: eventId },
});
return !!existing;
}
export async function markProcessed(eventId: string): Promise<void> {
await db.webhookEvent.create({
data: {
id: eventId,
processedAt: new Date(),
},
});
}
export async function withIdempotency<T>(
eventId: string,
handler: () => Promise<T>
): Promise<T | null> {
// Use database transaction to prevent TOCTOU race condition
return db.$transaction(async (tx) => {
const existing = await tx.webhookEvent.findUnique({
where: { id: eventId },
});
if (existing) {
// Replace with structured logger in production
console.log(`Event ${eventId} already processed, skipping`);
return null;
}
const result = await handler();
await tx.webhookEvent.create({
data: {
id: eventId,
processedAt: new Date(),
},
});
return result;
});
}Update webhook handlers to use idempotency:
typescript
// In Stripe webhook
const eventId = event.id;
await withIdempotency(eventId, async () => {
// Handle event
});始终实现幂等性以处理重复的Webhook交付:
yaml
precision_write:
files:
- path: "src/lib/webhooks/idempotency.ts"
content: |
import { db } from '@/lib/db';
export async function isProcessed(eventId: string): Promise<boolean> {
const existing = await db.webhookEvent.findUnique({
where: { id: eventId },
});
return !!existing;
}
export async function markProcessed(eventId: string): Promise<void> {
await db.webhookEvent.create({
data: {
id: eventId,
processedAt: new Date(),
},
});
}
export async function withIdempotency<T>(
eventId: string,
handler: () => Promise<T>
): Promise<T | null> {
// 使用数据库事务防止TOCTOU竞争条件
return db.$transaction(async (tx) => {
const existing = await tx.webhookEvent.findUnique({
where: { id: eventId },
});
if (existing) {
// 生产环境中替换为结构化日志
console.log(`Event ${eventId} already processed, skipping`);
return null;
}
const result = await handler();
await tx.webhookEvent.create({
data: {
id: eventId,
processedAt: new Date(),
},
});
return result;
});
}更新Webhook处理器以使用幂等性:
typescript
// 在Stripe Webhook中
const eventId = event.id;
await withIdempotency(eventId, async () => {
// 处理事件
});7. Testing Payment Flows
7. 测试支付流程
Use Test Mode Keys
使用测试模式密钥
Always use test mode keys in development:
yaml
precision_grep:
queries:
- id: check-test-keys
pattern: "(sk_test|pk_test)"
path: ".env"
output:
format: files_only在开发环境中始终使用测试模式密钥:
yaml
precision_grep:
queries:
- id: check-test-keys
pattern: "(sk_test|pk_test)"
path: ".env"
output:
format: files_onlyStripe CLI for Webhook Testing
使用Stripe CLI测试Webhook
Install and configure Stripe CLI:
yaml
precision_exec:
commands:
- cmd: "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
background: true
verbosity: minimal安装并配置Stripe CLI:
yaml
precision_exec:
commands:
- cmd: "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
background: true
verbosity: minimalTest Card Numbers
测试卡号
Document test card numbers in development docs:
yaml
precision_write:
files:
- path: "docs/testing-payments.md"
content: |
# Payment Testing Guide
## Test Cards (Stripe)
- Success: 4242 4242 4242 4242
- Decline: 4000 0000 0000 0002
- Requires authentication: 4000 0025 0000 3155
- Insufficient funds: 4000 0000 0000 9995
Use any future expiry date and any 3-digit CVC.
## Test Webhooks
1. Start webhook listener: `stripe listen --forward-to localhost:3000/api/webhooks/stripe`
2. Trigger events: `stripe trigger payment_intent.succeeded`
## LemonSqueezy Testing
Use sandbox mode in .env:
LEMONSQUEEZY_API_KEY=... (sandbox key)在开发文档中记录测试卡号:
yaml
precision_write:
files:
- path: "docs/testing-payments.md"
content: |
# 支付测试指南
## 测试卡号(Stripe)
- 支付成功:4242 4242 4242 4242
- 支付失败:4000 0000 0000 0002
- 需要身份验证:4000 0025 0000 3155
- 余额不足:4000 0000 0000 9995
可使用任意未来到期日期和任意3位CVC码。
## 测试Webhook
1. 启动Webhook监听器:`stripe listen --forward-to localhost:3000/api/webhooks/stripe`
2. 触发事件:`stripe trigger payment_intent.succeeded`
## LemonSqueezy测试
在.env中使用沙盒模式:
LEMONSQUEEZY_API_KEY=...(沙盒密钥)Automated Testing
自动化测试
Create tests for webhook handlers:
yaml
precision_write:
files:
- path: "src/__tests__/webhooks/stripe.test.ts"
content: |
import { POST } from '@/app/api/webhooks/stripe/route';
import { NextRequest } from 'next/server';
import Stripe from 'stripe';
describe('Stripe Webhook Handler', () => {
it('should verify webhook signature', async () => {
const mockEvent = {
id: 'evt_test_123',
type: 'checkout.session.completed',
data: { object: {} },
};
const request = new NextRequest('http://localhost:3000/api/webhooks/stripe', {
method: 'POST',
body: JSON.stringify(mockEvent),
headers: {
'stripe-signature': 'invalid_signature',
},
});
const response = await POST(request);
expect(response.status).toBe(400);
});
it('should handle checkout.session.completed', async () => {
// Test event handling
});
});为Webhook处理器创建测试:
yaml
precision_write:
files:
- path: "src/__tests__/webhooks/stripe.test.ts"
content: |
import { POST } from '@/app/api/webhooks/stripe/route';
import { NextRequest } from 'next/server';
import Stripe from 'stripe';
describe('Stripe Webhook Handler', () => {
it('should verify webhook signature', async () => {
const mockEvent = {
id: 'evt_test_123',
type: 'checkout.session.completed',
data: { object: {} },
};
const request = new NextRequest('http://localhost:3000/api/webhooks/stripe', {
method: 'POST',
body: JSON.stringify(mockEvent),
headers: {
'stripe-signature': 'invalid_signature',
},
});
const response = await POST(request);
expect(response.status).toBe(400);
});
it('should handle checkout.session.completed', async () => {
// 测试事件处理
});
});8. Security and PCI Compliance
8. 安全性与PCI合规
Never Handle Card Data Directly
绝不直接处理卡号数据
Use hosted checkout pages or Elements to keep card data out of your servers:
yaml
precision_grep:
queries:
- id: check-card-handling
pattern: "(card_number|cvv|cvc|card.*exp)"
glob: "src/**/*.{ts,js,tsx,jsx}"
output:
format: files_onlyIf any matches are found, refactor to use provider's hosted solutions.
使用托管结账页面或Elements组件,避免卡号数据进入你的服务器:
yaml
precision_grep:
queries:
- id: check-card-handling
pattern: "(card_number|cvv|cvc|card.*exp)"
glob: "src/**/*.{ts,js,tsx,jsx}"
output:
format: files_only如果找到匹配项,重构为使用服务商的托管解决方案。
Enforce HTTPS
强制使用HTTPS
yaml
precision_write:
files:
- path: "src/middleware.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Enforce HTTPS in production
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
return NextResponse.redirect(
`https://${request.headers.get('host')}${request.nextUrl.pathname}`,
301
);
}
return NextResponse.next();
}
export const config = {
matcher: '/api/checkout/:path*',
};yaml
precision_write:
files:
- path: "src/middleware.ts"
content: |
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// 在生产环境中强制使用HTTPS
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
return NextResponse.redirect(
`https://${request.headers.get('host')}${request.nextUrl.pathname}`,
301
);
}
return NextResponse.next();
}
export const config = {
matcher: '/api/checkout/:path*',
};Secure API Keys
安全存储API密钥
Verify API keys are not hardcoded:
yaml
precision_grep:
queries:
- id: hardcoded-keys
pattern: "(sk_live|sk_test)_[a-zA-Z0-9]{24,}"
glob: "src/**/*.{ts,js,tsx,jsx}"
output:
format: files_onlyIf found, move to environment variables immediately.
验证API密钥未被硬编码:
yaml
precision_grep:
queries:
- id: hardcoded-keys
pattern: "(sk_live|sk_test)_[a-zA-Z0-9]{24,}"
glob: "src/**/*.{ts,js,tsx,jsx}"
output:
format: files_only如果找到,立即迁移到环境变量中。
Content Security Policy
内容安全策略
Add CSP headers for payment pages:
yaml
precision_write:
files:
- path: "next.config.js"
mode: overwrite
content: |
module.exports = {
async headers() {
return [
{
source: '/(checkout|subscribe)/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://js.stripe.com",
"frame-src https://js.stripe.com",
"connect-src 'self' https://api.stripe.com",
].join('; '),
},
],
},
];
},
};为支付页面添加CSP头:
yaml
precision_write:
files:
- path: "next.config.js"
mode: overwrite
content: |
module.exports = {
async headers() {
return [
{
source: '/(checkout|subscribe)/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://js.stripe.com",
"frame-src https://js.stripe.com",
"connect-src 'self' https://api.stripe.com",
].join('; '),
},
],
},
];
},
};9. Validation
9. 验证
Run the validation script to ensure best practices:
yaml
precision_exec:
commands:
- cmd: "bash plugins/goodvibes/skills/outcome/payment-integration/scripts/validate-payments.sh ."
expect:
exit_code: 0
verbosity: standardThe script checks:
- Payment library installation
- API keys in .env.example (not .env)
- Webhook endpoint exists
- Webhook signature verification
- No hardcoded secrets
- HTTPS enforcement
- Error handling
运行验证脚本以确保遵循最佳实践:
yaml
precision_exec:
commands:
- cmd: "bash plugins/goodvibes/skills/outcome/payment-integration/scripts/validate-payments.sh ."
expect:
exit_code: 0
verbosity: standard该脚本检查以下内容:
- 支付库是否已安装
- API密钥是否在.env.example中(而非.env)
- Webhook端点是否存在
- Webhook签名验证是否实现
- 无硬编码密钥
- 是否强制使用HTTPS
- 错误处理是否完善
10. Database Schema for Payments
10. 支付相关数据库Schema
Add payment tracking to your database:
prisma
model Customer {
id String @id @default(cuid())
userId String @unique
stripeCustomerId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscriptions Subscription[]
payments Payment[]
@@index([stripeCustomerId])
}
model Subscription {
id String @id @default(cuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id])
stripeSubscriptionId String @unique
stripePriceId String
status String
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([customerId])
@@index([status])
}
model Payment {
id String @id @default(cuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id])
stripePaymentId String @unique
amount Int
currency String
status String
createdAt DateTime @default(now())
@@index([customerId])
@@index([status])
}
model WebhookEvent {
id String @id
processedAt DateTime @default(now())
@@index([processedAt])
}在数据库中添加支付跟踪相关模型:
prisma
model Customer {
id String @id @default(cuid())
userId String @unique
stripeCustomerId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscriptions Subscription[]
payments Payment[]
@@index([stripeCustomerId])
}
model Subscription {
id String @id @default(cuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id])
stripeSubscriptionId String @unique
stripePriceId String
status String
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([customerId])
@@index([status])
}
model Payment {
id String @id @default(cuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id])
stripePaymentId String @unique
amount Int
currency String
status String
createdAt DateTime @default(now())
@@index([customerId])
@@index([status])
}
model WebhookEvent {
id String @id
processedAt DateTime @default(now())
@@index([processedAt])
}Common Patterns
常见模式
Pattern: Proration on Plan Changes
模式:计划变更时的按比例计费
When upgrading/downgrading subscriptions:
typescript
const updated = await stripe.subscriptions.update(subscriptionId, {
items: [{ id: itemId, price: newPriceId }],
proration_behavior: 'create_prorations', // Credit/charge immediately
});当升级/降级订阅时:
typescript
const updated = await stripe.subscriptions.update(subscriptionId, {
items: [{ id: itemId, price: newPriceId }],
proration_behavior: 'create_prorations', // 立即进行信用/收费
});Pattern: Trial Periods
模式:试用期
Offer free trials:
typescript
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
subscription_data: {
trial_period_days: 14,
trial_settings: {
end_behavior: {
missing_payment_method: 'cancel', // Cancel if no payment method
},
},
},
});提供免费试用期:
typescript
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
subscription_data: {
trial_period_days: 14,
trial_settings: {
end_behavior: {
missing_payment_method: 'cancel', // 若无支付方式则取消
},
},
},
});Pattern: Webhook Retry Logic
模式:Webhook重试逻辑
Payment providers retry failed webhooks. Always:
- Return 200 OK quickly (process async if needed)
- Implement idempotency
- Log all webhook events
支付服务商会重试失败的Webhook。始终:
- 快速返回200 OK(如需处理可异步进行)
- 实现幂等性
- 记录所有Webhook事件
Pattern: Failed Payment Recovery
模式:失败支付恢复
Handle dunning (failed payment recovery):
typescript
case 'invoice.payment_failed':
const invoice = event.data.object as Stripe.Invoice;
if (invoice.attempt_count >= 3) {
// Cancel subscription after 3 failures
await stripe.subscriptions.cancel(invoice.subscription as string);
await notifyCustomer(invoice.customer as string, 'payment_failed_final');
} else {
await notifyCustomer(invoice.customer as string, 'payment_failed_retry');
}
break;处理催缴(失败支付恢复):
typescript
case 'invoice.payment_failed':
const invoice = event.data.object as Stripe.Invoice;
if (invoice.attempt_count >= 3) {
// 3次失败后取消订阅
await stripe.subscriptions.cancel(invoice.subscription as string);
await notifyCustomer(invoice.customer as string, 'payment_failed_final');
} else {
await notifyCustomer(invoice.customer as string, 'payment_failed_retry');
}
break;Anti-Patterns to Avoid
需避免的反模式
DON'T: Store Card Details
不要:存储卡号详情
Never store card numbers, CVV, or full card data. Use tokenization.
绝不存储卡号、CVV或完整的卡数据。使用令牌化。
DON'T: Skip Webhook Verification
不要:跳过Webhook签名验证
Always verify webhook signatures. Unverified webhooks are a security risk.
始终验证Webhook签名。未验证的Webhook存在安全风险。
DON'T: Use Client-Side Pricing
不要:在客户端处理定价
Never trust prices from the client. Always use server-side price lookup:
typescript
// BAD
const { amount } = await request.json();
await stripe.charges.create({ amount });
// GOOD
const { priceId } = await request.json();
const price = await stripe.prices.retrieve(priceId);
await stripe.charges.create({ amount: price.unit_amount });绝不信任来自客户端的价格。始终在服务器端查询价格:
typescript
// 错误做法
const { amount } = await request.json();
await stripe.charges.create({ amount });
// 正确做法
const { priceId } = await request.json();
const price = await stripe.prices.retrieve(priceId);
await stripe.charges.create({ amount: price.unit_amount });DON'T: Ignore Failed Webhooks
不要:忽略失败的Webhook
Monitor webhook delivery and investigate failures. Set up alerts.
监控Webhook交付情况并调查失败原因。设置告警。
DON'T: Hardcode Currency
不要:硬编码货币
Support multiple currencies if serving international customers:
typescript
const session = await stripe.checkout.sessions.create({
currency: userCountry === 'US' ? 'usd' : 'eur',
});如果服务国际客户,支持多种货币:
typescript
const session = await stripe.checkout.sessions.create({
currency: userCountry === 'US' ? 'usd' : 'eur',
});References
参考资料
See for:
references/payment-patterns.md- Provider comparison table
- Complete webhook event reference
- Subscription lifecycle state machine
- Testing strategies
- Security checklist
查看获取以下内容:
references/payment-patterns.md- 服务商对比表
- 完整的Webhook事件参考
- 订阅生命周期状态机
- 测试策略
- 安全检查清单
Next Steps
后续步骤
After implementing payment integration:
- Test thoroughly - Use test mode, simulate failures
- Monitor webhooks - Set up logging and alerting
- Document flows - Create internal docs for payment processes
- Plan for scale - Consider rate limits, concurrent webhooks
- Implement analytics - Track conversion rates, churn
- Review compliance - Ensure PCI-DSS compliance if applicable
- Set up monitoring - Track payment success rates, errors
完成支付集成后:
- 全面测试 - 使用测试模式,模拟各种失败场景
- 监控Webhook - 设置日志和告警
- 记录流程 - 创建支付流程的内部文档
- 规划扩展性 - 考虑速率限制、并发Webhook
- 实现分析 - 跟踪转化率、客户流失率
- 审核合规性 - 确保符合PCI-DSS合规要求(如适用)
- 设置监控 - 跟踪支付成功率、错误情况