Add subscribe
This commit is contained in:
parent
7e337cd7fa
commit
bc209d0203
@ -147,11 +147,12 @@ Required environment variables:
|
||||
- [ ] Free
|
||||
- [ ] 20 Prompt Limit
|
||||
- [ ] 3 Version Each Prompt Limit
|
||||
- [ ] 5 USD Credit Once Time
|
||||
- [ ] 5 USD Credit Once Time. Timeout After 1 Mounth.
|
||||
- [ ] 0 USD Credit Every Month.
|
||||
- [ ] 19.9 USD Paid Monthly
|
||||
- [ ] 500 Prompt Limit
|
||||
- [ ] 10 Version Each Prompt Limit
|
||||
- [ ] 20 USD Credit Every Month
|
||||
- [ ] 20 USD Credit Every Month. Valid 1 Month.
|
||||
- [ ] AI Prompt Showcase
|
||||
- [ ] User Can Share
|
||||
- [ ] Admin Can Review
|
||||
|
@ -90,7 +90,15 @@
|
||||
"confirmNewPassword": "Confirm new password",
|
||||
"updatePassword": "Update Password",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
"chinese": "中文",
|
||||
"loadingStudio": "Loading Profile...",
|
||||
"subscription": "Subscription",
|
||||
"creditBalance": "Credit Balance",
|
||||
"freePlan": "Free Plan",
|
||||
"proPlan": "Pro Plan",
|
||||
"usdCredit": "USD",
|
||||
"subscriptionInfo": "Subscription Info",
|
||||
"currentPlan": "Current Plan"
|
||||
},
|
||||
"studio": {
|
||||
"title": "AI Prompt Studio",
|
||||
@ -184,7 +192,8 @@
|
||||
"features": [
|
||||
"20 Prompt Limit",
|
||||
"3 Versions per Prompt",
|
||||
"$5 AI Credit Monthly"
|
||||
"$5 One-time Credit (Expires in 1 Month)",
|
||||
"$0 Monthly Credit"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
@ -193,7 +202,8 @@
|
||||
"features": [
|
||||
"500 Prompt Limit",
|
||||
"10 Versions per Prompt",
|
||||
"$20 AI Credit Monthly"
|
||||
"$20 Monthly Credit",
|
||||
"Purchase Permanent Credits"
|
||||
]
|
||||
},
|
||||
"getStartedFree": "Get Started Free",
|
||||
|
@ -90,7 +90,15 @@
|
||||
"confirmNewPasswordLabel": "确认新密码",
|
||||
"updatePassword": "更新密码",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
"chinese": "中文",
|
||||
"loadingStudio": "加载个人资料中...",
|
||||
"subscription": "订阅状态",
|
||||
"creditBalance": "信用余额",
|
||||
"freePlan": "免费版",
|
||||
"proPlan": "专业版",
|
||||
"usdCredit": "美元",
|
||||
"subscriptionInfo": "订阅信息",
|
||||
"currentPlan": "当前方案"
|
||||
},
|
||||
"studio": {
|
||||
"title": "AI 提示词工作室",
|
||||
@ -184,7 +192,8 @@
|
||||
"features": [
|
||||
"20 个提示词限制",
|
||||
"每个提示词 3 个版本",
|
||||
"每月 5 美元 AI 积分"
|
||||
"注册赠送 5 美元积分(1个月后过期)",
|
||||
"每月 0 美元积分"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
@ -193,7 +202,8 @@
|
||||
"features": [
|
||||
"500 个提示词限制",
|
||||
"每个提示词 10 个版本",
|
||||
"每月 20 美元 AI 积分"
|
||||
"每月 20 美元 AI 积分",
|
||||
"可购买永久积分"
|
||||
]
|
||||
},
|
||||
"getStartedFree": "免费开始",
|
||||
|
@ -23,10 +23,13 @@ model User {
|
||||
versionLimit Int @default(3) // 版本数量限制,可在用户配置中设置
|
||||
subscribePlan String @default("free") // 订阅计划: "free", "pro"
|
||||
maxVersionLimit Int @default(3) // 基于订阅的最大版本限制
|
||||
promptLimit Int @default(20) // 提示词数量限制
|
||||
creditBalance Float @default(5.0) // 信用余额,单位:美元
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
prompts Prompt[]
|
||||
credits Credit[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@ -100,3 +103,19 @@ model PromptTestRun {
|
||||
|
||||
@@map("prompt_test_runs")
|
||||
}
|
||||
|
||||
model Credit {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
amount Float // 信用额度数量
|
||||
type String // "system_gift", "subscription_monthly", "user_purchase"
|
||||
note String? // 备注说明
|
||||
expiresAt DateTime? // 过期时间,null表示永久有效
|
||||
isActive Boolean @default(true) // 是否激活状态
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("credits")
|
||||
}
|
||||
|
29
src/app/api/users/credits/route.ts
Normal file
29
src/app/api/users/credits/route.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getUserCreditSummary, checkAndRefreshProMonthlyCredit } from '@/lib/credits'
|
||||
|
||||
// GET /api/users/credits - 获取用户信用额度概览
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'User ID is required' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 检查Pro用户是否需要刷新月度额度
|
||||
await checkAndRefreshProMonthlyCredit(userId)
|
||||
|
||||
// 获取信用额度概览
|
||||
const creditSummary = await getUserCreditSummary(userId)
|
||||
|
||||
return NextResponse.json(creditSummary)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user credits:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch user credits' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
@ -24,6 +24,8 @@ export async function GET(request: NextRequest) {
|
||||
versionLimit: true,
|
||||
subscribePlan: true,
|
||||
maxVersionLimit: true,
|
||||
promptLimit: true,
|
||||
creditBalance: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
@ -107,6 +109,8 @@ export async function PUT(request: NextRequest) {
|
||||
versionLimit: true,
|
||||
subscribePlan: true,
|
||||
maxVersionLimit: true,
|
||||
promptLimit: true,
|
||||
creditBalance: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { createServerSupabaseClient } from '@/lib/supabase-server'
|
||||
import { addSystemGiftCredit } from '@/lib/credits'
|
||||
|
||||
// POST /api/users/sync - 同步Supabase用户到Prisma数据库
|
||||
export async function POST(_request: NextRequest) {
|
||||
@ -48,6 +49,9 @@ export async function POST(_request: NextRequest) {
|
||||
}
|
||||
})
|
||||
|
||||
// 为新用户添加系统赠送的5USD信用额度(1个月后过期)
|
||||
await addSystemGiftCredit(newUser.id)
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'User created successfully',
|
||||
user: newUser
|
||||
|
@ -13,7 +13,7 @@ import { Avatar } from '@/components/ui/avatar'
|
||||
import { LoadingSpinner, LoadingOverlay } from '@/components/ui/loading-spinner'
|
||||
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
|
||||
import { AvatarSkeleton, FormFieldSkeleton, TextAreaSkeleton } from '@/components/ui/skeleton'
|
||||
import { Camera, Save, Eye, EyeOff, Globe } from 'lucide-react'
|
||||
import { Camera, Save, Eye, EyeOff, Globe, CreditCard, Crown, Star } from 'lucide-react'
|
||||
|
||||
interface UserProfile {
|
||||
id: string
|
||||
@ -25,10 +25,32 @@ interface UserProfile {
|
||||
versionLimit: number
|
||||
subscribePlan: string
|
||||
maxVersionLimit: number
|
||||
promptLimit?: number
|
||||
creditBalance: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
interface CreditInfo {
|
||||
totalBalance: number
|
||||
activeCredits: {
|
||||
id: string
|
||||
amount: number
|
||||
type: string
|
||||
note: string | null
|
||||
expiresAt: Date | null
|
||||
createdAt: Date
|
||||
}[]
|
||||
expiredCredits: {
|
||||
id: string
|
||||
amount: number
|
||||
type: string
|
||||
note: string | null
|
||||
expiresAt: Date | null
|
||||
createdAt: Date
|
||||
}[]
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user, loading } = useAuth()
|
||||
const t = useTranslations('profile')
|
||||
@ -62,6 +84,7 @@ export default function ProfilePage() {
|
||||
const [profileLoading, setProfileLoading] = useState(true)
|
||||
const [fieldLoading, setFieldLoading] = useState<{ [key: string]: boolean }>({})
|
||||
const [avatarUploading, setAvatarUploading] = useState(false)
|
||||
const [creditInfo, setCreditInfo] = useState<CreditInfo | null>(null)
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
@ -71,12 +94,16 @@ export default function ProfilePage() {
|
||||
setProfileLoading(true)
|
||||
try {
|
||||
// Get profile data from our API
|
||||
const response = await fetch(`/api/users/profile?userId=${user.id}`)
|
||||
if (!response.ok) {
|
||||
const [profileResponse, creditsResponse] = await Promise.all([
|
||||
fetch(`/api/users/profile?userId=${user.id}`),
|
||||
fetch(`/api/users/credits?userId=${user.id}`)
|
||||
])
|
||||
|
||||
if (!profileResponse.ok) {
|
||||
throw new Error('Failed to fetch profile')
|
||||
}
|
||||
|
||||
const profileData: UserProfile = await response.json()
|
||||
const profileData: UserProfile = await profileResponse.json()
|
||||
|
||||
setProfile(profileData)
|
||||
setFormData({
|
||||
@ -89,6 +116,12 @@ export default function ProfilePage() {
|
||||
language: profileData.language || 'en',
|
||||
versionLimit: profileData.versionLimit
|
||||
})
|
||||
|
||||
// Load credit information
|
||||
if (creditsResponse.ok) {
|
||||
const creditData: CreditInfo = await creditsResponse.json()
|
||||
setCreditInfo(creditData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading profile:', error)
|
||||
setSaveStatus({ type: 'error', message: t('failedToLoadProfile') })
|
||||
@ -271,9 +304,10 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Avatar Section */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
{/* Avatar and Subscription Section */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
{/* Profile Picture */}
|
||||
{profileLoading ? (
|
||||
<AvatarSkeleton />
|
||||
) : (
|
||||
@ -310,6 +344,73 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
</LoadingOverlay>
|
||||
)}
|
||||
|
||||
{/* Subscription Status */}
|
||||
{profileLoading ? (
|
||||
<FormFieldSkeleton />
|
||||
) : (
|
||||
<div className="bg-card rounded-lg border border-border overflow-hidden">
|
||||
<div className="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30 border-b border-border">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-full ${profile?.subscribePlan === 'pro' ? 'bg-gradient-to-r from-yellow-400 to-orange-400' : 'bg-gray-100 dark:bg-gray-700'}`}>
|
||||
{profile?.subscribePlan === 'pro' ? (
|
||||
<Crown className="w-4 h-4 text-white" />
|
||||
) : (
|
||||
<Star className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground text-sm sm:text-base">{t('currentPlan')}</h3>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground truncate">
|
||||
{profile?.subscribePlan === 'pro' ? t('proPlan') : t('freePlan')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Credit Balance */}
|
||||
<div className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/30 dark:to-emerald-950/30 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CreditCard className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-foreground">{t('creditBalance')}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xl font-bold text-green-600 dark:text-green-400">
|
||||
${(creditInfo?.totalBalance || 0).toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{t('usdCredit')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Details */}
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-1 gap-2">
|
||||
<div className="flex justify-between py-1">
|
||||
<span className="text-muted-foreground text-xs sm:text-sm">Max Prompts:</span>
|
||||
<span className="font-medium text-foreground text-xs sm:text-sm">
|
||||
{profile?.subscribePlan === 'pro' ? '500' : '20'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-1">
|
||||
<span className="text-muted-foreground text-xs sm:text-sm">Max Versions:</span>
|
||||
<span className="font-medium text-foreground text-xs sm:text-sm">
|
||||
{profile?.maxVersionLimit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-1 col-span-2 sm:col-span-1">
|
||||
<span className="text-muted-foreground text-xs sm:text-sm">Monthly Credit:</span>
|
||||
<span className="font-medium text-foreground text-xs sm:text-sm">
|
||||
${profile?.subscribePlan === 'pro' ? '20.00' : '0.00'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profile Information */}
|
||||
|
@ -7,7 +7,6 @@ import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
|
||||
import { EditPromptModal } from '@/components/studio/EditPromptModal'
|
||||
import { PromptDetailModal } from '@/components/studio/PromptDetailModal'
|
||||
|
233
src/lib/credits.ts
Normal file
233
src/lib/credits.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export type CreditType = 'system_gift' | 'subscription_monthly' | 'user_purchase'
|
||||
|
||||
export interface CreditSummary {
|
||||
totalBalance: number
|
||||
activeCredits: {
|
||||
id: string
|
||||
amount: number
|
||||
type: CreditType
|
||||
note: string | null
|
||||
expiresAt: Date | null
|
||||
createdAt: Date
|
||||
}[]
|
||||
expiredCredits: {
|
||||
id: string
|
||||
amount: number
|
||||
type: CreditType
|
||||
note: string | null
|
||||
expiresAt: Date | null
|
||||
createdAt: Date
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的信用额度总览
|
||||
*/
|
||||
export async function getUserCreditSummary(userId: string): Promise<CreditSummary> {
|
||||
const now = new Date()
|
||||
|
||||
// 获取所有信用记录
|
||||
const allCredits = await prisma.credit.findMany({
|
||||
where: { userId, isActive: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
const activeCredits = []
|
||||
const expiredCredits = []
|
||||
let totalBalance = 0
|
||||
|
||||
for (const credit of allCredits) {
|
||||
const creditData = {
|
||||
id: credit.id,
|
||||
amount: credit.amount,
|
||||
type: credit.type as CreditType,
|
||||
note: credit.note,
|
||||
expiresAt: credit.expiresAt,
|
||||
createdAt: credit.createdAt
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (credit.expiresAt && credit.expiresAt < now) {
|
||||
expiredCredits.push(creditData)
|
||||
// 标记过期的信用为非激活状态
|
||||
await prisma.credit.update({
|
||||
where: { id: credit.id },
|
||||
data: { isActive: false }
|
||||
})
|
||||
} else {
|
||||
activeCredits.push(creditData)
|
||||
totalBalance += credit.amount
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalBalance,
|
||||
activeCredits,
|
||||
expiredCredits
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为新用户添加系统赠送的5USD信用额度(1个月后过期)
|
||||
*/
|
||||
export async function addSystemGiftCredit(userId: string): Promise<void> {
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setMonth(expiresAt.getMonth() + 1) // 1个月后过期
|
||||
|
||||
await prisma.credit.create({
|
||||
data: {
|
||||
userId,
|
||||
amount: 5.0,
|
||||
type: 'system_gift',
|
||||
note: '系统赠送 - 新用户礼包',
|
||||
expiresAt,
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 为Pro用户添加月度信用额度
|
||||
*/
|
||||
export async function addMonthlySubscriptionCredit(userId: string): Promise<void> {
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setMonth(expiresAt.getMonth() + 1) // 1个月后过期
|
||||
|
||||
await prisma.credit.create({
|
||||
data: {
|
||||
userId,
|
||||
amount: 20.0,
|
||||
type: 'subscription_monthly',
|
||||
note: 'Pro套餐月度额度',
|
||||
expiresAt,
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户购买信用额度(永久有效)
|
||||
*/
|
||||
export async function addUserPurchaseCredit(
|
||||
userId: string,
|
||||
amount: number,
|
||||
note?: string
|
||||
): Promise<void> {
|
||||
await prisma.credit.create({
|
||||
data: {
|
||||
userId,
|
||||
amount,
|
||||
type: 'user_purchase',
|
||||
note: note || '用户充值',
|
||||
expiresAt: null, // 永久有效
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Pro用户是否需要刷新月度额度
|
||||
*/
|
||||
export async function checkAndRefreshProMonthlyCredit(userId: string): Promise<boolean> {
|
||||
// 获取用户信息
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { subscribePlan: true }
|
||||
})
|
||||
|
||||
if (!user || user.subscribePlan !== 'pro') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 查找最近的月度订阅额度
|
||||
const lastMonthlyCredit = await prisma.credit.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
type: 'subscription_monthly',
|
||||
isActive: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
|
||||
// 如果没有月度额度记录,或者最近的已过期,则添加新的
|
||||
if (!lastMonthlyCredit || (lastMonthlyCredit.expiresAt && lastMonthlyCredit.expiresAt < now)) {
|
||||
await addMonthlySubscriptionCredit(userId)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 消费信用额度
|
||||
*/
|
||||
export async function consumeCredit(userId: string, amount: number): Promise<boolean> {
|
||||
const creditSummary = await getUserCreditSummary(userId)
|
||||
|
||||
if (creditSummary.totalBalance < amount) {
|
||||
return false // 余额不足
|
||||
}
|
||||
|
||||
// 按优先级消费额度:先消费即将过期的,再消费永久的
|
||||
const sortedCredits = creditSummary.activeCredits.sort((a, b) => {
|
||||
// 有过期时间的排在前面,没有过期时间的排在后面
|
||||
if (a.expiresAt && !b.expiresAt) return -1
|
||||
if (!a.expiresAt && b.expiresAt) return 1
|
||||
if (a.expiresAt && b.expiresAt) {
|
||||
return a.expiresAt.getTime() - b.expiresAt.getTime()
|
||||
}
|
||||
return a.createdAt.getTime() - b.createdAt.getTime()
|
||||
})
|
||||
|
||||
let remainingAmount = amount
|
||||
|
||||
for (const credit of sortedCredits) {
|
||||
if (remainingAmount <= 0) break
|
||||
|
||||
const consumeFromThis = Math.min(credit.amount, remainingAmount)
|
||||
const newAmount = credit.amount - consumeFromThis
|
||||
|
||||
if (newAmount <= 0) {
|
||||
// 这个额度用完了,标记为非激活
|
||||
await prisma.credit.update({
|
||||
where: { id: credit.id },
|
||||
data: { isActive: false }
|
||||
})
|
||||
} else {
|
||||
// 更新剩余额度
|
||||
await prisma.credit.update({
|
||||
where: { id: credit.id },
|
||||
data: { amount: newAmount }
|
||||
})
|
||||
}
|
||||
|
||||
remainingAmount -= consumeFromThis
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订阅计划的限制
|
||||
*/
|
||||
export function getSubscriptionLimits(plan: string) {
|
||||
switch (plan) {
|
||||
case 'pro':
|
||||
return {
|
||||
promptLimit: 500,
|
||||
maxVersionLimit: 10,
|
||||
monthlyCredit: 20.0
|
||||
}
|
||||
case 'free':
|
||||
default:
|
||||
return {
|
||||
promptLimit: 20,
|
||||
maxVersionLimit: 3,
|
||||
monthlyCredit: 0.0 // 免费版没有月度额度
|
||||
}
|
||||
}
|
||||
}
|
@ -4,13 +4,17 @@ export const SUBSCRIPTION_PLANS = {
|
||||
name: 'Free',
|
||||
maxVersionLimit: 3,
|
||||
promptLimit: 20,
|
||||
credit: 5
|
||||
monthlyCredit: 0, // 免费版没有月度额度
|
||||
initialCredit: 5, // 注册时一次性赠送5USD,1个月后过期
|
||||
price: 0
|
||||
},
|
||||
pro: {
|
||||
name: 'Pro',
|
||||
maxVersionLimit: 10,
|
||||
promptLimit: 500,
|
||||
credit: 20
|
||||
monthlyCredit: 20, // 每月20USD额度
|
||||
initialCredit: 0, // Pro用户不需要初始额度
|
||||
price: 19.9
|
||||
}
|
||||
} as const
|
||||
|
||||
@ -66,4 +70,31 @@ export function getVersionsToDelete(
|
||||
): number {
|
||||
const versionLimit = getUserVersionLimit(user)
|
||||
return Math.max(0, currentVersionCount - versionLimit + 1) // +1 因为要创建新版本
|
||||
}
|
||||
|
||||
// 获取用户的提示词限制
|
||||
export function getPromptLimit(subscribePlan: string): number {
|
||||
const plan = subscribePlan as SubscriptionPlan
|
||||
const planConfig = SUBSCRIPTION_PLANS[plan]
|
||||
|
||||
return planConfig?.promptLimit || SUBSCRIPTION_PLANS.free.promptLimit
|
||||
}
|
||||
|
||||
// 检查用户是否可以创建新提示词
|
||||
export async function canCreateNewPrompt(userId: string): Promise<boolean> {
|
||||
const { prisma } = await import('@/lib/prisma')
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { subscribePlan: true }
|
||||
})
|
||||
|
||||
if (!user) return false
|
||||
|
||||
const promptLimit = getPromptLimit(user.subscribePlan)
|
||||
const currentPromptCount = await prisma.prompt.count({
|
||||
where: { userId }
|
||||
})
|
||||
|
||||
return currentPromptCount < promptLimit
|
||||
}
|
Loading…
Reference in New Issue
Block a user