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; }; 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; 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) : {}; 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 & { 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; 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; }