diff --git a/lib/queries.ts b/lib/queries.ts index 4a8ad87..98adfdd 100644 --- a/lib/queries.ts +++ b/lib/queries.ts @@ -1,5 +1,22 @@ import { query } from "./db"; +// ── 查询缓存 ───────────────────────────────────────────────── +const queryCache = new Map(); +const CACHE_TTL = 120_000; // 2 分钟 + +function cached(key: string, fn: () => Promise): Promise { + 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 const REAL_MODEL = `COALESCE( CASE WHEN other IS NOT NULL AND other != '' AND other::jsonb ? 'upstream_model_name' @@ -61,10 +78,11 @@ export interface OverviewData { active_channels: number; } -export async function getOverview( +export function getOverview( startTs?: number, endTs?: number ): Promise { + return cached(cacheKey("overview", startTs, endTs), async () => { const params: (string | number | boolean | null)[] = []; const where = timeWhere(params, startTs, endTs); @@ -96,6 +114,7 @@ export async function getOverview( active_models: Number(r.active_models), active_channels: Number(r.active_channels), }; + }); } // ── 趋势 ────────────────────────────────────────────────────── @@ -111,11 +130,12 @@ export interface TrendPoint { quota: number; } -export async function getTrends( +export function getTrends( granularity: "day" | "week" | "month" = "day", startTs?: number, endTs?: number ): Promise { + return cached(cacheKey("trends", granularity, startTs, endTs), async () => { const params: (string | number | boolean | null)[] = []; 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), quota: Number(r.quota), })); -} - -// ── 排名 ────────────────────────────────────────────────────── + }); +}// ── 排名 ────────────────────────────────────────────────────── export interface RankingItem { rank: number; @@ -180,11 +199,12 @@ async function getChannelNames(): Promise> { return Object.fromEntries(rows.map((r) => [r.id, r.name])); } -export async function getUserRanking( +export function getUserRanking( startTs?: number, endTs?: number, limit = 50 ): Promise { + return cached(cacheKey("userRank", startTs, endTs, limit), async () => { const params: (string | number | boolean | null)[] = []; const where = timeWhere(params, startTs, endTs); params.push(limit); @@ -220,13 +240,15 @@ export async function getUserRanking( quota: Number(r.quota), quota_usd: Number(r.quota) / 500000, })); + }); } -export async function getModelRanking( +export function getModelRanking( startTs?: number, endTs?: number, limit = 50 ): Promise { + return cached(cacheKey("modelRank", startTs, endTs, limit), async () => { const params: (string | number | boolean | null)[] = []; const where = timeWhere(params, startTs, endTs); params.push(limit); @@ -258,13 +280,15 @@ export async function getModelRanking( quota: Number(r.quota), quota_usd: Number(r.quota) / 500000, })); + }); } -export async function getChannelRanking( +export function getChannelRanking( startTs?: number, endTs?: number, limit = 50 ): Promise { + return cached(cacheKey("channelRank", startTs, endTs, limit), async () => { const params: (string | number | boolean | null)[] = []; const where = timeWhere(params, startTs, endTs); params.push(limit); @@ -299,6 +323,7 @@ export async function getChannelRanking( quota: Number(r.quota), quota_usd: Number(r.quota) / 500000, })); + }); } // ── 下钻详情 ────────────────────────────────────────────────── @@ -310,11 +335,12 @@ export interface DetailBreakdown { quota: number; } -export async function getUserDetail( +export function getUserDetail( username: string, startTs?: number, endTs?: number ) { + return cached(cacheKey("userDetail", username, startTs, endTs), async () => { const params: (string | number | boolean | null)[] = []; const where = timeWhere(params, startTs, endTs); params.push(username); @@ -374,13 +400,15 @@ export async function getUserDetail( quota: Number(m.quota), })), }; + }); } -export async function getModelDetail( +export function getModelDetail( model: string, startTs?: number, endTs?: number ) { + return cached(cacheKey("modelDetail", model, startTs, endTs), async () => { const params: (string | number | boolean | null)[] = []; const where = timeWhere(params, startTs, endTs); params.push(model); @@ -430,13 +458,15 @@ export async function getModelDetail( quota: Number(u.quota), })), }; + }); } -export async function getChannelDetail( +export function getChannelDetail( channelId: number, startTs?: number, endTs?: number ) { + return cached(cacheKey("channelDetail", channelId, startTs, endTs), async () => { const params: (string | number | boolean | null)[] = []; const where = timeWhere(params, startTs, endTs); params.push(channelId); @@ -488,6 +518,7 @@ export async function getChannelDetail( quota: Number(m.quota), })), }; + }); } // ── 明细日志 ────────────────────────────────────────────────── @@ -530,6 +561,8 @@ export async function getLogs(options: { channelId?: number; tokenName?: string; }): Promise { + 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 params: (string | number | boolean | null)[] = []; let where = timeWhere(params, options.startTs, options.endTs); @@ -603,4 +636,5 @@ export async function getLogs(options: { token_name: r.token_name || "", })), }; + }); }