feat: split 3d model and world point costs
This commit is contained in:
parent
607fd3bc3e
commit
5c2ece9fb6
@ -7413,7 +7413,7 @@ export function AdminSettingsClient() {
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="pointsEnabled">启用积分消耗</Label>
|
<Label htmlFor="pointsEnabled">启用积分消耗</Label>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
开启后按模型单价和图片张数扣除积分
|
开启后按不同功能配置的单价扣除积分
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@ -7435,10 +7435,10 @@ export function AdminSettingsClient() {
|
|||||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="pointsBlockOnInsufficient">
|
<Label htmlFor="pointsBlockOnInsufficient">
|
||||||
余额不足时阻止生图
|
余额不足时阻止生成
|
||||||
</Label>
|
</Label>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
开启后积分不足直接返回错误,不执行生图任务
|
开启后积分不足直接返回错误,不执行生成任务
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@ -7459,35 +7459,72 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border p-3">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
<div className="space-y-1">
|
<div className="rounded-lg border p-3">
|
||||||
<Label htmlFor="points3dCost">3D生成单次积分消耗</Label>
|
<div className="space-y-1">
|
||||||
<div className="text-xs text-muted-foreground">
|
<Label htmlFor="points3dModelCost">
|
||||||
作用于 3D 生成功能(文生3D / 图生3D /
|
3D模型单次积分消耗
|
||||||
多视图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>
|
</div>
|
||||||
<div className="mt-2 w-full max-w-xs">
|
<div className="rounded-lg border p-3">
|
||||||
<Input
|
<div className="space-y-1">
|
||||||
id="points3dCost"
|
<Label htmlFor="points3dWorldCost">
|
||||||
min={0}
|
3D世界单次积分消耗
|
||||||
type="number"
|
</Label>
|
||||||
value={
|
<div className="text-xs text-muted-foreground">
|
||||||
siteSettings.pointsSystemConfig?.creditCostPer3D ?? 0
|
作用于 3D世界模式(文本 / 图片 / 多图 / 视频生成世界),每次提交任务扣除一次。
|
||||||
}
|
</div>
|
||||||
onChange={(e) =>
|
</div>
|
||||||
setSiteSettings({
|
<div className="mt-2 w-full max-w-xs">
|
||||||
...siteSettings,
|
<Input
|
||||||
pointsSystemConfig: {
|
id="points3dWorldCost"
|
||||||
...siteSettings.pointsSystemConfig,
|
min={0}
|
||||||
creditCostPer3D: Math.max(
|
type="number"
|
||||||
0,
|
value={
|
||||||
Number.parseInt(e.target.value || "0", 10) || 0
|
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>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@ -341,8 +341,8 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const pointsConfig = await getPointsSystemConfig();
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
const shouldChargePoints = shouldCharge3DPoints(taskType);
|
const shouldChargePoints = shouldCharge3DPoints(taskType);
|
||||||
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3D))
|
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DModel))
|
||||||
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3D)))
|
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DModel)))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -399,7 +399,7 @@ export async function POST(request: Request) {
|
|||||||
amount: unitCost,
|
amount: unitCost,
|
||||||
reason: "model3d_generation",
|
reason: "model3d_generation",
|
||||||
modelId: "tripo-3d",
|
modelId: "tripo-3d",
|
||||||
note: "3D生成消耗积分",
|
note: "3D模型生成消耗积分",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!consumeResult.ok) {
|
if (!consumeResult.ok) {
|
||||||
@ -412,7 +412,7 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
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) {
|
} catch (error) {
|
||||||
|
|||||||
@ -143,8 +143,8 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const pointsConfig = await getPointsSystemConfig();
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
const shouldChargePoints = shouldCharge3DPoints(mode);
|
const shouldChargePoints = shouldCharge3DPoints(mode);
|
||||||
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3D))
|
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DWorld))
|
||||||
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3D)))
|
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DWorld)))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -209,7 +209,7 @@ export async function POST(request: Request) {
|
|||||||
await consumeUserPoints({
|
await consumeUserPoints({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
amount: unitCost,
|
amount: unitCost,
|
||||||
reason: "model3d_generation",
|
reason: "world3d_generation",
|
||||||
modelId: "worldlabs-3d",
|
modelId: "worldlabs-3d",
|
||||||
note: "3D世界生成消耗积分",
|
note: "3D世界生成消耗积分",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,7 +22,8 @@ const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
|||||||
|
|
||||||
function mapLedgerReasonLabel(reason: string) {
|
function mapLedgerReasonLabel(reason: string) {
|
||||||
if (reason === "image_generation") return "图片生成";
|
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 === "video_generation") return "视频生成";
|
||||||
if (reason === "deduct") return "管理员扣减";
|
if (reason === "deduct") return "管理员扣减";
|
||||||
if (reason === "gift") return "管理员赠送";
|
if (reason === "gift") return "管理员赠送";
|
||||||
|
|||||||
@ -4,7 +4,8 @@ export const POINTS_PACKAGES_KEY = "points_packages";
|
|||||||
export type PointsSystemConfig = {
|
export type PointsSystemConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
blockOnInsufficient: boolean;
|
blockOnInsufficient: boolean;
|
||||||
creditCostPer3D: number;
|
creditCostPer3DModel: number;
|
||||||
|
creditCostPer3DWorld: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointsPackage = {
|
export type PointsPackage = {
|
||||||
@ -19,7 +20,8 @@ export type PointsPackage = {
|
|||||||
export const defaultPointsSystemConfig: PointsSystemConfig = {
|
export const defaultPointsSystemConfig: PointsSystemConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
blockOnInsufficient: true,
|
blockOnInsufficient: true,
|
||||||
creditCostPer3D: 0,
|
creditCostPer3DModel: 0,
|
||||||
|
creditCostPer3DWorld: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultPointsPackages: PointsPackage[] = [
|
export const defaultPointsPackages: PointsPackage[] = [
|
||||||
@ -46,15 +48,25 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
|
|||||||
return defaultPointsSystemConfig;
|
return defaultPointsSystemConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = value as Partial<PointsSystemConfig>;
|
const raw = value as Partial<PointsSystemConfig> & {
|
||||||
const parsed3DCost = Number(raw.creditCostPer3D);
|
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 {
|
return {
|
||||||
enabled: raw.enabled === true,
|
enabled: raw.enabled === true,
|
||||||
blockOnInsufficient: raw.blockOnInsufficient !== false,
|
blockOnInsufficient: raw.blockOnInsufficient !== false,
|
||||||
creditCostPer3D:
|
creditCostPer3DModel: normalizeCost(
|
||||||
Number.isFinite(parsed3DCost) && parsed3DCost > 0
|
raw.creditCostPer3DModel ?? legacy3DCost
|
||||||
? Math.min(100000, Math.round(parsed3DCost))
|
),
|
||||||
: 0,
|
creditCostPer3DWorld: normalizeCost(
|
||||||
|
raw.creditCostPer3DWorld ?? legacy3DCost
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user