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
This commit is contained in:
2026-05-11 11:53:43 +08:00
parent 853c22fdbe
commit ed73bb470c
2 changed files with 285 additions and 26 deletions
+1
View File
@@ -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:"-"`
+284 -26
View File
@@ -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,
})
}