// Package admin provides HTTP handlers for administrative operations including // dashboard statistics, user management, API key management, and account management. package admin import ( "strconv" "time" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // DashboardHandler handles admin dashboard statistics type DashboardHandler struct { dashboardService *service.DashboardService startTime time.Time // Server start time for uptime calculation } // NewDashboardHandler creates a new admin dashboard handler func NewDashboardHandler(dashboardService *service.DashboardService) *DashboardHandler { return &DashboardHandler{ dashboardService: dashboardService, startTime: time.Now(), } } // parseTimeRange parses start_date, end_date query parameters func parseTimeRange(c *gin.Context) (time.Time, time.Time) { now := timezone.Now() startDate := c.Query("start_date") endDate := c.Query("end_date") var startTime, endTime time.Time if startDate != "" { if t, err := timezone.ParseInLocation("2006-01-02", startDate); err == nil { startTime = t } else { startTime = timezone.StartOfDay(now.AddDate(0, 0, -7)) } } else { startTime = timezone.StartOfDay(now.AddDate(0, 0, -7)) } if endDate != "" { if t, err := timezone.ParseInLocation("2006-01-02", endDate); err == nil { endTime = t.Add(24 * time.Hour) // Include the end date } else { endTime = timezone.StartOfDay(now.AddDate(0, 0, 1)) } } else { endTime = timezone.StartOfDay(now.AddDate(0, 0, 1)) } return startTime, endTime } // GetStats handles getting dashboard statistics // GET /api/v1/admin/dashboard/stats func (h *DashboardHandler) GetStats(c *gin.Context) { stats, err := h.dashboardService.GetDashboardStats(c.Request.Context()) if err != nil { response.Error(c, 500, "Failed to get dashboard statistics") return } // Calculate uptime in seconds uptime := int64(time.Since(h.startTime).Seconds()) response.Success(c, gin.H{ // 用户统计 "total_users": stats.TotalUsers, "today_new_users": stats.TodayNewUsers, "active_users": stats.ActiveUsers, // API Key 统计 "total_api_keys": stats.TotalAPIKeys, "active_api_keys": stats.ActiveAPIKeys, // 账户统计 "total_accounts": stats.TotalAccounts, "normal_accounts": stats.NormalAccounts, "error_accounts": stats.ErrorAccounts, "ratelimit_accounts": stats.RateLimitAccounts, "overload_accounts": stats.OverloadAccounts, // 累计 Token 使用统计 "total_requests": stats.TotalRequests, "total_input_tokens": stats.TotalInputTokens, "total_output_tokens": stats.TotalOutputTokens, "total_cache_creation_tokens": stats.TotalCacheCreationTokens, "total_cache_read_tokens": stats.TotalCacheReadTokens, "total_tokens": stats.TotalTokens, "total_cost": stats.TotalCost, // 标准计费 "total_actual_cost": stats.TotalActualCost, // 实际扣除 // 今日 Token 使用统计 "today_requests": stats.TodayRequests, "today_input_tokens": stats.TodayInputTokens, "today_output_tokens": stats.TodayOutputTokens, "today_cache_creation_tokens": stats.TodayCacheCreationTokens, "today_cache_read_tokens": stats.TodayCacheReadTokens, "today_tokens": stats.TodayTokens, "today_cost": stats.TodayCost, // 今日标准计费 "today_actual_cost": stats.TodayActualCost, // 今日实际扣除 // 系统运行统计 "average_duration_ms": stats.AverageDurationMs, "uptime": uptime, // 性能指标 "rpm": stats.Rpm, "tpm": stats.Tpm, }) } // GetRealtimeMetrics handles getting real-time system metrics // GET /api/v1/admin/dashboard/realtime func (h *DashboardHandler) GetRealtimeMetrics(c *gin.Context) { // Return mock data for now response.Success(c, gin.H{ "active_requests": 0, "requests_per_minute": 0, "average_response_time": 0, "error_rate": 0.0, }) } // GetUsageTrend handles getting usage trend data // GET /api/v1/admin/dashboard/trend // 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") // 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.dashboardService.GetUsageTrendWithFilters(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID) if err != nil { response.Error(c, 500, "Failed to get usage trend") return } response.Success(c, gin.H{ "trend": trend, "start_date": startTime.Format("2006-01-02"), "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), "granularity": granularity, }) } // GetModelStats handles getting model usage statistics // GET /api/v1/admin/dashboard/models // 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) // 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.dashboardService.GetModelStatsWithFilters(c.Request.Context(), startTime, endTime, userID, apiKeyID) if err != nil { response.Error(c, 500, "Failed to get model statistics") return } response.Success(c, gin.H{ "models": stats, "start_date": startTime.Format("2006-01-02"), "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), }) } // GetAPIKeyUsageTrend handles getting API key usage trend data // GET /api/v1/admin/dashboard/api-keys-trend // Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), limit (default 5) func (h *DashboardHandler) GetAPIKeyUsageTrend(c *gin.Context) { startTime, endTime := parseTimeRange(c) granularity := c.DefaultQuery("granularity", "day") limitStr := c.DefaultQuery("limit", "5") limit, err := strconv.Atoi(limitStr) if err != nil || limit <= 0 { limit = 5 } trend, err := h.dashboardService.GetAPIKeyUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit) if err != nil { response.Error(c, 500, "Failed to get API key usage trend") return } response.Success(c, gin.H{ "trend": trend, "start_date": startTime.Format("2006-01-02"), "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), "granularity": granularity, }) } // GetUserUsageTrend handles getting user usage trend data // GET /api/v1/admin/dashboard/users-trend // Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), limit (default 12) func (h *DashboardHandler) GetUserUsageTrend(c *gin.Context) { startTime, endTime := parseTimeRange(c) granularity := c.DefaultQuery("granularity", "day") limitStr := c.DefaultQuery("limit", "12") limit, err := strconv.Atoi(limitStr) if err != nil || limit <= 0 { limit = 12 } trend, err := h.dashboardService.GetUserUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit) if err != nil { response.Error(c, 500, "Failed to get user usage trend") return } response.Success(c, gin.H{ "trend": trend, "start_date": startTime.Format("2006-01-02"), "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), "granularity": granularity, }) } // BatchUsersUsageRequest represents the request body for batch user usage stats type BatchUsersUsageRequest struct { UserIDs []int64 `json:"user_ids" binding:"required"` } // GetBatchUsersUsage handles getting usage stats for multiple users // POST /api/v1/admin/dashboard/users-usage func (h *DashboardHandler) GetBatchUsersUsage(c *gin.Context) { var req BatchUsersUsageRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } if len(req.UserIDs) == 0 { response.Success(c, gin.H{"stats": map[string]any{}}) return } stats, err := h.dashboardService.GetBatchUserUsageStats(c.Request.Context(), req.UserIDs) if err != nil { response.Error(c, 500, "Failed to get user usage stats") return } response.Success(c, gin.H{"stats": stats}) } // BatchAPIKeysUsageRequest represents the request body for batch api key usage stats type BatchAPIKeysUsageRequest struct { APIKeyIDs []int64 `json:"api_key_ids" binding:"required"` } // GetBatchAPIKeysUsage handles getting usage stats for multiple API keys // POST /api/v1/admin/dashboard/api-keys-usage func (h *DashboardHandler) GetBatchAPIKeysUsage(c *gin.Context) { var req BatchAPIKeysUsageRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } if len(req.APIKeyIDs) == 0 { response.Success(c, gin.H{"stats": map[string]any{}}) return } stats, err := h.dashboardService.GetBatchAPIKeyUsageStats(c.Request.Context(), req.APIKeyIDs) if err != nil { response.Error(c, 500, "Failed to get API key usage stats") return } response.Success(c, gin.H{"stats": stats}) }