feat: add gateway billing estimate and rate limit details

This commit is contained in:
2026-05-15 01:53:52 +08:00
parent bdc9be63d5
commit 37d0f919e5
14 changed files with 916 additions and 66 deletions
@@ -78,8 +78,10 @@ type UserGroupForm = {
description: string;
source: string;
priority: string;
rechargeDiscountPolicyJson: string;
billingDiscountPolicyJson: string;
rechargeDiscountFactor: string;
rechargeDiscountPolicy: Record<string, unknown>;
billingDiscountFactor: string;
billingDiscountPolicy: Record<string, unknown>;
rateLimitPolicyJson: string;
quotaPolicyJson: string;
metadataJson: string;
@@ -516,8 +518,8 @@ export function UserGroupsPanel(props: IdentityPanelProps) {
<Label><Select size="sm" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{userGroupStatuses.map(option)}</Select></Label>
<Label><Input size="sm" value={form.priority} inputMode="numeric" onChange={(event) => setForm({ ...form, priority: event.target.value })} /></Label>
<Label className="spanTwo"><Input size="sm" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /></Label>
<JsonField label="充值折扣策略 JSON" value={form.rechargeDiscountPolicyJson} onChange={(value) => setForm({ ...form, rechargeDiscountPolicyJson: value })} />
<JsonField label="计费折扣策略 JSON" value={form.billingDiscountPolicyJson} onChange={(value) => setForm({ ...form, billingDiscountPolicyJson: value })} />
<Label><Input size="sm" value={form.rechargeDiscountFactor} inputMode="decimal" placeholder="1 = 不打折,0.95 = 95 折" onChange={(event) => setForm({ ...form, rechargeDiscountFactor: event.target.value })} /></Label>
<Label><Input size="sm" value={form.billingDiscountFactor} inputMode="decimal" placeholder="1 = 不打折,0.95 = 95 折" onChange={(event) => setForm({ ...form, billingDiscountFactor: event.target.value })} /></Label>
<JsonField label="限流策略 JSON" value={form.rateLimitPolicyJson} onChange={(value) => setForm({ ...form, rateLimitPolicyJson: value })} />
<JsonField label="额度策略 JSON" value={form.quotaPolicyJson} onChange={(value) => setForm({ ...form, quotaPolicyJson: value })} />
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
@@ -769,8 +771,10 @@ function defaultUserGroupForm(): UserGroupForm {
description: '',
source: 'gateway',
priority: '100',
rechargeDiscountPolicyJson: '{}',
billingDiscountPolicyJson: '{}',
rechargeDiscountFactor: '1',
rechargeDiscountPolicy: {},
billingDiscountFactor: '1',
billingDiscountPolicy: {},
rateLimitPolicyJson: '{"rules":[]}',
quotaPolicyJson: '{}',
metadataJson: '{}',
@@ -785,8 +789,10 @@ function userGroupToForm(group: UserGroup): UserGroupForm {
description: group.description ?? '',
source: group.source,
priority: String(group.priority),
rechargeDiscountPolicyJson: stringifyJson(group.rechargeDiscountPolicy),
billingDiscountPolicyJson: stringifyJson(group.billingDiscountPolicy),
rechargeDiscountFactor: discountFactorText(group.rechargeDiscountPolicy),
rechargeDiscountPolicy: group.rechargeDiscountPolicy ?? {},
billingDiscountFactor: discountFactorText(group.billingDiscountPolicy),
billingDiscountPolicy: group.billingDiscountPolicy ?? {},
rateLimitPolicyJson: stringifyJson(group.rateLimitPolicy),
quotaPolicyJson: stringifyJson(group.quotaPolicy),
metadataJson: stringifyJson(group.metadata),
@@ -801,8 +807,8 @@ function formToUserGroupPayload(form: UserGroupForm): UserGroupUpsertRequest {
description: form.description.trim() || undefined,
source: form.source,
priority: Number(form.priority) || 100,
rechargeDiscountPolicy: parseJsonObject(form.rechargeDiscountPolicyJson, '充值折扣策略 JSON'),
billingDiscountPolicy: parseJsonObject(form.billingDiscountPolicyJson, '计费折扣策略 JSON'),
rechargeDiscountPolicy: discountPolicyPayload(form.rechargeDiscountPolicy, form.rechargeDiscountFactor, '充值折扣系数'),
billingDiscountPolicy: discountPolicyPayload(form.billingDiscountPolicy, form.billingDiscountFactor, '计费折扣系数'),
rateLimitPolicy: parseJsonObject(form.rateLimitPolicyJson, '限流策略 JSON'),
quotaPolicy: parseJsonObject(form.quotaPolicyJson, '额度策略 JSON'),
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
@@ -854,14 +860,57 @@ function newIdempotencyKey() {
}
function discountSummary(group: UserGroup) {
const billing = group.billingDiscountPolicy?.discountFactor ?? group.billingDiscountPolicy?.factor;
const recharge = group.rechargeDiscountPolicy?.discountFactor ?? group.rechargeDiscountPolicy?.factor;
const billing = discountFactorFromPolicy(group.billingDiscountPolicy);
const recharge = discountFactorFromPolicy(group.rechargeDiscountPolicy);
const parts = [];
if (billing) parts.push(`计费 ${billing}`);
if (recharge) parts.push(`充值 ${recharge}`);
if (billing) parts.push(`计费 ${trimNumber(billing)}`);
if (recharge) parts.push(`充值 ${trimNumber(recharge)}`);
return parts.join(' / ') || '未设置';
}
function discountFactorText(policy?: Record<string, unknown>) {
const value = discountFactorFromPolicy(policy);
return value ? trimNumber(value) : '1';
}
function discountFactorFromPolicy(policy?: Record<string, unknown>) {
return numberFromUnknown(policy?.discountFactor) ?? numberFromUnknown(policy?.factor);
}
function discountPolicyPayload(basePolicy: Record<string, unknown>, discountText: string, label: string) {
const policy = { ...basePolicy };
delete policy.discountFactor;
delete policy.factor;
const discount = optionalPositiveNumber(discountText, label);
if (discount && discount !== 1) {
policy.discountFactor = discount;
}
return Object.keys(policy).length ? policy : undefined;
}
function optionalPositiveNumber(value: string, label: string) {
const text = value.trim();
if (!text) return undefined;
const parsed = Number(text);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${label} 必须是大于 0 的数字`);
}
return parsed;
}
function numberFromUnknown(value: unknown) {
if (typeof value === 'number' && Number.isFinite(value) && value > 0) return value;
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
}
return undefined;
}
function trimNumber(value: number) {
return value.toFixed(6).replace(/\.?0+$/, '');
}
function policyKeys(value?: Record<string, unknown>) {
if (!value) return [];
return Object.keys(value).slice(0, 3);