From fcfcbe18601009614b940c13c4d99854265f6d78 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Mar 2026 01:15:30 +0800 Subject: [PATCH] skip points charging for vip users --- app/(chat)/api/chat/route.ts | 72 ++++++++++++++++-------- app/api/canvas/3d/tasks/route.ts | 34 ++++++----- app/api/user/membership-summary/route.ts | 1 + components/canvas/CanvasChatPanel.tsx | 3 + components/multimodal-input.tsx | 3 + lib/db/queries.ts | 28 ++++++++- 6 files changed, 100 insertions(+), 41 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index c8ac8a40f..51f59db34 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -2360,6 +2360,12 @@ async function saveGeneratedImages( const pointsConfig = await getPointsSystemConfig(); if (!pointsConfig.enabled) return; + const userData = await getUserById({ id: userId }).catch(() => undefined); + if (userData?.isHighVip) { + console.log(`[Points] Skip image points for VIP user=${userId} model=${modelId}`); + return; + } + const modelConfig = await resolveBillingModelByReference(modelId); const unitCost = getModelCreditCost(modelConfig, "image"); if (unitCost <= 0) return; @@ -2379,6 +2385,10 @@ async function saveGeneratedImages( console.warn( `[Points] Failed to consume points after generation. user=${userId} model=${modelId} cost=${totalCost}` ); + } else if (consumeResult.skipped) { + console.log( + `[Points] Skipped ${totalCost} image points for VIP user=${userId} model=${modelId}` + ); } else { console.log( `[Points] Consumed ${totalCost} points (${unitCost} x ${savedImageCount}). balance=${consumeResult.balance}` @@ -2426,6 +2436,12 @@ async function consumeGeneratedVideoPoints(params: { const pointsConfig = await getPointsSystemConfig(); if (!pointsConfig.enabled) return; + const userData = await getUserById({ id: params.userId }).catch(() => undefined); + if (userData?.isHighVip) { + console.log(`[Points] Skip video points for VIP user=${params.userId}`); + return; + } + let billingModel: any | undefined; for (const ref of params.modelRefs) { if (!ref) continue; @@ -2454,6 +2470,10 @@ async function consumeGeneratedVideoPoints(params: { console.warn( `[Points] Failed to consume video points. user=${params.userId} model=${billingModel.id} cost=${unitCost}` ); + } else if (consumeResult.skipped) { + console.log( + `[Points] Skipped ${unitCost} video points for VIP user=${params.userId} model=${billingModel.id}` + ); } else { console.log( `[Points] Consumed ${unitCost} points for video. balance=${consumeResult.balance}` @@ -4837,19 +4857,21 @@ export async function POST(request: Request) { ); const expectedCost = normalizedUnitCost * requestedImageCount; const userData = await getUserById({ id: session.user.id }); - const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); + if (!userData?.isHighVip) { + const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); - if (currentBalance < expectedCost) { - return NextResponse.json( - { - error: "insufficient_points", - message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${normalizedUnitCost} x ${requestedImageCount} 张)。`, - balance: currentBalance, - expectedCost, - unitCost: normalizedUnitCost, - }, - { status: 402 } - ); + if (currentBalance < expectedCost) { + return NextResponse.json( + { + error: "insufficient_points", + message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${normalizedUnitCost} x ${requestedImageCount} 张)。`, + balance: currentBalance, + expectedCost, + unitCost: normalizedUnitCost, + }, + { status: 402 } + ); + } } } } @@ -4875,19 +4897,21 @@ export async function POST(request: Request) { ) { const expectedCost = normalizedVideoUnitCost; const userData = await getUserById({ id: session.user.id }); - const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); + if (!userData?.isHighVip) { + const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); - if (currentBalance < expectedCost) { - return NextResponse.json( - { - error: "insufficient_points", - message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${normalizedVideoUnitCost} x 1 条视频)。`, - balance: currentBalance, - expectedCost, - unitCost: normalizedVideoUnitCost, - }, - { status: 402 } - ); + if (currentBalance < expectedCost) { + return NextResponse.json( + { + error: "insufficient_points", + message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${normalizedVideoUnitCost} x 1 条视频)。`, + balance: currentBalance, + expectedCost, + unitCost: normalizedVideoUnitCost, + }, + { status: 402 } + ); + } } } } diff --git a/app/api/canvas/3d/tasks/route.ts b/app/api/canvas/3d/tasks/route.ts index fc04faac3..50ed0b1b8 100644 --- a/app/api/canvas/3d/tasks/route.ts +++ b/app/api/canvas/3d/tasks/route.ts @@ -352,21 +352,23 @@ export async function POST(request: Request) { pointsConfig.blockOnInsufficient !== false ) { const userData = await getUserById({ id: session.user.id }); - const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); + if (!userData?.isHighVip) { + const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); - if (currentBalance < unitCost) { - return NextResponse.json( - { - success: false, - error: "积分不足", - code: "insufficient_points", - message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${unitCost}。`, - balance: currentBalance, - expectedCost: unitCost, - unitCost, - }, - { status: 402 } - ); + if (currentBalance < unitCost) { + return NextResponse.json( + { + success: false, + error: "积分不足", + code: "insufficient_points", + message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${unitCost}。`, + balance: currentBalance, + expectedCost: unitCost, + unitCost, + }, + { status: 402 } + ); + } } } @@ -404,6 +406,10 @@ export async function POST(request: Request) { console.warn( `[Points] Failed to consume 3D points. user=${session.user.id} task=${taskId} cost=${unitCost}` ); + } else if (consumeResult.skipped) { + console.log( + `[Points] Skipped ${unitCost} 3D points for VIP user=${session.user.id} task=${taskId}` + ); } else { console.log( `[Points] Consumed ${unitCost} points for 3D generation. user=${session.user.id} task=${taskId} balance=${consumeResult.balance}` diff --git a/app/api/user/membership-summary/route.ts b/app/api/user/membership-summary/route.ts index 8a5264752..a2c5716ed 100644 --- a/app/api/user/membership-summary/route.ts +++ b/app/api/user/membership-summary/route.ts @@ -72,6 +72,7 @@ export async function GET() { return NextResponse.json({ config, pointsSystemConfig: pointsConfig, + isHighVip: Boolean(dbUser?.isHighVip), currentPlanId, currentPlan, summary: { diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index b35b01f51..7204c66c5 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -2472,6 +2472,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF fetcher ); const { data: membershipSummary } = useSWR<{ + isHighVip?: boolean; pointsSystemConfig?: { enabled?: boolean; blockOnInsufficient?: boolean; @@ -6173,6 +6174,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const ensureImagePointsBeforeSend = useCallback(() => { if (isVideoModel || activeComposerMode === "agent") return true; + if (membershipSummary?.isHighVip) return true; const pointsEnabled = membershipSummary?.pointsSystemConfig?.enabled === true; const blockOnInsufficient = @@ -6213,6 +6215,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF currentModel?.imageGenConfig?.creditCostPerImage, imageParams?.imageCount, isVideoModel, + membershipSummary?.isHighVip, membershipSummary?.pointsSystemConfig?.blockOnInsufficient, membershipSummary?.pointsSystemConfig?.enabled, membershipSummary?.summary?.currentCredits, diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index af4729182..39480f872 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -36,6 +36,7 @@ type DynamicModel = { }; type PointsSummaryResponse = { + isHighVip?: boolean; pointsSystemConfig?: { enabled?: boolean; blockOnInsufficient?: boolean; @@ -372,6 +373,7 @@ function PureMultimodalInput({ const ensureImagePointsBeforeSend = useCallback(async () => { if (!isImageModel || isVideoModel) return true; + if (membershipSummary?.isHighVip) return true; const pointsEnabled = membershipSummary?.pointsSystemConfig?.enabled === true; const blockOnInsufficient = @@ -412,6 +414,7 @@ function PureMultimodalInput({ imageParams?.imageCount, isImageModel, isVideoModel, + membershipSummary?.isHighVip, membershipSummary?.pointsSystemConfig?.blockOnInsufficient, membershipSummary?.pointsSystemConfig?.enabled, membershipSummary?.summary?.currentCredits, diff --git a/lib/db/queries.ts b/lib/db/queries.ts index fc7db5820..2d7ab0ac8 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -1755,11 +1755,33 @@ export async function consumeUserPoints({ note?: string; }) { if (!Number.isFinite(amount) || amount <= 0) { - return { ok: true, balance: null as number | null }; + return { ok: true, balance: null as number | null, skipped: false }; } try { return await db.transaction(async (tx) => { + const currentUsers = await tx + .select({ + balance: user.pointsBalance, + isHighVip: user.isHighVip, + }) + .from(user) + .where(eq(user.id, userId)) + .limit(1); + + const currentUser = currentUsers[0]; + if (!currentUser) { + return { ok: false, balance: null as number | null, skipped: false }; + } + + if (currentUser.isHighVip) { + return { + ok: true, + balance: Math.max(0, Number(currentUser.balance || 0)), + skipped: true, + }; + } + const updated = await tx .update(user) .set({ @@ -1772,7 +1794,7 @@ export async function consumeUserPoints({ }); if (updated.length === 0) { - return { ok: false, balance: null as number | null }; + return { ok: false, balance: null as number | null, skipped: false }; } await tx.insert(pointsLedger).values({ @@ -1785,7 +1807,7 @@ export async function consumeUserPoints({ note: note || "图片生成消耗积分", }); - return { ok: true, balance: updated[0]?.balance ?? null }; + return { ok: true, balance: updated[0]?.balance ?? null, skipped: false }; }); } catch (_error) { throw new ChatSDKError("bad_request:database", "Failed to consume user points");