diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 4f93f702..14f9e05d 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -240,77 +240,77 @@ func (h *AccountHandler) List(c *gin.Context) { var windowCosts map[int64]float64 var activeSessions map[int64]int var rpmCounts map[int64]int - if !lite { - // Get current concurrency counts for all accounts - if h.concurrencyService != nil { - if cc, ccErr := h.concurrencyService.GetAccountConcurrencyBatch(c.Request.Context(), accountIDs); ccErr == nil && cc != nil { - concurrencyCounts = cc + + // 始终获取并发数(Redis ZCARD,极低开销) + if h.concurrencyService != nil { + if cc, ccErr := h.concurrencyService.GetAccountConcurrencyBatch(c.Request.Context(), accountIDs); ccErr == nil && cc != nil { + concurrencyCounts = cc + } + } + + // 识别需要查询窗口费用、会话数和 RPM 的账号(Anthropic OAuth/SetupToken 且启用了相应功能) + windowCostAccountIDs := make([]int64, 0) + sessionLimitAccountIDs := make([]int64, 0) + rpmAccountIDs := make([]int64, 0) + sessionIdleTimeouts := make(map[int64]time.Duration) // 各账号的会话空闲超时配置 + for i := range accounts { + acc := &accounts[i] + if acc.IsAnthropicOAuthOrSetupToken() { + if acc.GetWindowCostLimit() > 0 { + windowCostAccountIDs = append(windowCostAccountIDs, acc.ID) + } + if acc.GetMaxSessions() > 0 { + sessionLimitAccountIDs = append(sessionLimitAccountIDs, acc.ID) + sessionIdleTimeouts[acc.ID] = time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute + } + if acc.GetBaseRPM() > 0 { + rpmAccountIDs = append(rpmAccountIDs, acc.ID) } } - // 识别需要查询窗口费用、会话数和 RPM 的账号(Anthropic OAuth/SetupToken 且启用了相应功能) - windowCostAccountIDs := make([]int64, 0) - sessionLimitAccountIDs := make([]int64, 0) - rpmAccountIDs := make([]int64, 0) - sessionIdleTimeouts := make(map[int64]time.Duration) // 各账号的会话空闲超时配置 + } + + // 始终获取 RPM 计数(Redis GET,极低开销) + if len(rpmAccountIDs) > 0 && h.rpmCache != nil { + rpmCounts, _ = h.rpmCache.GetRPMBatch(c.Request.Context(), rpmAccountIDs) + if rpmCounts == nil { + rpmCounts = make(map[int64]int) + } + } + + // 始终获取活跃会话数(Redis ZCARD,低开销) + if len(sessionLimitAccountIDs) > 0 && h.sessionLimitCache != nil { + activeSessions, _ = h.sessionLimitCache.GetActiveSessionCountBatch(c.Request.Context(), sessionLimitAccountIDs, sessionIdleTimeouts) + if activeSessions == nil { + activeSessions = make(map[int64]int) + } + } + + // 仅非 lite 模式获取窗口费用(PostgreSQL 聚合查询,高开销) + if !lite && len(windowCostAccountIDs) > 0 { + windowCosts = make(map[int64]float64) + var mu sync.Mutex + g, gctx := errgroup.WithContext(c.Request.Context()) + g.SetLimit(10) // 限制并发数 + for i := range accounts { acc := &accounts[i] - if acc.IsAnthropicOAuthOrSetupToken() { - if acc.GetWindowCostLimit() > 0 { - windowCostAccountIDs = append(windowCostAccountIDs, acc.ID) - } - if acc.GetMaxSessions() > 0 { - sessionLimitAccountIDs = append(sessionLimitAccountIDs, acc.ID) - sessionIdleTimeouts[acc.ID] = time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute - } - if acc.GetBaseRPM() > 0 { - rpmAccountIDs = append(rpmAccountIDs, acc.ID) - } + if !acc.IsAnthropicOAuthOrSetupToken() || acc.GetWindowCostLimit() <= 0 { + continue } - } - - // 获取 RPM 计数(批量查询) - if len(rpmAccountIDs) > 0 && h.rpmCache != nil { - rpmCounts, _ = h.rpmCache.GetRPMBatch(c.Request.Context(), rpmAccountIDs) - if rpmCounts == nil { - rpmCounts = make(map[int64]int) - } - } - - // 获取活跃会话数(批量查询,传入各账号的 idleTimeout 配置) - if len(sessionLimitAccountIDs) > 0 && h.sessionLimitCache != nil { - activeSessions, _ = h.sessionLimitCache.GetActiveSessionCountBatch(c.Request.Context(), sessionLimitAccountIDs, sessionIdleTimeouts) - if activeSessions == nil { - activeSessions = make(map[int64]int) - } - } - - // 获取窗口费用(并行查询) - if len(windowCostAccountIDs) > 0 { - windowCosts = make(map[int64]float64) - var mu sync.Mutex - g, gctx := errgroup.WithContext(c.Request.Context()) - g.SetLimit(10) // 限制并发数 - - for i := range accounts { - acc := &accounts[i] - if !acc.IsAnthropicOAuthOrSetupToken() || acc.GetWindowCostLimit() <= 0 { - continue + accCopy := acc // 闭包捕获 + g.Go(func() error { + // 使用统一的窗口开始时间计算逻辑(考虑窗口过期情况) + startTime := accCopy.GetCurrentWindowStartTime() + stats, err := h.accountUsageService.GetAccountWindowStats(gctx, accCopy.ID, startTime) + if err == nil && stats != nil { + mu.Lock() + windowCosts[accCopy.ID] = stats.StandardCost // 使用标准费用 + mu.Unlock() } - accCopy := acc // 闭包捕获 - g.Go(func() error { - // 使用统一的窗口开始时间计算逻辑(考虑窗口过期情况) - startTime := accCopy.GetCurrentWindowStartTime() - stats, err := h.accountUsageService.GetAccountWindowStats(gctx, accCopy.ID, startTime) - if err == nil && stats != nil { - mu.Lock() - windowCosts[accCopy.ID] = stats.StandardCost // 使用标准费用 - mu.Unlock() - } - return nil // 不返回错误,允许部分失败 - }) - } - _ = g.Wait() + return nil // 不返回错误,允许部分失败 + }) } + _ = g.Wait() } // Build response with concurrency info diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 1616fe21..39d723be 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -546,18 +546,27 @@ const { handlePageSizeChange: baseHandlePageSizeChange } = useTableLoader({ fetchFn: adminAPI.accounts.list, - initialParams: { platform: '', type: '', status: '', group: '', search: '', lite: '1' } + initialParams: { platform: '', type: '', status: '', group: '', search: '' } }) const resetAutoRefreshCache = () => { autoRefreshETag.value = null } +const isFirstLoad = ref(true) + const load = async () => { hasPendingListSync.value = false resetAutoRefreshCache() pendingTodayStatsRefresh.value = false + if (isFirstLoad.value) { + ;(params as any).lite = '1' + } await baseLoad() + if (isFirstLoad.value) { + isFirstLoad.value = false + delete (params as any).lite + } await refreshTodayStatsBatch() } @@ -689,7 +698,7 @@ const refreshAccountsIncrementally = async () => { type?: string status?: string search?: string - lite?: string + }, { etag: autoRefreshETag.value } )