perf: 添加查询缓存层,避免重复解析 JSON(TTL 2 分钟)
This commit is contained in:
@@ -1,5 +1,22 @@
|
||||
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
|
||||
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<OverviewData> {
|
||||
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<TrendPoint[]> {
|
||||
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<Record<number, string>> {
|
||||
return Object.fromEntries(rows.map((r) => [r.id, r.name]));
|
||||
}
|
||||
|
||||
export async function getUserRanking(
|
||||
export function getUserRanking(
|
||||
startTs?: number,
|
||||
endTs?: number,
|
||||
limit = 50
|
||||
): Promise<RankingItem[]> {
|
||||
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<RankingItem[]> {
|
||||
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<RankingItem[]> {
|
||||
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<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 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 || "",
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user