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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user