fix subscribe

This commit is contained in:
songtianlun 2025-08-26 21:54:04 +08:00
parent 8282039b1d
commit 79ddfc6898
6 changed files with 280 additions and 4 deletions

View File

@ -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",

View File

@ -122,6 +122,8 @@
"monthlyAllowance": "月度额度",
"purchase": "购买充值",
"usage": "消费使用",
"subscriptionPayment": "订阅付费",
"subscriptionRefund": "订阅退款",
"simulation": "模拟器",
"apiCall": "API调用",
"export": "导出功能",

View File

@ -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()

View File

@ -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<string, unknown>) {
async function handleSubscriptionDeleted(subscription: Record<string, unknown>) {
try {
const customerId = subscription.customer as string
const stripeSubscriptionId = subscription.id as string
// 查找用户
const users = await prisma.$queryRaw<Array<{id: string, email: string, stripeCustomerId: string}>>`
@ -294,6 +296,39 @@ async function handleSubscriptionDeleted(subscription: Record<string, unknown>)
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<string, unknown>) {
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<string, unknown>) {
return
}
// 查找用户
const users = await prisma.$queryRaw<Array<{id: string, email: string, stripeCustomerId: string}>>`
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<string, unknown>)

View File

@ -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() {
<option value="subscription_monthly">{t('monthlyAllowance')}</option>
<option value="user_purchase">{t('purchase')}</option>
<option value="consumption">{t('usage')}</option>
<option value="subscription_payment">{t('subscriptionPayment')}</option>
<option value="subscription_refund">{t('subscriptionRefund')}</option>
</select>
<select

View File

@ -20,7 +20,7 @@ export interface CreditTransaction {
export interface CreateCreditTransactionOptions {
userId: string
amount: number
type: 'system_gift' | 'subscription_monthly' | 'user_purchase' | 'consumption'
type: 'system_gift' | 'subscription_monthly' | 'user_purchase' | 'consumption' | 'subscription_payment' | 'subscription_refund'
category?: string
note?: string
referenceId?: string
@ -85,7 +85,7 @@ export async function createCreditTransaction(
export async function addCredit(
userId: string,
amount: number,
type: 'system_gift' | 'subscription_monthly' | 'user_purchase',
type: 'system_gift' | 'subscription_monthly' | 'user_purchase' | 'subscription_refund',
note?: string,
expiresAt?: Date
): Promise<CreditTransaction> {
@ -298,3 +298,66 @@ export async function consumeCreditForSimulation(
note || `AI model simulation cost`
)
}
/**
*
*/
export async function addCreditForSubscription(
userId: string,
creditAmount: number,
subscriptionId: string,
planName: string,
note?: string
): Promise<CreditTransaction> {
return createCreditTransaction({
userId,
amount: creditAmount,
type: 'subscription_monthly',
category: 'subscription',
referenceId: subscriptionId,
referenceType: 'subscription',
note: note || `${planName} subscription monthly credit`
})
}
/**
*
*/
export async function recordSubscriptionPayment(
userId: string,
paymentAmount: number,
subscriptionId: string,
planName: string,
note?: string
): Promise<CreditTransaction> {
return createCreditTransaction({
userId,
amount: -paymentAmount, // 负数表示支出
type: 'subscription_payment',
category: 'subscription',
referenceId: subscriptionId,
referenceType: 'subscription',
note: note || `${planName} subscription payment`
})
}
/**
* 退
*/
export async function addCreditForSubscriptionRefund(
userId: string,
refundAmount: number,
subscriptionId: string,
planName: string,
note?: string
): Promise<CreditTransaction> {
return createCreditTransaction({
userId,
amount: refundAmount,
type: 'subscription_refund',
category: 'subscription',
referenceId: subscriptionId,
referenceType: 'subscription',
note: note || `${planName} subscription refund`
})
}