diff --git a/backend/internal/handler/admin/ops_realtime_handler.go b/backend/internal/handler/admin/ops_realtime_handler.go index c175dcd0..2d3cce4b 100644 --- a/backend/internal/handler/admin/ops_realtime_handler.go +++ b/backend/internal/handler/admin/ops_realtime_handler.go @@ -65,6 +65,10 @@ func (h *OpsHandler) GetConcurrencyStats(c *gin.Context) { // GetUserConcurrencyStats returns real-time concurrency usage for all active users. // GET /api/v1/admin/ops/user-concurrency +// +// Query params: +// - platform: optional, filter users by allowed platform +// - group_id: optional, filter users by allowed group func (h *OpsHandler) GetUserConcurrencyStats(c *gin.Context) { if h.opsService == nil { response.Error(c, http.StatusServiceUnavailable, "Ops service not available") @@ -84,7 +88,18 @@ func (h *OpsHandler) GetUserConcurrencyStats(c *gin.Context) { return } - users, collectedAt, err := h.opsService.GetUserConcurrencyStats(c.Request.Context()) + platformFilter := strings.TrimSpace(c.Query("platform")) + var groupID *int64 + if v := strings.TrimSpace(c.Query("group_id")); v != "" { + id, err := strconv.ParseInt(v, 10, 64) + if err != nil || id <= 0 { + response.BadRequest(c, "Invalid group_id") + return + } + groupID = &id + } + + users, collectedAt, err := h.opsService.GetUserConcurrencyStats(c.Request.Context(), platformFilter, groupID) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/service/ops_concurrency.go b/backend/internal/service/ops_concurrency.go index f6541d08..faac2d5b 100644 --- a/backend/internal/service/ops_concurrency.go +++ b/backend/internal/service/ops_concurrency.go @@ -344,8 +344,16 @@ func (s *OpsService) getUsersLoadMapBestEffort(ctx context.Context, users []User return out } -// GetUserConcurrencyStats returns real-time concurrency usage for all active users. -func (s *OpsService) GetUserConcurrencyStats(ctx context.Context) (map[int64]*UserConcurrencyInfo, *time.Time, error) { +// GetUserConcurrencyStats returns real-time concurrency usage for active users. +// +// Optional filters: +// - platformFilter: only include users who have access to groups belonging to that platform +// - groupIDFilter: only include users who have access to that specific group +func (s *OpsService) GetUserConcurrencyStats( + ctx context.Context, + platformFilter string, + groupIDFilter *int64, +) (map[int64]*UserConcurrencyInfo, *time.Time, error) { if err := s.RequireMonitoringEnabled(ctx); err != nil { return nil, nil, err } @@ -355,6 +363,15 @@ func (s *OpsService) GetUserConcurrencyStats(ctx context.Context) (map[int64]*Us return nil, nil, err } + // Build a set of allowed group IDs when filtering is requested. + var allowedGroupIDs map[int64]struct{} + if platformFilter != "" || (groupIDFilter != nil && *groupIDFilter > 0) { + allowedGroupIDs, err = s.buildAllowedGroupIDsForFilter(ctx, platformFilter, groupIDFilter) + if err != nil { + return nil, nil, err + } + } + collectedAt := time.Now() loadMap := s.getUsersLoadMapBestEffort(ctx, users) @@ -365,6 +382,12 @@ func (s *OpsService) GetUserConcurrencyStats(ctx context.Context) (map[int64]*Us continue } + // Apply group/platform filter: skip users whose AllowedGroups + // have no intersection with the matching group IDs. + if allowedGroupIDs != nil && !userMatchesGroupFilter(u.AllowedGroups, allowedGroupIDs) { + continue + } + load := loadMap[u.ID] currentInUse := int64(0) waiting := int64(0) @@ -394,3 +417,46 @@ func (s *OpsService) GetUserConcurrencyStats(ctx context.Context) (map[int64]*Us return result, &collectedAt, nil } + +// buildAllowedGroupIDsForFilter returns the set of group IDs that match the given +// platform and/or group ID filter. It reuses listAllAccountsForOps (which already +// supports platform filtering at the DB level) to collect group IDs from accounts. +func (s *OpsService) buildAllowedGroupIDsForFilter(ctx context.Context, platformFilter string, groupIDFilter *int64) (map[int64]struct{}, error) { + // Fast path: only group ID filter, no platform filter needed. + if platformFilter == "" && groupIDFilter != nil && *groupIDFilter > 0 { + return map[int64]struct{}{*groupIDFilter: {}}, nil + } + + // Use the same account-based approach as GetConcurrencyStats to collect group IDs. + accounts, err := s.listAllAccountsForOps(ctx, platformFilter) + if err != nil { + return nil, err + } + + groupIDs := make(map[int64]struct{}) + for _, acc := range accounts { + for _, grp := range acc.Groups { + if grp == nil || grp.ID <= 0 { + continue + } + // If groupIDFilter is set, only include that specific group. + if groupIDFilter != nil && *groupIDFilter > 0 && grp.ID != *groupIDFilter { + continue + } + groupIDs[grp.ID] = struct{}{} + } + } + + return groupIDs, nil +} + +// userMatchesGroupFilter returns true if the user's AllowedGroups contains +// at least one group ID in the allowed set. +func userMatchesGroupFilter(userGroups []int64, allowedGroupIDs map[int64]struct{}) bool { + for _, gid := range userGroups { + if _, ok := allowedGroupIDs[gid]; ok { + return true + } + } + return false +} diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index 9f980a12..523fbd00 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -366,8 +366,16 @@ export async function getConcurrencyStats(platform?: string, groupId?: number | return data } -export async function getUserConcurrencyStats(): Promise { - const { data } = await apiClient.get('/admin/ops/user-concurrency') +export async function getUserConcurrencyStats(platform?: string, groupId?: number | null): Promise { + const params: Record = {} + if (platform) { + params.platform = platform + } + if (typeof groupId === 'number' && groupId > 0) { + params.group_id = groupId + } + + const { data } = await apiClient.get('/admin/ops/user-concurrency', { params }) return data } diff --git a/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue b/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue index ca640ade..0956caa5 100644 --- a/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue +++ b/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue @@ -265,7 +265,7 @@ async function loadData() { try { if (showByUser.value) { // 用户视图模式只加载用户并发数据 - const userData = await opsAPI.getUserConcurrencyStats() + const userData = await opsAPI.getUserConcurrencyStats(props.platformFilter, props.groupIdFilter) userConcurrency.value = userData } else { // 常规模式加载账号/平台/分组数据 @@ -301,6 +301,14 @@ watch( } ) +// 过滤条件变化时重新加载数据 +watch( + [() => props.platformFilter, () => props.groupIdFilter], + () => { + loadData() + } +) + function getLoadBarClass(loadPct: number): string { if (loadPct >= 90) return 'bg-red-500 dark:bg-red-600' if (loadPct >= 70) return 'bg-orange-500 dark:bg-orange-600'