perf(backend): 优化数据库查询性能
- 合并多个独立查询为单个SQL查询 - 减少数据库往返次数 - 提升仪表板统计数据获取效率
This commit is contained in:
@@ -129,51 +129,67 @@ type DashboardStats = usagestats.DashboardStats
|
|||||||
func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) {
|
func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) {
|
||||||
var stats DashboardStats
|
var stats DashboardStats
|
||||||
today := timezone.Today()
|
today := timezone.Today()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
// 总用户数
|
// 合并用户统计查询
|
||||||
r.db.WithContext(ctx).Model(&userModel{}).Count(&stats.TotalUsers)
|
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统计查询
|
||||||
r.db.WithContext(ctx).Model(&userModel{}).
|
var apiKeyStats struct {
|
||||||
Where("created_at >= ?", today).
|
TotalApiKeys int64 `gorm:"column:total_api_keys"`
|
||||||
Count(&stats.TodayNewUsers)
|
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
|
||||||
|
|
||||||
// 今日活跃用户数 (今日有请求的用户)
|
// 合并账户统计查询
|
||||||
r.db.WithContext(ctx).Model(&usageLogModel{}).
|
var accountStats struct {
|
||||||
Distinct("user_id").
|
TotalAccounts int64 `gorm:"column:total_accounts"`
|
||||||
Where("created_at >= ?", today).
|
NormalAccounts int64 `gorm:"column:normal_accounts"`
|
||||||
Count(&stats.ActiveUsers)
|
ErrorAccounts int64 `gorm:"column:error_accounts"`
|
||||||
|
RateLimitAccounts int64 `gorm:"column:ratelimit_accounts"`
|
||||||
// 总 API Key 数
|
OverloadAccounts int64 `gorm:"column:overload_accounts"`
|
||||||
r.db.WithContext(ctx).Model(&apiKeyModel{}).Count(&stats.TotalApiKeys)
|
}
|
||||||
|
if err := r.db.WithContext(ctx).Raw(`
|
||||||
// 活跃 API Key 数
|
SELECT
|
||||||
r.db.WithContext(ctx).Model(&apiKeyModel{}).
|
COUNT(*) as total_accounts,
|
||||||
Where("status = ?", service.StatusActive).
|
COUNT(CASE WHEN status = ? AND schedulable = true THEN 1 END) as normal_accounts,
|
||||||
Count(&stats.ActiveApiKeys)
|
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
|
||||||
r.db.WithContext(ctx).Model(&accountModel{}).Count(&stats.TotalAccounts)
|
FROM accounts
|
||||||
|
`, service.StatusActive, service.StatusError, now, now).Scan(&accountStats).Error; err != nil {
|
||||||
// 正常账户数 (schedulable=true, status=active)
|
return nil, err
|
||||||
r.db.WithContext(ctx).Model(&accountModel{}).
|
}
|
||||||
Where("status = ? AND schedulable = ?", service.StatusActive, true).
|
stats.TotalAccounts = accountStats.TotalAccounts
|
||||||
Count(&stats.NormalAccounts)
|
stats.NormalAccounts = accountStats.NormalAccounts
|
||||||
|
stats.ErrorAccounts = accountStats.ErrorAccounts
|
||||||
// 异常账户数 (status=error)
|
stats.RateLimitAccounts = accountStats.RateLimitAccounts
|
||||||
r.db.WithContext(ctx).Model(&accountModel{}).
|
stats.OverloadAccounts = accountStats.OverloadAccounts
|
||||||
Where("status = ?", service.StatusError).
|
|
||||||
Count(&stats.ErrorAccounts)
|
|
||||||
|
|
||||||
// 限流账户数
|
|
||||||
r.db.WithContext(ctx).Model(&accountModel{}).
|
|
||||||
Where("rate_limited_at IS NOT NULL AND rate_limit_reset_at > ?", time.Now()).
|
|
||||||
Count(&stats.RateLimitAccounts)
|
|
||||||
|
|
||||||
// 过载账户数
|
|
||||||
r.db.WithContext(ctx).Model(&accountModel{}).
|
|
||||||
Where("overload_until IS NOT NULL AND overload_until > ?", time.Now()).
|
|
||||||
Count(&stats.OverloadAccounts)
|
|
||||||
|
|
||||||
// 累计 Token 统计
|
// 累计 Token 统计
|
||||||
var totalStats struct {
|
var totalStats struct {
|
||||||
@@ -273,6 +289,88 @@ func (r *usageLogRepository) ListByUserAndTimeRange(ctx context.Context, userID
|
|||||||
return usageLogModelsToService(logs), nil, err
|
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) {
|
func (r *usageLogRepository) ListByApiKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||||
var logs []usageLogModel
|
var logs []usageLogModel
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
|
|||||||
Reference in New Issue
Block a user