From fa3ea5ee4dec28294c40b162638c1dafb2d2e520 Mon Sep 17 00:00:00 2001 From: JIA-ss <627723154@qq.com> Date: Thu, 5 Feb 2026 11:41:25 +0800 Subject: [PATCH] feat(gateway): filter /v1/usage stats by API Key instead of UserID Previously the /v1/usage endpoint aggregated usage stats (today/total tokens, cost, RPM/TPM) across all API Keys belonging to the user. This made it impossible to distinguish usage from different API Keys (e.g. balance vs subscription keys). Now the usage stats are filtered by the current request's API Key ID, so each key only sees its own usage data. The balance/remaining fields are unaffected and still reflect the user-level wallet balance. Changes: - Add GetAPIKeyDashboardStats to repository interface and implementation - Add getPerformanceStatsByAPIKey helper (also fixes TPM to include cache_creation_tokens and cache_read_tokens) - Add GetAPIKeyDashboardStats to UsageService - Update Usage handler to call GetAPIKeyDashboardStats(apiKey.ID) Co-Authored-By: Claude Opus 4.5 --- backend/internal/handler/gateway_handler.go | 4 +- backend/internal/repository/usage_log_repo.go | 101 ++++++++++++++++++ backend/internal/server/api_contract_test.go | 4 + .../internal/service/account_usage_service.go | 1 + backend/internal/service/usage_service.go | 9 ++ 5 files changed, 117 insertions(+), 2 deletions(-) diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index 7cbf30eb..9aa6b72c 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -616,10 +616,10 @@ func (h *GatewayHandler) Usage(c *gin.Context) { return } - // Best-effort: 获取用量统计,失败不影响基础响应 + // Best-effort: 获取用量统计(按当前 API Key 过滤),失败不影响基础响应 var usageData gin.H if h.usageService != nil { - dashStats, err := h.usageService.GetUserDashboardStats(c.Request.Context(), subject.UserID) + dashStats, err := h.usageService.GetAPIKeyDashboardStats(c.Request.Context(), apiKey.ID) if err == nil && dashStats != nil { usageData = gin.H{ "today": gin.H{ diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index dc8f1460..2db1764f 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -1125,6 +1125,107 @@ func (r *usageLogRepository) GetUserDashboardStats(ctx context.Context, userID i return stats, nil } +// getPerformanceStatsByAPIKey 获取指定 API Key 的 RPM 和 TPM(近5分钟平均值) +func (r *usageLogRepository) getPerformanceStatsByAPIKey(ctx context.Context, apiKeyID int64) (rpm, tpm int64, err error) { + fiveMinutesAgo := time.Now().Add(-5 * time.Minute) + query := ` + SELECT + COUNT(*) as request_count, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as token_count + FROM usage_logs + WHERE created_at >= $1 AND api_key_id = $2` + args := []any{fiveMinutesAgo, apiKeyID} + + var requestCount int64 + var tokenCount int64 + if err := scanSingleRow(ctx, r.sql, query, args, &requestCount, &tokenCount); err != nil { + return 0, 0, err + } + return requestCount / 5, tokenCount / 5, nil +} + +// GetAPIKeyDashboardStats 获取指定 API Key 的仪表盘统计(按 api_key_id 过滤) +func (r *usageLogRepository) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*UserDashboardStats, error) { + stats := &UserDashboardStats{} + today := timezone.Today() + + // API Key 维度不需要统计 key 数量,设为 1 + stats.TotalAPIKeys = 1 + stats.ActiveAPIKeys = 1 + + // 累计 Token 统计 + totalStatsQuery := ` + SELECT + COUNT(*) as total_requests, + COALESCE(SUM(input_tokens), 0) as total_input_tokens, + COALESCE(SUM(output_tokens), 0) as total_output_tokens, + COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens, + COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens, + COALESCE(SUM(total_cost), 0) as total_cost, + COALESCE(SUM(actual_cost), 0) as total_actual_cost, + COALESCE(AVG(duration_ms), 0) as avg_duration_ms + FROM usage_logs + WHERE api_key_id = $1 + ` + if err := scanSingleRow( + ctx, + r.sql, + totalStatsQuery, + []any{apiKeyID}, + &stats.TotalRequests, + &stats.TotalInputTokens, + &stats.TotalOutputTokens, + &stats.TotalCacheCreationTokens, + &stats.TotalCacheReadTokens, + &stats.TotalCost, + &stats.TotalActualCost, + &stats.AverageDurationMs, + ); err != nil { + return nil, err + } + stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens + + // 今日 Token 统计 + todayStatsQuery := ` + SELECT + COUNT(*) as today_requests, + COALESCE(SUM(input_tokens), 0) as today_input_tokens, + COALESCE(SUM(output_tokens), 0) as today_output_tokens, + COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens, + COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens, + COALESCE(SUM(total_cost), 0) as today_cost, + COALESCE(SUM(actual_cost), 0) as today_actual_cost + FROM usage_logs + WHERE api_key_id = $1 AND created_at >= $2 + ` + if err := scanSingleRow( + ctx, + r.sql, + todayStatsQuery, + []any{apiKeyID, today}, + &stats.TodayRequests, + &stats.TodayInputTokens, + &stats.TodayOutputTokens, + &stats.TodayCacheCreationTokens, + &stats.TodayCacheReadTokens, + &stats.TodayCost, + &stats.TodayActualCost, + ); err != nil { + return nil, err + } + stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens + + // 性能指标:RPM 和 TPM(最近5分钟,按 API Key 过滤) + rpm, tpm, err := r.getPerformanceStatsByAPIKey(ctx, apiKeyID) + if err != nil { + return nil, err + } + stats.Rpm = rpm + stats.Tpm = tpm + + return stats, nil +} + // GetUserUsageTrendByUserID 获取指定用户的使用趋势 func (r *usageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) (results []TrendDataPoint, err error) { dateFormat := "YYYY-MM-DD" diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 44264e72..e197b776 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -1610,6 +1610,10 @@ func (r *stubUsageLogRepo) GetUserDashboardStats(ctx context.Context, userID int return nil, errors.New("not implemented") } +func (r *stubUsageLogRepo) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) { + return nil, errors.New("not implemented") +} + func (r *stubUsageLogRepo) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) { return nil, errors.New("not implemented") } diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index f3b3e20d..304c5781 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -41,6 +41,7 @@ type UsageLogRepository interface { // User dashboard stats GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error) + GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error) diff --git a/backend/internal/service/usage_service.go b/backend/internal/service/usage_service.go index aa0a5b87..5594e53f 100644 --- a/backend/internal/service/usage_service.go +++ b/backend/internal/service/usage_service.go @@ -288,6 +288,15 @@ func (s *UsageService) GetUserDashboardStats(ctx context.Context, userID int64) return stats, nil } +// GetAPIKeyDashboardStats returns dashboard summary stats filtered by API Key. +func (s *UsageService) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) { + stats, err := s.usageRepo.GetAPIKeyDashboardStats(ctx, apiKeyID) + if err != nil { + return nil, fmt.Errorf("get api key dashboard stats: %w", err) + } + return stats, nil +} + // GetUserUsageTrendByUserID returns per-user usage trend. func (s *UsageService) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) { trend, err := s.usageRepo.GetUserUsageTrendByUserID(ctx, userID, startTime, endTime, granularity)