fix(data): 同步模型调用次数至 global_models 读模型

- 模型列表改为读取 global_models.usage_count,避免每次扫描 usage 明细表
- 通过历史 backfill 与 usage upsert delta 维护该读模型
- 详情页保留实时 facts 兜底,统一排除 pending/streaming 状态
This commit is contained in:
mayrain
2026-05-05 17:34:02 +08:00
parent 53fa33b0c5
commit f0197b685f
6 changed files with 347 additions and 11 deletions
@@ -405,7 +405,7 @@ SELECT
gm.config,
COALESCE(gm_stats.provider_count, 0) AS provider_count,
COALESCE(gm_stats.active_provider_count, 0) AS active_provider_count,
COALESCE(gm.usage_count, 0)::bigint AS usage_count,
COALESCE(usage_stats.usage_count, gm.usage_count, 0)::bigint AS usage_count,
EXTRACT(EPOCH FROM gm.created_at)::bigint AS created_at_unix_ms,
EXTRACT(EPOCH FROM gm.updated_at)::bigint AS updated_at_unix_secs
FROM global_models gm
@@ -423,6 +423,12 @@ LEFT JOIN (
JOIN providers p ON p.id = m.provider_id
GROUP BY m.global_model_id
) gm_stats ON gm_stats.global_model_id = gm.id
LEFT JOIN LATERAL (
SELECT NULLIF(COUNT(*), 0)::bigint AS usage_count
FROM usage_billing_facts AS usage
WHERE usage.model = gm.name
AND usage.status NOT IN ('pending', 'streaming')
) usage_stats ON TRUE
WHERE gm.id = $1
LIMIT 1
"#,
@@ -452,7 +458,7 @@ SELECT
gm.config,
COALESCE(gm_stats.provider_count, 0) AS provider_count,
COALESCE(gm_stats.active_provider_count, 0) AS active_provider_count,
COALESCE(gm.usage_count, 0)::bigint AS usage_count,
COALESCE(usage_stats.usage_count, gm.usage_count, 0)::bigint AS usage_count,
EXTRACT(EPOCH FROM gm.created_at)::bigint AS created_at_unix_ms,
EXTRACT(EPOCH FROM gm.updated_at)::bigint AS updated_at_unix_secs
FROM global_models gm
@@ -470,6 +476,12 @@ LEFT JOIN (
JOIN providers p ON p.id = m.provider_id
GROUP BY m.global_model_id
) gm_stats ON gm_stats.global_model_id = gm.id
LEFT JOIN LATERAL (
SELECT NULLIF(COUNT(*), 0)::bigint AS usage_count
FROM usage_billing_facts AS usage
WHERE usage.model = gm.name
AND usage.status NOT IN ('pending', 'streaming')
) usage_stats ON TRUE
WHERE gm.name = $1
LIMIT 1
"#,
@@ -1205,7 +1217,10 @@ fn map_provider_active_global_model_row(
#[cfg(test)]
mod tests {
use super::{SqlxGlobalModelReadRepository, LIST_ADMIN_PROVIDER_MODELS_PREFIX};
use super::{
SqlxGlobalModelReadRepository, LIST_ADMIN_GLOBAL_MODELS_PREFIX,
LIST_ADMIN_PROVIDER_MODELS_PREFIX,
};
use crate::postgres::{PostgresPoolConfig, PostgresPoolFactory};
const ADMIN_PROVIDER_MODEL_REQUIRED_COLUMNS: &[&str] = &[
@@ -1241,6 +1256,64 @@ mod tests {
);
}
#[test]
fn admin_global_model_sql_projects_usage_count_from_billing_facts() {
let source = include_str!("sql.rs");
assert!(
!LIST_ADMIN_GLOBAL_MODELS_PREFIX.contains("usage_billing_facts"),
"admin global model list should read the maintained usage_count field"
);
assert!(
source.contains("WHERE usage.model = gm.name"),
"admin global model detail lookups should count usage facts for the selected model"
);
assert!(
source.contains("AND usage.status NOT IN ('pending', 'streaming')"),
"admin global model detail usage_count should use the maintained read-model status scope"
);
assert!(
source.contains("COALESCE(usage_stats.usage_count, gm.usage_count, 0)::bigint AS usage_count"),
"admin global model usage_count should prefer actual usage facts with stored-count fallback"
);
}
#[test]
fn admin_global_model_list_sql_counts_usage_without_full_fact_aggregate() {
assert!(
LIST_ADMIN_GLOBAL_MODELS_PREFIX.contains("COALESCE(gm.usage_count, 0)::bigint AS usage_count"),
"admin global model list should read maintained usage_count without per-request fact scans"
);
assert!(
!LIST_ADMIN_GLOBAL_MODELS_PREFIX.contains("GROUP BY usage.model"),
"admin global model list must not aggregate the full usage fact table before pagination"
);
}
#[test]
fn global_model_usage_count_read_model_has_backfill_and_delta_maintenance() {
let backfill_sql =
include_str!("../../../backfills/20260505120000_rebuild_global_model_usage_count.sql");
let delta_sql = include_str!("../usage/sql/queries/apply_global_model_usage_delta_sql.sql");
assert!(
backfill_sql.contains("UPDATE global_models AS gm"),
"global model usage_count backfill should refresh the read model"
);
assert!(
backfill_sql.contains("FROM usage_billing_facts AS usage"),
"global model usage_count backfill should rebuild from canonical usage facts"
);
assert!(
delta_sql.contains("UPDATE global_models"),
"usage writes should maintain the global model usage_count read model"
);
assert!(
delta_sql.contains("WHERE name = $1"),
"global model usage_count delta should target models by canonical model name"
);
}
#[tokio::test]
async fn repository_constructs_from_lazy_pool() {
let factory = PostgresPoolFactory::new(PostgresPoolConfig {