Add subscribe
This commit is contained in:
parent
7e337cd7fa
commit
bc209d0203
@ -147,11 +147,12 @@ Required environment variables:
|
|||||||
- [ ] Free
|
- [ ] Free
|
||||||
- [ ] 20 Prompt Limit
|
- [ ] 20 Prompt Limit
|
||||||
- [ ] 3 Version Each 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
|
- [ ] 19.9 USD Paid Monthly
|
||||||
- [ ] 500 Prompt Limit
|
- [ ] 500 Prompt Limit
|
||||||
- [ ] 10 Version Each Prompt Limit
|
- [ ] 10 Version Each Prompt Limit
|
||||||
- [ ] 20 USD Credit Every Month
|
- [ ] 20 USD Credit Every Month. Valid 1 Month.
|
||||||
- [ ] AI Prompt Showcase
|
- [ ] AI Prompt Showcase
|
||||||
- [ ] User Can Share
|
- [ ] User Can Share
|
||||||
- [ ] Admin Can Review
|
- [ ] Admin Can Review
|
||||||
|
@ -90,7 +90,15 @@
|
|||||||
"confirmNewPassword": "Confirm new password",
|
"confirmNewPassword": "Confirm new password",
|
||||||
"updatePassword": "Update Password",
|
"updatePassword": "Update Password",
|
||||||
"english": "English",
|
"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": {
|
"studio": {
|
||||||
"title": "AI Prompt Studio",
|
"title": "AI Prompt Studio",
|
||||||
@ -184,7 +192,8 @@
|
|||||||
"features": [
|
"features": [
|
||||||
"20 Prompt Limit",
|
"20 Prompt Limit",
|
||||||
"3 Versions per Prompt",
|
"3 Versions per Prompt",
|
||||||
"$5 AI Credit Monthly"
|
"$5 One-time Credit (Expires in 1 Month)",
|
||||||
|
"$0 Monthly Credit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pro": {
|
"pro": {
|
||||||
@ -193,7 +202,8 @@
|
|||||||
"features": [
|
"features": [
|
||||||
"500 Prompt Limit",
|
"500 Prompt Limit",
|
||||||
"10 Versions per Prompt",
|
"10 Versions per Prompt",
|
||||||
"$20 AI Credit Monthly"
|
"$20 Monthly Credit",
|
||||||
|
"Purchase Permanent Credits"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"getStartedFree": "Get Started Free",
|
"getStartedFree": "Get Started Free",
|
||||||
|
@ -90,7 +90,15 @@
|
|||||||
"confirmNewPasswordLabel": "确认新密码",
|
"confirmNewPasswordLabel": "确认新密码",
|
||||||
"updatePassword": "更新密码",
|
"updatePassword": "更新密码",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"chinese": "中文"
|
"chinese": "中文",
|
||||||
|
"loadingStudio": "加载个人资料中...",
|
||||||
|
"subscription": "订阅状态",
|
||||||
|
"creditBalance": "信用余额",
|
||||||
|
"freePlan": "免费版",
|
||||||
|
"proPlan": "专业版",
|
||||||
|
"usdCredit": "美元",
|
||||||
|
"subscriptionInfo": "订阅信息",
|
||||||
|
"currentPlan": "当前方案"
|
||||||
},
|
},
|
||||||
"studio": {
|
"studio": {
|
||||||
"title": "AI 提示词工作室",
|
"title": "AI 提示词工作室",
|
||||||
@ -184,7 +192,8 @@
|
|||||||
"features": [
|
"features": [
|
||||||
"20 个提示词限制",
|
"20 个提示词限制",
|
||||||
"每个提示词 3 个版本",
|
"每个提示词 3 个版本",
|
||||||
"每月 5 美元 AI 积分"
|
"注册赠送 5 美元积分(1个月后过期)",
|
||||||
|
"每月 0 美元积分"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pro": {
|
"pro": {
|
||||||
@ -193,7 +202,8 @@
|
|||||||
"features": [
|
"features": [
|
||||||
"500 个提示词限制",
|
"500 个提示词限制",
|
||||||
"每个提示词 10 个版本",
|
"每个提示词 10 个版本",
|
||||||
"每月 20 美元 AI 积分"
|
"每月 20 美元 AI 积分",
|
||||||
|
"可购买永久积分"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"getStartedFree": "免费开始",
|
"getStartedFree": "免费开始",
|
||||||
|
@ -23,10 +23,13 @@ model User {
|
|||||||
versionLimit Int @default(3) // 版本数量限制,可在用户配置中设置
|
versionLimit Int @default(3) // 版本数量限制,可在用户配置中设置
|
||||||
subscribePlan String @default("free") // 订阅计划: "free", "pro"
|
subscribePlan String @default("free") // 订阅计划: "free", "pro"
|
||||||
maxVersionLimit Int @default(3) // 基于订阅的最大版本限制
|
maxVersionLimit Int @default(3) // 基于订阅的最大版本限制
|
||||||
|
promptLimit Int @default(20) // 提示词数量限制
|
||||||
|
creditBalance Float @default(5.0) // 信用余额,单位:美元
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
prompts Prompt[]
|
prompts Prompt[]
|
||||||
|
credits Credit[]
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
@ -100,3 +103,19 @@ model PromptTestRun {
|
|||||||
|
|
||||||
@@map("prompt_test_runs")
|
@@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,
|
versionLimit: true,
|
||||||
subscribePlan: true,
|
subscribePlan: true,
|
||||||
maxVersionLimit: true,
|
maxVersionLimit: true,
|
||||||
|
promptLimit: true,
|
||||||
|
creditBalance: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true
|
updatedAt: true
|
||||||
}
|
}
|
||||||
@ -107,6 +109,8 @@ export async function PUT(request: NextRequest) {
|
|||||||
versionLimit: true,
|
versionLimit: true,
|
||||||
subscribePlan: true,
|
subscribePlan: true,
|
||||||
maxVersionLimit: true,
|
maxVersionLimit: true,
|
||||||
|
promptLimit: true,
|
||||||
|
creditBalance: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true
|
updatedAt: true
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { createServerSupabaseClient } from '@/lib/supabase-server'
|
import { createServerSupabaseClient } from '@/lib/supabase-server'
|
||||||
|
import { addSystemGiftCredit } from '@/lib/credits'
|
||||||
|
|
||||||
// POST /api/users/sync - 同步Supabase用户到Prisma数据库
|
// POST /api/users/sync - 同步Supabase用户到Prisma数据库
|
||||||
export async function POST(_request: NextRequest) {
|
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({
|
return NextResponse.json({
|
||||||
message: 'User created successfully',
|
message: 'User created successfully',
|
||||||
user: newUser
|
user: newUser
|
||||||
|
@ -13,7 +13,7 @@ import { Avatar } from '@/components/ui/avatar'
|
|||||||
import { LoadingSpinner, LoadingOverlay } from '@/components/ui/loading-spinner'
|
import { LoadingSpinner, LoadingOverlay } from '@/components/ui/loading-spinner'
|
||||||
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
|
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
|
||||||
import { AvatarSkeleton, FormFieldSkeleton, TextAreaSkeleton } from '@/components/ui/skeleton'
|
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 {
|
interface UserProfile {
|
||||||
id: string
|
id: string
|
||||||
@ -25,10 +25,32 @@ interface UserProfile {
|
|||||||
versionLimit: number
|
versionLimit: number
|
||||||
subscribePlan: string
|
subscribePlan: string
|
||||||
maxVersionLimit: number
|
maxVersionLimit: number
|
||||||
|
promptLimit?: number
|
||||||
|
creditBalance: number
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
updatedAt?: 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() {
|
export default function ProfilePage() {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
const t = useTranslations('profile')
|
const t = useTranslations('profile')
|
||||||
@ -62,6 +84,7 @@ export default function ProfilePage() {
|
|||||||
const [profileLoading, setProfileLoading] = useState(true)
|
const [profileLoading, setProfileLoading] = useState(true)
|
||||||
const [fieldLoading, setFieldLoading] = useState<{ [key: string]: boolean }>({})
|
const [fieldLoading, setFieldLoading] = useState<{ [key: string]: boolean }>({})
|
||||||
const [avatarUploading, setAvatarUploading] = useState(false)
|
const [avatarUploading, setAvatarUploading] = useState(false)
|
||||||
|
const [creditInfo, setCreditInfo] = useState<CreditInfo | null>(null)
|
||||||
|
|
||||||
const supabase = createClient()
|
const supabase = createClient()
|
||||||
|
|
||||||
@ -71,12 +94,16 @@ export default function ProfilePage() {
|
|||||||
setProfileLoading(true)
|
setProfileLoading(true)
|
||||||
try {
|
try {
|
||||||
// Get profile data from our API
|
// Get profile data from our API
|
||||||
const response = await fetch(`/api/users/profile?userId=${user.id}`)
|
const [profileResponse, creditsResponse] = await Promise.all([
|
||||||
if (!response.ok) {
|
fetch(`/api/users/profile?userId=${user.id}`),
|
||||||
|
fetch(`/api/users/credits?userId=${user.id}`)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!profileResponse.ok) {
|
||||||
throw new Error('Failed to fetch profile')
|
throw new Error('Failed to fetch profile')
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileData: UserProfile = await response.json()
|
const profileData: UserProfile = await profileResponse.json()
|
||||||
|
|
||||||
setProfile(profileData)
|
setProfile(profileData)
|
||||||
setFormData({
|
setFormData({
|
||||||
@ -89,6 +116,12 @@ export default function ProfilePage() {
|
|||||||
language: profileData.language || 'en',
|
language: profileData.language || 'en',
|
||||||
versionLimit: profileData.versionLimit
|
versionLimit: profileData.versionLimit
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Load credit information
|
||||||
|
if (creditsResponse.ok) {
|
||||||
|
const creditData: CreditInfo = await creditsResponse.json()
|
||||||
|
setCreditInfo(creditData)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading profile:', error)
|
console.error('Error loading profile:', error)
|
||||||
setSaveStatus({ type: 'error', message: t('failedToLoadProfile') })
|
setSaveStatus({ type: 'error', message: t('failedToLoadProfile') })
|
||||||
@ -271,9 +304,10 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||||
{/* Avatar Section */}
|
{/* Avatar and Subscription Section */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1 space-y-6">
|
||||||
|
{/* Profile Picture */}
|
||||||
{profileLoading ? (
|
{profileLoading ? (
|
||||||
<AvatarSkeleton />
|
<AvatarSkeleton />
|
||||||
) : (
|
) : (
|
||||||
@ -310,6 +344,73 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</LoadingOverlay>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Profile Information */}
|
{/* Profile Information */}
|
||||||
|
@ -7,7 +7,6 @@ import { useRouter } from 'next/navigation'
|
|||||||
import { Header } from '@/components/layout/Header'
|
import { Header } from '@/components/layout/Header'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
|
||||||
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
|
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
|
||||||
import { EditPromptModal } from '@/components/studio/EditPromptModal'
|
import { EditPromptModal } from '@/components/studio/EditPromptModal'
|
||||||
import { PromptDetailModal } from '@/components/studio/PromptDetailModal'
|
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',
|
name: 'Free',
|
||||||
maxVersionLimit: 3,
|
maxVersionLimit: 3,
|
||||||
promptLimit: 20,
|
promptLimit: 20,
|
||||||
credit: 5
|
monthlyCredit: 0, // 免费版没有月度额度
|
||||||
|
initialCredit: 5, // 注册时一次性赠送5USD,1个月后过期
|
||||||
|
price: 0
|
||||||
},
|
},
|
||||||
pro: {
|
pro: {
|
||||||
name: 'Pro',
|
name: 'Pro',
|
||||||
maxVersionLimit: 10,
|
maxVersionLimit: 10,
|
||||||
promptLimit: 500,
|
promptLimit: 500,
|
||||||
credit: 20
|
monthlyCredit: 20, // 每月20USD额度
|
||||||
|
initialCredit: 0, // Pro用户不需要初始额度
|
||||||
|
price: 19.9
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@ -66,4 +70,31 @@ export function getVersionsToDelete(
|
|||||||
): number {
|
): number {
|
||||||
const versionLimit = getUserVersionLimit(user)
|
const versionLimit = getUserVersionLimit(user)
|
||||||
return Math.max(0, currentVersionCount - versionLimit + 1) // +1 因为要创建新版本
|
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