feat(admin): 计费页加「余额硬拦截」开关 + 各租户「充值」按钮

闭合积分环的配置/操作端,接到 P4 后端:
- 计费规则区:加「余额硬拦截」开关(credit_enforce),与汇率合成一个「保存规则」。
- 用量观测·租户排行:每行加「充值」按钮(正=充值、负=校正),调 /admin/credits/grant,
  充值后即时刷新余额。
- api:getBillingConfig/saveBillingConfig 带 credit_enforce;新增 grantCredits。

live 验证(preview):开关渲染+可存;点公司A「充值 10」→余额 5.92→15.92、账本落对租户;
tsc + 41 vitest 全过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-07 13:53:52 +08:00
parent 93741a5504
commit b46382817c
3 changed files with 89 additions and 30 deletions
+20 -6
View File
@@ -143,17 +143,21 @@ export async function savePricing(p: Pricing): Promise<void> {
}
}
// —— 全局计费规则(token→积分汇率)——
export async function getBillingConfig(): Promise<{ tokens_per_credit: number }> {
// —— 全局计费规则(token→积分汇率 + 硬拦截开关)——
export async function getBillingConfig(): Promise<{ tokens_per_credit: number; credit_enforce: boolean }> {
const res = guard(await fetch(`${ADMIN}/billing-config`, { headers: authHeaders() }));
if (!res.ok) throw new Error(`billing config failed: ${res.status}`);
const d = (await res.json()) as { tokens_per_credit?: string };
return { tokens_per_credit: Number(d.tokens_per_credit) || 1000 }; // 空/未设 → 回退默认 1000
const d = (await res.json()) as { tokens_per_credit?: string; credit_enforce?: boolean };
return { tokens_per_credit: Number(d.tokens_per_credit) || 1000, credit_enforce: !!d.credit_enforce }; // 空/未设 → 回退默认
}
export async function saveBillingConfig(tokensPerCredit: number): Promise<void> {
export async function saveBillingConfig(tokensPerCredit: number, creditEnforce: boolean): Promise<void> {
const res = guard(
await fetch(`${ADMIN}/billing-config`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ tokens_per_credit: tokensPerCredit }) }),
await fetch(`${ADMIN}/billing-config`, {
method: "PUT",
headers: authHeaders(true),
body: JSON.stringify({ tokens_per_credit: tokensPerCredit, credit_enforce: creditEnforce }),
}),
);
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
@@ -161,6 +165,16 @@ export async function saveBillingConfig(tokensPerCredit: number): Promise<void>
}
}
// —— 充值/发放积分(正=充值,负=校正)——
export async function grantCredits(tenantId: string, credits: number, memo: string): Promise<number> {
const res = guard(
await fetch(`${ADMIN}/credits/grant`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ tenant_id: tenantId, credits, memo }) }),
);
const d = (await res.json().catch(() => ({}))) as { balance_micro?: number; error?: string };
if (!res.ok) throw new Error(d.error ?? `grant failed: ${res.status}`);
return d.balance_micro ?? 0;
}
// gatewayOnline 用公开的 /healthz 探活(不受鉴权影响)。
export async function gatewayOnline(): Promise<boolean> {
try {