mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,23 @@ mod sql;
|
||||
pub(crate) use aether_data_contracts::repository::usage::{
|
||||
StoredProviderApiKeyUsageSummary, StoredProviderUsageSummary, StoredProviderUsageWindow,
|
||||
StoredRequestUsageAudit, StoredUsageAuditAggregation, StoredUsageAuditSummary,
|
||||
StoredUsageDailySummary, StoredUsageLeaderboardSummary, StoredUsageTimeSeriesBucket,
|
||||
UpsertUsageRecord, UsageAuditAggregationGroupBy, UsageAuditAggregationQuery,
|
||||
UsageAuditListQuery, UsageAuditSummaryQuery, UsageDailyHeatmapQuery, UsageLeaderboardGroupBy,
|
||||
UsageLeaderboardQuery, UsageReadRepository, UsageRepository, UsageTimeSeriesGranularity,
|
||||
UsageTimeSeriesQuery, UsageWriteRepository,
|
||||
StoredUsageBreakdownSummaryRow, StoredUsageCacheAffinityHitSummary,
|
||||
StoredUsageCacheAffinityIntervalRow, StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary,
|
||||
StoredUsageDailySummary, StoredUsageDashboardDailyBreakdownRow,
|
||||
StoredUsageDashboardProviderCount, StoredUsageDashboardSummary,
|
||||
StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary,
|
||||
StoredUsagePerformancePercentilesRow, StoredUsageSettledCostSummary,
|
||||
StoredUsageTimeSeriesBucket, UpsertUsageRecord, UsageAuditAggregationGroupBy,
|
||||
UsageAuditAggregationQuery, UsageAuditKeywordSearchQuery, UsageAuditListQuery,
|
||||
UsageAuditSummaryQuery, UsageBreakdownGroupBy, UsageBreakdownSummaryQuery,
|
||||
UsageCacheAffinityHitSummaryQuery, UsageCacheAffinityIntervalGroupBy,
|
||||
UsageCacheAffinityIntervalQuery, UsageCacheHitSummaryQuery, UsageCostSavingsSummaryQuery,
|
||||
UsageDailyHeatmapQuery, UsageDashboardDailyBreakdownQuery, UsageDashboardProviderCountsQuery,
|
||||
UsageDashboardSummaryQuery, UsageErrorDistributionQuery, UsageLeaderboardGroupBy,
|
||||
UsageLeaderboardQuery, UsageMonitoringErrorCountQuery, UsageMonitoringErrorListQuery,
|
||||
UsagePerformancePercentilesQuery, UsageReadRepository, UsageRepository,
|
||||
UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
|
||||
UsageWriteRepository,
|
||||
};
|
||||
pub use memory::InMemoryUsageReadRepository;
|
||||
pub use sql::SqlxUsageReadRepository;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,29 @@ impl UserReadRepository for InMemoryUserReadRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
|
||||
let username_search = username_search.trim().to_ascii_lowercase();
|
||||
if username_search.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.by_id
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.values()
|
||||
.filter(|user| {
|
||||
user.username
|
||||
.to_ascii_lowercase()
|
||||
.contains(&username_search)
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
|
||||
@@ -21,6 +21,20 @@ WHERE id = ANY($1::text[])
|
||||
ORDER BY id ASC
|
||||
"#;
|
||||
|
||||
const LIST_USERS_BY_USERNAME_SEARCH_SQL: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
role::text AS role,
|
||||
is_active,
|
||||
is_deleted
|
||||
FROM users
|
||||
WHERE is_deleted IS FALSE
|
||||
AND LOWER(username) LIKE $1
|
||||
ORDER BY id ASC
|
||||
"#;
|
||||
|
||||
const LIST_NON_ADMIN_EXPORT_USERS_SQL: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
@@ -199,6 +213,24 @@ impl SqlxUserReadRepository {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
|
||||
let username_search = username_search.trim();
|
||||
if username_search.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_USERS_BY_USERNAME_SEARCH_SQL)
|
||||
.bind(format!("%{}%", username_search.to_ascii_lowercase()))
|
||||
.fetch(&self.pool),
|
||||
map_user_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
@@ -383,6 +415,13 @@ impl UserReadRepository for SqlxUserReadRepository {
|
||||
self.list_users_by_ids(user_ids).await
|
||||
}
|
||||
|
||||
async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
|
||||
self.list_users_by_username_search(username_search).await
|
||||
}
|
||||
|
||||
async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
|
||||
@@ -393,6 +393,11 @@ pub trait UserReadRepository: Send + Sync {
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<StoredUserSummary>, crate::DataLayerError>;
|
||||
|
||||
async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, crate::DataLayerError>;
|
||||
|
||||
async fn list_export_users(&self) -> Result<Vec<StoredUserExportRow>, crate::DataLayerError>;
|
||||
|
||||
async fn list_export_users_page(
|
||||
|
||||
Reference in New Issue
Block a user