feat: 账本域 billing_domain 与成员支付可见性收紧
三表增加账本域标签,成员支付参数改为同域透明、跨域隔离、未标域默认藏;管理面登记/筛选/行内编辑与文档中心同步对齐。
This commit is contained in:
@@ -30,6 +30,7 @@ function toKeyItem(row: ApiKeyRow & { member_name?: string | null }): KeyItem {
|
||||
status: row.status as "active" | "disabled",
|
||||
member_id: row.member_id,
|
||||
member_name: row.member_name ?? null,
|
||||
billing_domain: row.billing_domain,
|
||||
registered_at: toUnixSec(row.registered_at)!,
|
||||
log_synced_at: toUnixSec(row.log_synced_at),
|
||||
usage_synced_at: toUnixSec(row.usage_synced_at),
|
||||
@@ -59,6 +60,7 @@ export function adminKeyRoutes() {
|
||||
query: z.object({
|
||||
member_id: z.string().optional(),
|
||||
status: z.enum(["active", "disabled"]).optional(),
|
||||
billing_domain: z.string().optional(),
|
||||
recent_failed: z.enum(["true", "false"]).optional(),
|
||||
}),
|
||||
},
|
||||
@@ -77,12 +79,15 @@ export function adminKeyRoutes() {
|
||||
const filter: {
|
||||
member_id?: number | "unassigned";
|
||||
status?: string;
|
||||
billing_domain?: string | "none";
|
||||
} = {};
|
||||
if (q.member_id === "unassigned") filter.member_id = "unassigned";
|
||||
else if (q.member_id) filter.member_id = Number(q.member_id);
|
||||
if (q.status === "active" || q.status === "disabled") {
|
||||
filter.status = q.status;
|
||||
}
|
||||
if (q.billing_domain === "none") filter.billing_domain = "none";
|
||||
else if (q.billing_domain) filter.billing_domain = q.billing_domain;
|
||||
let rows = await c.get("repos").keys.listAll(filter);
|
||||
if (q.recent_failed === "true") {
|
||||
const snap = await c.get("repos").engine.getSnapshot();
|
||||
@@ -144,6 +149,10 @@ export function adminKeyRoutes() {
|
||||
}
|
||||
member_id = n;
|
||||
}
|
||||
let billing_domain: string | null = null;
|
||||
if (typeof body?.billing_domain === "string" && body.billing_domain.trim()) {
|
||||
billing_domain = body.billing_domain.trim();
|
||||
}
|
||||
if (!sk_key || !sk_key.startsWith("sk-")) {
|
||||
return jsonError(
|
||||
c,
|
||||
@@ -208,6 +217,7 @@ export function adminKeyRoutes() {
|
||||
key_hash,
|
||||
key_ciphertext,
|
||||
member_id,
|
||||
billing_domain,
|
||||
token_name: usage.name ?? "",
|
||||
});
|
||||
// 首次登记即写入上游额度字段(表设计 §4:首轮日志同步异步补)
|
||||
@@ -241,7 +251,7 @@ export function adminKeyRoutes() {
|
||||
createRoute({
|
||||
method: "patch",
|
||||
path: "/keys/{id}",
|
||||
summary: "启停 / 归属变更",
|
||||
summary: "启停 / 归属 / 账本域变更",
|
||||
description: "部分更新;审计按实际变更字段各落一行",
|
||||
tags: ["Key 管理"],
|
||||
security: [{ adminSession: [] }],
|
||||
@@ -301,6 +311,19 @@ export function adminKeyRoutes() {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (body && "billing_domain" in body) {
|
||||
let nextDomain: string | null = null;
|
||||
if (typeof body.billing_domain === "string" && body.billing_domain.trim()) {
|
||||
nextDomain = body.billing_domain.trim();
|
||||
}
|
||||
if (nextDomain !== before.billing_domain) {
|
||||
patch.billing_domain = nextDomain;
|
||||
actions.push({
|
||||
action: "key.reassign",
|
||||
summary: `key #${before.id} · 账本域变更`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return jsonError(
|
||||
c,
|
||||
|
||||
@@ -37,6 +37,7 @@ function toSubscriptionItem(
|
||||
quota_usd,
|
||||
paid_amount: row.paid_amount,
|
||||
paid_at: toUnixSec(row.paid_at),
|
||||
billing_domain: row.billing_domain,
|
||||
remark: row.remark,
|
||||
created_at: toUnixSec(row.created_at)!,
|
||||
};
|
||||
@@ -52,6 +53,7 @@ function parseSubBody(body: z.infer<typeof createSubscriptionBodySchema>):
|
||||
quota_granted: number;
|
||||
paid_amount: number | null;
|
||||
paid_at: Date | null;
|
||||
billing_domain: string | null;
|
||||
remark: string;
|
||||
};
|
||||
} {
|
||||
@@ -74,6 +76,10 @@ function parseSubBody(body: z.infer<typeof createSubscriptionBodySchema>):
|
||||
quota_granted: body.quota_granted,
|
||||
paid_amount: body.paid_amount ?? null,
|
||||
paid_at: body.paid_at ? new Date(body.paid_at) : null,
|
||||
billing_domain:
|
||||
typeof body.billing_domain === "string" && body.billing_domain.trim()
|
||||
? body.billing_domain.trim()
|
||||
: null,
|
||||
remark: body.remark ?? "",
|
||||
},
|
||||
};
|
||||
@@ -90,6 +96,11 @@ export function adminSubscriptionRoutes() {
|
||||
summary: "订阅权益清单",
|
||||
tags: ["支付记录"],
|
||||
security: [{ adminSession: [] }],
|
||||
request: {
|
||||
query: z.object({
|
||||
billing_domain: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "subscription 数组",
|
||||
@@ -103,7 +114,14 @@ export function adminSubscriptionRoutes() {
|
||||
async (c) => {
|
||||
const repos = c.get("repos");
|
||||
const timeline = resolveQpuTimeline(await repos.qpu.listTimeline());
|
||||
const rows = await repos.subscriptions.listAll();
|
||||
const q = c.req.query();
|
||||
const filter =
|
||||
q.billing_domain === "none"
|
||||
? { billing_domain: "none" as const }
|
||||
: q.billing_domain
|
||||
? { billing_domain: q.billing_domain }
|
||||
: undefined;
|
||||
const rows = await repos.subscriptions.listAll(filter);
|
||||
return c.json(
|
||||
rows.map((row) =>
|
||||
toSubscriptionItem(
|
||||
|
||||
@@ -36,6 +36,7 @@ function toTopupItem(row: TopupRow, qpu: number): TopupItem {
|
||||
paid_at: toUnixSec(row.paid_at)!,
|
||||
quota_derived,
|
||||
quota_usd: quotaToUsdStr(quota_derived, qpu),
|
||||
billing_domain: row.billing_domain,
|
||||
remark: row.remark,
|
||||
created_at: toUnixSec(row.created_at)!,
|
||||
};
|
||||
@@ -53,6 +54,7 @@ function parseTopupBody(body: z.infer<typeof createTopupBodySchema>):
|
||||
payment_provider: string | null;
|
||||
status: string | null;
|
||||
paid_at: Date;
|
||||
billing_domain: string | null;
|
||||
remark: string;
|
||||
};
|
||||
} {
|
||||
@@ -71,6 +73,10 @@ function parseTopupBody(body: z.infer<typeof createTopupBodySchema>):
|
||||
payment_provider: body.payment_provider ?? null,
|
||||
status: body.status ?? "success",
|
||||
paid_at: new Date(body.paid_at),
|
||||
billing_domain:
|
||||
typeof body.billing_domain === "string" && body.billing_domain.trim()
|
||||
? body.billing_domain.trim()
|
||||
: null,
|
||||
remark: body.remark ?? "",
|
||||
},
|
||||
};
|
||||
@@ -87,6 +93,11 @@ export function adminTopupRoutes() {
|
||||
summary: "充值入账清单",
|
||||
tags: ["支付记录"],
|
||||
security: [{ adminSession: [] }],
|
||||
request: {
|
||||
query: z.object({
|
||||
billing_domain: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "topup 数组",
|
||||
@@ -100,7 +111,14 @@ export function adminTopupRoutes() {
|
||||
async (c) => {
|
||||
const repos = c.get("repos");
|
||||
const timeline = resolveQpuTimeline(await repos.qpu.listTimeline());
|
||||
const rows = await repos.topups.listAll();
|
||||
const q = c.req.query();
|
||||
const filter =
|
||||
q.billing_domain === "none"
|
||||
? { billing_domain: "none" as const }
|
||||
: q.billing_domain
|
||||
? { billing_domain: q.billing_domain }
|
||||
: undefined;
|
||||
const rows = await repos.topups.listAll(filter);
|
||||
return c.json(
|
||||
rows.map((row) => toTopupItem(row, qpuAt(row.paid_at, timeline))),
|
||||
200,
|
||||
|
||||
@@ -71,7 +71,7 @@ export function memberViewRoutes() {
|
||||
path: "/overview",
|
||||
summary: "单页聚合",
|
||||
description:
|
||||
"名下 key 的 type=2 用量 + 来源分解 + 支付参数(完全公开透明)",
|
||||
"名下 key 的 type=2 用量 + 来源分解 + 支付参数(同账本域内可见)",
|
||||
tags: ["成员视图"],
|
||||
security: [{ memberToken: [] }],
|
||||
request: {
|
||||
@@ -159,8 +159,13 @@ export function memberViewRoutes() {
|
||||
usage: sumUsage(v.rows, timeline),
|
||||
}));
|
||||
|
||||
// 「该时段涉及」:订阅 = 日志出现过的 subscription_id ∪ 进行中;充值 = paid_at ∈ [from,to)
|
||||
// 「该时段涉及」:订阅 = 日志出现过的 subscription_id ∪ 我域内进行中;
|
||||
// 充值 = 我域内且 paid_at ∈ [from,to)。未标域默认藏(仅「涉及我日志」的订阅例外)。
|
||||
const { quotaToUsdStr, qpuAt } = await import("@new-api-key-usage/core");
|
||||
const myDomains = new Set<string>();
|
||||
for (const k of allKeys) {
|
||||
if (k.billing_domain) myDomains.add(k.billing_domain);
|
||||
}
|
||||
const subRows = await repos.subscriptions.listAll();
|
||||
const usedSubIds = new Set(
|
||||
rows.map((x) => x.subscription_id).filter((v): v is number => v != null),
|
||||
@@ -172,8 +177,11 @@ export function memberViewRoutes() {
|
||||
s.period_end > new Date() ||
|
||||
(s.period_start <= range.to &&
|
||||
(s.period_end ?? new Date()) >= range.from);
|
||||
const involved = usedSubIds.has(s.subscription_id) || ongoing;
|
||||
if (!involved) continue;
|
||||
const involved = usedSubIds.has(s.subscription_id);
|
||||
const inMyDomain =
|
||||
s.billing_domain != null && myDomains.has(s.billing_domain);
|
||||
// 涉及我日志的始终可见(事实直连);其余仅我域内进行中可见
|
||||
if (!involved && !(ongoing && inMyDomain)) continue;
|
||||
const qpu = qpuAt(s.paid_at ?? s.created_at, timeline);
|
||||
subscriptionsOut.push({
|
||||
id: s.id,
|
||||
@@ -184,6 +192,7 @@ export function memberViewRoutes() {
|
||||
quota_usd: quotaToUsdStr(s.quota_granted, qpu),
|
||||
paid_amount: s.paid_amount,
|
||||
paid_at: toUnixSec(s.paid_at),
|
||||
billing_domain: s.billing_domain,
|
||||
remark: s.remark,
|
||||
created_at: toUnixSec(s.created_at)!,
|
||||
});
|
||||
@@ -191,6 +200,7 @@ export function memberViewRoutes() {
|
||||
const topupRows = await repos.topups.listAll();
|
||||
const topupsOut = [];
|
||||
for (const t of topupRows) {
|
||||
if (t.billing_domain == null || !myDomains.has(t.billing_domain)) continue;
|
||||
if (!(t.paid_at >= range.from && t.paid_at < range.to)) continue;
|
||||
const qpu = qpuAt(t.paid_at, timeline);
|
||||
const quota_derived = Math.round(t.amount * qpu);
|
||||
@@ -204,6 +214,7 @@ export function memberViewRoutes() {
|
||||
payment_provider: t.payment_provider,
|
||||
status: t.status,
|
||||
paid_at: toUnixSec(t.paid_at)!,
|
||||
billing_domain: t.billing_domain,
|
||||
quota_derived,
|
||||
quota_usd: quotaToUsdStr(quota_derived, qpu),
|
||||
remark: t.remark,
|
||||
|
||||
Reference in New Issue
Block a user