From ed73bb470cbd851c37b69d532df02b9f0ae891d1 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 11 May 2026 11:53:43 +0800 Subject: [PATCH] feat: agent devices/sessions filter by agent-associated users only - Add created_by field to AppUser model to track who created the user - Set created_by when agent creates users - Extract getAgentUserIDs() helper: finds users who recharged with agent/sub-agent cards OR were created by agent/sub-agent - Extract getAgentAppIDs() helper: finds agent's authorized applications - Refactor handleGetUsers and checkAgentUserPermission to use getAgentUserIDs - Create agent-specific handleGetDevices: filter by agent user IDs + app IDs - Create agent-specific handleGetSessions: filter by agent user IDs + app IDs - Replace admin handler references with agent-specific handlers for GET routes --- backend/internal/model/models.go | 1 + backend/internal/router/agent/agent.go | 310 ++++++++++++++++++++++--- 2 files changed, 285 insertions(+), 26 deletions(-) diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index f76acd0..a640590 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -61,6 +61,7 @@ type AppUser struct { IsTrialUser bool `gorm:"default:false" json:"is_trial_user"` TrialStartAt *time.Time `json:"trial_start_at"` TrialEndAt *time.Time `json:"trial_end_at"` + CreatedBy *uint `gorm:"index" json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/backend/internal/router/agent/agent.go b/backend/internal/router/agent/agent.go index 2c495ca..eb7ab92 100644 --- a/backend/internal/router/agent/agent.go +++ b/backend/internal/router/agent/agent.go @@ -37,12 +37,12 @@ func SetupAgentRoutes(r *gin.RouterGroup) { r.GET("/cloud-variables", handleGetCloudVariables) r.GET("/cloud-variables/:id/records", handleGetCloudVariableRecords) - r.GET("/devices", admin.HandleGetDevices) + r.GET("/devices", handleGetDevices) r.PUT("/devices/:id/status", admin.HandleUpdateDeviceStatus) r.DELETE("/devices/:id", admin.HandleUnbindDevice) r.POST("/devices/:id/force-offline", admin.HandleForceOfflineDevice) - r.GET("/sessions", admin.HandleGetSessions) + r.GET("/sessions", handleGetSessions) r.DELETE("/sessions/:id", admin.HandleDeleteSession) } @@ -435,17 +435,9 @@ func generateCardCode() string { func handleGetUsers(c *gin.Context) { userID := c.GetUint("user_id") - agentIDs := []uint{userID} - var childAgents []model.User - database.DB.Where("parent_agent_id = ? AND role = ?", userID, "agent").Find(&childAgents) - for _, child := range childAgents { - agentIDs = append(agentIDs, child.ID) - } + appUserIDs := getAgentUserIDs(userID) - var cardIDs []uint - database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs) - - if len(cardIDs) == 0 { + if len(appUserIDs) == 0 { response.Success(c, gin.H{ "users": []interface{}{}, "total": 0, @@ -456,12 +448,6 @@ func handleGetUsers(c *gin.Context) { return } - var appUserIDs []uint - database.DB.Model(&model.RechargeRecord{}). - Where("card_id IN ? AND status = ?", cardIDs, "success"). - Distinct("user_id"). - Pluck("user_id", &appUserIDs) - if len(appUserIDs) == 0 { response.Success(c, gin.H{ "users": []interface{}{}, @@ -1099,6 +1085,7 @@ func handleCreateUser(c *gin.Context) { Avatar: "", Status: "active", ApplicationID: app.ID, + CreatedBy: &userID, } if err := tx.Create(&user).Error; err != nil { @@ -1285,7 +1272,7 @@ func handleUpdateUserStatus(c *gin.Context) { response.Success(c, user) } -func checkAgentUserPermission(userID uint, appUserID uint) bool { +func getAgentUserIDs(userID uint) []uint { agentIDs := []uint{userID} var childAgents []model.User database.DB.Where("parent_agent_id = ? AND role = ?", userID, "agent").Find(&childAgents) @@ -1296,16 +1283,53 @@ func checkAgentUserPermission(userID uint, appUserID uint) bool { var cardIDs []uint database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs) - if len(cardIDs) == 0 { - return false + userIDSet := make(map[uint]bool) + + if len(cardIDs) > 0 { + var rechargedUserIDs []uint + database.DB.Model(&model.RechargeRecord{}). + Where("card_id IN ? AND status = ?", cardIDs, "success"). + Distinct("user_id"). + Pluck("user_id", &rechargedUserIDs) + for _, id := range rechargedUserIDs { + userIDSet[id] = true + } } - var count int64 - database.DB.Model(&model.RechargeRecord{}). - Where("user_id = ? AND card_id IN ? AND status = ?", appUserID, cardIDs, "success"). - Count(&count) + var createdUserIDs []uint + database.DB.Model(&model.AppUser{}). + Where("created_by IN ?", agentIDs). + Pluck("id", &createdUserIDs) + for _, id := range createdUserIDs { + userIDSet[id] = true + } - return count > 0 + result := make([]uint, 0, len(userIDSet)) + for id := range userIDSet { + result = append(result, id) + } + return result +} + +func getAgentAppIDs(userID uint) []uint { + var agentApps []model.AgentApplication + database.DB.Where("agent_id = ? AND is_received = ?", userID, true).Find(&agentApps) + + appIDs := make([]uint, 0, len(agentApps)) + for _, agentApp := range agentApps { + appIDs = append(appIDs, agentApp.ApplicationID) + } + return appIDs +} + +func checkAgentUserPermission(userID uint, appUserID uint) bool { + agentUserIDs := getAgentUserIDs(userID) + for _, id := range agentUserIDs { + if id == appUserID { + return true + } + } + return false } func handleGetUser(c *gin.Context) { @@ -1373,3 +1397,237 @@ func handleUpdateUser(c *gin.Context) { response.Success(c, user) } + +func handleGetDevices(c *gin.Context) { + userID := c.GetUint("user_id") + + appUserIDs := getAgentUserIDs(userID) + if len(appUserIDs) == 0 { + response.Success(c, gin.H{ + "devices": []interface{}{}, + "total": 0, + "banned_count": 0, + }) + return + } + + appIDs := getAgentAppIDs(userID) + if len(appIDs) == 0 { + response.Success(c, gin.H{ + "devices": []interface{}{}, + "total": 0, + "banned_count": 0, + }) + return + } + + statusFilter := c.Query("status") + applicationIDFilter := c.Query("application_id") + + query := database.DB.Model(&model.UserDevice{}). + Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs) + + if statusFilter != "" { + query = query.Where("status = ?", statusFilter) + } + if applicationIDFilter != "" { + appID, _ := strconv.Atoi(applicationIDFilter) + if appID > 0 { + query = query.Where("application_id = ?", appID) + } + } + + var total int64 + query.Count(&total) + + var bannedCount int64 + database.DB.Model(&model.UserDevice{}). + Where("user_id IN ? AND application_id IN ? AND status = ?", appUserIDs, appIDs, "banned"). + Count(&bannedCount) + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + offset := (page - 1) * pageSize + + var devices []model.UserDevice + findQuery := database.DB.Preload("User").Preload("Application"). + Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs) + + if statusFilter != "" { + findQuery = findQuery.Where("status = ?", statusFilter) + } + if applicationIDFilter != "" { + appID, _ := strconv.Atoi(applicationIDFilter) + if appID > 0 { + findQuery = findQuery.Where("application_id = ?", appID) + } + } + + if err := findQuery.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&devices).Error; err != nil { + response.Error(c, 500, "获取设备列表失败") + return + } + + deviceIDs := make([]uint, len(devices)) + for i, d := range devices { + deviceIDs[i] = d.ID + } + + onlineSessionMap := make(map[uint]int) + if len(deviceIDs) > 0 { + type SessionCountResult struct { + DeviceID uint + Count int + } + var sessionCounts []SessionCountResult + database.DB.Model(&model.DeviceSession{}). + Select("device_id, COUNT(*) as count"). + Where("device_id IN ? AND last_heartbeat > ?", deviceIDs, time.Now().Add(-5*time.Minute)). + Group("device_id"). + Find(&sessionCounts) + for _, sc := range sessionCounts { + onlineSessionMap[sc.DeviceID] = sc.Count + } + } + + type DeviceWithDetails struct { + model.UserDevice + OnlineSessions int `json:"online_sessions"` + } + + result := make([]DeviceWithDetails, 0, len(devices)) + for _, device := range devices { + result = append(result, DeviceWithDetails{ + UserDevice: device, + OnlineSessions: onlineSessionMap[device.ID], + }) + } + + response.Success(c, gin.H{ + "devices": result, + "total": total, + "page": page, + "page_size": pageSize, + "total_page": (total + int64(pageSize) - 1) / int64(pageSize), + "banned_count": bannedCount, + }) +} + +func handleGetSessions(c *gin.Context) { + userID := c.GetUint("user_id") + + appUserIDs := getAgentUserIDs(userID) + if len(appUserIDs) == 0 { + response.Success(c, gin.H{ + "sessions": []interface{}{}, + "total": 0, + }) + return + } + + appIDs := getAgentAppIDs(userID) + if len(appIDs) == 0 { + response.Success(c, gin.H{ + "sessions": []interface{}{}, + "total": 0, + }) + return + } + + appIDFilter := c.Query("app_id") + searchFilter := c.Query("search") + + query := database.DB.Model(&model.DeviceSession{}). + Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs) + + if appIDFilter != "" { + appID, _ := strconv.Atoi(appIDFilter) + if appID > 0 { + query = query.Where("application_id = ?", appID) + } + } + + var total int64 + query.Count(&total) + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + offset := (page - 1) * pageSize + + var sessions []model.DeviceSession + findQuery := database.DB.Where("user_id IN ? AND application_id IN ?", appUserIDs, appIDs) + + if appIDFilter != "" { + appID, _ := strconv.Atoi(appIDFilter) + if appID > 0 { + findQuery = findQuery.Where("application_id = ?", appID) + } + } + + if err := findQuery.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&sessions).Error; err != nil { + response.Error(c, 500, "获取会话列表失败") + return + } + + type SessionWithDetails struct { + model.DeviceSession + DeviceID string `json:"device_identifier"` + DeviceName string `json:"device_name"` + Username string `json:"username"` + AppName string `json:"app_name"` + IsOnline bool `json:"is_online"` + } + + result := make([]SessionWithDetails, 0, len(sessions)) + for _, session := range sessions { + var device model.UserDevice + if err := database.DB.First(&device, session.DeviceID).Error; err != nil { + continue + } + + var user model.AppUser + if err := database.DB.First(&user, session.UserID).Error; err != nil { + continue + } + + var app model.Application + if err := database.DB.First(&app, session.ApplicationID).Error; err != nil { + continue + } + + heartbeatTimeout := app.HeartbeatTimeout + if heartbeatTimeout == 0 { + heartbeatTimeout = 300 + } + timeoutThreshold := time.Now().Add(-time.Duration(heartbeatTimeout) * time.Second) + isOnline := session.LastHeartbeat != nil && session.LastHeartbeat.After(timeoutThreshold) + + if searchFilter != "" { + searchLower := strings.ToLower(searchFilter) + instanceIDMatch := strings.Contains(strings.ToLower(session.InstanceID), searchLower) + deviceIDMatch := strings.Contains(strings.ToLower(device.DeviceID), searchLower) + usernameMatch := strings.Contains(strings.ToLower(user.Username), searchLower) + deviceNameMatch := strings.Contains(strings.ToLower(device.DeviceName), searchLower) + + if !instanceIDMatch && !deviceIDMatch && !usernameMatch && !deviceNameMatch { + continue + } + } + + result = append(result, SessionWithDetails{ + DeviceSession: session, + DeviceID: device.DeviceID, + DeviceName: device.DeviceName, + Username: user.Username, + AppName: app.Name, + IsOnline: isOnline, + }) + } + + response.Success(c, gin.H{ + "sessions": result, + "total": total, + "page": page, + "page_size": pageSize, + }) +}