skip points charging for vip users
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-08 01:15:30 +08:00
parent a77bea08b9
commit fcfcbe1860
6 changed files with 100 additions and 41 deletions

View File

@ -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 }
);
}
}
}
}

View File

@ -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}`

View File

@ -72,6 +72,7 @@ export async function GET() {
return NextResponse.json({
config,
pointsSystemConfig: pointsConfig,
isHighVip: Boolean(dbUser?.isHighVip),
currentPlanId,
currentPlan,
summary: {

View File

@ -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,

View File

@ -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,

View File

@ -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");