feat(gateway/data): 新增 usage 关键词搜索、缓存命中摘要、结算成本摘要及 dashboard 聚合查询能力

- 新增 UsageAuditKeywordSearchQuery,支持多关键词、用户名、API Key 交叉过滤
- 新增 UsageCacheHitSummaryQuery/StoredUsageCacheHitSummary,统计请求缓存命中率
- 新增 UsageSettledCostSummaryQuery/StoredUsageSettledCostSummary,汇总结算成本
- 新增 UsageDashboardSummaryQuery、UsageBreakdownSummaryQuery 等 dashboard 聚合类型
- SQL 层实现对应查询方法,含 list_by_ids、list_usage_audits_by_keyword_search、count_usage_audits_by_keyword_search
- 将上述能力通过 GatewayDataState / AdminAppState 暴露给 handler 层
- user_me_usage 及 dashboard_filters 切换为新查询接口,移除旧的内存过滤逻辑
- 同步更新 memory 层及测试
This commit is contained in:
fawney19
2026-04-17 00:58:46 +08:00
parent 6e5af5ef70
commit 1e0bc61526
48 changed files with 5241 additions and 1162 deletions

View File

@@ -188,6 +188,34 @@ impl AuthApiKeyReadRepository for InMemoryAuthApiKeySnapshotRepository {
.collect())
}
async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
let name_search = name_search.trim().to_ascii_lowercase();
if name_search.is_empty() {
return Ok(Vec::new());
}
let index = self
.index
.read()
.expect("auth api key snapshot repository lock");
Ok(index
.export_by_api_key_id
.values()
.filter(|record| {
record
.name
.as_deref()
.unwrap_or_default()
.to_ascii_lowercase()
.contains(&name_search)
})
.cloned()
.collect())
}
async fn list_export_standalone_api_keys_page(
&self,
query: &StandaloneApiKeyExportListQuery,

View File

@@ -182,6 +182,30 @@ WHERE api_keys.id = ANY($1::TEXT[])
ORDER BY api_keys.id ASC
"#;
const LIST_EXPORT_BY_NAME_SEARCH_SQL: &str = r#"
SELECT
api_keys.user_id,
api_keys.id AS api_key_id,
api_keys.key_hash,
api_keys.key_encrypted,
api_keys.name,
api_keys.allowed_providers,
api_keys.allowed_api_formats,
api_keys.allowed_models,
api_keys.rate_limit,
api_keys.concurrent_limit,
api_keys.force_capabilities,
api_keys.is_active,
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
api_keys.auto_delete_on_expiry,
api_keys.total_requests,
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
api_keys.is_standalone
FROM api_keys
WHERE LOWER(COALESCE(api_keys.name, '')) LIKE $1
ORDER BY api_keys.id ASC
"#;
const LIST_EXPORT_STANDALONE_SQL: &str = r#"
SELECT
api_keys.user_id,
@@ -751,6 +775,24 @@ impl SqlxAuthApiKeySnapshotReadRepository {
.await
}
pub async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
let name_search = name_search.trim();
if name_search.is_empty() {
return Ok(Vec::new());
}
Self::collect_query_rows(
sqlx::query(LIST_EXPORT_BY_NAME_SEARCH_SQL)
.bind(format!("%{}%", name_search.to_ascii_lowercase()))
.fetch(&self.pool),
map_auth_api_key_export_row,
)
.await
}
pub async fn summarize_export_api_keys_by_user_ids(
&self,
user_ids: &[String],
@@ -886,6 +928,13 @@ impl AuthApiKeyReadRepository for SqlxAuthApiKeySnapshotReadRepository {
Self::list_export_api_keys_by_ids(self, api_key_ids).await
}
async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
Self::list_export_api_keys_by_name_search(self, name_search).await
}
async fn list_export_standalone_api_keys_page(
&self,
query: &StandaloneApiKeyExportListQuery,

View File

@@ -447,6 +447,11 @@ pub trait AuthApiKeyReadRepository: Send + Sync {
api_key_ids: &[String],
) -> Result<Vec<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
async fn list_export_standalone_api_keys_page(
&self,
query: &StandaloneApiKeyExportListQuery,