package repository import ( "context" "time" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "gorm.io/gorm" ) type usageLogRepository struct { db *gorm.DB } func NewUsageLogRepository(db *gorm.DB) service.UsageLogRepository { return &usageLogRepository{db: db} } // getPerformanceStats 获取 RPM 和 TPM(近5分钟平均值,可选按用户过滤) func (r *usageLogRepository) getPerformanceStats(ctx context.Context, userID int64) (rpm, tpm int64) { fiveMinutesAgo := time.Now().Add(-5 * time.Minute) var perfStats struct { RequestCount int64 `gorm:"column:request_count"` TokenCount int64 `gorm:"column:token_count"` } db := r.db.WithContext(ctx).Model(&usageLogModel{}). Select(` COUNT(*) as request_count, COALESCE(SUM(input_tokens + output_tokens), 0) as token_count `). Where("created_at >= ?", fiveMinutesAgo) if userID > 0 { db = db.Where("user_id = ?", userID) } db.Scan(&perfStats) // 返回5分钟平均值 return perfStats.RequestCount / 5, perfStats.TokenCount / 5 } func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) error { m := usageLogModelFromService(log) err := r.db.WithContext(ctx).Create(m).Error if err == nil { applyUsageLogModelToService(log, m) } return err } func (r *usageLogRepository) GetByID(ctx context.Context, id int64) (*service.UsageLog, error) { var log usageLogModel err := r.db.WithContext(ctx).First(&log, id).Error if err != nil { return nil, translatePersistenceError(err, service.ErrUsageLogNotFound, nil) } return usageLogModelToService(&log), nil } func (r *usageLogRepository) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel var total int64 db := r.db.WithContext(ctx).Model(&usageLogModel{}).Where("user_id = ?", userID) if err := db.Count(&total).Error; err != nil { return nil, nil, err } if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil { return nil, nil, err } return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil } func (r *usageLogRepository) ListByApiKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel var total int64 db := r.db.WithContext(ctx).Model(&usageLogModel{}).Where("api_key_id = ?", apiKeyID) if err := db.Count(&total).Error; err != nil { return nil, nil, err } if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil { return nil, nil, err } return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil } // UserStats 用户使用统计 type UserStats struct { TotalRequests int64 `json:"total_requests"` TotalTokens int64 `json:"total_tokens"` TotalCost float64 `json:"total_cost"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` CacheReadTokens int64 `json:"cache_read_tokens"` } func (r *usageLogRepository) GetUserStats(ctx context.Context, userID int64, startTime, endTime time.Time) (*UserStats, error) { var stats UserStats err := r.db.WithContext(ctx).Model(&usageLogModel{}). Select(` COUNT(*) as total_requests, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, COALESCE(SUM(actual_cost), 0) as total_cost, COALESCE(SUM(input_tokens), 0) as input_tokens, COALESCE(SUM(output_tokens), 0) as output_tokens, COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens `). Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). Scan(&stats).Error return &stats, err } // DashboardStats 仪表盘统计 type DashboardStats = usagestats.DashboardStats func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) { var stats DashboardStats today := timezone.Today() now := time.Now() // 合并用户统计查询 var userStats struct { TotalUsers int64 `gorm:"column:total_users"` TodayNewUsers int64 `gorm:"column:today_new_users"` ActiveUsers int64 `gorm:"column:active_users"` } if err := r.db.WithContext(ctx).Raw(` SELECT COUNT(*) as total_users, COUNT(CASE WHEN created_at >= ? THEN 1 END) as today_new_users, (SELECT COUNT(DISTINCT user_id) FROM usage_logs WHERE created_at >= ?) as active_users FROM users `, today, today).Scan(&userStats).Error; err != nil { return nil, err } stats.TotalUsers = userStats.TotalUsers stats.TodayNewUsers = userStats.TodayNewUsers stats.ActiveUsers = userStats.ActiveUsers // 合并API Key统计查询 var apiKeyStats struct { TotalApiKeys int64 `gorm:"column:total_api_keys"` ActiveApiKeys int64 `gorm:"column:active_api_keys"` } if err := r.db.WithContext(ctx).Raw(` SELECT COUNT(*) as total_api_keys, COUNT(CASE WHEN status = ? THEN 1 END) as active_api_keys FROM api_keys `, service.StatusActive).Scan(&apiKeyStats).Error; err != nil { return nil, err } stats.TotalApiKeys = apiKeyStats.TotalApiKeys stats.ActiveApiKeys = apiKeyStats.ActiveApiKeys // 合并账户统计查询 var accountStats struct { TotalAccounts int64 `gorm:"column:total_accounts"` NormalAccounts int64 `gorm:"column:normal_accounts"` ErrorAccounts int64 `gorm:"column:error_accounts"` RateLimitAccounts int64 `gorm:"column:ratelimit_accounts"` OverloadAccounts int64 `gorm:"column:overload_accounts"` } if err := r.db.WithContext(ctx).Raw(` SELECT COUNT(*) as total_accounts, COUNT(CASE WHEN status = ? AND schedulable = true THEN 1 END) as normal_accounts, COUNT(CASE WHEN status = ? THEN 1 END) as error_accounts, COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > ? THEN 1 END) as ratelimit_accounts, COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > ? THEN 1 END) as overload_accounts FROM accounts `, service.StatusActive, service.StatusError, now, now).Scan(&accountStats).Error; err != nil { return nil, err } stats.TotalAccounts = accountStats.TotalAccounts stats.NormalAccounts = accountStats.NormalAccounts stats.ErrorAccounts = accountStats.ErrorAccounts stats.RateLimitAccounts = accountStats.RateLimitAccounts stats.OverloadAccounts = accountStats.OverloadAccounts // 累计 Token 统计 var totalStats struct { TotalRequests int64 `gorm:"column:total_requests"` TotalInputTokens int64 `gorm:"column:total_input_tokens"` TotalOutputTokens int64 `gorm:"column:total_output_tokens"` TotalCacheCreationTokens int64 `gorm:"column:total_cache_creation_tokens"` TotalCacheReadTokens int64 `gorm:"column:total_cache_read_tokens"` TotalCost float64 `gorm:"column:total_cost"` TotalActualCost float64 `gorm:"column:total_actual_cost"` AverageDurationMs float64 `gorm:"column:avg_duration_ms"` } r.db.WithContext(ctx).Model(&usageLogModel{}). 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 `). Scan(&totalStats) stats.TotalRequests = totalStats.TotalRequests stats.TotalInputTokens = totalStats.TotalInputTokens stats.TotalOutputTokens = totalStats.TotalOutputTokens stats.TotalCacheCreationTokens = totalStats.TotalCacheCreationTokens stats.TotalCacheReadTokens = totalStats.TotalCacheReadTokens stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens stats.TotalCost = totalStats.TotalCost stats.TotalActualCost = totalStats.TotalActualCost stats.AverageDurationMs = totalStats.AverageDurationMs // 今日 Token 统计 var todayStats struct { TodayRequests int64 `gorm:"column:today_requests"` TodayInputTokens int64 `gorm:"column:today_input_tokens"` TodayOutputTokens int64 `gorm:"column:today_output_tokens"` TodayCacheCreationTokens int64 `gorm:"column:today_cache_creation_tokens"` TodayCacheReadTokens int64 `gorm:"column:today_cache_read_tokens"` TodayCost float64 `gorm:"column:today_cost"` TodayActualCost float64 `gorm:"column:today_actual_cost"` } r.db.WithContext(ctx).Model(&usageLogModel{}). 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 `). Where("created_at >= ?", today). Scan(&todayStats) stats.TodayRequests = todayStats.TodayRequests stats.TodayInputTokens = todayStats.TodayInputTokens stats.TodayOutputTokens = todayStats.TodayOutputTokens stats.TodayCacheCreationTokens = todayStats.TodayCacheCreationTokens stats.TodayCacheReadTokens = todayStats.TodayCacheReadTokens stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens stats.TodayCost = todayStats.TodayCost stats.TodayActualCost = todayStats.TodayActualCost // 性能指标:RPM 和 TPM(最近1分钟,全局) stats.Rpm, stats.Tpm = r.getPerformanceStats(ctx, 0) return &stats, nil } func (r *usageLogRepository) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel var total int64 db := r.db.WithContext(ctx).Model(&usageLogModel{}).Where("account_id = ?", accountID) if err := db.Count(&total).Error; err != nil { return nil, nil, err } if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil { return nil, nil, err } return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil } func (r *usageLogRepository) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel err := r.db.WithContext(ctx). Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). Order("id DESC"). Find(&logs).Error return usageLogModelsToService(logs), nil, err } // GetUserStatsAggregated returns aggregated usage statistics for a user using database-level aggregation func (r *usageLogRepository) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) { var stats struct { TotalRequests int64 `gorm:"column:total_requests"` TotalInputTokens int64 `gorm:"column:total_input_tokens"` TotalOutputTokens int64 `gorm:"column:total_output_tokens"` TotalCacheTokens int64 `gorm:"column:total_cache_tokens"` TotalCost float64 `gorm:"column:total_cost"` TotalActualCost float64 `gorm:"column:total_actual_cost"` AverageDurationMs float64 `gorm:"column:avg_duration_ms"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). 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 + cache_read_tokens), 0) as total_cache_tokens, COALESCE(SUM(total_cost), 0) as total_cost, COALESCE(SUM(actual_cost), 0) as total_actual_cost, COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms `). Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). Scan(&stats).Error if err != nil { return nil, err } return &usagestats.UsageStats{ TotalRequests: stats.TotalRequests, TotalInputTokens: stats.TotalInputTokens, TotalOutputTokens: stats.TotalOutputTokens, TotalCacheTokens: stats.TotalCacheTokens, TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens, TotalCost: stats.TotalCost, TotalActualCost: stats.TotalActualCost, AverageDurationMs: stats.AverageDurationMs, }, nil } // GetApiKeyStatsAggregated returns aggregated usage statistics for an API key using database-level aggregation func (r *usageLogRepository) GetApiKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) { var stats struct { TotalRequests int64 `gorm:"column:total_requests"` TotalInputTokens int64 `gorm:"column:total_input_tokens"` TotalOutputTokens int64 `gorm:"column:total_output_tokens"` TotalCacheTokens int64 `gorm:"column:total_cache_tokens"` TotalCost float64 `gorm:"column:total_cost"` TotalActualCost float64 `gorm:"column:total_actual_cost"` AverageDurationMs float64 `gorm:"column:avg_duration_ms"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). 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 + cache_read_tokens), 0) as total_cache_tokens, COALESCE(SUM(total_cost), 0) as total_cost, COALESCE(SUM(actual_cost), 0) as total_actual_cost, COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms `). Where("api_key_id = ? AND created_at >= ? AND created_at < ?", apiKeyID, startTime, endTime). Scan(&stats).Error if err != nil { return nil, err } return &usagestats.UsageStats{ TotalRequests: stats.TotalRequests, TotalInputTokens: stats.TotalInputTokens, TotalOutputTokens: stats.TotalOutputTokens, TotalCacheTokens: stats.TotalCacheTokens, TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens, TotalCost: stats.TotalCost, TotalActualCost: stats.TotalActualCost, AverageDurationMs: stats.AverageDurationMs, }, nil } func (r *usageLogRepository) ListByApiKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel err := r.db.WithContext(ctx). Where("api_key_id = ? AND created_at >= ? AND created_at < ?", apiKeyID, startTime, endTime). Order("id DESC"). Find(&logs).Error return usageLogModelsToService(logs), nil, err } func (r *usageLogRepository) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel err := r.db.WithContext(ctx). Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime). Order("id DESC"). Find(&logs).Error return usageLogModelsToService(logs), nil, err } func (r *usageLogRepository) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel err := r.db.WithContext(ctx). Where("model = ? AND created_at >= ? AND created_at < ?", modelName, startTime, endTime). Order("id DESC"). Find(&logs).Error return usageLogModelsToService(logs), nil, err } func (r *usageLogRepository) Delete(ctx context.Context, id int64) error { return r.db.WithContext(ctx).Delete(&usageLogModel{}, id).Error } // GetAccountTodayStats 获取账号今日统计 func (r *usageLogRepository) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) { today := timezone.Today() var stats struct { Requests int64 `gorm:"column:requests"` Tokens int64 `gorm:"column:tokens"` Cost float64 `gorm:"column:cost"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). Select(` COUNT(*) as requests, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens, COALESCE(SUM(actual_cost), 0) as cost `). Where("account_id = ? AND created_at >= ?", accountID, today). Scan(&stats).Error if err != nil { return nil, err } return &usagestats.AccountStats{ Requests: stats.Requests, Tokens: stats.Tokens, Cost: stats.Cost, }, nil } // GetAccountWindowStats 获取账号时间窗口内的统计 func (r *usageLogRepository) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) { var stats struct { Requests int64 `gorm:"column:requests"` Tokens int64 `gorm:"column:tokens"` Cost float64 `gorm:"column:cost"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). Select(` COUNT(*) as requests, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens, COALESCE(SUM(actual_cost), 0) as cost `). Where("account_id = ? AND created_at >= ?", accountID, startTime). Scan(&stats).Error if err != nil { return nil, err } return &usagestats.AccountStats{ Requests: stats.Requests, Tokens: stats.Tokens, Cost: stats.Cost, }, nil } // TrendDataPoint represents a single point in trend data type TrendDataPoint = usagestats.TrendDataPoint // ModelStat represents usage statistics for a single model type ModelStat = usagestats.ModelStat // UserUsageTrendPoint represents user usage trend data point type UserUsageTrendPoint = usagestats.UserUsageTrendPoint // ApiKeyUsageTrendPoint represents API key usage trend data point 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) { var results []ApiKeyUsageTrendPoint // Choose date format based on granularity var dateFormat string if granularity == "hour" { dateFormat = "YYYY-MM-DD HH24:00" } else { dateFormat = "YYYY-MM-DD" } // Use raw SQL for complex subquery query := ` WITH top_keys AS ( SELECT api_key_id FROM usage_logs WHERE created_at >= ? AND created_at < ? GROUP BY api_key_id ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC LIMIT ? ) SELECT TO_CHAR(u.created_at, '` + dateFormat + `') as date, u.api_key_id, COALESCE(k.name, '') as key_name, COUNT(*) as requests, COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens FROM usage_logs u LEFT JOIN api_keys k ON u.api_key_id = k.id WHERE u.api_key_id IN (SELECT api_key_id FROM top_keys) AND u.created_at >= ? AND u.created_at < ? GROUP BY date, u.api_key_id, k.name ORDER BY date ASC, tokens DESC ` err := r.db.WithContext(ctx).Raw(query, startTime, endTime, limit, startTime, endTime).Scan(&results).Error if err != nil { return nil, err } return results, nil } // GetUserUsageTrend returns usage trend data grouped by user and date func (r *usageLogRepository) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]UserUsageTrendPoint, error) { var results []UserUsageTrendPoint // Choose date format based on granularity var dateFormat string if granularity == "hour" { dateFormat = "YYYY-MM-DD HH24:00" } else { dateFormat = "YYYY-MM-DD" } // Use raw SQL for complex subquery query := ` WITH top_users AS ( SELECT user_id FROM usage_logs WHERE created_at >= ? AND created_at < ? GROUP BY user_id ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC LIMIT ? ) SELECT TO_CHAR(u.created_at, '` + dateFormat + `') as date, u.user_id, COALESCE(us.email, '') as email, COUNT(*) as requests, COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens, COALESCE(SUM(u.total_cost), 0) as cost, COALESCE(SUM(u.actual_cost), 0) as actual_cost FROM usage_logs u LEFT JOIN users us ON u.user_id = us.id WHERE u.user_id IN (SELECT user_id FROM top_users) AND u.created_at >= ? AND u.created_at < ? GROUP BY date, u.user_id, us.email ORDER BY date ASC, tokens DESC ` err := r.db.WithContext(ctx).Raw(query, startTime, endTime, limit, startTime, endTime).Scan(&results).Error if err != nil { return nil, err } return results, nil } // UserDashboardStats 用户仪表盘统计 type UserDashboardStats = usagestats.UserDashboardStats // GetUserDashboardStats 获取用户专属的仪表盘统计 func (r *usageLogRepository) GetUserDashboardStats(ctx context.Context, userID int64) (*UserDashboardStats, error) { var stats UserDashboardStats today := timezone.Today() // API Key 统计 r.db.WithContext(ctx).Model(&apiKeyModel{}). Where("user_id = ?", userID). Count(&stats.TotalApiKeys) r.db.WithContext(ctx).Model(&apiKeyModel{}). Where("user_id = ? AND status = ?", userID, service.StatusActive). Count(&stats.ActiveApiKeys) // 累计 Token 统计 var totalStats struct { TotalRequests int64 `gorm:"column:total_requests"` TotalInputTokens int64 `gorm:"column:total_input_tokens"` TotalOutputTokens int64 `gorm:"column:total_output_tokens"` TotalCacheCreationTokens int64 `gorm:"column:total_cache_creation_tokens"` TotalCacheReadTokens int64 `gorm:"column:total_cache_read_tokens"` TotalCost float64 `gorm:"column:total_cost"` TotalActualCost float64 `gorm:"column:total_actual_cost"` AverageDurationMs float64 `gorm:"column:avg_duration_ms"` } r.db.WithContext(ctx).Model(&usageLogModel{}). 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 `). Where("user_id = ?", userID). Scan(&totalStats) stats.TotalRequests = totalStats.TotalRequests stats.TotalInputTokens = totalStats.TotalInputTokens stats.TotalOutputTokens = totalStats.TotalOutputTokens stats.TotalCacheCreationTokens = totalStats.TotalCacheCreationTokens stats.TotalCacheReadTokens = totalStats.TotalCacheReadTokens stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens stats.TotalCost = totalStats.TotalCost stats.TotalActualCost = totalStats.TotalActualCost stats.AverageDurationMs = totalStats.AverageDurationMs // 今日 Token 统计 var todayStats struct { TodayRequests int64 `gorm:"column:today_requests"` TodayInputTokens int64 `gorm:"column:today_input_tokens"` TodayOutputTokens int64 `gorm:"column:today_output_tokens"` TodayCacheCreationTokens int64 `gorm:"column:today_cache_creation_tokens"` TodayCacheReadTokens int64 `gorm:"column:today_cache_read_tokens"` TodayCost float64 `gorm:"column:today_cost"` TodayActualCost float64 `gorm:"column:today_actual_cost"` } r.db.WithContext(ctx).Model(&usageLogModel{}). 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 `). Where("user_id = ? AND created_at >= ?", userID, today). Scan(&todayStats) stats.TodayRequests = todayStats.TodayRequests stats.TodayInputTokens = todayStats.TodayInputTokens stats.TodayOutputTokens = todayStats.TodayOutputTokens stats.TodayCacheCreationTokens = todayStats.TodayCacheCreationTokens stats.TodayCacheReadTokens = todayStats.TodayCacheReadTokens stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens stats.TodayCost = todayStats.TodayCost stats.TodayActualCost = todayStats.TodayActualCost // 性能指标:RPM 和 TPM(最近1分钟,仅统计该用户的请求) stats.Rpm, stats.Tpm = r.getPerformanceStats(ctx, userID) return &stats, nil } // GetUserUsageTrendByUserID 获取指定用户的使用趋势 func (r *usageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]TrendDataPoint, error) { var results []TrendDataPoint var dateFormat string if granularity == "hour" { dateFormat = "YYYY-MM-DD HH24:00" } else { dateFormat = "YYYY-MM-DD" } err := r.db.WithContext(ctx).Model(&usageLogModel{}). 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("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). Group("date"). Order("date ASC"). Scan(&results).Error if err != nil { return nil, err } return results, nil } // GetUserModelStats 获取指定用户的模型统计 func (r *usageLogRepository) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]ModelStat, error) { var results []ModelStat err := r.db.WithContext(ctx).Model(&usageLogModel{}). 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("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). Group("model"). Order("total_tokens DESC"). Scan(&results).Error if err != nil { return nil, err } return results, nil } // UsageLogFilters represents filters for usage log queries 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) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel var total int64 db := r.db.WithContext(ctx).Model(&usageLogModel{}) // Apply filters if filters.UserID > 0 { db = db.Where("user_id = ?", filters.UserID) } if filters.ApiKeyID > 0 { db = db.Where("api_key_id = ?", filters.ApiKeyID) } if filters.AccountID > 0 { db = db.Where("account_id = ?", filters.AccountID) } if filters.GroupID > 0 { db = db.Where("group_id = ?", filters.GroupID) } if filters.Model != "" { db = db.Where("model = ?", filters.Model) } if filters.Stream != nil { db = db.Where("stream = ?", *filters.Stream) } if filters.BillingType != nil { db = db.Where("billing_type = ?", *filters.BillingType) } if filters.StartTime != nil { db = db.Where("created_at >= ?", *filters.StartTime) } if filters.EndTime != nil { db = db.Where("created_at <= ?", *filters.EndTime) } if err := db.Count(&total).Error; err != nil { return nil, nil, err } // Preload user, api_key, account, and group for display if err := db.Preload("User").Preload("ApiKey").Preload("Account").Preload("Group"). Offset(params.Offset()).Limit(params.Limit()). Order("id DESC").Find(&logs).Error; err != nil { return nil, nil, err } return usageLogModelsToService(logs), paginationResultFromTotal(total, params), nil } // UsageStats represents usage statistics type UsageStats = usagestats.UsageStats // BatchUserUsageStats represents usage stats for a single user 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) { if len(userIDs) == 0 { return make(map[int64]*BatchUserUsageStats), nil } today := timezone.Today() result := make(map[int64]*BatchUserUsageStats) // Initialize result map for _, id := range userIDs { result[id] = &BatchUserUsageStats{UserID: id} } // Get total actual_cost per user var totalStats []struct { UserID int64 `gorm:"column:user_id"` TotalCost float64 `gorm:"column:total_cost"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). Select("user_id, COALESCE(SUM(actual_cost), 0) as total_cost"). Where("user_id IN ?", userIDs). Group("user_id"). Scan(&totalStats).Error if err != nil { return nil, err } for _, stat := range totalStats { if s, ok := result[stat.UserID]; ok { s.TotalActualCost = stat.TotalCost } } // Get today actual_cost per user var todayStats []struct { UserID int64 `gorm:"column:user_id"` TodayCost float64 `gorm:"column:today_cost"` } err = r.db.WithContext(ctx).Model(&usageLogModel{}). Select("user_id, COALESCE(SUM(actual_cost), 0) as today_cost"). Where("user_id IN ? AND created_at >= ?", userIDs, today). Group("user_id"). Scan(&todayStats).Error if err != nil { return nil, err } for _, stat := range todayStats { if s, ok := result[stat.UserID]; ok { s.TodayActualCost = stat.TodayCost } } return result, nil } // BatchApiKeyUsageStats represents usage stats for a single API key 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) { if len(apiKeyIDs) == 0 { return make(map[int64]*BatchApiKeyUsageStats), nil } today := timezone.Today() result := make(map[int64]*BatchApiKeyUsageStats) // Initialize result map for _, id := range apiKeyIDs { result[id] = &BatchApiKeyUsageStats{ApiKeyID: id} } // Get total actual_cost per api key var totalStats []struct { ApiKeyID int64 `gorm:"column:api_key_id"` TotalCost float64 `gorm:"column:total_cost"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). Select("api_key_id, COALESCE(SUM(actual_cost), 0) as total_cost"). Where("api_key_id IN ?", apiKeyIDs). Group("api_key_id"). Scan(&totalStats).Error if err != nil { return nil, err } for _, stat := range totalStats { if s, ok := result[stat.ApiKeyID]; ok { s.TotalActualCost = stat.TotalCost } } // Get today actual_cost per api key var todayStats []struct { ApiKeyID int64 `gorm:"column:api_key_id"` TodayCost float64 `gorm:"column:today_cost"` } err = r.db.WithContext(ctx).Model(&usageLogModel{}). Select("api_key_id, COALESCE(SUM(actual_cost), 0) as today_cost"). Where("api_key_id IN ? AND created_at >= ?", apiKeyIDs, today). Group("api_key_id"). Scan(&todayStats).Error if err != nil { return nil, err } for _, stat := range todayStats { if s, ok := result[stat.ApiKeyID]; ok { s.TodayActualCost = stat.TodayCost } } 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(&usageLogModel{}). 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, accountID int64) ([]ModelStat, error) { var results []ModelStat db := r.db.WithContext(ctx).Model(&usageLogModel{}). 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) } if accountID > 0 { db = db.Where("account_id = ?", accountID) } 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 { TotalRequests int64 `gorm:"column:total_requests"` TotalInputTokens int64 `gorm:"column:total_input_tokens"` TotalOutputTokens int64 `gorm:"column:total_output_tokens"` TotalCacheTokens int64 `gorm:"column:total_cache_tokens"` TotalCost float64 `gorm:"column:total_cost"` TotalActualCost float64 `gorm:"column:total_actual_cost"` AverageDurationMs float64 `gorm:"column:avg_duration_ms"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). 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 + cache_read_tokens), 0) as total_cache_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 `). Where("created_at >= ? AND created_at <= ?", startTime, endTime). Scan(&stats).Error if err != nil { return nil, err } return &UsageStats{ TotalRequests: stats.TotalRequests, TotalInputTokens: stats.TotalInputTokens, TotalOutputTokens: stats.TotalOutputTokens, TotalCacheTokens: stats.TotalCacheTokens, TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens, TotalCost: stats.TotalCost, TotalActualCost: stats.TotalActualCost, AverageDurationMs: stats.AverageDurationMs, }, nil } // AccountUsageHistory represents daily usage history for an account type AccountUsageHistory = usagestats.AccountUsageHistory // AccountUsageSummary represents summary statistics for an account type AccountUsageSummary = usagestats.AccountUsageSummary // AccountUsageStatsResponse represents the full usage statistics response for an account 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) { daysCount := int(endTime.Sub(startTime).Hours()/24) + 1 if daysCount <= 0 { daysCount = 30 } // Get daily history var historyResults []struct { Date string `gorm:"column:date"` Requests int64 `gorm:"column:requests"` Tokens int64 `gorm:"column:tokens"` Cost float64 `gorm:"column:cost"` ActualCost float64 `gorm:"column:actual_cost"` } err := r.db.WithContext(ctx).Model(&usageLogModel{}). Select(` TO_CHAR(created_at, 'YYYY-MM-DD') as date, COUNT(*) as requests, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens, COALESCE(SUM(total_cost), 0) as cost, COALESCE(SUM(actual_cost), 0) as actual_cost `). Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime). Group("date"). Order("date ASC"). Scan(&historyResults).Error if err != nil { return nil, err } // Build history with labels history := make([]AccountUsageHistory, 0, len(historyResults)) for _, h := range historyResults { // Parse date to get label (MM/DD) t, _ := time.Parse("2006-01-02", h.Date) label := t.Format("01/02") history = append(history, AccountUsageHistory{ Date: h.Date, Label: label, Requests: h.Requests, Tokens: h.Tokens, Cost: h.Cost, ActualCost: h.ActualCost, }) } // Calculate summary var totalActualCost, totalStandardCost float64 var totalRequests, totalTokens int64 var highestCostDay, highestRequestDay *AccountUsageHistory for i := range history { h := &history[i] totalActualCost += h.ActualCost totalStandardCost += h.Cost totalRequests += h.Requests totalTokens += h.Tokens if highestCostDay == nil || h.ActualCost > highestCostDay.ActualCost { highestCostDay = h } if highestRequestDay == nil || h.Requests > highestRequestDay.Requests { highestRequestDay = h } } actualDaysUsed := len(history) if actualDaysUsed == 0 { actualDaysUsed = 1 } // Get average duration var avgDuration struct { AvgDurationMs float64 `gorm:"column:avg_duration_ms"` } r.db.WithContext(ctx).Model(&usageLogModel{}). Select("COALESCE(AVG(duration_ms), 0) as avg_duration_ms"). Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime). Scan(&avgDuration) summary := AccountUsageSummary{ Days: daysCount, ActualDaysUsed: actualDaysUsed, TotalCost: totalActualCost, TotalStandardCost: totalStandardCost, TotalRequests: totalRequests, TotalTokens: totalTokens, AvgDailyCost: totalActualCost / float64(actualDaysUsed), AvgDailyRequests: float64(totalRequests) / float64(actualDaysUsed), AvgDailyTokens: float64(totalTokens) / float64(actualDaysUsed), AvgDurationMs: avgDuration.AvgDurationMs, } // Set today's stats todayStr := timezone.Now().Format("2006-01-02") for i := range history { if history[i].Date == todayStr { summary.Today = &struct { Date string `json:"date"` Cost float64 `json:"cost"` Requests int64 `json:"requests"` Tokens int64 `json:"tokens"` }{ Date: history[i].Date, Cost: history[i].ActualCost, Requests: history[i].Requests, Tokens: history[i].Tokens, } break } } // Set highest cost day if highestCostDay != nil { summary.HighestCostDay = &struct { Date string `json:"date"` Label string `json:"label"` Cost float64 `json:"cost"` Requests int64 `json:"requests"` }{ Date: highestCostDay.Date, Label: highestCostDay.Label, Cost: highestCostDay.ActualCost, Requests: highestCostDay.Requests, } } // Set highest request day if highestRequestDay != nil { summary.HighestRequestDay = &struct { Date string `json:"date"` Label string `json:"label"` Requests int64 `json:"requests"` Cost float64 `json:"cost"` }{ Date: highestRequestDay.Date, Label: highestRequestDay.Label, Requests: highestRequestDay.Requests, Cost: highestRequestDay.ActualCost, } } // Get model statistics using the unified method models, err := r.GetModelStatsWithFilters(ctx, startTime, endTime, 0, 0, accountID) if err != nil { models = []ModelStat{} } return &AccountUsageStatsResponse{ History: history, Summary: summary, Models: models, }, nil } type usageLogModel struct { ID int64 `gorm:"primaryKey"` UserID int64 `gorm:"index;not null"` ApiKeyID int64 `gorm:"index;not null"` AccountID int64 `gorm:"index;not null"` RequestID string `gorm:"size:64"` Model string `gorm:"size:100;index;not null"` GroupID *int64 `gorm:"index"` SubscriptionID *int64 `gorm:"index"` InputTokens int `gorm:"default:0;not null"` OutputTokens int `gorm:"default:0;not null"` CacheCreationTokens int `gorm:"default:0;not null"` CacheReadTokens int `gorm:"default:0;not null"` CacheCreation5mTokens int `gorm:"default:0;not null"` CacheCreation1hTokens int `gorm:"default:0;not null"` InputCost float64 `gorm:"type:decimal(20,10);default:0;not null"` OutputCost float64 `gorm:"type:decimal(20,10);default:0;not null"` CacheCreationCost float64 `gorm:"type:decimal(20,10);default:0;not null"` CacheReadCost float64 `gorm:"type:decimal(20,10);default:0;not null"` TotalCost float64 `gorm:"type:decimal(20,10);default:0;not null"` ActualCost float64 `gorm:"type:decimal(20,10);default:0;not null"` RateMultiplier float64 `gorm:"type:decimal(10,4);default:1;not null"` BillingType int8 `gorm:"type:smallint;default:0;not null"` Stream bool `gorm:"default:false;not null"` DurationMs *int FirstTokenMs *int CreatedAt time.Time `gorm:"index;not null"` User *userModel `gorm:"foreignKey:UserID"` ApiKey *apiKeyModel `gorm:"foreignKey:ApiKeyID"` Account *accountModel `gorm:"foreignKey:AccountID"` Group *groupModel `gorm:"foreignKey:GroupID"` Subscription *userSubscriptionModel `gorm:"foreignKey:SubscriptionID"` } func (usageLogModel) TableName() string { return "usage_logs" } func usageLogModelToService(m *usageLogModel) *service.UsageLog { if m == nil { return nil } return &service.UsageLog{ ID: m.ID, UserID: m.UserID, ApiKeyID: m.ApiKeyID, AccountID: m.AccountID, RequestID: m.RequestID, Model: m.Model, GroupID: m.GroupID, SubscriptionID: m.SubscriptionID, InputTokens: m.InputTokens, OutputTokens: m.OutputTokens, CacheCreationTokens: m.CacheCreationTokens, CacheReadTokens: m.CacheReadTokens, CacheCreation5mTokens: m.CacheCreation5mTokens, CacheCreation1hTokens: m.CacheCreation1hTokens, InputCost: m.InputCost, OutputCost: m.OutputCost, CacheCreationCost: m.CacheCreationCost, CacheReadCost: m.CacheReadCost, TotalCost: m.TotalCost, ActualCost: m.ActualCost, RateMultiplier: m.RateMultiplier, BillingType: m.BillingType, Stream: m.Stream, DurationMs: m.DurationMs, FirstTokenMs: m.FirstTokenMs, CreatedAt: m.CreatedAt, User: userModelToService(m.User), ApiKey: apiKeyModelToService(m.ApiKey), Account: accountModelToService(m.Account), Group: groupModelToService(m.Group), Subscription: userSubscriptionModelToService(m.Subscription), } } func usageLogModelsToService(models []usageLogModel) []service.UsageLog { out := make([]service.UsageLog, 0, len(models)) for i := range models { if s := usageLogModelToService(&models[i]); s != nil { out = append(out, *s) } } return out } func usageLogModelFromService(log *service.UsageLog) *usageLogModel { if log == nil { return nil } return &usageLogModel{ ID: log.ID, UserID: log.UserID, ApiKeyID: log.ApiKeyID, AccountID: log.AccountID, RequestID: log.RequestID, Model: log.Model, GroupID: log.GroupID, SubscriptionID: log.SubscriptionID, InputTokens: log.InputTokens, OutputTokens: log.OutputTokens, CacheCreationTokens: log.CacheCreationTokens, CacheReadTokens: log.CacheReadTokens, CacheCreation5mTokens: log.CacheCreation5mTokens, CacheCreation1hTokens: log.CacheCreation1hTokens, InputCost: log.InputCost, OutputCost: log.OutputCost, CacheCreationCost: log.CacheCreationCost, CacheReadCost: log.CacheReadCost, TotalCost: log.TotalCost, ActualCost: log.ActualCost, RateMultiplier: log.RateMultiplier, BillingType: log.BillingType, Stream: log.Stream, DurationMs: log.DurationMs, FirstTokenMs: log.FirstTokenMs, CreatedAt: log.CreatedAt, } } func applyUsageLogModelToService(log *service.UsageLog, m *usageLogModel) { if log == nil || m == nil { return } log.ID = m.ID log.CreatedAt = m.CreatedAt }