diff --git a/src/app/api/credits/stats/route.ts b/src/app/api/credits/stats/route.ts index bf30f40..f06d958 100644 --- a/src/app/api/credits/stats/route.ts +++ b/src/app/api/credits/stats/route.ts @@ -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() - - if (authError || !user) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ) + const session = await auth.api.getSession({ + headers: await headers() + }) + + 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) { diff --git a/src/app/api/credits/stripe-topup/route.ts b/src/app/api/credits/stripe-topup/route.ts index 20555e1..656139d 100644 --- a/src/app/api/credits/stripe-topup/route.ts +++ b/src/app/api/credits/stripe-topup/route.ts @@ -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 } ) } diff --git a/src/app/api/credits/transactions/route.ts b/src/app/api/credits/transactions/route.ts index 3ab2666..31e3a5c 100644 --- a/src/app/api/credits/transactions/route.ts +++ b/src/app/api/credits/transactions/route.ts @@ -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() - - if (authError || !user) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ) + const session = await auth.api.getSession({ + headers: await headers() + }) + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + const user = session.user + const { searchParams } = new URL(request.url) // Parse query parameters diff --git a/src/app/api/credits/verify-payment/route.ts b/src/app/api/credits/verify-payment/route.ts index 0f36491..ec29bcd 100644 --- a/src/app/api/credits/verify-payment/route.ts +++ b/src/app/api/credits/verify-payment/route.ts @@ -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 } diff --git a/src/app/api/users/credits/route.ts b/src/app/api/users/credits/route.ts index 4d9afe8..43276fd 100644 --- a/src/app/api/users/credits/route.ts +++ b/src/app/api/users/credits/route.ts @@ -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() - - if (authError || !user) { + const session = await auth.api.getSession({ + headers: await headers() + }) + + 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 diff --git a/src/app/credits/page.tsx b/src/app/credits/page.tsx index 310d87f..5b71c59 100644 --- a/src/app/credits/page.tsx +++ b/src/app/credits/page.tsx @@ -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([]) @@ -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) { diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 7205750..f399bac 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -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') diff --git a/src/hooks/useBetterAuth.ts b/src/hooks/useBetterAuth.ts index 2f5f4fa..d175ed3 100644 --- a/src/hooks/useBetterAuth.ts +++ b/src/hooks/useBetterAuth.ts @@ -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 = new Map() +const pendingPromises: Map> = 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({ - user: null, - userData: null, - loading: true, - isAdmin: false, - }) - + const [state, setState] = useState(globalAuthState) const { data: session, isPending } = useSession() - const isInitializedRef = useRef(false) - const currentUserIdRef = useRef(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((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,