feat: split 3d model and world point costs

This commit is contained in:
Your Name 2026-04-05 21:54:11 +08:00
parent 607fd3bc3e
commit 5c2ece9fb6
5 changed files with 96 additions and 46 deletions

View File

@ -7413,7 +7413,7 @@ export function AdminSettingsClient() {
<div className="space-y-0.5">
<Label htmlFor="pointsEnabled">启用积分消耗</Label>
<div className="text-xs text-muted-foreground">
开启后按模型单价和图片张数扣除积分
开启后按不同功能配置的单价扣除积分
</div>
</div>
<Switch
@ -7435,10 +7435,10 @@ export function AdminSettingsClient() {
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label htmlFor="pointsBlockOnInsufficient">
余额不足时阻止生图
余额不足时阻止生成
</Label>
<div className="text-xs text-muted-foreground">
开启后积分不足直接返回错误,不执行生图任务
开启后积分不足直接返回错误,不执行生成任务
</div>
</div>
<Switch
@ -7459,35 +7459,72 @@ export function AdminSettingsClient() {
/>
</div>
</div>
<div className="rounded-lg border p-3">
<div className="space-y-1">
<Label htmlFor="points3dCost">3D生成单次积分消耗</Label>
<div className="text-xs text-muted-foreground">
作用于 3D 生成功能(文生3D / 图生3D /
多视图3D),每次提交任务扣除一次。
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div className="rounded-lg border p-3">
<div className="space-y-1">
<Label htmlFor="points3dModelCost">
3D模型单次积分消耗
</Label>
<div className="text-xs text-muted-foreground">
作用于 3D模型模式(文生3D / 图生3D / 多视图3D),每次提交任务扣除一次。
</div>
</div>
<div className="mt-2 w-full max-w-xs">
<Input
id="points3dModelCost"
min={0}
type="number"
value={
siteSettings.pointsSystemConfig?.creditCostPer3DModel ??
0
}
onChange={(e) =>
setSiteSettings({
...siteSettings,
pointsSystemConfig: {
...siteSettings.pointsSystemConfig,
creditCostPer3DModel: Math.max(
0,
Number.parseInt(e.target.value || "0", 10) || 0
),
},
})
}
/>
</div>
</div>
<div className="mt-2 w-full max-w-xs">
<Input
id="points3dCost"
min={0}
type="number"
value={
siteSettings.pointsSystemConfig?.creditCostPer3D ?? 0
}
onChange={(e) =>
setSiteSettings({
...siteSettings,
pointsSystemConfig: {
...siteSettings.pointsSystemConfig,
creditCostPer3D: Math.max(
0,
Number.parseInt(e.target.value || "0", 10) || 0
),
},
})
}
/>
<div className="rounded-lg border p-3">
<div className="space-y-1">
<Label htmlFor="points3dWorldCost">
3D世界单次积分消耗
</Label>
<div className="text-xs text-muted-foreground">
作用于 3D世界模式(文本 / 图片 / 多图 / 视频生成世界),每次提交任务扣除一次。
</div>
</div>
<div className="mt-2 w-full max-w-xs">
<Input
id="points3dWorldCost"
min={0}
type="number"
value={
siteSettings.pointsSystemConfig?.creditCostPer3DWorld ??
0
}
onChange={(e) =>
setSiteSettings({
...siteSettings,
pointsSystemConfig: {
...siteSettings.pointsSystemConfig,
creditCostPer3DWorld: Math.max(
0,
Number.parseInt(e.target.value || "0", 10) || 0
),
},
})
}
/>
</div>
</div>
</div>
<div className="space-y-2">

View File

@ -341,8 +341,8 @@ export async function POST(request: Request) {
const pointsConfig = await getPointsSystemConfig();
const shouldChargePoints = shouldCharge3DPoints(taskType);
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3D))
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3D)))
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DModel))
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DModel)))
: 0;
if (
@ -399,7 +399,7 @@ export async function POST(request: Request) {
amount: unitCost,
reason: "model3d_generation",
modelId: "tripo-3d",
note: "3D生成消耗积分",
note: "3D模型生成消耗积分",
});
if (!consumeResult.ok) {
@ -412,7 +412,7 @@ export async function POST(request: Request) {
);
} else {
console.log(
`[Points] Consumed ${unitCost} points for 3D generation. user=${session.user.id} task=${taskId} balance=${consumeResult.balance}`
`[Points] Consumed ${unitCost} points for 3D model generation. user=${session.user.id} task=${taskId} balance=${consumeResult.balance}`
);
}
} catch (error) {

View File

@ -143,8 +143,8 @@ export async function POST(request: Request) {
const pointsConfig = await getPointsSystemConfig();
const shouldChargePoints = shouldCharge3DPoints(mode);
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3D))
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3D)))
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DWorld))
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DWorld)))
: 0;
if (
@ -209,7 +209,7 @@ export async function POST(request: Request) {
await consumeUserPoints({
userId: session.user.id,
amount: unitCost,
reason: "model3d_generation",
reason: "world3d_generation",
modelId: "worldlabs-3d",
note: "3D世界生成消耗积分",
});

View File

@ -22,7 +22,8 @@ const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
function mapLedgerReasonLabel(reason: string) {
if (reason === "image_generation") return "图片生成";
if (reason === "model3d_generation") return "3D生成";
if (reason === "model3d_generation") return "3D模型生成";
if (reason === "world3d_generation") return "3D世界生成";
if (reason === "video_generation") return "视频生成";
if (reason === "deduct") return "管理员扣减";
if (reason === "gift") return "管理员赠送";

View File

@ -4,7 +4,8 @@ export const POINTS_PACKAGES_KEY = "points_packages";
export type PointsSystemConfig = {
enabled: boolean;
blockOnInsufficient: boolean;
creditCostPer3D: number;
creditCostPer3DModel: number;
creditCostPer3DWorld: number;
};
export type PointsPackage = {
@ -19,7 +20,8 @@ export type PointsPackage = {
export const defaultPointsSystemConfig: PointsSystemConfig = {
enabled: false,
blockOnInsufficient: true,
creditCostPer3D: 0,
creditCostPer3DModel: 0,
creditCostPer3DWorld: 0,
};
export const defaultPointsPackages: PointsPackage[] = [
@ -46,15 +48,25 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
return defaultPointsSystemConfig;
}
const raw = value as Partial<PointsSystemConfig>;
const parsed3DCost = Number(raw.creditCostPer3D);
const raw = value as Partial<PointsSystemConfig> & {
creditCostPer3D?: number;
};
const normalizeCost = (input: unknown) => {
const parsedValue = Number(input);
return Number.isFinite(parsedValue) && parsedValue > 0
? Math.min(100000, Math.round(parsedValue))
: 0;
};
const legacy3DCost = normalizeCost(raw.creditCostPer3D);
return {
enabled: raw.enabled === true,
blockOnInsufficient: raw.blockOnInsufficient !== false,
creditCostPer3D:
Number.isFinite(parsed3DCost) && parsed3DCost > 0
? Math.min(100000, Math.round(parsed3DCost))
: 0,
creditCostPer3DModel: normalizeCost(
raw.creditCostPer3DModel ?? legacy3DCost
),
creditCostPer3DWorld: normalizeCost(
raw.creditCostPer3DWorld ?? legacy3DCost
),
};
}