fix stripe
This commit is contained in:
parent
81b33f85f2
commit
c765368382
3
package-lock.json
generated
3
package-lock.json
generated
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user