perf(usage): user-me usage 查询避免全量加载,默认限制 7 天范围

- UsageAuditListQuery 新增 limit 字段,支持在 SQL 层 LIMIT
- user-me usage 端点无时间范围时默认查最近 7 天
- active usage 端点无 ids 过滤时限制查最近 1 小时
- InMemory 实现同步支持 limit truncate
This commit is contained in:
fawney19
2026-04-15 16:25:57 +08:00
parent fbb8249c0d
commit 43e7ad112f
8 changed files with 37 additions and 2 deletions

View File

@@ -26,6 +26,7 @@ pub(crate) async fn list_usage_for_range(
provider_name: filters.provider_name.clone(), provider_name: filters.provider_name.clone(),
model: filters.model.clone(), model: filters.model.clone(),
statuses: None, statuses: None,
limit: None,
}) })
.await .await
} }
@@ -46,6 +47,7 @@ pub(crate) async fn list_usage_for_optional_range(
provider_name: filters.provider_name.clone(), provider_name: filters.provider_name.clone(),
model: filters.model.clone(), model: filters.model.clone(),
statuses: None, statuses: None,
limit: None,
}) })
.await .await
} }

View File

@@ -17,6 +17,7 @@ pub(in super::super) async fn list_recent_completed_usage_for_cache_affinity(
provider_name: None, provider_name: None,
model: None, model: None,
statuses: None, statuses: None,
limit: None,
}) })
.await?; .await?;
items.retain(|item| item.status == "completed"); items.retain(|item| item.status == "completed");

View File

@@ -486,6 +486,7 @@ async fn dashboard_list_usage_for_range(
provider_name: None, provider_name: None,
model: None, model: None,
statuses: None, statuses: None,
limit: None,
}) })
.await .await
{ {
@@ -1215,6 +1216,7 @@ pub(super) async fn handle_dashboard_provider_status_get(
provider_name: None, provider_name: None,
model: None, model: None,
statuses: None, statuses: None,
limit: None,
}) })
.await .await
{ {

View File

@@ -690,9 +690,22 @@ pub(super) async fn handle_users_me_usage_get(
Err(detail) => return admin_stats_bad_request_response(detail), Err(detail) => return admin_stats_bad_request_response(detail),
}; };
// When no time range is specified, default to 7 days to avoid full-table scans.
let effective_time_range = time_range.or_else(|| {
let today = Utc::now().date_naive();
let start_date = today
.checked_sub_signed(chrono::Duration::days(6))
.unwrap_or(today);
Some(AdminStatsTimeRange {
start_date,
end_date: today,
tz_offset_minutes: 0,
})
});
let usage = match list_usage_for_optional_range( let usage = match list_usage_for_optional_range(
&AdminAppState::new(state), &AdminAppState::new(state),
time_range.as_ref(), effective_time_range.as_ref(),
&AdminStatsUsageFilter { &AdminStatsUsageFilter {
user_id: Some(auth.user.id.clone()), user_id: Some(auth.user.id.clone()),
provider_name: None, provider_name: None,
@@ -872,14 +885,22 @@ pub(super) async fn handle_users_me_usage_active_get(
Err(response) => return response, Err(response) => return response,
}; };
let ids = parse_users_me_usage_ids(request_context.request_query_string.as_deref()); let ids = parse_users_me_usage_ids(request_context.request_query_string.as_deref());
// When polling for active (pending/streaming) requests without specific ids,
// limit to the last 1 hour to avoid scanning all historical records.
let created_from = if ids.is_none() {
Some(Utc::now().timestamp().saturating_sub(3600) as u64)
} else {
None
};
let items = match state let items = match state
.list_usage_audits(&UsageAuditListQuery { .list_usage_audits(&UsageAuditListQuery {
created_from_unix_secs: None, created_from_unix_secs: created_from,
created_until_unix_secs: None, created_until_unix_secs: None,
user_id: Some(auth.user.id.clone()), user_id: Some(auth.user.id.clone()),
provider_name: None, provider_name: None,
model: None, model: None,
statuses: None, statuses: None,
limit: None,
}) })
.await .await
{ {
@@ -952,6 +973,7 @@ pub(super) async fn handle_users_me_usage_interval_timeline_get(
provider_name: None, provider_name: None,
model: None, model: None,
statuses: None, statuses: None,
limit: None,
}) })
.await .await
{ {

View File

@@ -218,6 +218,7 @@ pub(super) async fn handle_wallet_today_cost(
provider_name: None, provider_name: None,
model: None, model: None,
statuses: None, statuses: None,
limit: None,
}) })
.await .await
{ {

View File

@@ -481,6 +481,7 @@ pub struct UsageAuditListQuery {
pub provider_name: Option<String>, pub provider_name: Option<String>,
pub model: Option<String>, pub model: Option<String>,
pub statuses: Option<Vec<String>>, pub statuses: Option<Vec<String>>,
pub limit: Option<usize>,
} }
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]

View File

@@ -214,6 +214,9 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
.cmp(&right.created_at_unix_ms) .cmp(&right.created_at_unix_ms)
.then_with(|| left.request_id.cmp(&right.request_id)) .then_with(|| left.request_id.cmp(&right.request_id))
}); });
if let Some(limit) = query.limit {
items.truncate(limit);
}
Ok(items) Ok(items)
} }

View File

@@ -1213,6 +1213,9 @@ impl SqlxUsageReadRepository {
} }
builder.push(" ORDER BY \"usage\".created_at ASC, \"usage\".request_id ASC"); builder.push(" ORDER BY \"usage\".created_at ASC, \"usage\".request_id ASC");
if let Some(limit) = query.limit {
builder.push(" LIMIT ").push_bind(limit as i64);
}
let query = builder.build(); let query = builder.build();
let mut rows = query.fetch(&self.pool); let mut rows = query.fetch(&self.pool);
let mut items = Vec::new(); let mut items = Vec::new();