fix build
This commit is contained in:
parent
0681738a27
commit
5b36748048
@ -166,3 +166,18 @@ import { useUser } from '@/hooks/useUser'
|
|||||||
4. **错误率**:监控缓存相关的错误
|
4. **错误率**:监控缓存相关的错误
|
||||||
|
|
||||||
这个优化显著改善了应用的性能和用户体验,同时降低了服务器负载和运营成本。
|
这个优化显著改善了应用的性能和用户体验,同时降低了服务器负载和运营成本。
|
||||||
|
|
||||||
|
## 构建状态
|
||||||
|
|
||||||
|
✅ **构建成功** - 所有 TypeScript 错误已修复,应用可以正常构建和部署。
|
||||||
|
|
||||||
|
剩余的警告(非关键):
|
||||||
|
- Google Analytics 脚本建议使用 `next/script` 组件
|
||||||
|
- Avatar 组件建议使用 `next/image` 优化图片加载
|
||||||
|
|
||||||
|
## 部署建议
|
||||||
|
|
||||||
|
1. **生产环境验证**:在生产环境中监控 API 调用频率和缓存命中率
|
||||||
|
2. **性能监控**:使用 `/debug/cache` 页面定期检查缓存性能
|
||||||
|
3. **错误监控**:关注缓存相关的错误日志
|
||||||
|
4. **用户反馈**:收集用户对页面加载速度改善的反馈
|
||||||
|
@ -48,7 +48,7 @@ export async function POST(request: NextRequest) {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case 'invoice.payment_failed':
|
case 'invoice.payment_failed':
|
||||||
await handlePaymentFailed(event.data.object as unknown as Record<string, unknown>)
|
await handlePaymentFailed()
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -277,7 +277,7 @@ async function handlePaymentSucceeded(invoice: Record<string, unknown>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePaymentFailed(_invoice: Record<string, unknown>) {
|
async function handlePaymentFailed() {
|
||||||
try {
|
try {
|
||||||
// 这里可以添加额外的逻辑,比如发送提醒邮件等
|
// 这里可以添加额外的逻辑,比如发送提醒邮件等
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ import { Header } from '@/components/layout/Header'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Check, Crown, Star } from 'lucide-react'
|
import { Check, Crown, Star } from 'lucide-react'
|
||||||
import { SubscribeButton } from '@/components/subscription/SubscribeButton'
|
import { SubscribeButton } from '@/components/subscription/SubscribeButton'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import type { SubscriptionPlan } from '@prisma/client'
|
// Remove unused import
|
||||||
import {
|
import {
|
||||||
isPlanPro,
|
isPlanPro,
|
||||||
isPlanFree,
|
isPlanFree,
|
||||||
@ -20,16 +20,32 @@ import {
|
|||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const { user, userData } = useAuthUser()
|
const { user, userData } = useAuthUser()
|
||||||
const t = useTranslations('pricing')
|
const t = useTranslations('pricing')
|
||||||
const [plans, setPlans] = useState<any[]>([])
|
const [plans, setPlans] = useState<Array<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
displayName?: string
|
||||||
|
description: string
|
||||||
|
price: number
|
||||||
|
currency: string
|
||||||
|
interval: string
|
||||||
|
features: string[]
|
||||||
|
stripePriceId: string
|
||||||
|
isPopular?: boolean
|
||||||
|
}>>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const fetchPlans = async () => {
|
const isCurrentPlan = useCallback((planId: string) => {
|
||||||
|
if (!userData) return false
|
||||||
|
return userData.subscriptionPlanId === planId
|
||||||
|
}, [userData])
|
||||||
|
|
||||||
|
const fetchPlans = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/subscription-plans')
|
const response = await fetch('/api/subscription-plans')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
// 过滤套餐:只显示真正的免费套餐、用户当前套餐,以及有 stripePriceId 的付费套餐
|
// 过滤套餐:只显示真正的免费套餐、用户当前套餐,以及有 stripePriceId 的付费套餐
|
||||||
const filteredPlans = (data.plans || []).filter((plan: SubscriptionPlan) => {
|
const filteredPlans = (data.plans || []).filter((plan: { id: string; name: string; price: number; stripePriceId?: string }) => {
|
||||||
// 只显示官方的免费套餐(ID为'free'或名称为'free')
|
// 只显示官方的免费套餐(ID为'free'或名称为'free')
|
||||||
if (isPlanFree(plan) && (plan.id === 'free' || plan.name.toLowerCase() === 'free')) {
|
if (isPlanFree(plan) && (plan.id === 'free' || plan.name.toLowerCase() === 'free')) {
|
||||||
return true
|
return true
|
||||||
@ -52,18 +68,13 @@ export default function PricingPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [userData, isCurrentPlan])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPlans()
|
fetchPlans()
|
||||||
}, [userData]) // 依赖 userData,确保用户数据加载后再过滤套餐
|
}, [fetchPlans]) // 依赖 fetchPlans
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const isCurrentPlan = (planId: string) => {
|
|
||||||
if (!userData) return false
|
|
||||||
return userData.subscriptionPlanId === planId
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -136,7 +147,7 @@ export default function PricingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-bold text-card-foreground mb-2">
|
<h3 className="text-2xl font-bold text-card-foreground mb-2">
|
||||||
{plan.displayName}
|
{plan.displayName || plan.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-4xl font-bold text-card-foreground mb-2">
|
<div className="text-4xl font-bold text-card-foreground mb-2">
|
||||||
{formatPlanPrice(plan, t)}
|
{formatPlanPrice(plan, t)}
|
||||||
@ -217,7 +228,7 @@ export default function PricingPage() {
|
|||||||
return (
|
return (
|
||||||
<SubscribeButton
|
<SubscribeButton
|
||||||
priceId={plan.stripePriceId}
|
priceId={plan.stripePriceId}
|
||||||
planName={plan.displayName}
|
planName={plan.displayName || plan.name}
|
||||||
className={`w-full ${isPro ? 'bg-primary hover:bg-primary/90' : ''}`}
|
className={`w-full ${isPro ? 'bg-primary hover:bg-primary/90' : ''}`}
|
||||||
>
|
>
|
||||||
{t('subscribeNow')}
|
{t('subscribeNow')}
|
||||||
|
@ -10,7 +10,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { LegacyAvatar } from '@/components/ui/avatar'
|
import { LegacyAvatar } from '@/components/ui/avatar'
|
||||||
import { LoadingSpinner, LoadingOverlay, GradientLoading } from '@/components/ui/loading-spinner'
|
import { LoadingSpinner, LoadingOverlay } from '@/components/ui/loading-spinner'
|
||||||
import { AvatarSkeleton, FormFieldSkeleton, TextAreaSkeleton } from '@/components/ui/skeleton'
|
import { AvatarSkeleton, FormFieldSkeleton, TextAreaSkeleton } from '@/components/ui/skeleton'
|
||||||
import { Save, Eye, EyeOff, CreditCard, Crown, Star } from 'lucide-react'
|
import { Save, Eye, EyeOff, CreditCard, Crown, Star } from 'lucide-react'
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ interface SubscriptionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SubscriptionPage() {
|
export default function SubscriptionPage() {
|
||||||
const { user, userData, loading, triggerSubscriptionUpdate } = useAuthUser()
|
const { user, userData, loading } = useAuthUser()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const t = useTranslations('subscription')
|
const t = useTranslations('subscription')
|
||||||
|
|
||||||
@ -111,40 +111,7 @@ export default function SubscriptionPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleManageSubscription = async () => {
|
|
||||||
setActionLoading(true)
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/subscription/manage', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
action: 'portal'
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const { url } = await response.json()
|
|
||||||
window.location.href = url
|
|
||||||
} else {
|
|
||||||
const errorData = await response.json()
|
|
||||||
console.error('Portal error:', errorData)
|
|
||||||
|
|
||||||
// 如果是客户门户未配置的错误,提供替代方案
|
|
||||||
if (errorData.details?.includes('billing portal') || errorData.details?.includes('portal')) {
|
|
||||||
alert('Billing portal is not yet configured. Please contact support for subscription management.')
|
|
||||||
} else {
|
|
||||||
throw new Error(errorData.error || 'Failed to create portal session')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Portal failed:', error)
|
|
||||||
alert('Failed to open billing portal. Please try again.')
|
|
||||||
} finally {
|
|
||||||
setActionLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSyncSubscription = async () => {
|
const handleSyncSubscription = async () => {
|
||||||
setActionLoading(true)
|
setActionLoading(true)
|
||||||
|
@ -126,7 +126,7 @@ export function PromptCard({ prompt, onViewIncrement }: PromptCardProps) {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
errorMessage = errorData.error || `HTTP ${response.status}: ${response.statusText}`
|
errorMessage = errorData.error || `HTTP ${response.status}: ${response.statusText}`
|
||||||
} catch (e) {
|
} catch {
|
||||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ export function PromptCard({ prompt, onViewIncrement }: PromptCardProps) {
|
|||||||
}, 300)
|
}, 300)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Create error notification for network/other errors
|
// Create error notification for network/other errors
|
||||||
const notification = document.createElement('div')
|
const notification = document.createElement('div')
|
||||||
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-md shadow-lg z-50 transition-all duration-300'
|
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-md shadow-lg z-50 transition-all duration-300'
|
||||||
|
@ -128,7 +128,7 @@ export function PromptDetailModal({
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
errorMessage = errorData.error || `HTTP ${response.status}: ${response.statusText}`
|
errorMessage = errorData.error || `HTTP ${response.status}: ${response.statusText}`
|
||||||
} catch (e) {
|
} catch {
|
||||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ export function PromptDetailModal({
|
|||||||
}, 300)
|
}, 300)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Create error notification for network/other errors
|
// Create error notification for network/other errors
|
||||||
const notification = document.createElement('div')
|
const notification = document.createElement('div')
|
||||||
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-md shadow-lg z-50 transition-all duration-300'
|
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-md shadow-lg z-50 transition-all duration-300'
|
||||||
|
@ -116,7 +116,7 @@ export function QuickUpgradeButton({ className, children }: QuickUpgradeButtonPr
|
|||||||
} else {
|
} else {
|
||||||
setError('Failed to load Pro plan')
|
setError('Failed to load Pro plan')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
setError('Failed to load Pro plan')
|
setError('Failed to load Pro plan')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@ -122,7 +122,7 @@ export function useAuthUser() {
|
|||||||
const syncOptions: SyncOptions = { trigger }
|
const syncOptions: SyncOptions = { trigger }
|
||||||
|
|
||||||
// 并行执行同步和获取用户数据
|
// 并行执行同步和获取用户数据
|
||||||
const [, userData] = await Promise.all([
|
await Promise.all([
|
||||||
syncUserToDatabase(user.id, syncOptions),
|
syncUserToDatabase(user.id, syncOptions),
|
||||||
fetchUserData(user.id, syncOptions),
|
fetchUserData(user.id, syncOptions),
|
||||||
])
|
])
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import type { SubscriptionPlan } from '@prisma/client'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅相关的工具函数
|
* 订阅相关的工具函数
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 支持部分套餐数据的类型
|
// 支持部分套餐数据的类型
|
||||||
type PlanLike = { price: number } | SubscriptionPlan | null | undefined
|
type PlanLike = { price: number; id?: string; name?: string; features?: unknown; limits?: unknown } | null | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断套餐是否为 Pro 级别(价格超过 19)
|
* 判断套餐是否为 Pro 级别(价格超过 19)
|
||||||
@ -104,7 +102,7 @@ export function formatPlanPrice(plan: PlanLike & { interval?: string }, t?: (key
|
|||||||
/**
|
/**
|
||||||
* 获取套餐限制的类型安全访问器
|
* 获取套餐限制的类型安全访问器
|
||||||
*/
|
*/
|
||||||
export function getPlanLimits(plan: SubscriptionPlan | null | undefined): {
|
export function getPlanLimits(plan: PlanLike): {
|
||||||
promptLimit: number
|
promptLimit: number
|
||||||
maxVersionLimit: number
|
maxVersionLimit: number
|
||||||
creditMonthly: number
|
creditMonthly: number
|
||||||
@ -117,7 +115,7 @@ export function getPlanLimits(plan: SubscriptionPlan | null | undefined): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const limits = plan.limits as Record<string, unknown>
|
const limits = (plan.limits as Record<string, unknown>) || {}
|
||||||
return {
|
return {
|
||||||
promptLimit: (limits?.promptLimit as number) || 500,
|
promptLimit: (limits?.promptLimit as number) || 500,
|
||||||
maxVersionLimit: (limits?.maxVersionLimit as number) || 3,
|
maxVersionLimit: (limits?.maxVersionLimit as number) || 3,
|
||||||
@ -128,17 +126,17 @@ export function getPlanLimits(plan: SubscriptionPlan | null | undefined): {
|
|||||||
/**
|
/**
|
||||||
* 获取套餐功能的类型安全访问器
|
* 获取套餐功能的类型安全访问器
|
||||||
*/
|
*/
|
||||||
export function getPlanFeatures(plan: SubscriptionPlan | null | undefined): string[] {
|
export function getPlanFeatures(plan: PlanLike): string[] {
|
||||||
if (!plan) return []
|
if (!plan) return []
|
||||||
|
|
||||||
const features = plan.features as Record<string, unknown>
|
const features = (plan.features as Record<string, unknown>) || {}
|
||||||
return Object.keys(features).filter(key => features[key] === true)
|
return Object.keys(features).filter(key => features[key] === true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查套餐是否包含特定功能
|
* 检查套餐是否包含特定功能
|
||||||
*/
|
*/
|
||||||
export function planHasFeature(plan: SubscriptionPlan | null | undefined, feature: string): boolean {
|
export function planHasFeature(plan: PlanLike, feature: string): boolean {
|
||||||
const features = getPlanFeatures(plan)
|
const features = getPlanFeatures(plan)
|
||||||
return features.includes(feature)
|
return features.includes(feature)
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ const CACHE_CONFIG = {
|
|||||||
class UserCache {
|
class UserCache {
|
||||||
private userDataCache = new Map<string, CacheItem<UserData>>()
|
private userDataCache = new Map<string, CacheItem<UserData>>()
|
||||||
private syncTimestamps = new Map<string, number>()
|
private syncTimestamps = new Map<string, number>()
|
||||||
private syncPromises = new Map<string, Promise<any>>()
|
private syncPromises = new Map<string, Promise<void>>()
|
||||||
|
|
||||||
// 获取缓存的用户数据
|
// 获取缓存的用户数据
|
||||||
getUserData(userId: string): UserData | null {
|
getUserData(userId: string): UserData | null {
|
||||||
@ -85,7 +85,7 @@ class UserCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取或创建同步Promise(防止重复同步)
|
// 获取或创建同步Promise(防止重复同步)
|
||||||
getSyncPromise(userId: string, syncFn: () => Promise<any>): Promise<any> {
|
getSyncPromise(userId: string, syncFn: () => Promise<void>): Promise<void> {
|
||||||
const existing = this.syncPromises.get(userId)
|
const existing = this.syncPromises.get(userId)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user