feat(admin): 计费页长出「充值渠道」块 —— 兑换码生成/台账 + 积分包定价
P5.1 收尾:此前生成码只有 API。计费页现在从上到下 = 计费规则(积分→token 汇率) → 充值渠道(钱→积分:兑换码 + 微信定价用的积分包) → 用量观测, 两层汇率在同一页可见、各管各的。 - 兑换码:面额/张数/备注生成;**明文码只在生成响应显示一次**(等同现金, 台账 GET /admin/redeem-codes 服务端脱敏只露首尾,丢码重生成、不提供找回 ——顺手把接口这个第二明文出口堵了);台账含核销状态。 - 积分包:新增/上下架(微信 P5.2 上线前把定价面备好);admin api.ts 补 packs/redeem-codes 四个函数。 - launch.json 加 admin-console-alt(:5176)——5174 被用户自己的 sundynix-site 占着,不动别人端口。 live:5176 登录→生成 5 张(绿色一次性面板+复制全部)→配「入门包 1000分/¥9.9」 在售可下架→台账脱敏 curl 复核;tsc+41 vitest 全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,57 @@ export async function grantCredits(tenantId: string, credits: number, memo: stri
|
||||
return d.balance_micro ?? 0;
|
||||
}
|
||||
|
||||
// ---- 充值渠道(P5.1):积分包配置 + 兑换码 ----
|
||||
export interface CreditPack {
|
||||
id: string;
|
||||
name: string;
|
||||
credits_micro: number;
|
||||
price_fen: number;
|
||||
active: boolean;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
export async function adminPacks(): Promise<CreditPack[]> {
|
||||
const res = guard(await fetch(`${ADMIN}/packs`, { headers: authHeaders() }));
|
||||
const d = (await res.json().catch(() => ({}))) as { packs?: CreditPack[]; error?: string };
|
||||
if (!res.ok) throw new Error(d.error ?? `packs failed: ${res.status}`);
|
||||
return d.packs ?? [];
|
||||
}
|
||||
|
||||
// savePack:id 空 = 新建。credits 单位为「积分」(面向人,服务端转 micro)。
|
||||
export async function savePack(p: { id?: string; name: string; credits: number; price_fen: number; active: boolean; sort: number }): Promise<void> {
|
||||
const res = guard(await fetch(`${ADMIN}/packs`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(p) }));
|
||||
const d = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
if (!res.ok) throw new Error(d.error ?? `save failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export interface RedeemCodeRow {
|
||||
id: string;
|
||||
code: string;
|
||||
credits_micro: number;
|
||||
status: string; // unused / used
|
||||
used_tenant: string;
|
||||
used_at: string | null;
|
||||
memo: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export async function genRedeemCodes(credits: number, count: number, memo: string): Promise<string[]> {
|
||||
const res = guard(
|
||||
await fetch(`${ADMIN}/redeem-codes`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ credits, count, memo }) }),
|
||||
);
|
||||
const d = (await res.json().catch(() => ({}))) as { codes?: string[]; error?: string };
|
||||
if (!res.ok) throw new Error(d.error ?? `生成失败: ${res.status}`);
|
||||
return d.codes ?? [];
|
||||
}
|
||||
|
||||
export async function listRedeemCodes(): Promise<RedeemCodeRow[]> {
|
||||
const res = guard(await fetch(`${ADMIN}/redeem-codes`, { headers: authHeaders() }));
|
||||
const d = (await res.json().catch(() => ({}))) as { codes?: RedeemCodeRow[]; error?: string };
|
||||
if (!res.ok) throw new Error(d.error ?? `codes failed: ${res.status}`);
|
||||
return d.codes ?? [];
|
||||
}
|
||||
|
||||
// gatewayOnline 用公开的 /healthz 探活(不受鉴权影响)。
|
||||
export async function gatewayOnline(): Promise<boolean> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user