148 lines
6.3 KiB
TypeScript
148 lines
6.3 KiB
TypeScript
import type {
|
|
BillingConditionValue,
|
|
BillingRuleConditions,
|
|
BillingParameterSnapshot,
|
|
BillingPriceRule,
|
|
BillingQuote,
|
|
BillingSelectedParameterTier,
|
|
BillingUnit,
|
|
GenerationCapability,
|
|
GenerationProvider
|
|
} from "@/lib/types";
|
|
|
|
export const BILLING_CURRENCY = "CNY" as const;
|
|
|
|
export const BILLING_UNIT_OPTIONS: Array<{ value: BillingUnit; label: string }> = [
|
|
{ value: "request", label: "按次" },
|
|
{ value: "image", label: "按张" },
|
|
{ value: "video_second", label: "按秒" }
|
|
];
|
|
|
|
export function calculateBillingAmountFen(standardUnitPriceFen: number, quantity: number, markupMultiplier: number): number {
|
|
if (!Number.isFinite(standardUnitPriceFen) || standardUnitPriceFen < 0) throw new Error("标准单价必须是非负金额。");
|
|
if (!Number.isFinite(quantity) || quantity <= 0) throw new Error("计费数量必须大于 0。");
|
|
if (!Number.isFinite(markupMultiplier) || markupMultiplier < 1) throw new Error("上浮倍率不能低于 1.00。");
|
|
return Math.max(0, Math.ceil(standardUnitPriceFen * quantity * markupMultiplier));
|
|
}
|
|
|
|
export function quoteFromPriceRule(input: {
|
|
rule: BillingPriceRule;
|
|
provider: GenerationProvider;
|
|
capability: GenerationCapability;
|
|
reqKey: string;
|
|
quantity: number;
|
|
parameters?: BillingParameterSnapshot;
|
|
conditions?: BillingRuleConditions;
|
|
quantitySource?: BillingPriceRule["quantitySource"];
|
|
}): BillingQuote {
|
|
const { rule } = input;
|
|
if (rule.provider !== input.provider || rule.capability !== input.capability) {
|
|
throw new Error("计费规则与生成服务不匹配。");
|
|
}
|
|
const parameterPricing = resolveBillingParameterPricing(rule, input.parameters);
|
|
if (!parameterPricing) throw new Error("当前生成参数没有对应的平台标准价格。");
|
|
const standardUnitPriceFen = parameterPricing.standardUnitPriceFen;
|
|
const markupMultiplier = parameterPricing.markupMultiplier;
|
|
return {
|
|
priceRuleId: rule.id,
|
|
provider: input.provider,
|
|
capability: input.capability,
|
|
reqKey: input.reqKey,
|
|
variantKey: rule.variantKey,
|
|
unit: rule.unit,
|
|
quantity: input.quantity,
|
|
standardUnitPriceFen,
|
|
markupMultiplier,
|
|
amountFen: calculateBillingAmountFen(standardUnitPriceFen, input.quantity, markupMultiplier),
|
|
currency: BILLING_CURRENCY,
|
|
conditions: input.conditions ?? rule.conditions,
|
|
quantitySource: input.quantitySource ?? rule.quantitySource,
|
|
parameters: input.parameters,
|
|
baseStandardUnitPriceFen: rule.parameterDimensions?.length ? rule.standardUnitPriceFen : undefined,
|
|
parameterTiers: parameterPricing.parameterTiers.length ? parameterPricing.parameterTiers : undefined,
|
|
source: rule.source
|
|
};
|
|
}
|
|
|
|
export type BillingParameterPricing = {
|
|
standardUnitPriceFen: number;
|
|
markupMultiplier: number;
|
|
parameterTiers: BillingSelectedParameterTier[];
|
|
};
|
|
|
|
export function resolveBillingParameterPricing(rule: BillingPriceRule, parameters: BillingParameterSnapshot = {}): BillingParameterPricing | null {
|
|
const dimensions = rule.parameterDimensions?.filter((dimension) => dimension.tiers.length) || [];
|
|
if (!dimensions.length) {
|
|
return {
|
|
standardUnitPriceFen: rule.standardUnitPriceFen,
|
|
markupMultiplier: rule.markupMultiplier,
|
|
parameterTiers: []
|
|
};
|
|
}
|
|
|
|
let standardFactor = 1;
|
|
let markupMultiplier = rule.parameterDimensions?.length ? 1 : rule.markupMultiplier;
|
|
const parameterTiers: BillingSelectedParameterTier[] = [];
|
|
for (const dimension of dimensions) {
|
|
const actualValue = parameters[dimension.key] ?? dimension.defaultValue ?? dimension.baselineValue;
|
|
const tier = dimension.tiers.find((candidate) => candidate.enabled && billingConditionMatchesValue(candidate.match ?? candidate.value, actualValue));
|
|
if (!tier) return null;
|
|
const factor = Number(tier.standardFactor);
|
|
const tierMarkup = Number(tier.markupMultiplier);
|
|
if (!Number.isFinite(factor) || factor <= 0 || !Number.isFinite(tierMarkup) || tierMarkup < 1) return null;
|
|
standardFactor *= factor;
|
|
// A combination has one customer-facing uplift. The highest selected
|
|
// tier multiplier wins so editing one expensive tier never lowers the
|
|
// configured uplift of another selected tier.
|
|
markupMultiplier = Math.max(markupMultiplier, tierMarkup);
|
|
parameterTiers.push({
|
|
...tier,
|
|
dimensionKey: dimension.key,
|
|
dimensionLabel: dimension.label,
|
|
actualValue,
|
|
standardUnitPriceFen: Math.ceil(rule.standardUnitPriceFen * factor)
|
|
});
|
|
}
|
|
return {
|
|
standardUnitPriceFen: Math.ceil(rule.standardUnitPriceFen * standardFactor),
|
|
markupMultiplier,
|
|
parameterTiers
|
|
};
|
|
}
|
|
|
|
export function billingConditionMatchesValue(condition: BillingConditionValue, actual: string | number | boolean): boolean {
|
|
if (typeof condition === "object" && condition !== null && !Array.isArray(condition)) {
|
|
if (Array.isArray(condition.values) && !condition.values.some((value) => billingScalarEquals(value, actual))) return false;
|
|
const numericActual = Number(actual);
|
|
if (condition.min !== undefined && (!Number.isFinite(numericActual) || numericActual < condition.min)) return false;
|
|
if (condition.max !== undefined && (!Number.isFinite(numericActual) || numericActual > condition.max)) return false;
|
|
return true;
|
|
}
|
|
return billingScalarEquals(condition as string | number | boolean, actual);
|
|
}
|
|
|
|
function billingScalarEquals(left: string | number | boolean, right: string | number | boolean): boolean {
|
|
if (typeof left === "number" || typeof right === "number") return Number(left) === Number(right);
|
|
if (typeof left === "boolean" || typeof right === "boolean") return Boolean(left) === Boolean(right);
|
|
return String(left).trim().toLowerCase() === String(right).trim().toLowerCase();
|
|
}
|
|
|
|
export function formatBillingAmount(fen: number, currency = BILLING_CURRENCY): string {
|
|
const amount = Math.max(0, Number(fen || 0)) / 100;
|
|
return new Intl.NumberFormat("zh-CN", {
|
|
style: "currency",
|
|
currency,
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}).format(amount);
|
|
}
|
|
|
|
export function billingUnitLabel(unit: BillingUnit): string {
|
|
return BILLING_UNIT_OPTIONS.find((item) => item.value === unit)?.label || unit;
|
|
}
|
|
|
|
export function billingQuantityLabel(quantity: number, unit: BillingUnit): string {
|
|
const value = Number.isInteger(quantity) ? String(quantity) : quantity.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
|
|
return `${value}${unit === "video_second" ? " 秒" : unit === "image" ? " 张" : " 次"}`;
|
|
}
|