mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
fix(overview): 成本分析“提供商用量”展示提供商名称而非 ID
成本分析页「提供商用量」表格的“提供商”列渲染接口返回的 label,而后端在 UsageAnalyticsView::Breakdown + group_by=provider 时把 label 直接写成了 分组键 provider_id,页面因此显示 provider-1 这类内部编号。 - label 改为按「providers.name → 使用记录里的 provider_name 快照 → provider_id 兜底」解析;分组键 id 仍是 provider_id,“查看”链接、按 提供商匹配的已登记支出列、时间序列图表均不受影响。 - 空白与 unknown/unknow/pending 不当作名称;provider_id 为空时保持空标签, 前端沿用“未归属提供商”文案。 - 内存仓储(无数据库部署与测试)同步同一口径,避免两种实现表现不一致。 - 新增用例:内存侧 overview_provider_breakdown_labels_rows_with_provider_name, SQL 侧 provider_breakdown_labels_resolve_catalog_name_before_recorded_name_and_id。
This commit is contained in:
@@ -59,6 +59,27 @@ fn available(row: &StoredRequestUsageAudit, key: &str) -> bool {
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
!= Some(false)
|
||||
}
|
||||
/// 提供商明细分组的展示名:分组键依旧是 provider_id(保证与 PostgreSQL 实现一致),
|
||||
/// 只是把展示标签换成使用记录里的提供商名称快照,名称缺失或为历史占位值时回退到 provider_id。
|
||||
/// provider_id 为空说明无法归属,保持 None 让前端显示“未归属提供商”。
|
||||
fn provider_display_label(
|
||||
rows: &[&StoredRequestUsageAudit],
|
||||
group_id: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let id = group_id?;
|
||||
rows.iter()
|
||||
.map(|row| row.provider_name.trim())
|
||||
.filter(|name| {
|
||||
!name.is_empty()
|
||||
&& !matches!(
|
||||
name.to_ascii_lowercase().as_str(),
|
||||
"unknown" | "unknow" | "pending"
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.map(str::to_owned)
|
||||
.or_else(|| Some(id.to_owned()))
|
||||
}
|
||||
// The legacy audit contract stores epoch seconds despite its historical field name.
|
||||
fn usage_started_ms(row: &StoredRequestUsageAudit) -> u64 {
|
||||
row.created_at_unix_ms.saturating_mul(1000)
|
||||
@@ -656,10 +677,17 @@ impl InMemoryUsageReadRepository {
|
||||
};
|
||||
groups.entry(group).or_default().push(row);
|
||||
}
|
||||
// 提供商明细分组需要单独解析展示名,其余分组仍然用分组键本身作为标签。
|
||||
let provider_breakdown = query.view == UsageAnalyticsView::Breakdown
|
||||
&& query.group_by == UsageAnalyticsGroupBy::Provider;
|
||||
let mut grouped = groups
|
||||
.into_iter()
|
||||
.map(|(id, rows)| UsageAnalyticsRow {
|
||||
label: id.clone(),
|
||||
label: if provider_breakdown {
|
||||
provider_display_label(&rows, id.as_deref())
|
||||
} else {
|
||||
id.clone()
|
||||
},
|
||||
bucket_start: (query.view != UsageAnalyticsView::Breakdown)
|
||||
.then(|| id.clone())
|
||||
.flatten(),
|
||||
|
||||
@@ -160,6 +160,52 @@ async fn overview_model_performance_merges_provider_samples_without_pagination()
|
||||
assert_eq!(filtered.model_rows, vec![result.model_rows[0].clone()]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn overview_provider_breakdown_labels_rows_with_provider_name() {
|
||||
use aether_data_contracts::repository::usage::*;
|
||||
let at = chrono::DateTime::parse_from_rfc3339("2026-09-12T10:05:00Z").unwrap();
|
||||
// 同一 provider_id 的两条记录,展示标签应解析成提供商名称而不是 provider_id。
|
||||
let mut first = sample_usage("provider-label-1", at.timestamp());
|
||||
first.provider_id = Some("provider-1".into());
|
||||
first.provider_name = "Provider One".into();
|
||||
let mut second = sample_usage("provider-label-2", at.timestamp());
|
||||
second.provider_id = Some("provider-1".into());
|
||||
second.provider_name = "Provider One".into();
|
||||
// 名称为历史占位值时回退到 provider_id。
|
||||
let mut unnamed = sample_usage("provider-label-3", at.timestamp());
|
||||
unnamed.provider_id = Some("provider-2".into());
|
||||
unnamed.provider_name = "unknown".into();
|
||||
// provider_id 为空说明无法归属,标签保持为空,由前端显示“未归属提供商”。
|
||||
let mut unattributed = sample_usage("provider-label-4", at.timestamp());
|
||||
unattributed.provider_id = None;
|
||||
unattributed.provider_name = "legacy".into();
|
||||
let repo = InMemoryUsageReadRepository::seed([first, second, unnamed, unattributed]);
|
||||
let result = repo
|
||||
.query_usage_analytics(&UsageAnalyticsQuery {
|
||||
from_unix_ms: (at - chrono::Duration::minutes(5)).timestamp_millis() as u64,
|
||||
to_unix_ms: (at + chrono::Duration::minutes(55)).timestamp_millis() as u64,
|
||||
timezone: "UTC".into(),
|
||||
view: UsageAnalyticsView::Breakdown,
|
||||
group_by: UsageAnalyticsGroupBy::Provider,
|
||||
limit: 25,
|
||||
descending: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.total, 3);
|
||||
let rows = result
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| (row.id.as_deref(), row.label.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(rows.contains(&(Some("provider-1"), Some("Provider One"))));
|
||||
assert!(rows.contains(&(Some("provider-2"), Some("provider-2"))));
|
||||
assert!(rows.contains(&(None, None)));
|
||||
// 明细分组不填充 bucket_start。
|
||||
assert!(result.rows.iter().all(|row| row.bucket_start.is_none()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn overview_memory_chart_hour_buckets_are_utc_in_half_hour_zones() {
|
||||
use aether_data_contracts::repository::usage::*;
|
||||
|
||||
Reference in New Issue
Block a user