diff --git a/package-lock.json b/package-lock.json index 631b876..5941d24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index d8705d4..b02fc6c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9831f87..cd01652 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") +} diff --git a/src/app/api/subscription/create/route.ts b/src/app/api/subscription/create/route.ts index 9650849..307aad1 100644 --- a/src/app/api/subscription/create/route.ts +++ b/src/app/api/subscription/create/route.ts @@ -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) { diff --git a/src/app/api/webhooks/stripe/route.ts b/src/app/api/webhooks/stripe/route.ts index 262594a..173377d 100644 --- a/src/app/api/webhooks/stripe/route.ts +++ b/src/app/api/webhooks/stripe/route.ts @@ -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) { 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) } catch (error) { - console.error('Error handling checkout session completed:', error) + console.error('❌ Error handling checkout session completed:', error) } } async function handleSubscriptionUpdate(subscription: Record) { 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) { }) 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) } diff --git a/src/lib/subscription-service.ts b/src/lib/subscription-service.ts index 8cc0174..2e80657 100644 --- a/src/lib/subscription-service.ts +++ b/src/lib/subscription-service.ts @@ -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 { - 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 + ) { + 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 + ) { + 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 - - 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' - } - } + }) } /**