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:
@@ -61,6 +61,7 @@ type AppUser struct {
|
|||||||
IsTrialUser bool `gorm:"default:false" json:"is_trial_user"`
|
IsTrialUser bool `gorm:"default:false" json:"is_trial_user"`
|
||||||
TrialStartAt *time.Time `json:"trial_start_at"`
|
TrialStartAt *time.Time `json:"trial_start_at"`
|
||||||
TrialEndAt *time.Time `json:"trial_end_at"`
|
TrialEndAt *time.Time `json:"trial_end_at"`
|
||||||
|
CreatedBy *uint `gorm:"index" json:"created_by"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ func SetupAgentRoutes(r *gin.RouterGroup) {
|
|||||||
r.GET("/cloud-variables", handleGetCloudVariables)
|
r.GET("/cloud-variables", handleGetCloudVariables)
|
||||||
r.GET("/cloud-variables/:id/records", handleGetCloudVariableRecords)
|
r.GET("/cloud-variables/:id/records", handleGetCloudVariableRecords)
|
||||||
|
|
||||||
r.GET("/devices", admin.HandleGetDevices)
|
r.GET("/devices", handleGetDevices)
|
||||||
r.PUT("/devices/:id/status", admin.HandleUpdateDeviceStatus)
|
r.PUT("/devices/:id/status", admin.HandleUpdateDeviceStatus)
|
||||||
r.DELETE("/devices/:id", admin.HandleUnbindDevice)
|
r.DELETE("/devices/:id", admin.HandleUnbindDevice)
|
||||||
r.POST("/devices/:id/force-offline", admin.HandleForceOfflineDevice)
|
r.POST("/devices/:id/force-offline", admin.HandleForceOfflineDevice)
|
||||||
|
|
||||||
r.GET("/sessions", admin.HandleGetSessions)
|
r.GET("/sessions", handleGetSessions)
|
||||||
r.DELETE("/sessions/:id", admin.HandleDeleteSession)
|
r.DELETE("/sessions/:id", admin.HandleDeleteSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,17 +435,9 @@ func generateCardCode() string {
|
|||||||
func handleGetUsers(c *gin.Context) {
|
func handleGetUsers(c *gin.Context) {
|
||||||
userID := c.GetUint("user_id")
|
userID := c.GetUint("user_id")
|
||||||
|
|
||||||
agentIDs := []uint{userID}
|
appUserIDs := getAgentUserIDs(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cardIDs []uint
|
if len(appUserIDs) == 0 {
|
||||||
database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs)
|
|
||||||
|
|
||||||
if len(cardIDs) == 0 {
|
|
||||||
response.Success(c, gin.H{
|
response.Success(c, gin.H{
|
||||||
"users": []interface{}{},
|
"users": []interface{}{},
|
||||||
"total": 0,
|
"total": 0,
|
||||||
@@ -456,12 +448,6 @@ func handleGetUsers(c *gin.Context) {
|
|||||||
return
|
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 {
|
if len(appUserIDs) == 0 {
|
||||||
response.Success(c, gin.H{
|
response.Success(c, gin.H{
|
||||||
"users": []interface{}{},
|
"users": []interface{}{},
|
||||||
@@ -1099,6 +1085,7 @@ func handleCreateUser(c *gin.Context) {
|
|||||||
Avatar: "",
|
Avatar: "",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
ApplicationID: app.ID,
|
ApplicationID: app.ID,
|
||||||
|
CreatedBy: &userID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(&user).Error; err != nil {
|
if err := tx.Create(&user).Error; err != nil {
|
||||||
@@ -1285,7 +1272,7 @@ func handleUpdateUserStatus(c *gin.Context) {
|
|||||||
response.Success(c, user)
|
response.Success(c, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAgentUserPermission(userID uint, appUserID uint) bool {
|
func getAgentUserIDs(userID uint) []uint {
|
||||||
agentIDs := []uint{userID}
|
agentIDs := []uint{userID}
|
||||||
var childAgents []model.User
|
var childAgents []model.User
|
||||||
database.DB.Where("parent_agent_id = ? AND role = ?", userID, "agent").Find(&childAgents)
|
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
|
var cardIDs []uint
|
||||||
database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs)
|
database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs)
|
||||||
|
|
||||||
if len(cardIDs) == 0 {
|
userIDSet := make(map[uint]bool)
|
||||||
return false
|
|
||||||
|
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
|
var createdUserIDs []uint
|
||||||
database.DB.Model(&model.RechargeRecord{}).
|
database.DB.Model(&model.AppUser{}).
|
||||||
Where("user_id = ? AND card_id IN ? AND status = ?", appUserID, cardIDs, "success").
|
Where("created_by IN ?", agentIDs).
|
||||||
Count(&count)
|
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) {
|
func handleGetUser(c *gin.Context) {
|
||||||
@@ -1373,3 +1397,237 @@ func handleUpdateUser(c *gin.Context) {
|
|||||||
|
|
||||||
response.Success(c, user)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user