From bbf4024dc7913708d5ccdb27c4300a7b53a317ad Mon Sep 17 00:00:00 2001 From: Forest Date: Wed, 24 Dec 2025 08:41:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor(usage):=20=E7=A7=BB=E5=8A=A8=20usage?= =?UTF-8?q?=20=E6=9F=A5=E8=AF=A2=E5=88=B0=20services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.golangci.yml | 9 +- backend/cmd/server/wire_gen.go | 9 +- .../internal/handler/admin/account_handler.go | 7 +- .../handler/admin/dashboard_handler.go | 26 +-- .../internal/handler/admin/usage_handler.go | 27 ++- backend/internal/handler/usage_handler.go | 13 +- .../pkg/usagestats/usage_log_types.go | 201 ++++++++++++++++++ backend/internal/repository/usage_log_repo.go | 185 ++-------------- .../internal/service/account_usage_service.go | 9 + backend/internal/service/api_key_service.go | 8 + backend/internal/service/dashboard_service.go | 77 +++++++ backend/internal/service/ports/usage_log.go | 21 ++ backend/internal/service/usage_service.go | 55 +++++ backend/internal/service/wire.go | 1 + 14 files changed, 430 insertions(+), 218 deletions(-) create mode 100644 backend/internal/pkg/usagestats/usage_log_types.go create mode 100644 backend/internal/service/dashboard_service.go diff --git a/backend/.golangci.yml b/backend/.golangci.yml index 415a02ac..ec16bc0f 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -17,10 +17,17 @@ linters: service-no-repository: list-mode: original files: - - internal/service/** + - "**/internal/service/**" deny: - pkg: sub2api/internal/repository desc: "service must not import repository" + handler-no-repository: + list-mode: original + files: + - "**/internal/handler/**" + deny: + - pkg: sub2api/internal/repository + desc: "handler must not import repository" errcheck: # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. # Such cases aren't reported by default. diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index ad87843f..41804775 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -58,7 +58,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService) usageLogRepository := repository.NewUsageLogRepository(db) usageService := service.NewUsageService(usageLogRepository, userRepository) - usageHandler := handler.NewUsageHandler(usageService, usageLogRepository, apiKeyService) + usageHandler := handler.NewUsageHandler(usageService, apiKeyService) redeemCodeRepository := repository.NewRedeemCodeRepository(db) billingCache := repository.NewBillingCache(client) billingCacheService := service.NewBillingCacheService(billingCache, userRepository, userSubscriptionRepository) @@ -67,7 +67,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService) redeemHandler := handler.NewRedeemHandler(redeemService) subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService) - dashboardHandler := admin.NewDashboardHandler(usageLogRepository) + dashboardService := service.NewDashboardService(usageLogRepository) + dashboardHandler := admin.NewDashboardHandler(dashboardService) accountRepository := repository.NewAccountRepository(db) proxyRepository := repository.NewProxyRepository(db) proxyExitInfoProber := repository.NewProxyExitInfoProber() @@ -83,7 +84,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher) httpUpstream := repository.NewHTTPUpstream(configConfig) accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, httpUpstream) - accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService, usageLogRepository) + accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService) oAuthHandler := admin.NewOAuthHandler(oAuthService) openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService) proxyHandler := admin.NewProxyHandler(adminService) @@ -95,7 +96,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo) systemHandler := handler.ProvideSystemHandler(updateService) adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService) - adminUsageHandler := admin.NewUsageHandler(usageLogRepository, apiKeyRepository, usageService, adminService) + adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService) adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler) gatewayCache := repository.NewGatewayCache(client) pricingRemoteClient := repository.NewPricingRemoteClient() diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index b5a2d794..74b26187 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -7,7 +7,6 @@ import ( "sub2api/internal/pkg/openai" "sub2api/internal/pkg/response" "sub2api/internal/pkg/timezone" - "sub2api/internal/repository" "sub2api/internal/service" "github.com/gin-gonic/gin" @@ -33,11 +32,10 @@ type AccountHandler struct { rateLimitService *service.RateLimitService accountUsageService *service.AccountUsageService accountTestService *service.AccountTestService - usageLogRepo *repository.UsageLogRepository } // NewAccountHandler creates a new admin account handler -func NewAccountHandler(adminService service.AdminService, oauthService *service.OAuthService, openaiOAuthService *service.OpenAIOAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService, usageLogRepo *repository.UsageLogRepository) *AccountHandler { +func NewAccountHandler(adminService service.AdminService, oauthService *service.OAuthService, openaiOAuthService *service.OpenAIOAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService) *AccountHandler { return &AccountHandler{ adminService: adminService, oauthService: oauthService, @@ -45,7 +43,6 @@ func NewAccountHandler(adminService service.AdminService, oauthService *service. rateLimitService: rateLimitService, accountUsageService: accountUsageService, accountTestService: accountTestService, - usageLogRepo: usageLogRepo, } } @@ -314,7 +311,7 @@ func (h *AccountHandler) GetStats(c *gin.Context) { endTime := timezone.StartOfDay(now.AddDate(0, 0, 1)) startTime := timezone.StartOfDay(now.AddDate(0, 0, -days+1)) - stats, err := h.usageLogRepo.GetAccountUsageStats(c.Request.Context(), accountID, startTime, endTime) + stats, err := h.accountUsageService.GetAccountUsageStats(c.Request.Context(), accountID, startTime, endTime) if err != nil { response.InternalError(c, "Failed to get account stats: "+err.Error()) return diff --git a/backend/internal/handler/admin/dashboard_handler.go b/backend/internal/handler/admin/dashboard_handler.go index 99f9cad9..4ccda37b 100644 --- a/backend/internal/handler/admin/dashboard_handler.go +++ b/backend/internal/handler/admin/dashboard_handler.go @@ -4,7 +4,7 @@ import ( "strconv" "sub2api/internal/pkg/response" "sub2api/internal/pkg/timezone" - "sub2api/internal/repository" + "sub2api/internal/service" "time" "github.com/gin-gonic/gin" @@ -12,15 +12,15 @@ import ( // DashboardHandler handles admin dashboard statistics type DashboardHandler struct { - usageRepo *repository.UsageLogRepository - startTime time.Time // Server start time for uptime calculation + dashboardService *service.DashboardService + startTime time.Time // Server start time for uptime calculation } // NewDashboardHandler creates a new admin dashboard handler -func NewDashboardHandler(usageRepo *repository.UsageLogRepository) *DashboardHandler { +func NewDashboardHandler(dashboardService *service.DashboardService) *DashboardHandler { return &DashboardHandler{ - usageRepo: usageRepo, - startTime: time.Now(), + dashboardService: dashboardService, + startTime: time.Now(), } } @@ -58,7 +58,7 @@ func parseTimeRange(c *gin.Context) (time.Time, time.Time) { // GetStats handles getting dashboard statistics // GET /api/v1/admin/dashboard/stats func (h *DashboardHandler) GetStats(c *gin.Context) { - stats, err := h.usageRepo.GetDashboardStats(c.Request.Context()) + stats, err := h.dashboardService.GetDashboardStats(c.Request.Context()) if err != nil { response.Error(c, 500, "Failed to get dashboard statistics") return @@ -142,7 +142,7 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) { } } - trend, err := h.usageRepo.GetUsageTrendWithFilters(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID) + 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 @@ -175,7 +175,7 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) { } } - stats, err := h.usageRepo.GetModelStatsWithFilters(c.Request.Context(), startTime, endTime, userID, apiKeyID, 0) + 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 @@ -200,7 +200,7 @@ func (h *DashboardHandler) GetApiKeyUsageTrend(c *gin.Context) { limit = 5 } - trend, err := h.usageRepo.GetApiKeyUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit) + 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 @@ -226,7 +226,7 @@ func (h *DashboardHandler) GetUserUsageTrend(c *gin.Context) { limit = 12 } - trend, err := h.usageRepo.GetUserUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit) + 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 @@ -259,7 +259,7 @@ func (h *DashboardHandler) GetBatchUsersUsage(c *gin.Context) { return } - stats, err := h.usageRepo.GetBatchUserUsageStats(c.Request.Context(), req.UserIDs) + stats, err := h.dashboardService.GetBatchUserUsageStats(c.Request.Context(), req.UserIDs) if err != nil { response.Error(c, 500, "Failed to get user usage stats") return @@ -287,7 +287,7 @@ func (h *DashboardHandler) GetBatchApiKeysUsage(c *gin.Context) { return } - stats, err := h.usageRepo.GetBatchApiKeyUsageStats(c.Request.Context(), req.ApiKeyIDs) + 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 diff --git a/backend/internal/handler/admin/usage_handler.go b/backend/internal/handler/admin/usage_handler.go index 00501a52..f057095f 100644 --- a/backend/internal/handler/admin/usage_handler.go +++ b/backend/internal/handler/admin/usage_handler.go @@ -7,7 +7,7 @@ import ( "sub2api/internal/pkg/pagination" "sub2api/internal/pkg/response" "sub2api/internal/pkg/timezone" - "sub2api/internal/repository" + "sub2api/internal/pkg/usagestats" "sub2api/internal/service" "github.com/gin-gonic/gin" @@ -15,24 +15,21 @@ import ( // UsageHandler handles admin usage-related requests type UsageHandler struct { - usageRepo *repository.UsageLogRepository - apiKeyRepo *repository.ApiKeyRepository - usageService *service.UsageService - adminService service.AdminService + usageService *service.UsageService + apiKeyService *service.ApiKeyService + adminService service.AdminService } // NewUsageHandler creates a new admin usage handler func NewUsageHandler( - usageRepo *repository.UsageLogRepository, - apiKeyRepo *repository.ApiKeyRepository, usageService *service.UsageService, + apiKeyService *service.ApiKeyService, adminService service.AdminService, ) *UsageHandler { return &UsageHandler{ - usageRepo: usageRepo, - apiKeyRepo: apiKeyRepo, - usageService: usageService, - adminService: adminService, + usageService: usageService, + apiKeyService: apiKeyService, + adminService: adminService, } } @@ -84,14 +81,14 @@ func (h *UsageHandler) List(c *gin.Context) { } params := pagination.PaginationParams{Page: page, PageSize: pageSize} - filters := repository.UsageLogFilters{ + filters := usagestats.UsageLogFilters{ UserID: userID, ApiKeyID: apiKeyID, StartTime: startTime, EndTime: endTime, } - records, result, err := h.usageRepo.ListWithFilters(c.Request.Context(), params, filters) + records, result, err := h.usageService.ListWithFilters(c.Request.Context(), params, filters) if err != nil { response.InternalError(c, "Failed to list usage records: "+err.Error()) return @@ -179,7 +176,7 @@ func (h *UsageHandler) Stats(c *gin.Context) { } // Get global stats - stats, err := h.usageRepo.GetGlobalStats(c.Request.Context(), startTime, endTime) + stats, err := h.usageService.GetGlobalStats(c.Request.Context(), startTime, endTime) if err != nil { response.InternalError(c, "Failed to get usage statistics: "+err.Error()) return @@ -237,7 +234,7 @@ func (h *UsageHandler) SearchApiKeys(c *gin.Context) { userID = id } - keys, err := h.apiKeyRepo.SearchApiKeys(c.Request.Context(), userID, keyword, 30) + keys, err := h.apiKeyService.SearchApiKeys(c.Request.Context(), userID, keyword, 30) if err != nil { response.InternalError(c, "Failed to search API keys: "+err.Error()) return diff --git a/backend/internal/handler/usage_handler.go b/backend/internal/handler/usage_handler.go index 3738ca28..4459398c 100644 --- a/backend/internal/handler/usage_handler.go +++ b/backend/internal/handler/usage_handler.go @@ -8,7 +8,6 @@ import ( "sub2api/internal/pkg/pagination" "sub2api/internal/pkg/response" "sub2api/internal/pkg/timezone" - "sub2api/internal/repository" "sub2api/internal/service" "github.com/gin-gonic/gin" @@ -17,15 +16,13 @@ import ( // UsageHandler handles usage-related requests type UsageHandler struct { usageService *service.UsageService - usageRepo *repository.UsageLogRepository apiKeyService *service.ApiKeyService } // NewUsageHandler creates a new UsageHandler -func NewUsageHandler(usageService *service.UsageService, usageRepo *repository.UsageLogRepository, apiKeyService *service.ApiKeyService) *UsageHandler { +func NewUsageHandler(usageService *service.UsageService, apiKeyService *service.ApiKeyService) *UsageHandler { return &UsageHandler{ usageService: usageService, - usageRepo: usageRepo, apiKeyService: apiKeyService, } } @@ -260,7 +257,7 @@ func (h *UsageHandler) DashboardStats(c *gin.Context) { return } - stats, err := h.usageRepo.GetUserDashboardStats(c.Request.Context(), user.ID) + stats, err := h.usageService.GetUserDashboardStats(c.Request.Context(), user.ID) if err != nil { response.InternalError(c, "Failed to get dashboard statistics") return @@ -287,7 +284,7 @@ func (h *UsageHandler) DashboardTrend(c *gin.Context) { startTime, endTime := parseUserTimeRange(c) granularity := c.DefaultQuery("granularity", "day") - trend, err := h.usageRepo.GetUserUsageTrendByUserID(c.Request.Context(), user.ID, startTime, endTime, granularity) + trend, err := h.usageService.GetUserUsageTrendByUserID(c.Request.Context(), user.ID, startTime, endTime, granularity) if err != nil { response.InternalError(c, "Failed to get usage trend") return @@ -318,7 +315,7 @@ func (h *UsageHandler) DashboardModels(c *gin.Context) { startTime, endTime := parseUserTimeRange(c) - stats, err := h.usageRepo.GetUserModelStats(c.Request.Context(), user.ID, startTime, endTime) + stats, err := h.usageService.GetUserModelStats(c.Request.Context(), user.ID, startTime, endTime) if err != nil { response.InternalError(c, "Failed to get model statistics") return @@ -387,7 +384,7 @@ func (h *UsageHandler) DashboardApiKeysUsage(c *gin.Context) { return } - stats, err := h.usageRepo.GetBatchApiKeyUsageStats(c.Request.Context(), validApiKeyIDs) + stats, err := h.usageService.GetBatchApiKeyUsageStats(c.Request.Context(), validApiKeyIDs) if err != nil { response.InternalError(c, "Failed to get API key usage stats") return diff --git a/backend/internal/pkg/usagestats/usage_log_types.go b/backend/internal/pkg/usagestats/usage_log_types.go new file mode 100644 index 00000000..9a9be5d2 --- /dev/null +++ b/backend/internal/pkg/usagestats/usage_log_types.go @@ -0,0 +1,201 @@ +package usagestats + +import "time" + +// DashboardStats 仪表盘统计 +type DashboardStats struct { + // 用户统计 + TotalUsers int64 `json:"total_users"` + TodayNewUsers int64 `json:"today_new_users"` // 今日新增用户数 + ActiveUsers int64 `json:"active_users"` // 今日有请求的用户数 + + // API Key 统计 + TotalApiKeys int64 `json:"total_api_keys"` + ActiveApiKeys int64 `json:"active_api_keys"` // 状态为 active 的 API Key 数 + + // 账户统计 + TotalAccounts int64 `json:"total_accounts"` + NormalAccounts int64 `json:"normal_accounts"` // 正常账户数 (schedulable=true, status=active) + ErrorAccounts int64 `json:"error_accounts"` // 异常账户数 (status=error) + RateLimitAccounts int64 `json:"ratelimit_accounts"` // 限流账户数 + OverloadAccounts int64 `json:"overload_accounts"` // 过载账户数 + + // 累计 Token 使用统计 + TotalRequests int64 `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` + TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` // 累计标准计费 + TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 + + // 今日 Token 使用统计 + TodayRequests int64 `json:"today_requests"` + TodayInputTokens int64 `json:"today_input_tokens"` + TodayOutputTokens int64 `json:"today_output_tokens"` + TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` + TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` + TodayTokens int64 `json:"today_tokens"` + TodayCost float64 `json:"today_cost"` // 今日标准计费 + TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 + + // 系统运行统计 + AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间 +} + +// TrendDataPoint represents a single point in trend data +type TrendDataPoint struct { + Date string `json:"date"` + Requests int64 `json:"requests"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheTokens int64 `json:"cache_tokens"` + TotalTokens int64 `json:"total_tokens"` + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 +} + +// ModelStat represents usage statistics for a single model +type ModelStat struct { + Model string `json:"model"` + Requests int64 `json:"requests"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + TotalTokens int64 `json:"total_tokens"` + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 +} + +// UserUsageTrendPoint represents user usage trend data point +type UserUsageTrendPoint struct { + Date string `json:"date"` + UserID int64 `json:"user_id"` + Email string `json:"email"` + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 +} + +// ApiKeyUsageTrendPoint represents API key usage trend data point +type ApiKeyUsageTrendPoint struct { + Date string `json:"date"` + ApiKeyID int64 `json:"api_key_id"` + KeyName string `json:"key_name"` + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` +} + +// UserDashboardStats 用户仪表盘统计 +type UserDashboardStats struct { + // API Key 统计 + TotalApiKeys int64 `json:"total_api_keys"` + ActiveApiKeys int64 `json:"active_api_keys"` + + // 累计 Token 使用统计 + TotalRequests int64 `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` + TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` // 累计标准计费 + TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 + + // 今日 Token 使用统计 + TodayRequests int64 `json:"today_requests"` + TodayInputTokens int64 `json:"today_input_tokens"` + TodayOutputTokens int64 `json:"today_output_tokens"` + TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` + TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` + TodayTokens int64 `json:"today_tokens"` + TodayCost float64 `json:"today_cost"` // 今日标准计费 + TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 + + // 性能统计 + AverageDurationMs float64 `json:"average_duration_ms"` +} + +// UsageLogFilters represents filters for usage log queries +type UsageLogFilters struct { + UserID int64 + ApiKeyID int64 + StartTime *time.Time + EndTime *time.Time +} + +// UsageStats represents usage statistics +type UsageStats struct { + TotalRequests int64 `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheTokens int64 `json:"total_cache_tokens"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` + TotalActualCost float64 `json:"total_actual_cost"` + AverageDurationMs float64 `json:"average_duration_ms"` +} + +// BatchUserUsageStats represents usage stats for a single user +type BatchUserUsageStats struct { + UserID int64 `json:"user_id"` + TodayActualCost float64 `json:"today_actual_cost"` + TotalActualCost float64 `json:"total_actual_cost"` +} + +// BatchApiKeyUsageStats represents usage stats for a single API key +type BatchApiKeyUsageStats struct { + ApiKeyID int64 `json:"api_key_id"` + TodayActualCost float64 `json:"today_actual_cost"` + TotalActualCost float64 `json:"total_actual_cost"` +} + +// AccountUsageHistory represents daily usage history for an account +type AccountUsageHistory struct { + Date string `json:"date"` + Label string `json:"label"` + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` + Cost float64 `json:"cost"` + ActualCost float64 `json:"actual_cost"` +} + +// AccountUsageSummary represents summary statistics for an account +type AccountUsageSummary struct { + Days int `json:"days"` + ActualDaysUsed int `json:"actual_days_used"` + TotalCost float64 `json:"total_cost"` + TotalStandardCost float64 `json:"total_standard_cost"` + TotalRequests int64 `json:"total_requests"` + TotalTokens int64 `json:"total_tokens"` + AvgDailyCost float64 `json:"avg_daily_cost"` + AvgDailyRequests float64 `json:"avg_daily_requests"` + AvgDailyTokens float64 `json:"avg_daily_tokens"` + AvgDurationMs float64 `json:"avg_duration_ms"` + Today *struct { + Date string `json:"date"` + Cost float64 `json:"cost"` + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` + } `json:"today"` + HighestCostDay *struct { + Date string `json:"date"` + Label string `json:"label"` + Cost float64 `json:"cost"` + Requests int64 `json:"requests"` + } `json:"highest_cost_day"` + HighestRequestDay *struct { + Date string `json:"date"` + Label string `json:"label"` + Requests int64 `json:"requests"` + Cost float64 `json:"cost"` + } `json:"highest_request_day"` +} + +// AccountUsageStatsResponse represents the full usage statistics response for an account +type AccountUsageStatsResponse struct { + History []AccountUsageHistory `json:"history"` + Summary AccountUsageSummary `json:"summary"` + Models []ModelStat `json:"models"` +} diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index 6df392e4..3aafc92c 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -113,46 +113,7 @@ func (r *UsageLogRepository) GetUserStats(ctx context.Context, userID int64, sta } // DashboardStats 仪表盘统计 -type DashboardStats struct { - // 用户统计 - TotalUsers int64 `json:"total_users"` - TodayNewUsers int64 `json:"today_new_users"` // 今日新增用户数 - ActiveUsers int64 `json:"active_users"` // 今日有请求的用户数 - - // API Key 统计 - TotalApiKeys int64 `json:"total_api_keys"` - ActiveApiKeys int64 `json:"active_api_keys"` // 状态为 active 的 API Key 数 - - // 账户统计 - TotalAccounts int64 `json:"total_accounts"` - NormalAccounts int64 `json:"normal_accounts"` // 正常账户数 (schedulable=true, status=active) - ErrorAccounts int64 `json:"error_accounts"` // 异常账户数 (status=error) - RateLimitAccounts int64 `json:"ratelimit_accounts"` // 限流账户数 - OverloadAccounts int64 `json:"overload_accounts"` // 过载账户数 - - // 累计 Token 使用统计 - TotalRequests int64 `json:"total_requests"` - TotalInputTokens int64 `json:"total_input_tokens"` - TotalOutputTokens int64 `json:"total_output_tokens"` - TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` - TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` - TotalTokens int64 `json:"total_tokens"` - TotalCost float64 `json:"total_cost"` // 累计标准计费 - TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 - - // 今日 Token 使用统计 - TodayRequests int64 `json:"today_requests"` - TodayInputTokens int64 `json:"today_input_tokens"` - TodayOutputTokens int64 `json:"today_output_tokens"` - TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` - TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` - TodayTokens int64 `json:"today_tokens"` - TodayCost float64 `json:"today_cost"` // 今日标准计费 - TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 - - // 系统运行统计 - AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间 -} +type DashboardStats = usagestats.DashboardStats func (r *UsageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) { var stats DashboardStats @@ -398,47 +359,16 @@ func (r *UsageLogRepository) GetAccountWindowStats(ctx context.Context, accountI } // TrendDataPoint represents a single point in trend data -type TrendDataPoint struct { - Date string `json:"date"` - Requests int64 `json:"requests"` - InputTokens int64 `json:"input_tokens"` - OutputTokens int64 `json:"output_tokens"` - CacheTokens int64 `json:"cache_tokens"` - TotalTokens int64 `json:"total_tokens"` - Cost float64 `json:"cost"` // 标准计费 - ActualCost float64 `json:"actual_cost"` // 实际扣除 -} +type TrendDataPoint = usagestats.TrendDataPoint // ModelStat represents usage statistics for a single model -type ModelStat struct { - Model string `json:"model"` - Requests int64 `json:"requests"` - InputTokens int64 `json:"input_tokens"` - OutputTokens int64 `json:"output_tokens"` - TotalTokens int64 `json:"total_tokens"` - Cost float64 `json:"cost"` // 标准计费 - ActualCost float64 `json:"actual_cost"` // 实际扣除 -} +type ModelStat = usagestats.ModelStat // UserUsageTrendPoint represents user usage trend data point -type UserUsageTrendPoint struct { - Date string `json:"date"` - UserID int64 `json:"user_id"` - Email string `json:"email"` - Requests int64 `json:"requests"` - Tokens int64 `json:"tokens"` - Cost float64 `json:"cost"` // 标准计费 - ActualCost float64 `json:"actual_cost"` // 实际扣除 -} +type UserUsageTrendPoint = usagestats.UserUsageTrendPoint // ApiKeyUsageTrendPoint represents API key usage trend data point -type ApiKeyUsageTrendPoint struct { - Date string `json:"date"` - ApiKeyID int64 `json:"api_key_id"` - KeyName string `json:"key_name"` - Requests int64 `json:"requests"` - Tokens int64 `json:"tokens"` -} +type ApiKeyUsageTrendPoint = usagestats.ApiKeyUsageTrendPoint // GetApiKeyUsageTrend returns usage trend data grouped by API key and date func (r *UsageLogRepository) GetApiKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]ApiKeyUsageTrendPoint, error) { @@ -531,34 +461,7 @@ func (r *UsageLogRepository) GetUserUsageTrend(ctx context.Context, startTime, e } // UserDashboardStats 用户仪表盘统计 -type UserDashboardStats struct { - // API Key 统计 - TotalApiKeys int64 `json:"total_api_keys"` - ActiveApiKeys int64 `json:"active_api_keys"` - - // 累计 Token 使用统计 - TotalRequests int64 `json:"total_requests"` - TotalInputTokens int64 `json:"total_input_tokens"` - TotalOutputTokens int64 `json:"total_output_tokens"` - TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` - TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` - TotalTokens int64 `json:"total_tokens"` - TotalCost float64 `json:"total_cost"` // 累计标准计费 - TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 - - // 今日 Token 使用统计 - TodayRequests int64 `json:"today_requests"` - TodayInputTokens int64 `json:"today_input_tokens"` - TodayOutputTokens int64 `json:"today_output_tokens"` - TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` - TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` - TodayTokens int64 `json:"today_tokens"` - TodayCost float64 `json:"today_cost"` // 今日标准计费 - TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 - - // 性能统计 - AverageDurationMs float64 `json:"average_duration_ms"` -} +type UserDashboardStats = usagestats.UserDashboardStats // GetUserDashboardStats 获取用户专属的仪表盘统计 func (r *UsageLogRepository) GetUserDashboardStats(ctx context.Context, userID int64) (*UserDashboardStats, error) { @@ -705,12 +608,7 @@ func (r *UsageLogRepository) GetUserModelStats(ctx context.Context, userID int64 } // UsageLogFilters represents filters for usage log queries -type UsageLogFilters struct { - UserID int64 - ApiKeyID int64 - StartTime *time.Time - EndTime *time.Time -} +type UsageLogFilters = usagestats.UsageLogFilters // ListWithFilters lists usage logs with optional filters (for admin) func (r *UsageLogRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters UsageLogFilters) ([]model.UsageLog, *pagination.PaginationResult, error) { @@ -758,23 +656,10 @@ func (r *UsageLogRepository) ListWithFilters(ctx context.Context, params paginat } // UsageStats represents usage statistics -type UsageStats struct { - TotalRequests int64 `json:"total_requests"` - TotalInputTokens int64 `json:"total_input_tokens"` - TotalOutputTokens int64 `json:"total_output_tokens"` - TotalCacheTokens int64 `json:"total_cache_tokens"` - TotalTokens int64 `json:"total_tokens"` - TotalCost float64 `json:"total_cost"` - TotalActualCost float64 `json:"total_actual_cost"` - AverageDurationMs float64 `json:"average_duration_ms"` -} +type UsageStats = usagestats.UsageStats // BatchUserUsageStats represents usage stats for a single user -type BatchUserUsageStats struct { - UserID int64 `json:"user_id"` - TodayActualCost float64 `json:"today_actual_cost"` - TotalActualCost float64 `json:"total_actual_cost"` -} +type BatchUserUsageStats = usagestats.BatchUserUsageStats // GetBatchUserUsageStats gets today and total actual_cost for multiple users func (r *UsageLogRepository) GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*BatchUserUsageStats, error) { @@ -834,11 +719,7 @@ func (r *UsageLogRepository) GetBatchUserUsageStats(ctx context.Context, userIDs } // BatchApiKeyUsageStats represents usage stats for a single API key -type BatchApiKeyUsageStats struct { - ApiKeyID int64 `json:"api_key_id"` - TodayActualCost float64 `json:"today_actual_cost"` - TotalActualCost float64 `json:"total_actual_cost"` -} +type BatchApiKeyUsageStats = usagestats.BatchApiKeyUsageStats // GetBatchApiKeyUsageStats gets today and total actual_cost for multiple API keys func (r *UsageLogRepository) GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*BatchApiKeyUsageStats, error) { @@ -1012,53 +893,13 @@ func (r *UsageLogRepository) GetGlobalStats(ctx context.Context, startTime, endT } // AccountUsageHistory represents daily usage history for an account -type AccountUsageHistory struct { - Date string `json:"date"` - Label string `json:"label"` - Requests int64 `json:"requests"` - Tokens int64 `json:"tokens"` - Cost float64 `json:"cost"` - ActualCost float64 `json:"actual_cost"` -} +type AccountUsageHistory = usagestats.AccountUsageHistory // AccountUsageSummary represents summary statistics for an account -type AccountUsageSummary struct { - Days int `json:"days"` - ActualDaysUsed int `json:"actual_days_used"` - TotalCost float64 `json:"total_cost"` - TotalStandardCost float64 `json:"total_standard_cost"` - TotalRequests int64 `json:"total_requests"` - TotalTokens int64 `json:"total_tokens"` - AvgDailyCost float64 `json:"avg_daily_cost"` - AvgDailyRequests float64 `json:"avg_daily_requests"` - AvgDailyTokens float64 `json:"avg_daily_tokens"` - AvgDurationMs float64 `json:"avg_duration_ms"` - Today *struct { - Date string `json:"date"` - Cost float64 `json:"cost"` - Requests int64 `json:"requests"` - Tokens int64 `json:"tokens"` - } `json:"today"` - HighestCostDay *struct { - Date string `json:"date"` - Label string `json:"label"` - Cost float64 `json:"cost"` - Requests int64 `json:"requests"` - } `json:"highest_cost_day"` - HighestRequestDay *struct { - Date string `json:"date"` - Label string `json:"label"` - Requests int64 `json:"requests"` - Cost float64 `json:"cost"` - } `json:"highest_request_day"` -} +type AccountUsageSummary = usagestats.AccountUsageSummary // AccountUsageStatsResponse represents the full usage statistics response for an account -type AccountUsageStatsResponse struct { - History []AccountUsageHistory `json:"history"` - Summary AccountUsageSummary `json:"summary"` - Models []ModelStat `json:"models"` -} +type AccountUsageStatsResponse = usagestats.AccountUsageStatsResponse // GetAccountUsageStats returns comprehensive usage statistics for an account over a time range func (r *UsageLogRepository) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*AccountUsageStatsResponse, error) { diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 280eb4c2..36ee193f 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -8,6 +8,7 @@ import ( "time" "sub2api/internal/model" + "sub2api/internal/pkg/usagestats" "sub2api/internal/service/ports" ) @@ -176,6 +177,14 @@ func (s *AccountUsageService) GetTodayStats(ctx context.Context, accountID int64 }, nil } +func (s *AccountUsageService) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) { + stats, err := s.usageLogRepo.GetAccountUsageStats(ctx, accountID, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("get account usage stats failed: %w", err) + } + return stats, nil +} + // fetchOAuthUsage 从Anthropic API获取OAuth账号的使用量 func (s *AccountUsageService) fetchOAuthUsage(ctx context.Context, account *model.Account) (*UsageInfo, error) { accessToken := account.GetCredential("access_token") diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index 1d047633..80b78069 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -455,3 +455,11 @@ func (s *ApiKeyService) canUserBindGroupInternal(user *model.User, group *model. // 标准类型分组:使用原有逻辑 return user.CanBindGroup(group.ID, group.IsExclusive) } + +func (s *ApiKeyService) SearchApiKeys(ctx context.Context, userID int64, keyword string, limit int) ([]model.ApiKey, error) { + keys, err := s.apiKeyRepo.SearchApiKeys(ctx, userID, keyword, limit) + if err != nil { + return nil, fmt.Errorf("search api keys: %w", err) + } + return keys, nil +} diff --git a/backend/internal/service/dashboard_service.go b/backend/internal/service/dashboard_service.go new file mode 100644 index 00000000..c40eeefa --- /dev/null +++ b/backend/internal/service/dashboard_service.go @@ -0,0 +1,77 @@ +package service + +import ( + "context" + "fmt" + "time" + + "sub2api/internal/pkg/usagestats" + "sub2api/internal/service/ports" +) + +// DashboardService provides aggregated statistics for admin dashboard. +type DashboardService struct { + usageRepo ports.UsageLogRepository +} + +func NewDashboardService(usageRepo ports.UsageLogRepository) *DashboardService { + return &DashboardService{ + usageRepo: usageRepo, + } +} + +func (s *DashboardService) GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) { + stats, err := s.usageRepo.GetDashboardStats(ctx) + if err != nil { + return nil, fmt.Errorf("get dashboard stats: %w", err) + } + return stats, nil +} + +func (s *DashboardService) GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID int64) ([]usagestats.TrendDataPoint, error) { + trend, err := s.usageRepo.GetUsageTrendWithFilters(ctx, startTime, endTime, granularity, userID, apiKeyID) + if err != nil { + return nil, fmt.Errorf("get usage trend with filters: %w", err) + } + return trend, nil +} + +func (s *DashboardService) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID int64) ([]usagestats.ModelStat, error) { + stats, err := s.usageRepo.GetModelStatsWithFilters(ctx, startTime, endTime, userID, apiKeyID, 0) + if err != nil { + return nil, fmt.Errorf("get model stats with filters: %w", err) + } + return stats, nil +} + +func (s *DashboardService) GetApiKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.ApiKeyUsageTrendPoint, error) { + trend, err := s.usageRepo.GetApiKeyUsageTrend(ctx, startTime, endTime, granularity, limit) + if err != nil { + return nil, fmt.Errorf("get api key usage trend: %w", err) + } + return trend, nil +} + +func (s *DashboardService) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error) { + trend, err := s.usageRepo.GetUserUsageTrend(ctx, startTime, endTime, granularity, limit) + if err != nil { + return nil, fmt.Errorf("get user usage trend: %w", err) + } + return trend, nil +} + +func (s *DashboardService) GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*usagestats.BatchUserUsageStats, error) { + stats, err := s.usageRepo.GetBatchUserUsageStats(ctx, userIDs) + if err != nil { + return nil, fmt.Errorf("get batch user usage stats: %w", err) + } + return stats, nil +} + +func (s *DashboardService) GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*usagestats.BatchApiKeyUsageStats, error) { + stats, err := s.usageRepo.GetBatchApiKeyUsageStats(ctx, apiKeyIDs) + if err != nil { + return nil, fmt.Errorf("get batch api key usage stats: %w", err) + } + return stats, nil +} diff --git a/backend/internal/service/ports/usage_log.go b/backend/internal/service/ports/usage_log.go index d8ac8a37..53db06c0 100644 --- a/backend/internal/service/ports/usage_log.go +++ b/backend/internal/service/ports/usage_log.go @@ -25,4 +25,25 @@ type UsageLogRepository interface { GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) + + // Admin dashboard stats + GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) + GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID int64) ([]usagestats.TrendDataPoint, error) + GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID int64) ([]usagestats.ModelStat, error) + GetApiKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.ApiKeyUsageTrendPoint, error) + GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error) + GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*usagestats.BatchUserUsageStats, error) + GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*usagestats.BatchApiKeyUsageStats, error) + + // User dashboard stats + GetUserDashboardStats(ctx context.Context, userID 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) + + // Admin usage listing/stats + ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]model.UsageLog, *pagination.PaginationResult, error) + GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error) + + // Account stats + GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) } diff --git a/backend/internal/service/usage_service.go b/backend/internal/service/usage_service.go index ead44de1..928bd9bb 100644 --- a/backend/internal/service/usage_service.go +++ b/backend/internal/service/usage_service.go @@ -6,6 +6,7 @@ import ( "fmt" "sub2api/internal/model" "sub2api/internal/pkg/pagination" + "sub2api/internal/pkg/usagestats" "sub2api/internal/service/ports" "time" @@ -282,3 +283,57 @@ func (s *UsageService) Delete(ctx context.Context, id int64) error { } return nil } + +// GetUserDashboardStats returns per-user dashboard summary stats. +func (s *UsageService) GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error) { + stats, err := s.usageRepo.GetUserDashboardStats(ctx, userID) + if err != nil { + return nil, fmt.Errorf("get user 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) + if err != nil { + return nil, fmt.Errorf("get user usage trend: %w", err) + } + return trend, nil +} + +// GetUserModelStats returns per-user model usage stats. +func (s *UsageService) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error) { + stats, err := s.usageRepo.GetUserModelStats(ctx, userID, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("get user model stats: %w", err) + } + return stats, nil +} + +// GetBatchApiKeyUsageStats returns today/total actual_cost for given api keys. +func (s *UsageService) GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*usagestats.BatchApiKeyUsageStats, error) { + stats, err := s.usageRepo.GetBatchApiKeyUsageStats(ctx, apiKeyIDs) + if err != nil { + return nil, fmt.Errorf("get batch api key usage stats: %w", err) + } + return stats, nil +} + +// ListWithFilters lists usage logs with admin filters. +func (s *UsageService) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]model.UsageLog, *pagination.PaginationResult, error) { + logs, result, err := s.usageRepo.ListWithFilters(ctx, params, filters) + if err != nil { + return nil, nil, fmt.Errorf("list usage logs with filters: %w", err) + } + return logs, result, nil +} + +// GetGlobalStats returns global usage stats for a time range. +func (s *UsageService) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error) { + stats, err := s.usageRepo.GetGlobalStats(ctx, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("get global usage stats: %w", err) + } + return stats, nil +} diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index fa1c1d8f..f0f0fae7 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -56,6 +56,7 @@ var ProviderSet = wire.NewSet( NewProxyService, NewRedeemService, NewUsageService, + NewDashboardService, ProvidePricingService, NewBillingService, NewBillingCacheService,