fix topup

This commit is contained in:
songtianlun 2025-08-30 12:48:06 +08:00
parent 57c23e21bb
commit 89549e00ff
8 changed files with 315 additions and 224 deletions

View File

@ -1,20 +1,19 @@
import { NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { auth } from '@/lib/auth'
import { getCreditStats } from '@/lib/services/credit'
import { headers } from 'next/headers'
export async function GET() {
try {
const supabase = await createServerSupabaseClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
const session = await auth.api.getSession({
headers: await headers()
})
if (authError || !user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const stats = await getCreditStats(user.id)
const stats = await getCreditStats(session.user.id)
return NextResponse.json(stats)
} catch (error) {

View File

@ -1,7 +1,8 @@
import { NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { createOrGetStripeCustomer, createPaymentSession } from '@/lib/stripe'
import { auth } from '@/lib/auth'
import { createOrGetStripeCustomer, stripe } from '@/lib/stripe'
import { prisma } from '@/lib/prisma'
import { headers } from 'next/headers'
export async function POST(request: Request) {
try {
@ -22,73 +23,82 @@ export async function POST(request: Request) {
)
}
const supabase = await createServerSupabaseClient()
// 获取Better Auth会话
const session = await auth.api.getSession({
headers: await headers()
})
// 获取当前用户
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 确保用户存在于数据库中,并获取用户信息
const userData = await prisma.user.upsert({
const user = session.user
// 获取或创建用户数据不需要upsert直接查找即可
let userData = await prisma.user.findUnique({
where: { id: user.id },
update: {
email: user.email || '',
updatedAt: new Date()
},
create: {
id: user.id,
email: user.email || '',
name: user.user_metadata?.full_name || user.email?.split('@')[0] || 'User',
emailVerified: false,
username: user.user_metadata?.username || null,
subscriptionPlanId: 'free',
createdAt: new Date(),
updatedAt: new Date()
},
select: {
email: true,
username: true
name: true
}
})
try {
// 创建或获取 Stripe 客户
const customer = await createOrGetStripeCustomer(
user.id,
userData.email,
userData.username || undefined
)
// 创建支付会话
const session = await createPaymentSession(
customer.id,
Math.round(amount * 100), // 转换为美分
`Credit top-up: $${amount}`,
`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/credits/success?session_id={CHECKOUT_SESSION_ID}`,
`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/credits/cancel`
)
return NextResponse.json({
success: true,
sessionId: session.id,
sessionUrl: session.url
// 如果用户不存在创建用户这种情况应该很少发生因为用户应该已经通过sync API创建了
if (!userData) {
userData = await prisma.user.create({
data: {
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.emailVerified ?? false,
subscriptionPlanId: 'free'
},
select: {
email: true,
name: true
}
})
} catch (stripeError) {
console.error('Stripe operation failed:', stripeError)
return NextResponse.json(
{ error: 'Failed to create payment session' },
{ status: 500 }
)
}
// 创建或获取 Stripe 客户
const stripeCustomer = await createOrGetStripeCustomer(
user.id,
userData.email,
userData.name || 'User'
)
// 创建支付会话
const checkoutSession = await stripe.checkout.sessions.create({
customer: stripeCustomer.id,
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: 'Credit Top-up',
description: `Credit top-up for user ${user.id}`,
},
unit_amount: amount * 100, // 转换为美分
},
quantity: 1,
},
],
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/credits/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/credits/cancel`,
metadata: {
type: 'credit_topup',
amount: amount.toString(),
userId: user.id // 添加userId到metadata用于验证
},
})
return NextResponse.json({ url: checkoutSession.url })
} catch (error) {
console.error('Stripe top-up failed:', error)
return NextResponse.json(
{ error: 'Failed to process top-up request' },
{ error: 'Failed to create payment session' },
{ status: 500 }
)
}

View File

@ -1,19 +1,20 @@
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { auth } from '@/lib/auth'
import { getCreditTransactions } from '@/lib/services/credit'
import { headers } from 'next/headers'
export async function GET(request: NextRequest) {
try {
const supabase = await createServerSupabaseClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
const session = await auth.api.getSession({
headers: await headers()
})
if (authError || !user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const user = session.user
const { searchParams } = new URL(request.url)
// Parse query parameters

View File

@ -1,8 +1,9 @@
import { NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { auth } from '@/lib/auth'
import { stripe } from '@/lib/stripe'
import { addCredit } from '@/lib/services/credit'
import { prisma } from '@/lib/prisma'
import { headers } from 'next/headers'
export async function GET(request: Request) {
try {
@ -16,106 +17,99 @@ export async function GET(request: Request) {
)
}
const supabase = await createServerSupabaseClient()
// 获取Better Auth会话
const session = await auth.api.getSession({
headers: await headers()
})
// 获取当前用户
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const user = session.user
try {
// 从 Stripe 获取支付会话信息
const session = await stripe.checkout.sessions.retrieve(sessionId)
const stripeSession = await stripe.checkout.sessions.retrieve(sessionId)
if (!session) {
return NextResponse.json(
{ error: 'Session not found' },
{ status: 404 }
)
}
// 验证会话是否成功支付
if (session.payment_status !== 'paid') {
if (stripeSession.payment_status !== 'paid') {
return NextResponse.json(
{ error: 'Payment not completed' },
{ status: 400 }
)
}
// 获取金额
const amount = session.metadata?.amount ? parseFloat(session.metadata.amount) : 0
if (amount <= 0) {
// 验证支付会话属于当前用户
if (stripeSession.metadata?.userId !== user.id) {
return NextResponse.json(
{ error: 'Invalid amount in session' },
{ status: 400 }
{ error: 'Payment session does not belong to current user' },
{ status: 403 }
)
}
// 确保用户存在于数据库中
await prisma.user.upsert({
where: { id: user.id },
update: {
email: user.email || '',
updatedAt: new Date()
},
create: {
id: user.id,
email: user.email || '',
name: user.user_metadata?.full_name || user.email?.split('@')[0] || 'User',
emailVerified: false,
username: user.user_metadata?.username || null,
subscriptionPlanId: 'free',
createdAt: new Date(),
updatedAt: new Date()
}
// 获取或创建用户数据
let userData = await prisma.user.findUnique({
where: { id: user.id }
})
// 如果用户不存在,创建用户
if (!userData) {
userData = await prisma.user.create({
data: {
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.emailVerified ?? false,
subscriptionPlanId: 'free'
}
})
}
// 检查是否已经处理过这个支付会话
const existingCredit = await prisma.credit.findFirst({
where: {
referenceId: sessionId,
referenceType: 'stripe_payment'
referenceType: 'stripe_session'
}
})
if (existingCredit) {
// 已经处理过,直接返回成功
return NextResponse.json({
success: true,
amount,
message: 'Payment already processed'
message: 'Payment already processed',
amount: existingCredit.amount,
sessionId
})
}
// 添加信用额度
const creditTransaction = await addCredit(
// 从支付会话获取金额Stripe金额以分为单位
const amount = (stripeSession.amount_total || 0) / 100
if (amount <= 0) {
return NextResponse.json(
{ error: 'Invalid payment amount' },
{ status: 400 }
)
}
// 添加信用记录
await addCredit(
user.id,
amount,
'user_purchase',
`Stripe payment: $${amount}`
`Stripe payment - Session: ${sessionId}`
// 充值的信用不会过期,不设置过期时间
)
// 更新 credit 记录的 referenceId 和 referenceType
await prisma.credit.update({
where: { id: creditTransaction.id },
data: {
referenceId: sessionId,
referenceType: 'stripe_payment'
}
})
return NextResponse.json({
success: true,
message: 'Payment verified and credits added',
amount,
message: `Successfully added $${amount} to your account`,
transaction: creditTransaction
sessionId
})
} catch (stripeError) {
console.error('Stripe verification failed:', stripeError)
console.error('Stripe error:', stripeError)
return NextResponse.json(
{ error: 'Failed to verify payment with Stripe' },
{ status: 500 }

View File

@ -1,18 +1,21 @@
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { auth } from '@/lib/auth'
import { getUserBalance } from '@/lib/services/credit'
import { prisma } from '@/lib/prisma'
import { headers } from 'next/headers'
// GET /api/users/credits - 获取用户信用额度概览(向后兼容)
export async function GET(request: NextRequest) {
try {
const supabase = await createServerSupabaseClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
const session = await auth.api.getSession({
headers: await headers()
})
if (authError || !user) {
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const user = session.user
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId') || user.id

View File

@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useTranslations } from 'next-intl'
import { useAuthUser } from '@/hooks/useAuthUser'
import { useBetterAuth } from '@/hooks/useBetterAuth'
import { Header } from '@/components/layout/Header'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
@ -51,7 +51,7 @@ interface CreditTransactionsResult {
}
export default function CreditsPage() {
const { user, loading } = useAuthUser()
const { user, loading } = useBetterAuth()
const t = useTranslations('credits')
const [transactions, setTransactions] = useState<CreditTransaction[]>([])
@ -206,10 +206,11 @@ export default function CreditsPage() {
const data = await response.json()
if (response.ok && data.success && data.sessionUrl) {
if (response.ok && data.url) {
// 重定向到 Stripe Checkout
window.location.href = data.sessionUrl
window.location.href = data.url
} else {
console.error("failed to create payment session: ", data.error)
alert(data.error || 'Failed to create payment session')
}
} catch (error) {

View File

@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useTranslations } from 'next-intl'
import { useAuthUser } from '@/hooks/useAuthUser'
import { useBetterAuth } from '@/hooks/useBetterAuth'
import { Header } from '@/components/layout/Header'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@ -50,7 +50,7 @@ interface CreditInfo {
}
export default function ProfilePage() {
const { user, loading, triggerProfileUpdate } = useAuthUser()
const { user, loading, triggerProfileUpdate } = useBetterAuth()
const t = useTranslations('profile')
const tCommon = useTranslations('common')
const tAuth = useTranslations('auth')

View File

@ -1,10 +1,14 @@
'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useEffect, useState, useCallback } from 'react'
import { useSession } from '@/lib/auth-client'
import { userCache, UserData, SyncTrigger, SyncOptions, shouldSync } from '@/lib/user-cache'
import type { User } from '@/lib/auth'
// 全局防抖变量
const debounceTimeouts: Map<string, NodeJS.Timeout> = new Map()
const pendingPromises: Map<string, Promise<void>> = new Map()
interface AuthUserState {
user: User | null
userData: UserData | null
@ -12,20 +16,48 @@ interface AuthUserState {
isAdmin: boolean
}
// 全局状态管理,避免多个钩子实例重复调用
let globalAuthState: AuthUserState = {
user: null,
userData: null,
loading: true,
isAdmin: false,
}
const stateListeners: Set<(state: AuthUserState) => void> = new Set()
let isGlobalInitialized = false
let currentUserId: string | null = null
let lastSyncTime: number = 0
// 广播状态变化给所有监听器
const notifyStateChange = (newState: AuthUserState) => {
globalAuthState = { ...newState }
stateListeners.forEach(listener => listener(newState))
}
export function useBetterAuth() {
const [state, setState] = useState<AuthUserState>({
user: null,
userData: null,
loading: true,
isAdmin: false,
})
const [state, setState] = useState<AuthUserState>(globalAuthState)
const { data: session, isPending } = useSession()
const isInitializedRef = useRef(false)
const currentUserIdRef = useRef<string | null>(null)
// 同步用户数据到数据库
const syncUserToDatabase = useCallback(async (userId: string, options: SyncOptions) => {
// 注册状态监听器
useEffect(() => {
const listener = (newState: AuthUserState) => {
setState(newState)
}
stateListeners.add(listener)
return () => {
stateListeners.delete(listener)
}
}, [])
// 全局同步函数,防止重复调用
const globalSyncUserToDatabase = useCallback(async (userId: string, options: SyncOptions) => {
// 检查是否在短时间内已经同步过
const now = Date.now()
if (!options.force && now - lastSyncTime < 5000) { // 5秒内不重复同步
return
}
if (!shouldSync(userId, options)) {
return
}
@ -42,6 +74,7 @@ export function useBetterAuth() {
}
userCache.recordSync(userId)
lastSyncTime = Date.now()
console.log(`User synced to database: ${userId} (trigger: ${options.trigger})`)
} catch (error) {
console.error('Failed to sync user to database:', error)
@ -50,17 +83,18 @@ export function useBetterAuth() {
})
}, [])
// 获取用户数据
const fetchUserData = useCallback(async (userId: string, options: SyncOptions) => {
// 全局获取用户数据函数
const globalFetchUserData = useCallback(async (userId: string, options: SyncOptions) => {
// 先检查缓存
if (!options.skipCache) {
const cachedData = userCache.getUserData(userId)
if (cachedData) {
setState(prev => ({
...prev,
const newState = {
...globalAuthState,
userData: cachedData,
isAdmin: cachedData.isAdmin || false,
}))
}
notifyStateChange(newState)
return cachedData
}
}
@ -77,12 +111,13 @@ export function useBetterAuth() {
// 更新缓存
userCache.setUserData(userId, userData)
// 更新状态
setState(prev => ({
...prev,
// 更新全局状态
const newState = {
...globalAuthState,
userData,
isAdmin: userData.isAdmin || false,
}))
}
notifyStateChange(newState)
return userData
} catch (error) {
@ -91,47 +126,98 @@ export function useBetterAuth() {
}
}, [])
// 处理用户状态变化
const handleUserChange = useCallback(async (
// 立即更新用户状态(不防抖)
const updateUserState = useCallback((user: User | null) => {
const newState = {
...globalAuthState,
user,
loading: false,
}
notifyStateChange(newState)
// 如果用户变化了,清除旧用户的缓存
if (currentUserId && currentUserId !== user?.id) {
userCache.clearUserCache(currentUserId)
lastSyncTime = 0 // 重置同步时间
}
currentUserId = user?.id || null
if (!user) {
const userLoggedOutState = {
...globalAuthState,
user: null,
userData: null,
isAdmin: false,
loading: false,
}
notifyStateChange(userLoggedOutState)
}
}, [])
// 全局处理用户数据同步(可以防抖)
const globalHandleUserDataSync = useCallback(async (
user: User | null,
trigger: SyncTrigger = SyncTrigger.INITIAL_LOAD
) => {
// 更新用户状态
setState(prev => ({
...prev,
user,
loading: false,
}))
if (!user) return
// 如果用户变化了,清除旧用户的缓存
if (currentUserIdRef.current && currentUserIdRef.current !== user?.id) {
userCache.clearUserCache(currentUserIdRef.current)
}
currentUserIdRef.current = user?.id || null
if (!user) {
setState(prev => ({
...prev,
userData: null,
isAdmin: false,
}))
// 避免重复处理相同用户
if (trigger === SyncTrigger.INITIAL_LOAD && isGlobalInitialized && user.id === currentUserId) {
return
}
try {
const syncOptions: SyncOptions = { trigger }
// 并行执行同步和获取用户数据
// 并行执行同步和获取用户数据,但添加去重机制
await Promise.all([
syncUserToDatabase(user.id, syncOptions),
fetchUserData(user.id, syncOptions),
globalSyncUserToDatabase(user.id, syncOptions),
globalFetchUserData(user.id, syncOptions),
])
console.log(`User data loaded: ${user.id} (trigger: ${trigger})`)
} catch (error) {
console.error('Failed to handle user change:', error)
}
}, [syncUserToDatabase, fetchUserData])
}, [globalSyncUserToDatabase, globalFetchUserData])
// 创建防抖版本的用户数据同步处理
const debouncedUserDataSync = useCallback(async (
user: User | null,
trigger: SyncTrigger = SyncTrigger.INITIAL_LOAD
) => {
const key = `${user?.id || 'anonymous'}-${trigger}`
// 清除之前的超时
const existingTimeout = debounceTimeouts.get(key)
if (existingTimeout) {
clearTimeout(existingTimeout)
}
// 如果有正在执行的Promise直接返回
const existingPromise = pendingPromises.get(key)
if (existingPromise) {
return existingPromise
}
// 创建新的防抖Promise
const debouncePromise = new Promise<void>((resolve) => {
const timeout = setTimeout(async () => {
debounceTimeouts.delete(key)
try {
await globalHandleUserDataSync(user, trigger)
} finally {
pendingPromises.delete(key)
resolve()
}
}, 300)
debounceTimeouts.set(key, timeout)
})
pendingPromises.set(key, debouncePromise)
return debouncePromise
}, [globalHandleUserDataSync])
// 监听 Better Auth 会话变化
useEffect(() => {
@ -141,12 +227,18 @@ export function useBetterAuth() {
if (!mounted) return
const user = session?.user || null
await handleUserChange(user,
!isInitializedRef.current ? SyncTrigger.INITIAL_LOAD : SyncTrigger.SIGN_IN
)
const trigger = !isGlobalInitialized ? SyncTrigger.INITIAL_LOAD : SyncTrigger.SIGN_IN
if (!isInitializedRef.current) {
isInitializedRef.current = true
// 立即更新用户状态(不延迟)
updateUserState(user)
// 异步进行数据同步(防抖延迟)
if (user) {
debouncedUserDataSync(user, trigger)
}
if (!isGlobalInitialized) {
isGlobalInitialized = true
}
}
@ -157,22 +249,23 @@ export function useBetterAuth() {
return () => {
mounted = false
}
}, [session, isPending, handleUserChange])
}, [session, isPending, updateUserState, debouncedUserDataSync])
// 设置loading状态
useEffect(() => {
setState(prev => ({
...prev,
const newState = {
...globalAuthState,
loading: isPending
}))
}
notifyStateChange(newState)
}, [isPending])
// 手动刷新用户数据
const refreshUserData = useCallback(async (force = false) => {
if (!state.user) return
if (!globalAuthState.user) return
try {
setState(prev => ({ ...prev, loading: true }))
notifyStateChange({ ...globalAuthState, loading: true })
const syncOptions: SyncOptions = {
trigger: SyncTrigger.MANUAL_REFRESH,
@ -181,15 +274,15 @@ export function useBetterAuth() {
}
await Promise.all([
syncUserToDatabase(state.user.id, syncOptions),
fetchUserData(state.user.id, syncOptions),
globalSyncUserToDatabase(globalAuthState.user.id, syncOptions),
globalFetchUserData(globalAuthState.user.id, syncOptions),
])
} catch (error) {
console.error('Failed to refresh user data:', error)
} finally {
setState(prev => ({ ...prev, loading: false }))
notifyStateChange({ ...globalAuthState, loading: false })
}
}, [state.user, syncUserToDatabase, fetchUserData])
}, [globalSyncUserToDatabase, globalFetchUserData])
// 登出
const signOut = useCallback(async () => {
@ -198,10 +291,15 @@ export function useBetterAuth() {
await betterSignOut()
// 清除缓存
if (currentUserIdRef.current) {
userCache.clearUserCache(currentUserIdRef.current)
if (currentUserId) {
userCache.clearUserCache(currentUserId)
}
// 重置全局状态
isGlobalInitialized = false
currentUserId = null
lastSyncTime = 0
// 重定向到首页
window.location.href = '/'
} catch (error) {
@ -211,40 +309,25 @@ export function useBetterAuth() {
// 触发用户信息更新(用于个人资料更新后)
const triggerProfileUpdate = useCallback(async () => {
if (!state.user) return
const syncOptions: SyncOptions = {
trigger: SyncTrigger.PROFILE_UPDATE,
force: true,
skipCache: true,
}
if (!globalAuthState.user) return
try {
await Promise.all([
syncUserToDatabase(state.user.id, syncOptions),
fetchUserData(state.user.id, syncOptions),
])
await globalHandleUserDataSync(globalAuthState.user, SyncTrigger.PROFILE_UPDATE)
} catch (error) {
console.error('Failed to update profile:', error)
}
}, [state.user, syncUserToDatabase, fetchUserData])
}, [globalHandleUserDataSync])
// 触发订阅状态更新
const triggerSubscriptionUpdate = useCallback(async () => {
if (!state.user) return
const syncOptions: SyncOptions = {
trigger: SyncTrigger.SUBSCRIPTION_CHANGE,
force: true,
skipCache: true,
}
if (!globalAuthState.user) return
try {
await fetchUserData(state.user.id, syncOptions)
await globalHandleUserDataSync(globalAuthState.user, SyncTrigger.SUBSCRIPTION_CHANGE)
} catch (error) {
console.error('Failed to update subscription:', error)
}
}, [state.user, fetchUserData])
}, [globalHandleUserDataSync])
return {
user: state.user,