340 lines
9.8 KiB
TypeScript
340 lines
9.8 KiB
TypeScript
import type { EnhanceProKreaModel } from "@/lib/enhance-pro/config";
|
|
|
|
export const POINTS_SYSTEM_CONFIG_KEY = "points_system_config";
|
|
export const POINTS_PACKAGES_KEY = "points_packages";
|
|
|
|
export type EnhanceProPointsChargeMode = "fixed" | "resolution_bucket";
|
|
|
|
export type EnhanceProPointsResolutionBucket = {
|
|
id: string;
|
|
name: string;
|
|
maxMegapixels: number;
|
|
cost: number;
|
|
};
|
|
|
|
export type EnhanceProPointsPricingConfig = {
|
|
enabled: boolean;
|
|
chargeMode: EnhanceProPointsChargeMode;
|
|
fixedCost: number;
|
|
batchSizeMultiplierEnabled: boolean;
|
|
resolutionBuckets: EnhanceProPointsResolutionBucket[];
|
|
modelMultipliers: Record<EnhanceProKreaModel, number>;
|
|
};
|
|
|
|
export type PointsSystemConfig = {
|
|
enabled: boolean;
|
|
blockOnInsufficient: boolean;
|
|
creditCostPer3DModel: number;
|
|
creditCostPer3DWorld: number;
|
|
enhanceProPricing: EnhanceProPointsPricingConfig;
|
|
};
|
|
|
|
export type PointsPackage = {
|
|
id: string;
|
|
name: string;
|
|
points: number;
|
|
price: number;
|
|
currency: string;
|
|
isEnabled: boolean;
|
|
};
|
|
|
|
export const defaultPointsSystemConfig: PointsSystemConfig = {
|
|
enabled: false,
|
|
blockOnInsufficient: true,
|
|
creditCostPer3DModel: 0,
|
|
creditCostPer3DWorld: 0,
|
|
enhanceProPricing: {
|
|
enabled: false,
|
|
chargeMode: "resolution_bucket",
|
|
fixedCost: 45,
|
|
batchSizeMultiplierEnabled: true,
|
|
resolutionBuckets: [
|
|
{
|
|
id: "enhance-pro-1mp",
|
|
name: "1MP 以内",
|
|
maxMegapixels: 1,
|
|
cost: 20,
|
|
},
|
|
{
|
|
id: "enhance-pro-2mp",
|
|
name: "2MP 以内",
|
|
maxMegapixels: 2,
|
|
cost: 30,
|
|
},
|
|
{
|
|
id: "enhance-pro-4mp",
|
|
name: "4MP 以内",
|
|
maxMegapixels: 4,
|
|
cost: 45,
|
|
},
|
|
{
|
|
id: "enhance-pro-8mp",
|
|
name: "8MP 以内",
|
|
maxMegapixels: 8,
|
|
cost: 65,
|
|
},
|
|
{
|
|
id: "enhance-pro-16mp",
|
|
name: "16MP 以内",
|
|
maxMegapixels: 16,
|
|
cost: 90,
|
|
},
|
|
],
|
|
modelMultipliers: {
|
|
Redefine: 1,
|
|
Recovery: 1,
|
|
"Recovery V2": 1.1,
|
|
Reimagine: 1.25,
|
|
},
|
|
},
|
|
};
|
|
|
|
export const defaultPointsPackages: PointsPackage[] = [
|
|
{
|
|
id: "starter-1000",
|
|
name: "入门包",
|
|
points: 1000,
|
|
price: 9.9,
|
|
currency: "CNY",
|
|
isEnabled: true,
|
|
},
|
|
{
|
|
id: "pro-5000",
|
|
name: "专业包",
|
|
points: 5000,
|
|
price: 39.9,
|
|
currency: "CNY",
|
|
isEnabled: true,
|
|
},
|
|
];
|
|
|
|
function normalizeCost(input: unknown) {
|
|
const parsedValue = Number(input);
|
|
return Number.isFinite(parsedValue) && parsedValue > 0
|
|
? Math.min(100000, Math.round(parsedValue))
|
|
: 0;
|
|
}
|
|
|
|
function normalizeMultiplier(input: unknown, fallback: number) {
|
|
const parsedValue = Number(input);
|
|
return Number.isFinite(parsedValue) && parsedValue > 0
|
|
? Math.min(10, Math.max(0.1, Number(parsedValue.toFixed(2))))
|
|
: fallback;
|
|
}
|
|
|
|
function normalizeMegapixels(input: unknown, fallback: number) {
|
|
const parsedValue = Number(input);
|
|
return Number.isFinite(parsedValue) && parsedValue > 0
|
|
? Math.min(100, Math.max(0.1, Number(parsedValue.toFixed(2))))
|
|
: fallback;
|
|
}
|
|
|
|
function normalizeEnhanceProResolutionBucket(
|
|
value: unknown,
|
|
index: number,
|
|
fallback: EnhanceProPointsResolutionBucket
|
|
): EnhanceProPointsResolutionBucket | null {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return fallback;
|
|
}
|
|
|
|
const raw = value as Partial<EnhanceProPointsResolutionBucket>;
|
|
const id = String(raw.id || fallback.id || `enhance-pro-bucket-${index + 1}`).trim();
|
|
const name = String(raw.name || fallback.name || `档位 ${index + 1}`).trim();
|
|
const maxMegapixels = normalizeMegapixels(
|
|
raw.maxMegapixels,
|
|
fallback.maxMegapixels
|
|
);
|
|
const cost = normalizeCost(raw.cost ?? fallback.cost);
|
|
|
|
if (!id || !name || cost <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: id.slice(0, 64),
|
|
name: name.slice(0, 64),
|
|
maxMegapixels,
|
|
cost,
|
|
};
|
|
}
|
|
|
|
export function normalizeEnhanceProPointsPricingConfig(
|
|
value: unknown,
|
|
legacyFixedCost?: number
|
|
): EnhanceProPointsPricingConfig {
|
|
const defaults = defaultPointsSystemConfig.enhanceProPricing;
|
|
const raw =
|
|
value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Partial<EnhanceProPointsPricingConfig>)
|
|
: {};
|
|
|
|
const resolutionBuckets = Array.isArray(raw.resolutionBuckets)
|
|
? raw.resolutionBuckets
|
|
.map((item, index) =>
|
|
normalizeEnhanceProResolutionBucket(
|
|
item,
|
|
index,
|
|
defaults.resolutionBuckets[index] || defaults.resolutionBuckets[defaults.resolutionBuckets.length - 1]
|
|
)
|
|
)
|
|
.filter((item): item is EnhanceProPointsResolutionBucket => Boolean(item))
|
|
.sort((left, right) => left.maxMegapixels - right.maxMegapixels)
|
|
: defaults.resolutionBuckets;
|
|
|
|
const modelMultipliersRaw =
|
|
raw.modelMultipliers && typeof raw.modelMultipliers === "object"
|
|
? raw.modelMultipliers
|
|
: defaults.modelMultipliers;
|
|
|
|
return {
|
|
enabled: raw.enabled === true,
|
|
chargeMode: raw.chargeMode === "fixed" ? "fixed" : "resolution_bucket",
|
|
fixedCost: normalizeCost(raw.fixedCost ?? legacyFixedCost ?? defaults.fixedCost),
|
|
batchSizeMultiplierEnabled: raw.batchSizeMultiplierEnabled !== false,
|
|
resolutionBuckets:
|
|
resolutionBuckets.length > 0 ? resolutionBuckets : defaults.resolutionBuckets,
|
|
modelMultipliers: {
|
|
Redefine: normalizeMultiplier(
|
|
modelMultipliersRaw?.Redefine,
|
|
defaults.modelMultipliers.Redefine
|
|
),
|
|
Recovery: normalizeMultiplier(
|
|
modelMultipliersRaw?.Recovery,
|
|
defaults.modelMultipliers.Recovery
|
|
),
|
|
"Recovery V2": normalizeMultiplier(
|
|
modelMultipliersRaw?.["Recovery V2"],
|
|
defaults.modelMultipliers["Recovery V2"]
|
|
),
|
|
Reimagine: normalizeMultiplier(
|
|
modelMultipliersRaw?.Reimagine,
|
|
defaults.modelMultipliers.Reimagine
|
|
),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function getEnhanceProResolutionMegapixels(
|
|
width: number,
|
|
height: number
|
|
) {
|
|
const safeWidth = Math.max(1, Math.round(Number(width) || 1));
|
|
const safeHeight = Math.max(1, Math.round(Number(height) || 1));
|
|
return Number(((safeWidth * safeHeight) / 1_048_576).toFixed(2));
|
|
}
|
|
|
|
export function calculateEnhanceProPointsEstimate(
|
|
pointsConfig: PointsSystemConfig,
|
|
params: {
|
|
width: number;
|
|
height: number;
|
|
batchSize?: number;
|
|
model?: EnhanceProKreaModel;
|
|
}
|
|
) {
|
|
const pricing = normalizeEnhanceProPointsPricingConfig(
|
|
pointsConfig.enhanceProPricing
|
|
);
|
|
const model: EnhanceProKreaModel =
|
|
params.model === "Recovery" ||
|
|
params.model === "Recovery V2" ||
|
|
params.model === "Reimagine" ||
|
|
params.model === "Redefine"
|
|
? params.model
|
|
: "Redefine";
|
|
const megapixels = getEnhanceProResolutionMegapixels(params.width, params.height);
|
|
const batchSize = Math.max(1, Math.round(Number(params.batchSize) || 1));
|
|
const batchMultiplier = pricing.batchSizeMultiplierEnabled ? batchSize : 1;
|
|
const modelMultiplier =
|
|
pricing.modelMultipliers[model] || defaultPointsSystemConfig.enhanceProPricing.modelMultipliers[model];
|
|
const bucket =
|
|
pricing.chargeMode === "resolution_bucket"
|
|
? pricing.resolutionBuckets.find((item) => megapixels <= item.maxMegapixels) ||
|
|
pricing.resolutionBuckets[pricing.resolutionBuckets.length - 1] ||
|
|
null
|
|
: null;
|
|
const baseCost =
|
|
pricing.chargeMode === "fixed"
|
|
? pricing.fixedCost
|
|
: Math.max(0, Number(bucket?.cost || 0));
|
|
const rawCost = baseCost * batchMultiplier * modelMultiplier;
|
|
const cost = baseCost > 0 ? Math.max(1, Math.ceil(rawCost)) : 0;
|
|
const enabled = pointsConfig.enabled === true && pricing.enabled === true;
|
|
|
|
return {
|
|
enabled,
|
|
chargeable: enabled && cost > 0,
|
|
chargeMode: pricing.chargeMode,
|
|
megapixels,
|
|
batchSize,
|
|
batchMultiplier,
|
|
model,
|
|
modelMultiplier,
|
|
baseCost,
|
|
cost,
|
|
bucketId: bucket?.id || null,
|
|
bucketName: bucket?.name || null,
|
|
};
|
|
}
|
|
|
|
export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig {
|
|
if (!value || typeof value !== "object") {
|
|
return defaultPointsSystemConfig;
|
|
}
|
|
|
|
const raw = value as Partial<PointsSystemConfig> & {
|
|
creditCostPer3D?: number;
|
|
creditCostPerEnhancePro?: number;
|
|
};
|
|
const legacy3DCost = normalizeCost(raw.creditCostPer3D);
|
|
const legacyEnhanceProCost = normalizeCost(raw.creditCostPerEnhancePro);
|
|
return {
|
|
enabled: raw.enabled === true,
|
|
blockOnInsufficient: raw.blockOnInsufficient !== false,
|
|
creditCostPer3DModel: normalizeCost(
|
|
raw.creditCostPer3DModel ?? legacy3DCost
|
|
),
|
|
creditCostPer3DWorld: normalizeCost(
|
|
raw.creditCostPer3DWorld ?? legacy3DCost
|
|
),
|
|
enhanceProPricing: normalizeEnhanceProPointsPricingConfig(
|
|
raw.enhanceProPricing,
|
|
legacyEnhanceProCost
|
|
),
|
|
};
|
|
}
|
|
|
|
export function normalizePointsPackages(value: unknown): PointsPackage[] {
|
|
if (!Array.isArray(value)) {
|
|
return defaultPointsPackages;
|
|
}
|
|
|
|
const normalized = value
|
|
.map((item, index) => {
|
|
if (!item || typeof item !== "object") return null;
|
|
const raw = item as Partial<PointsPackage>;
|
|
const id = String(raw.id || `package-${index + 1}`).trim();
|
|
const name = String(raw.name || "").trim();
|
|
const points = Number(raw.points);
|
|
const price = Number(raw.price);
|
|
const currency = String(raw.currency || "CNY").trim().toUpperCase();
|
|
|
|
if (!id || !name || !Number.isFinite(points) || points <= 0) return null;
|
|
if (!Number.isFinite(price) || price < 0) return null;
|
|
|
|
return {
|
|
id: id.slice(0, 64),
|
|
name: name.slice(0, 64),
|
|
points: Math.round(points),
|
|
price: Number(price.toFixed(2)),
|
|
currency: currency.slice(0, 8) || "CNY",
|
|
isEnabled: raw.isEnabled !== false,
|
|
} satisfies PointsPackage;
|
|
})
|
|
.filter((item): item is PointsPackage => Boolean(item))
|
|
.slice(0, 30);
|
|
|
|
return normalized.length > 0 ? normalized : defaultPointsPackages;
|
|
}
|