perf: 添加查询缓存层,避免重复解析 JSON(TTL 2 分钟)
This commit is contained in:
@@ -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 || "",
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user