perf: 添加查询缓存层,避免重复解析 JSON(TTL 2 分钟)

This commit is contained in:
2026-04-22 09:59:02 +08:00
parent 52de6c5dc0
commit 67e43b02bf

View File

@@ -1,5 +1,22 @@
import { query } from "./db"; import { query } from "./db";
// ── 查询缓存 ─────────────────────────────────────────────────
const queryCache = new Map<string, { data: unknown; ts: number }>();
const CACHE_TTL = 120_000; // 2 分钟
function cached<T>(key: string, fn: () => Promise<T>): Promise<T> {
const hit = queryCache.get(key);
if (hit && Date.now() - hit.ts < CACHE_TTL) return Promise.resolve(hit.data as T);
return fn().then((data) => {
queryCache.set(key, { data, ts: Date.now() });
return data;
});
}
function cacheKey(...parts: unknown[]): string {
return parts.map((p) => String(p ?? "")).join(":");
}
// 真实模型名表达式:优先取 other 里的 upstream_model_name // 真实模型名表达式:优先取 other 里的 upstream_model_name
const REAL_MODEL = `COALESCE( const REAL_MODEL = `COALESCE(
CASE WHEN other IS NOT NULL AND other != '' AND other::jsonb ? 'upstream_model_name' CASE WHEN other IS NOT NULL AND other != '' AND other::jsonb ? 'upstream_model_name'
@@ -61,10 +78,11 @@ export interface OverviewData {
active_channels: number; active_channels: number;
} }
export async function getOverview( export function getOverview(
startTs?: number, startTs?: number,
endTs?: number endTs?: number
): Promise<OverviewData> { ): Promise<OverviewData> {
return cached(cacheKey("overview", startTs, endTs), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
@@ -96,6 +114,7 @@ export async function getOverview(
active_models: Number(r.active_models), active_models: Number(r.active_models),
active_channels: Number(r.active_channels), active_channels: Number(r.active_channels),
}; };
});
} }
// ── 趋势 ────────────────────────────────────────────────────── // ── 趋势 ──────────────────────────────────────────────────────
@@ -111,11 +130,12 @@ export interface TrendPoint {
quota: number; quota: number;
} }
export async function getTrends( export function getTrends(
granularity: "day" | "week" | "month" = "day", granularity: "day" | "week" | "month" = "day",
startTs?: number, startTs?: number,
endTs?: number endTs?: number
): Promise<TrendPoint[]> { ): Promise<TrendPoint[]> {
return cached(cacheKey("trends", granularity, startTs, endTs), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
@@ -148,9 +168,8 @@ export async function getTrends(
total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens) + Number(r.cache_creation_tokens) + Number(r.cache_read_tokens), total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens) + Number(r.cache_creation_tokens) + Number(r.cache_read_tokens),
quota: Number(r.quota), quota: Number(r.quota),
})); }));
} });
}// ── 排名 ──────────────────────────────────────────────────────
// ── 排名 ──────────────────────────────────────────────────────
export interface RankingItem { export interface RankingItem {
rank: number; rank: number;
@@ -180,11 +199,12 @@ async function getChannelNames(): Promise<Record<number, string>> {
return Object.fromEntries(rows.map((r) => [r.id, r.name])); return Object.fromEntries(rows.map((r) => [r.id, r.name]));
} }
export async function getUserRanking( export function getUserRanking(
startTs?: number, startTs?: number,
endTs?: number, endTs?: number,
limit = 50 limit = 50
): Promise<RankingItem[]> { ): Promise<RankingItem[]> {
return cached(cacheKey("userRank", startTs, endTs, limit), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
params.push(limit); params.push(limit);
@@ -220,13 +240,15 @@ export async function getUserRanking(
quota: Number(r.quota), quota: Number(r.quota),
quota_usd: Number(r.quota) / 500000, quota_usd: Number(r.quota) / 500000,
})); }));
});
} }
export async function getModelRanking( export function getModelRanking(
startTs?: number, startTs?: number,
endTs?: number, endTs?: number,
limit = 50 limit = 50
): Promise<RankingItem[]> { ): Promise<RankingItem[]> {
return cached(cacheKey("modelRank", startTs, endTs, limit), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
params.push(limit); params.push(limit);
@@ -258,13 +280,15 @@ export async function getModelRanking(
quota: Number(r.quota), quota: Number(r.quota),
quota_usd: Number(r.quota) / 500000, quota_usd: Number(r.quota) / 500000,
})); }));
});
} }
export async function getChannelRanking( export function getChannelRanking(
startTs?: number, startTs?: number,
endTs?: number, endTs?: number,
limit = 50 limit = 50
): Promise<RankingItem[]> { ): Promise<RankingItem[]> {
return cached(cacheKey("channelRank", startTs, endTs, limit), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
params.push(limit); params.push(limit);
@@ -299,6 +323,7 @@ export async function getChannelRanking(
quota: Number(r.quota), quota: Number(r.quota),
quota_usd: Number(r.quota) / 500000, quota_usd: Number(r.quota) / 500000,
})); }));
});
} }
// ── 下钻详情 ────────────────────────────────────────────────── // ── 下钻详情 ──────────────────────────────────────────────────
@@ -310,11 +335,12 @@ export interface DetailBreakdown {
quota: number; quota: number;
} }
export async function getUserDetail( export function getUserDetail(
username: string, username: string,
startTs?: number, startTs?: number,
endTs?: number endTs?: number
) { ) {
return cached(cacheKey("userDetail", username, startTs, endTs), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
params.push(username); params.push(username);
@@ -374,13 +400,15 @@ export async function getUserDetail(
quota: Number(m.quota), quota: Number(m.quota),
})), })),
}; };
});
} }
export async function getModelDetail( export function getModelDetail(
model: string, model: string,
startTs?: number, startTs?: number,
endTs?: number endTs?: number
) { ) {
return cached(cacheKey("modelDetail", model, startTs, endTs), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
params.push(model); params.push(model);
@@ -430,13 +458,15 @@ export async function getModelDetail(
quota: Number(u.quota), quota: Number(u.quota),
})), })),
}; };
});
} }
export async function getChannelDetail( export function getChannelDetail(
channelId: number, channelId: number,
startTs?: number, startTs?: number,
endTs?: number endTs?: number
) { ) {
return cached(cacheKey("channelDetail", channelId, startTs, endTs), async () => {
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
const where = timeWhere(params, startTs, endTs); const where = timeWhere(params, startTs, endTs);
params.push(channelId); params.push(channelId);
@@ -488,6 +518,7 @@ export async function getChannelDetail(
quota: Number(m.quota), quota: Number(m.quota),
})), })),
}; };
});
} }
// ── 明细日志 ────────────────────────────────────────────────── // ── 明细日志 ──────────────────────────────────────────────────
@@ -530,6 +561,8 @@ export async function getLogs(options: {
channelId?: number; channelId?: number;
tokenName?: string; tokenName?: string;
}): Promise<LogsResult> { }): Promise<LogsResult> {
const ck = cacheKey("logs", options.page, options.pageSize, options.startTs, options.endTs, options.username, options.model, options.channelId, options.tokenName);
return cached(ck, async () => {
const { page = 1, pageSize = 100 } = options; const { page = 1, pageSize = 100 } = options;
const params: (string | number | boolean | null)[] = []; const params: (string | number | boolean | null)[] = [];
let where = timeWhere(params, options.startTs, options.endTs); let where = timeWhere(params, options.startTs, options.endTs);
@@ -603,4 +636,5 @@ export async function getLogs(options: {
token_name: r.token_name || "", token_name: r.token_name || "",
})), })),
}; };
});
} }