584 lines
18 KiB
TypeScript
584 lines
18 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"
|
|
| "scale_factor";
|
|
|
|
export type EnhanceProPointsResolutionBucket = {
|
|
id: string;
|
|
name: string;
|
|
maxMegapixels: number;
|
|
cost: number;
|
|
};
|
|
|
|
export type EnhanceProPointsPricingConfig = {
|
|
enabled: boolean;
|
|
chargeMode: EnhanceProPointsChargeMode;
|
|
fixedCost: number;
|
|
chargeOnlyWhenUpscaling: boolean;
|
|
batchSizeMultiplierEnabled: boolean;
|
|
scaleFactorCosts: Record<string, number>;
|
|
resolutionBuckets: EnhanceProPointsResolutionBucket[];
|
|
modelMultipliers: Record<EnhanceProKreaModel, number>;
|
|
};
|
|
|
|
export type PointsSystemConfig = {
|
|
enabled: boolean;
|
|
blockOnInsufficient: boolean;
|
|
creditCostPer3DModel: number;
|
|
creditCostPer3DModelPbr: number;
|
|
creditCostPer3DModelSmartLowPoly: number;
|
|
creditCostPer3DModelQuad: number;
|
|
creditCostPer3DModelRefine: number;
|
|
creditCostPer3DModelTexture: number;
|
|
creditCostPer3DModelAutoRig: number;
|
|
creditCostPer3DModelHighpolyToLowpoly: number;
|
|
creditCostPer3DWorld: number;
|
|
creditCostPer3DWorldMarble11: number;
|
|
creditCostPer3DWorldMarble11Plus: number;
|
|
creditCostPerImageMultiView: number;
|
|
creditCostPerImageRemoveBackground: number;
|
|
creditCostPerImageBoxCutout: number;
|
|
creditCostPerImageWatermarkRemove: number;
|
|
creditCostPerImageExtend: number;
|
|
creditCostPerImageRepaint: number;
|
|
enhanceProPricing: EnhanceProPointsPricingConfig;
|
|
};
|
|
|
|
export type ThreeDTaskCreditParams = {
|
|
taskType?: string | null;
|
|
pbr?: boolean | null;
|
|
quad?: boolean | null;
|
|
smartLowPoly?: boolean | null;
|
|
};
|
|
|
|
export type ImageToolCreditFeature =
|
|
| "multi-view"
|
|
| "remove-background"
|
|
| "box-cutout"
|
|
| "watermark-remove"
|
|
| "extend"
|
|
| "repaint";
|
|
|
|
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,
|
|
creditCostPer3DModelPbr: 0,
|
|
creditCostPer3DModelSmartLowPoly: 0,
|
|
creditCostPer3DModelQuad: 0,
|
|
creditCostPer3DModelRefine: 0,
|
|
creditCostPer3DModelTexture: 0,
|
|
creditCostPer3DModelAutoRig: 0,
|
|
creditCostPer3DModelHighpolyToLowpoly: 0,
|
|
creditCostPer3DWorld: 0,
|
|
creditCostPer3DWorldMarble11: 0,
|
|
creditCostPer3DWorldMarble11Plus: 0,
|
|
creditCostPerImageMultiView: 0,
|
|
creditCostPerImageRemoveBackground: 0,
|
|
creditCostPerImageBoxCutout: 0,
|
|
creditCostPerImageWatermarkRemove: 0,
|
|
creditCostPerImageExtend: 0,
|
|
creditCostPerImageRepaint: 0,
|
|
enhanceProPricing: {
|
|
enabled: false,
|
|
chargeMode: "scale_factor",
|
|
fixedCost: 20,
|
|
chargeOnlyWhenUpscaling: false,
|
|
batchSizeMultiplierEnabled: true,
|
|
scaleFactorCosts: {
|
|
"1": 20,
|
|
"1.25": 25,
|
|
"1.5": 30,
|
|
"2": 40,
|
|
"3": 60,
|
|
"4": 80,
|
|
},
|
|
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,
|
|
Reimagine: 1,
|
|
},
|
|
},
|
|
};
|
|
|
|
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 normalizeScaleFactor(input: unknown, fallback: number) {
|
|
const parsedValue = Number(input);
|
|
return Number.isFinite(parsedValue) && parsedValue > 0
|
|
? Math.min(32, Math.max(1, Number(parsedValue.toFixed(2))))
|
|
: fallback;
|
|
}
|
|
|
|
function tryParseScaleFactor(input: unknown) {
|
|
const parsedValue = Number(input);
|
|
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return null;
|
|
return Math.min(32, Math.max(1, Number(parsedValue.toFixed(2))));
|
|
}
|
|
|
|
export function formatEnhanceProScaleFactorKey(input: unknown) {
|
|
const normalized = normalizeScaleFactor(input, 1);
|
|
return Number(normalized.toFixed(2)).toString();
|
|
}
|
|
|
|
function normalizeEnhanceProScaleFactorCosts(value: unknown) {
|
|
const defaults = defaultPointsSystemConfig.enhanceProPricing.scaleFactorCosts;
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return defaults;
|
|
}
|
|
|
|
const normalizedEntries = Object.entries(value as Record<string, unknown>)
|
|
.map(([factorKey, costValue]) => {
|
|
const factor = tryParseScaleFactor(factorKey);
|
|
if (factor === null) return null;
|
|
return [formatEnhanceProScaleFactorKey(factor), normalizeCost(costValue)] as const;
|
|
})
|
|
.filter((entry): entry is readonly [string, number] => Boolean(entry))
|
|
.sort((left, right) => Number(left[0]) - Number(right[0]));
|
|
|
|
if (normalizedEntries.length === 0) {
|
|
return defaults;
|
|
}
|
|
|
|
return Object.fromEntries(normalizedEntries);
|
|
}
|
|
|
|
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;
|
|
const chargeMode: EnhanceProPointsChargeMode =
|
|
raw.chargeMode === "fixed" ||
|
|
raw.chargeMode === "resolution_bucket" ||
|
|
raw.chargeMode === "scale_factor"
|
|
? raw.chargeMode
|
|
: defaults.chargeMode;
|
|
|
|
return {
|
|
enabled: raw.enabled === true,
|
|
chargeMode,
|
|
fixedCost: normalizeCost(raw.fixedCost ?? legacyFixedCost ?? defaults.fixedCost),
|
|
chargeOnlyWhenUpscaling: raw.chargeOnlyWhenUpscaling !== false,
|
|
batchSizeMultiplierEnabled: raw.batchSizeMultiplierEnabled !== false,
|
|
scaleFactorCosts: normalizeEnhanceProScaleFactorCosts(
|
|
raw.scaleFactorCosts
|
|
),
|
|
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;
|
|
upscalingActivated?: boolean;
|
|
scaleFactor?: number;
|
|
}
|
|
) {
|
|
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 upscalingActivated = params.upscalingActivated === true;
|
|
const scaleFactor = normalizeScaleFactor(params.scaleFactor ?? 1, 1);
|
|
const billingScaleFactor = upscalingActivated ? scaleFactor : 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 scaleFactorCosts = normalizeEnhanceProScaleFactorCosts(
|
|
pricing.scaleFactorCosts
|
|
);
|
|
const scaleFactorCostEntries = Object.entries(scaleFactorCosts)
|
|
.map(([factorKey, cost]) => ({
|
|
factorKey,
|
|
factor: Number(factorKey),
|
|
cost,
|
|
}))
|
|
.filter((item) => Number.isFinite(item.factor))
|
|
.sort((left, right) => left.factor - right.factor);
|
|
const scaleFactorCost =
|
|
scaleFactorCostEntries.find(
|
|
(item) => Math.abs(item.factor - billingScaleFactor) < 0.001
|
|
) || null;
|
|
const linearScaleFactorCost = Math.ceil(pricing.fixedCost * billingScaleFactor);
|
|
const baseCost =
|
|
pricing.chargeOnlyWhenUpscaling && !upscalingActivated
|
|
? 0
|
|
: pricing.chargeMode === "fixed"
|
|
? pricing.fixedCost
|
|
: pricing.chargeMode === "scale_factor"
|
|
? Math.max(0, Number(scaleFactorCost?.cost ?? linearScaleFactorCost))
|
|
: 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,
|
|
chargeOnlyWhenUpscaling: pricing.chargeOnlyWhenUpscaling,
|
|
upscalingActivated,
|
|
scaleFactor,
|
|
billingScaleFactor,
|
|
megapixels,
|
|
batchSize,
|
|
batchMultiplier,
|
|
model,
|
|
modelMultiplier,
|
|
baseCost,
|
|
cost,
|
|
scaleFactorCostKey: scaleFactorCost?.factorKey || null,
|
|
bucketId: bucket?.id || null,
|
|
bucketName: bucket?.name || null,
|
|
};
|
|
}
|
|
|
|
export function get3DWorldCreditCost(
|
|
pointsConfig: PointsSystemConfig,
|
|
modelId?: string | null
|
|
) {
|
|
const normalizedModelId = String(modelId || "").trim().toLowerCase();
|
|
if (normalizedModelId === "marble-1.1-plus") {
|
|
return normalizeCost(pointsConfig.creditCostPer3DWorldMarble11Plus);
|
|
}
|
|
if (normalizedModelId === "marble-1.1") {
|
|
return normalizeCost(pointsConfig.creditCostPer3DWorldMarble11);
|
|
}
|
|
return normalizeCost(pointsConfig.creditCostPer3DWorld);
|
|
}
|
|
|
|
export function get3DTaskCreditCost(
|
|
pointsConfig: PointsSystemConfig,
|
|
params: ThreeDTaskCreditParams
|
|
) {
|
|
const taskType = String(params.taskType || "").trim();
|
|
if (
|
|
taskType === "text_to_model" ||
|
|
taskType === "image_to_model" ||
|
|
taskType === "multiview_to_model"
|
|
) {
|
|
let cost = normalizeCost(pointsConfig.creditCostPer3DModel);
|
|
if (params.pbr === true) {
|
|
cost += normalizeCost(pointsConfig.creditCostPer3DModelPbr);
|
|
}
|
|
if (params.smartLowPoly === true) {
|
|
cost += normalizeCost(pointsConfig.creditCostPer3DModelSmartLowPoly);
|
|
}
|
|
if (params.quad === true) {
|
|
cost += normalizeCost(pointsConfig.creditCostPer3DModelQuad);
|
|
}
|
|
return cost;
|
|
}
|
|
|
|
if (taskType === "refine_model") {
|
|
return normalizeCost(pointsConfig.creditCostPer3DModelRefine);
|
|
}
|
|
if (taskType === "texture_model") {
|
|
return normalizeCost(pointsConfig.creditCostPer3DModelTexture);
|
|
}
|
|
if (taskType === "animate_rig") {
|
|
return normalizeCost(pointsConfig.creditCostPer3DModelAutoRig);
|
|
}
|
|
if (taskType === "highpoly_to_lowpoly") {
|
|
return normalizeCost(pointsConfig.creditCostPer3DModelHighpolyToLowpoly);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
export function getImageToolCreditCost(
|
|
pointsConfig: PointsSystemConfig,
|
|
feature: ImageToolCreditFeature | null | undefined
|
|
) {
|
|
switch (feature) {
|
|
case "multi-view":
|
|
return normalizeCost(pointsConfig.creditCostPerImageMultiView);
|
|
case "remove-background":
|
|
return normalizeCost(pointsConfig.creditCostPerImageRemoveBackground);
|
|
case "box-cutout":
|
|
return normalizeCost(pointsConfig.creditCostPerImageBoxCutout);
|
|
case "watermark-remove":
|
|
return normalizeCost(pointsConfig.creditCostPerImageWatermarkRemove);
|
|
case "extend":
|
|
return normalizeCost(pointsConfig.creditCostPerImageExtend);
|
|
case "repaint":
|
|
return normalizeCost(pointsConfig.creditCostPerImageRepaint);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
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 generic3DWorldCost = normalizeCost(
|
|
raw.creditCostPer3DWorld ?? legacy3DCost
|
|
);
|
|
const legacyEnhanceProCost = normalizeCost(raw.creditCostPerEnhancePro);
|
|
return {
|
|
enabled: raw.enabled === true,
|
|
blockOnInsufficient: raw.blockOnInsufficient !== false,
|
|
creditCostPer3DModel: normalizeCost(
|
|
raw.creditCostPer3DModel ?? legacy3DCost
|
|
),
|
|
creditCostPer3DModelPbr: normalizeCost(raw.creditCostPer3DModelPbr),
|
|
creditCostPer3DModelSmartLowPoly: normalizeCost(
|
|
raw.creditCostPer3DModelSmartLowPoly
|
|
),
|
|
creditCostPer3DModelQuad: normalizeCost(raw.creditCostPer3DModelQuad),
|
|
creditCostPer3DModelRefine: normalizeCost(raw.creditCostPer3DModelRefine),
|
|
creditCostPer3DModelTexture: normalizeCost(raw.creditCostPer3DModelTexture),
|
|
creditCostPer3DModelAutoRig: normalizeCost(raw.creditCostPer3DModelAutoRig),
|
|
creditCostPer3DModelHighpolyToLowpoly: normalizeCost(
|
|
raw.creditCostPer3DModelHighpolyToLowpoly
|
|
),
|
|
creditCostPer3DWorld: normalizeCost(
|
|
raw.creditCostPer3DWorld ?? legacy3DCost
|
|
),
|
|
creditCostPer3DWorldMarble11: normalizeCost(
|
|
raw.creditCostPer3DWorldMarble11 ?? generic3DWorldCost
|
|
),
|
|
creditCostPer3DWorldMarble11Plus: normalizeCost(
|
|
raw.creditCostPer3DWorldMarble11Plus ?? generic3DWorldCost
|
|
),
|
|
creditCostPerImageMultiView: normalizeCost(raw.creditCostPerImageMultiView),
|
|
creditCostPerImageRemoveBackground: normalizeCost(
|
|
raw.creditCostPerImageRemoveBackground
|
|
),
|
|
creditCostPerImageBoxCutout: normalizeCost(raw.creditCostPerImageBoxCutout),
|
|
creditCostPerImageWatermarkRemove: normalizeCost(
|
|
raw.creditCostPerImageWatermarkRemove
|
|
),
|
|
creditCostPerImageExtend: normalizeCost(raw.creditCostPerImageExtend),
|
|
creditCostPerImageRepaint: normalizeCost(raw.creditCostPerImageRepaint),
|
|
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;
|
|
}
|