From 79ddfc6898c376df3ab9808fed2f6a463eab1fec Mon Sep 17 00:00:00 2001 From: songtianlun Date: Tue, 26 Aug 2025 21:54:04 +0800 Subject: [PATCH] fix subscribe --- messages/en.json | 3 +- messages/zh.json | 2 + scripts/test-subscription-credits.ts | 105 +++++++++++++++++++++++++++ src/app/api/webhooks/stripe/route.ts | 97 ++++++++++++++++++++++++- src/app/credits/page.tsx | 10 +++ src/lib/services/credit.ts | 67 ++++++++++++++++- 6 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 scripts/test-subscription-credits.ts diff --git a/messages/en.json b/messages/en.json index 73849ba..fa042ac 100644 --- a/messages/en.json +++ b/messages/en.json @@ -122,6 +122,8 @@ "monthlyAllowance": "Monthly Allowance", "purchase": "Purchase", "usage": "Usage", + "subscriptionPayment": "Subscription Payment", + "subscriptionRefund": "Subscription Refund", "simulation": "Simulation", "apiCall": "API Call", "export": "Export", @@ -169,7 +171,6 @@ "descending": "Descending", "itemsPerPage": "Items per page", "page": "Page", - "of": "of", "total": "total", "editPrompt": "Edit Prompt", "deletePrompt": "Delete Prompt", diff --git a/messages/zh.json b/messages/zh.json index 7e2deb3..adebd89 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -122,6 +122,8 @@ "monthlyAllowance": "月度额度", "purchase": "购买充值", "usage": "消费使用", + "subscriptionPayment": "订阅付费", + "subscriptionRefund": "订阅退款", "simulation": "模拟器", "apiCall": "API调用", "export": "导出功能", diff --git a/scripts/test-subscription-credits.ts b/scripts/test-subscription-credits.ts new file mode 100644 index 0000000..fa974d8 --- /dev/null +++ b/scripts/test-subscription-credits.ts @@ -0,0 +1,105 @@ +import { prisma } from '../src/lib/prisma' +import { addCreditForSubscription, getUserBalance, getCreditStats } from '../src/lib/services/credit' + +async function testSubscriptionCredits() { + try { + console.log('🧪 Testing subscription credit integration...') + + // 找到一个测试用户 + const user = await prisma.user.findFirst() + if (!user) { + console.log('❌ No user found. Please create a user first.') + return + } + + console.log(`👤 Testing with user: ${user.email} (ID: ${user.id})`) + + // 获取用户当前余额 + const initialBalance = await getUserBalance(user.id) + console.log(`💰 Initial balance: $${initialBalance.toFixed(2)}`) + + // 查找Pro套餐 + const proPlan = await prisma.subscriptionPlan.findFirst({ + where: { name: 'pro', isActive: true } + }) + + if (!proPlan) { + console.log('❌ Pro plan not found. Please run the seed script first.') + return + } + + console.log(`📋 Using plan: ${proPlan.displayName}`) + + // 创建一个模拟订阅记录(通常这是由Stripe webhook创建的) + const subscription = await prisma.subscription.create({ + data: { + userId: user.id, + subscriptionPlanId: proPlan.id, + stripeSubscriptionId: `sub_test_${Date.now()}`, + stripeCustomerId: user.stripeCustomerId || `cus_test_${Date.now()}`, + isActive: true, + startDate: new Date(), + endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天后 + status: 'active' + } + }) + + console.log(`✅ Created test subscription: ${subscription.id}`) + + // 获取套餐的月度信用额度 + const planLimits = proPlan.limits as { creditMonthly?: number } + const monthlyCreditAmount = planLimits?.creditMonthly || 0 + + if (monthlyCreditAmount > 0) { + console.log(`💳 Adding monthly credit allowance: $${monthlyCreditAmount}`) + + // 使用我们的新function添加订阅信用 + const creditTransaction = await addCreditForSubscription( + user.id, + monthlyCreditAmount, + subscription.id, + proPlan.displayName, + `${proPlan.displayName} monthly credit allowance - ${new Date().toISOString().slice(0, 7)}` + ) + + console.log(`✅ Created credit transaction: ${creditTransaction.id}`) + console.log(` Amount: $${creditTransaction.amount.toFixed(2)}`) + console.log(` Balance after: $${creditTransaction.balance.toFixed(2)}`) + console.log(` Type: ${creditTransaction.type}`) + console.log(` Category: ${creditTransaction.category}`) + console.log(` Reference: ${creditTransaction.referenceType}:${creditTransaction.referenceId}`) + } + + // 获取更新后的余额和统计 + const finalBalance = await getUserBalance(user.id) + const stats = await getCreditStats(user.id) + + console.log(`\n📊 Final Results:`) + console.log(` Current Balance: $${finalBalance.toFixed(2)}`) + console.log(` Balance Increase: $${(finalBalance - initialBalance).toFixed(2)}`) + console.log(` Total Earned: $${stats.totalEarned.toFixed(2)}`) + console.log(` This Month Earned: $${stats.thisMonthEarned.toFixed(2)}`) + + // 显示最新的credit交易记录 + const recentTransactions = await prisma.credit.findMany({ + where: { userId: user.id }, + orderBy: { createdAt: 'desc' }, + take: 3 + }) + + console.log(`\n📝 Recent Credit Transactions:`) + recentTransactions.forEach(tx => { + console.log(` ${tx.createdAt.toISOString().slice(0, 19)} | ${tx.type.padEnd(20)} | ${tx.amount >= 0 ? '+' : ''}$${tx.amount.toFixed(2).padStart(8)} | Balance: $${tx.balance.toFixed(2)}`) + if (tx.note) console.log(` Note: ${tx.note}`) + }) + + console.log('\n✅ Subscription credit integration test completed successfully!') + + } catch (error) { + console.error('❌ Error testing subscription credits:', error) + } finally { + await prisma.$disconnect() + } +} + +testSubscriptionCredits() \ No newline at end of file diff --git a/src/app/api/webhooks/stripe/route.ts b/src/app/api/webhooks/stripe/route.ts index 69544a9..4a40c52 100644 --- a/src/app/api/webhooks/stripe/route.ts +++ b/src/app/api/webhooks/stripe/route.ts @@ -3,6 +3,7 @@ import { stripe, STRIPE_CONFIG } from '@/lib/stripe' import { prisma } from '@/lib/prisma' import { headers } from 'next/headers' import { SubscriptionService } from '@/lib/subscription-service' +import { addCreditForSubscription, recordSubscriptionPayment, addCreditForSubscriptionRefund } from '@/lib/services/credit' export async function POST(request: NextRequest) { try { @@ -279,6 +280,7 @@ async function handleSubscriptionUpdate(subscription: Record) { async function handleSubscriptionDeleted(subscription: Record) { try { const customerId = subscription.customer as string + const stripeSubscriptionId = subscription.id as string // 查找用户 const users = await prisma.$queryRaw>` @@ -294,6 +296,39 @@ async function handleSubscriptionDeleted(subscription: Record) return } + // 查找对应的数据库订阅记录 + const dbSubscription = await prisma.subscription.findFirst({ + where: { stripeSubscriptionId: stripeSubscriptionId }, + include: { subscriptionPlan: true } + }) + + if (dbSubscription) { + // 标记数据库中的订阅为已取消 + await prisma.subscription.update({ + where: { id: dbSubscription.id }, + data: { + isActive: false, + status: 'canceled', + updatedAt: new Date() + } + }) + + console.log(`✅ Marked subscription ${dbSubscription.id} as canceled in database`) + + // TODO: 如果需要记录退款,可以在这里添加 + // 这取决于具体的退款政策和Stripe配置 + // const refundAmount = calculateRefundAmount(dbSubscription) + // if (refundAmount > 0) { + // await addCreditForSubscriptionRefund( + // user.id, + // refundAmount, + // dbSubscription.id, + // dbSubscription.subscriptionPlan.displayName, + // `Refund for canceled ${dbSubscription.subscriptionPlan.displayName} subscription` + // ) + // } + } + // 将用户降级为免费计划 await SubscriptionService.updateUserSubscriptionPlan(user.id, 'free') @@ -309,11 +344,12 @@ async function handlePaymentSucceeded(invoice: Record) { const subscriptionId = invoice.subscription as string const customerId = invoice.customer as string + const amountPaid = (invoice.amount_paid as number) / 100 // Stripe amounts are in cents console.log('Payment details:', { subscriptionId, customerId, - amount: invoice.amount_paid, + amount: amountPaid, status: invoice.status }) @@ -322,11 +358,70 @@ async function handlePaymentSucceeded(invoice: Record) { return } + // 查找用户 + const users = await prisma.$queryRaw>` + SELECT id, email, "stripeCustomerId" + FROM users + WHERE "stripeCustomerId" = ${customerId} + LIMIT 1 + ` + const user = users[0] || null + + if (!user) { + console.error('❌ User not found for customer:', customerId) + return + } + // 获取订阅详情 console.log('📞 Retrieving subscription from Stripe...') const subscription = await stripe.subscriptions.retrieve(subscriptionId) console.log('✅ Retrieved subscription:', subscription.id, 'status:', subscription.status) + // 获取套餐信息 + const priceId = subscription.items.data[0]?.price?.id + if (priceId) { + const plan = await prisma.subscriptionPlan.findFirst({ + where: { stripePriceId: priceId, isActive: true } + }) + + if (plan) { + // 查找对应的数据库订阅记录 + const dbSubscription = await prisma.subscription.findFirst({ + where: { stripeSubscriptionId: subscriptionId } + }) + + if (dbSubscription) { + console.log('💳 Recording subscription payment and credit') + + // 1. 记录订阅付费支出(可选,如果想跟踪用户支出) + // await recordSubscriptionPayment( + // user.id, + // amountPaid, + // dbSubscription.id, + // plan.displayName, + // `${plan.displayName} subscription payment - ${new Date().toISOString().slice(0, 7)}` + // ) + + // 2. 获取套餐配置中的月度信用额度 + const planLimits = plan.limits as { creditMonthly?: number } + const monthlyCreditAmount = planLimits?.creditMonthly || 0 + + if (monthlyCreditAmount > 0) { + // 添加月度信用额度 + await addCreditForSubscription( + user.id, + monthlyCreditAmount, + dbSubscription.id, + plan.displayName, + `${plan.displayName} monthly credit allowance - ${new Date().toISOString().slice(0, 7)}` + ) + + console.log(`✅ Added ${monthlyCreditAmount} USD monthly credit for user ${user.id}`) + } + } + } + } + // 处理订阅更新(激活逻辑) await handleSubscriptionUpdate(subscription as unknown as Record) diff --git a/src/app/credits/page.tsx b/src/app/credits/page.tsx index 611ca45..be31787 100644 --- a/src/app/credits/page.tsx +++ b/src/app/credits/page.tsx @@ -141,6 +141,10 @@ export default function CreditsPage() { return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200' case 'consumption': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' + case 'subscription_payment': + return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200' + case 'subscription_refund': + return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200' } @@ -156,6 +160,10 @@ export default function CreditsPage() { return t('purchase') case 'consumption': return t('usage') + case 'subscription_payment': + return t('subscriptionPayment') + case 'subscription_refund': + return t('subscriptionRefund') default: return type } @@ -277,6 +285,8 @@ export default function CreditsPage() { + +