From 0484c59ead42d56b983cc2af20012b2560b2004e Mon Sep 17 00:00:00 2001 From: shaw Date: Sat, 20 Dec 2025 10:06:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20/admin/usage=E9=A1=B5=E9=9D=A2=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=A8=A1=E5=9E=8B=E5=88=86=E5=B8=83=E6=83=85=E5=86=B5?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/admin/dashboard_handler.go | 34 +++- backend/internal/repository/usage_log_repo.go | 132 +++++++------ frontend/src/api/admin/dashboard.ts | 11 +- .../charts/ModelDistributionChart.vue | 125 ++++++++++++ .../src/components/charts/TokenUsageTrend.vue | 182 ++++++++++++++++++ frontend/src/views/admin/DashboardView.vue | 160 ++------------- frontend/src/views/admin/UsageView.vue | 85 +++++++- frontend/tsconfig.tsbuildinfo | 2 +- 8 files changed, 518 insertions(+), 213 deletions(-) create mode 100644 frontend/src/components/charts/ModelDistributionChart.vue create mode 100644 frontend/src/components/charts/TokenUsageTrend.vue diff --git a/backend/internal/handler/admin/dashboard_handler.go b/backend/internal/handler/admin/dashboard_handler.go index 3b971eab..1995306a 100644 --- a/backend/internal/handler/admin/dashboard_handler.go +++ b/backend/internal/handler/admin/dashboard_handler.go @@ -127,12 +127,25 @@ func (h *DashboardHandler) GetRealtimeMetrics(c *gin.Context) { // GetUsageTrend handles getting usage trend data // GET /api/v1/admin/dashboard/trend -// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour) +// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), user_id, api_key_id func (h *DashboardHandler) GetUsageTrend(c *gin.Context) { startTime, endTime := parseTimeRange(c) granularity := c.DefaultQuery("granularity", "day") - trend, err := h.usageRepo.GetUsageTrend(c.Request.Context(), startTime, endTime, granularity) + // Parse optional filter params + var userID, apiKeyID int64 + if userIDStr := c.Query("user_id"); userIDStr != "" { + if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil { + userID = id + } + } + if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" { + if id, err := strconv.ParseInt(apiKeyIDStr, 10, 64); err == nil { + apiKeyID = id + } + } + + trend, err := h.usageRepo.GetUsageTrendWithFilters(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID) if err != nil { response.Error(c, 500, "Failed to get usage trend") return @@ -148,11 +161,24 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) { // GetModelStats handles getting model usage statistics // GET /api/v1/admin/dashboard/models -// Query params: start_date, end_date (YYYY-MM-DD) +// Query params: start_date, end_date (YYYY-MM-DD), user_id, api_key_id func (h *DashboardHandler) GetModelStats(c *gin.Context) { startTime, endTime := parseTimeRange(c) - stats, err := h.usageRepo.GetModelStats(c.Request.Context(), startTime, endTime) + // Parse optional filter params + var userID, apiKeyID int64 + if userIDStr := c.Query("user_id"); userIDStr != "" { + if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil { + userID = id + } + } + if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" { + if id, err := strconv.ParseInt(apiKeyIDStr, 10, 64); err == nil { + apiKeyID = id + } + } + + stats, err := h.usageRepo.GetModelStatsWithFilters(c.Request.Context(), startTime, endTime, userID, apiKeyID) if err != nil { response.Error(c, 500, "Failed to get model statistics") return diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index 080b975e..c32517de 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -431,68 +431,6 @@ type UserUsageTrendPoint struct { ActualCost float64 `json:"actual_cost"` // 实际扣除 } -// GetUsageTrend returns usage trend data grouped by date -// granularity: "day" or "hour" -func (r *UsageLogRepository) GetUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string) ([]TrendDataPoint, error) { - var results []TrendDataPoint - - // Choose date format based on granularity - var dateFormat string - if granularity == "hour" { - dateFormat = "YYYY-MM-DD HH24:00" - } else { - dateFormat = "YYYY-MM-DD" - } - - err := r.db.WithContext(ctx).Model(&model.UsageLog{}). - Select(` - TO_CHAR(created_at, ?) as date, - COUNT(*) as requests, - COALESCE(SUM(input_tokens), 0) as input_tokens, - COALESCE(SUM(output_tokens), 0) as output_tokens, - COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_tokens, - COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, - COALESCE(SUM(total_cost), 0) as cost, - COALESCE(SUM(actual_cost), 0) as actual_cost - `, dateFormat). - Where("created_at >= ? AND created_at < ?", startTime, endTime). - Group("date"). - Order("date ASC"). - Scan(&results).Error - - if err != nil { - return nil, err - } - - return results, nil -} - -// GetModelStats returns usage statistics grouped by model -func (r *UsageLogRepository) GetModelStats(ctx context.Context, startTime, endTime time.Time) ([]ModelStat, error) { - var results []ModelStat - - err := r.db.WithContext(ctx).Model(&model.UsageLog{}). - Select(` - model, - COUNT(*) as requests, - COALESCE(SUM(input_tokens), 0) as input_tokens, - COALESCE(SUM(output_tokens), 0) as output_tokens, - COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, - COALESCE(SUM(total_cost), 0) as cost, - COALESCE(SUM(actual_cost), 0) as actual_cost - `). - Where("created_at >= ? AND created_at < ?", startTime, endTime). - Group("model"). - Order("total_tokens DESC"). - Scan(&results).Error - - if err != nil { - return nil, err - } - - return results, nil -} - // ApiKeyUsageTrendPoint represents API key usage trend data point type ApiKeyUsageTrendPoint struct { Date string `json:"date"` @@ -959,6 +897,76 @@ func (r *UsageLogRepository) GetBatchApiKeyUsageStats(ctx context.Context, apiKe return result, nil } +// GetUsageTrendWithFilters returns usage trend data with optional user/api_key filters +func (r *UsageLogRepository) GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID int64) ([]TrendDataPoint, error) { + var results []TrendDataPoint + + var dateFormat string + if granularity == "hour" { + dateFormat = "YYYY-MM-DD HH24:00" + } else { + dateFormat = "YYYY-MM-DD" + } + + db := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + TO_CHAR(created_at, ?) as date, + COUNT(*) as requests, + COALESCE(SUM(input_tokens), 0) as input_tokens, + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_tokens, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, + COALESCE(SUM(total_cost), 0) as cost, + COALESCE(SUM(actual_cost), 0) as actual_cost + `, dateFormat). + Where("created_at >= ? AND created_at < ?", startTime, endTime) + + if userID > 0 { + db = db.Where("user_id = ?", userID) + } + if apiKeyID > 0 { + db = db.Where("api_key_id = ?", apiKeyID) + } + + err := db.Group("date").Order("date ASC").Scan(&results).Error + if err != nil { + return nil, err + } + + return results, nil +} + +// GetModelStatsWithFilters returns model statistics with optional user/api_key filters +func (r *UsageLogRepository) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID int64) ([]ModelStat, error) { + var results []ModelStat + + db := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + model, + COUNT(*) as requests, + COALESCE(SUM(input_tokens), 0) as input_tokens, + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, + COALESCE(SUM(total_cost), 0) as cost, + COALESCE(SUM(actual_cost), 0) as actual_cost + `). + Where("created_at >= ? AND created_at < ?", startTime, endTime) + + if userID > 0 { + db = db.Where("user_id = ?", userID) + } + if apiKeyID > 0 { + db = db.Where("api_key_id = ?", apiKeyID) + } + + err := db.Group("model").Order("total_tokens DESC").Scan(&results).Error + if err != nil { + return nil, err + } + + return results, nil +} + // GetGlobalStats gets usage statistics for all users within a time range func (r *UsageLogRepository) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*UsageStats, error) { var stats struct { diff --git a/frontend/src/api/admin/dashboard.ts b/frontend/src/api/admin/dashboard.ts index 36f4ee68..39253538 100644 --- a/frontend/src/api/admin/dashboard.ts +++ b/frontend/src/api/admin/dashboard.ts @@ -38,6 +38,8 @@ export interface TrendParams { start_date?: string; end_date?: string; granularity?: 'day' | 'hour'; + user_id?: number; + api_key_id?: number; } export interface TrendResponse { @@ -57,6 +59,13 @@ export async function getUsageTrend(params?: TrendParams): Promise { +export async function getModelStats(params?: ModelStatsParams): Promise { const { data } = await apiClient.get('/admin/dashboard/models', { params }); return data; } diff --git a/frontend/src/components/charts/ModelDistributionChart.vue b/frontend/src/components/charts/ModelDistributionChart.vue new file mode 100644 index 00000000..d9da764b --- /dev/null +++ b/frontend/src/components/charts/ModelDistributionChart.vue @@ -0,0 +1,125 @@ + + + diff --git a/frontend/src/components/charts/TokenUsageTrend.vue b/frontend/src/components/charts/TokenUsageTrend.vue new file mode 100644 index 00000000..480e9fea --- /dev/null +++ b/frontend/src/components/charts/TokenUsageTrend.vue @@ -0,0 +1,182 @@ + + + diff --git a/frontend/src/views/admin/DashboardView.vue b/frontend/src/views/admin/DashboardView.vue index 29b2f6b1..8f186c29 100644 --- a/frontend/src/views/admin/DashboardView.vue +++ b/frontend/src/views/admin/DashboardView.vue @@ -180,51 +180,14 @@
- -
-

{{ t('admin.dashboard.modelDistribution') }}

-
-
- -
- {{ t('admin.dashboard.noDataAvailable') }} -
-
-
- - - - - - - - - - - - - - - - - - - -
{{ t('admin.dashboard.model') }}{{ t('admin.dashboard.requests') }}{{ t('admin.dashboard.tokens') }}{{ t('admin.dashboard.actual') }}{{ t('admin.dashboard.standard') }}
{{ model.model }}{{ formatNumber(model.requests) }}{{ formatTokens(model.total_tokens) }}${{ formatCost(model.actual_cost) }}${{ formatCost(model.cost) }}
-
-
-
- - -
-

{{ t('admin.dashboard.tokenUsageTrend') }}

-
- -
- {{ t('admin.dashboard.noDataAvailable') }} -
-
-
+ +
@@ -244,7 +207,7 @@