fix stripe

This commit is contained in:
songtianlun 2025-08-06 21:06:33 +08:00
parent 81b33f85f2
commit c765368382
6 changed files with 235 additions and 77 deletions

3
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
"@prisma/client": "^6.12.0",
"@prisma/client": "^6.13.0",
"@stripe/stripe-js": "^7.8.0",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
@ -1445,6 +1445,7 @@
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.13.0.tgz",
"integrity": "sha512-8m2+I3dQovkV8CkDMluiwEV1TxV9EXdT6xaCz39O6jYw7mkf5gwfmi+cL4LJsEPwz5tG7sreBwkRpEMJedGYUQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},

View File

@ -23,7 +23,7 @@
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^6.12.0",
"@prisma/client": "^6.13.0",
"@stripe/stripe-js": "^7.8.0",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",

View File

@ -40,6 +40,7 @@ model User {
subscriptionPlan SubscriptionPlan @relation(fields: [subscriptionPlanId], references: [id])
prompts Prompt[]
credits Credit[]
subscriptions Subscription[]
@@map("users")
}
@ -66,6 +67,7 @@ model SubscriptionPlan {
// 关联关系
users User[]
subscriptions Subscription[]
@@map("subscription_plans")
}
@ -173,3 +175,32 @@ model Credit {
@@map("credits")
}
// 订阅记录模型
model Subscription {
id String @id @default(cuid())
userId String // 用户 ID
subscriptionPlanId String // 订阅套餐 ID
stripeSubscriptionId String? @unique // Stripe 订阅 ID
stripeCustomerId String? // Stripe 客户 ID冗余存储便于查询
// 订阅状态和时间
isActive Boolean @default(false) // 是否有效
startDate DateTime? // 开始时间(订阅激活时设置)
endDate DateTime? // 结束时间(订阅激活时设置)
// 订阅状态
status String @default("pending") // "pending", "active", "canceled", "expired", "failed"
// 元数据
metadata Json? // 额外的元数据(如 Stripe 的原始数据)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关联关系
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
subscriptionPlan SubscriptionPlan @relation(fields: [subscriptionPlanId], references: [id])
@@map("subscriptions")
}

View File

@ -3,6 +3,7 @@ import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { createOrGetStripeCustomer, createSubscriptionSession } from '@/lib/stripe'
import { prisma } from '@/lib/prisma'
import { SubscriptionService } from '@/lib/subscription-service'
export async function POST(request: NextRequest) {
try {
@ -55,12 +56,14 @@ export async function POST(request: NextRequest) {
)
// 创建订阅会话
console.log('🛒 Creating subscription session for customer:', customer.id, 'price:', priceId)
const session = await createSubscriptionSession(
customer.id,
priceId,
`${process.env.NEXT_PUBLIC_APP_URL}/subscription?success=true`,
`${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`
)
console.log('✅ Created session:', session.id, 'mode:', session.mode)
// 保存 Stripe 客户 ID 到数据库
await prisma.user.update({
@ -68,6 +71,14 @@ export async function POST(request: NextRequest) {
data: { stripeCustomerId: customer.id }
})
// 创建初始的订阅记录(待激活状态)
await SubscriptionService.createPendingSubscription(
user.id,
plan.id,
undefined, // Stripe 订阅 ID 将在 webhook 中设置
customer.id
)
return NextResponse.json({ sessionId: session.id, url: session.url })
} catch (error) {

View File

@ -6,11 +6,13 @@ import { SubscriptionService } from '@/lib/subscription-service'
export async function POST(request: NextRequest) {
try {
console.log('🔔 Webhook received')
const body = await request.text()
const headersList = await headers()
const signature = headersList.get('stripe-signature')
if (!signature) {
console.log('❌ No signature found')
return NextResponse.json({ error: 'No signature' }, { status: 400 })
}
@ -21,6 +23,8 @@ export async function POST(request: NextRequest) {
STRIPE_CONFIG.webhookSecret
)
console.log(`📨 Processing event: ${event.type}`)
// 处理不同类型的事件
switch (event.type) {
case 'checkout.session.completed':
@ -61,29 +65,47 @@ export async function POST(request: NextRequest) {
async function handleCheckoutSessionCompleted(session: Record<string, unknown>) {
try {
console.log('🛒 Processing checkout session completed')
console.log('Session data:', JSON.stringify(session, null, 2))
const subscriptionId = session.subscription as string
console.log('Subscription ID from session:', subscriptionId)
if (!subscriptionId) {
console.log('❌ No subscription ID found in checkout session')
return
}
// 获取订阅详情
console.log('📞 Retrieving subscription from Stripe...')
const subscription = await stripe.subscriptions.retrieve(subscriptionId)
console.log('✅ Retrieved subscription:', subscription.id, 'status:', subscription.status)
// 处理订阅更新
await handleSubscriptionUpdate(subscription as unknown as Record<string, unknown>)
} catch (error) {
console.error('Error handling checkout session completed:', error)
console.error('Error handling checkout session completed:', error)
}
}
async function handleSubscriptionUpdate(subscription: Record<string, unknown>) {
try {
console.log('🔄 Processing subscription update')
const customerId = subscription.customer as string
const status = subscription.status as string
const stripeSubscriptionId = subscription.id as string
const items = subscription.items as { data: Array<{ price: { id: string } }> }
const priceId = items?.data[0]?.price?.id
const currentPeriodStart = subscription.current_period_start as number
const currentPeriodEnd = subscription.current_period_end as number
console.log(`📊 Subscription details:`, {
customerId,
status,
stripeSubscriptionId,
priceId
})
// 查找用户
const user = await prisma.user.findFirst({
@ -91,21 +113,79 @@ async function handleSubscriptionUpdate(subscription: Record<string, unknown>) {
})
if (!user) {
console.error('User not found for customer:', customerId)
console.error('User not found for customer:', customerId)
return
}
// 根据订阅状态确定套餐
let planId = 'free'
console.log(`👤 Found user: ${user.id}`)
if (status === 'active' || status === 'trialing') {
// 根据 Stripe 价格 ID 获取套餐 ID
planId = await SubscriptionService.getPlanIdByStripePriceId(priceId)
const planId = await SubscriptionService.getPlanIdByStripePriceId(priceId)
// 检查是否已有待激活的订阅记录(通过用户 ID 查找)
const existingSubscription = await prisma.subscription.findFirst({
where: {
userId: user.id,
status: 'pending',
subscriptionPlanId: planId
},
orderBy: { createdAt: 'desc' }
})
if (existingSubscription) {
// 激活现有的订阅记录,并设置 Stripe 订阅 ID
await prisma.subscription.update({
where: { id: existingSubscription.id },
data: {
stripeSubscriptionId,
isActive: true,
status: 'active',
startDate: new Date(currentPeriodStart * 1000),
endDate: new Date(currentPeriodEnd * 1000),
metadata: subscription ? JSON.parse(JSON.stringify(subscription)) : undefined,
updatedAt: new Date()
}
})
console.log(`✅ Activated existing subscription ${existingSubscription.id} for user ${user.id}`)
} else {
// 检查是否是续订(已有活跃订阅)
const activeSubscription = await SubscriptionService.getUserActiveSubscription(user.id)
if (activeSubscription) {
// 这是续订,创建新的订阅记录
await SubscriptionService.createRenewalSubscription(
user.id,
planId,
stripeSubscriptionId,
new Date(currentPeriodStart * 1000),
new Date(currentPeriodEnd * 1000),
customerId,
subscription
)
console.log(`Created renewal subscription for user ${user.id}`)
} else {
// 直接创建并激活订阅(可能是通过其他方式创建的订阅)
await SubscriptionService.createRenewalSubscription(
user.id,
planId,
stripeSubscriptionId,
new Date(currentPeriodStart * 1000),
new Date(currentPeriodEnd * 1000),
customerId,
subscription
)
console.log(`Created new subscription for user ${user.id}`)
}
}
}
// 更新用户订阅套餐
// 更新用户的默认订阅套餐(保持向后兼容)
const planId = status === 'active' || status === 'trialing'
? await SubscriptionService.getPlanIdByStripePriceId(priceId)
: 'free'
await SubscriptionService.updateUserSubscriptionPlan(user.id, planId)
console.log(`Updated user ${user.id} subscription to plan: ${planId}`)
} catch (error) {
console.error('Error handling subscription update:', error)
}

View File

@ -1,5 +1,4 @@
import { prisma } from '@/lib/prisma'
import { getCustomerSubscriptions } from '@/lib/stripe'
import type { SubscriptionPlan } from '@prisma/client'
import { isPlanPro } from '@/lib/subscription-utils'
@ -102,83 +101,119 @@ export class SubscriptionService {
}
/**
* Stripe
*
*/
static async getUserSubscriptionStatus(userId: string): Promise<SubscriptionStatus> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
stripeCustomerId: true,
subscriptionPlanId: true,
subscriptionPlan: true
// 查找用户当前有效的订阅
const activeSubscription = await this.getUserActiveSubscription(userId)
if (activeSubscription) {
return {
isActive: true,
planId: activeSubscription.subscriptionPlanId,
stripeSubscriptionId: activeSubscription.stripeSubscriptionId || undefined,
currentPeriodStart: activeSubscription.startDate || undefined,
currentPeriodEnd: activeSubscription.endDate || undefined,
status: activeSubscription.status
}
}
// 没有有效订阅,返回免费套餐
return {
isActive: true,
planId: 'free'
}
}
/**
*
*/
static async getUserActiveSubscription(userId: string) {
const now = new Date()
return await prisma.subscription.findFirst({
where: {
userId,
isActive: true,
status: 'active',
startDate: { lte: now },
endDate: { gte: now }
},
orderBy: { createdAt: 'desc' }
})
}
/**
*
*/
static async createPendingSubscription(
userId: string,
planId: string,
stripeSubscriptionId?: string,
stripeCustomerId?: string
) {
return await prisma.subscription.create({
data: {
userId,
subscriptionPlanId: planId,
stripeSubscriptionId,
stripeCustomerId,
isActive: false,
status: 'pending'
}
})
}
if (!user) {
throw new Error('User not found')
}
// 如果 plan 没有 stripePriceId则直接返回当前套餐有效
// 无需从 Stripe 实时获取
if (!user.subscriptionPlan?.stripePriceId) {
return {
/**
* Stripe
*/
static async activateSubscription(
stripeSubscriptionId: string,
startDate: Date,
endDate: Date,
metadata?: Record<string, unknown>
) {
return await prisma.subscription.updateMany({
where: {
stripeSubscriptionId,
status: 'pending'
},
data: {
isActive: true,
planId: user.subscriptionPlanId || 'free'
status: 'active',
startDate,
endDate,
metadata: metadata ? JSON.parse(JSON.stringify(metadata)) : undefined,
updatedAt: new Date()
}
}
})
}
// 如果没有匹配到上述规则,则需要从 Stripe 获取实时状态
// 如果此时没有 Stripe 客户 ID则返免费套餐状态
if (!user.stripeCustomerId) {
return {
/**
* Stripe
*/
static async createRenewalSubscription(
userId: string,
planId: string,
stripeSubscriptionId: string,
startDate: Date,
endDate: Date,
stripeCustomerId?: string,
metadata?: Record<string, unknown>
) {
return await prisma.subscription.create({
data: {
userId,
subscriptionPlanId: planId,
stripeSubscriptionId,
stripeCustomerId,
isActive: true,
planId: 'free'
status: 'active',
startDate,
endDate,
metadata: metadata ? JSON.parse(JSON.stringify(metadata)) : undefined
}
}
try {
// 从 Stripe 获取实时订阅状态
const subscriptions = await getCustomerSubscriptions(user.stripeCustomerId)
const activeSubscription = subscriptions.find(sub =>
sub.status === 'active' || sub.status === 'trialing'
)
if (activeSubscription) {
// 根据 Stripe 价格 ID 确定套餐
const items = activeSubscription.items?.data || []
const priceId = items[0]?.price?.id
// 查找对应的套餐
const plan = await prisma.subscriptionPlan.findFirst({
where: { stripePriceId: priceId }
})
const subData = activeSubscription as unknown as Record<string, unknown>
return {
isActive: true,
planId: plan?.id || 'free',
stripeSubscriptionId: activeSubscription.id,
currentPeriodStart: new Date((subData.current_period_start as number) * 1000),
currentPeriodEnd: new Date((subData.current_period_end as number) * 1000),
cancelAtPeriodEnd: subData.cancel_at_period_end as boolean,
status: activeSubscription.status
}
}
// 没有活跃订阅,返回免费套餐
return {
isActive: true,
planId: 'free'
}
} catch (error) {
console.error('Error fetching subscription status from Stripe:', error)
// Stripe 错误时,返回用户当前套餐状态
return {
isActive: true,
planId: user.subscriptionPlanId || 'free'
}
}
})
}
/**