feat: add balance display, user columns, device mgmt and sessions for agent
- Backend: add balance to agent stats API - Backend: return full user details (balance, expiry, online_status, device_count, application) in agent users API - Backend: export admin device/session handlers, add agent routes for devices and sessions - Backend: add card type permission check and balance deduction for agent user creation - Backend: extract checkAgentUserPermission helper, add user edit API - Frontend: add balance card to agent dashboard - Frontend: add application, online_status, device_count, balance, expiry_at columns to agent user table - Frontend: add agent devices page (reuse admin data-table component) - Frontend: add agent sessions page (reuse admin data-table component) - Frontend: add devices and sessions to agent navigation and routes - Add i18n keys for agent devices, sessions, user columns, dashboard balance
This commit is contained in:
@@ -22,22 +22,22 @@ type DeviceWithDetails struct {
|
||||
func SetupDeviceRoutes(r *gin.RouterGroup) {
|
||||
devices := r.Group("/devices")
|
||||
{
|
||||
devices.GET("", handleGetDevices)
|
||||
devices.PUT("/:id/status", handleUpdateDeviceStatus)
|
||||
devices.DELETE("/:id", handleUnbindDevice)
|
||||
devices.DELETE("/batch", handleBatchUnbindDevices)
|
||||
devices.POST("/batch/status", handleBatchUpdateDeviceStatus)
|
||||
devices.POST("/:id/force-offline", handleForceOfflineDevice)
|
||||
devices.GET("", HandleGetDevices)
|
||||
devices.PUT("/:id/status", HandleUpdateDeviceStatus)
|
||||
devices.DELETE("/:id", HandleUnbindDevice)
|
||||
devices.DELETE("/batch", HandleBatchUnbindDevices)
|
||||
devices.POST("/batch/status", HandleBatchUpdateDeviceStatus)
|
||||
devices.POST("/:id/force-offline", HandleForceOfflineDevice)
|
||||
}
|
||||
|
||||
sessions := r.Group("/sessions")
|
||||
{
|
||||
sessions.GET("", handleGetSessions)
|
||||
sessions.DELETE("/:id", handleDeleteSession)
|
||||
sessions.GET("", HandleGetSessions)
|
||||
sessions.DELETE("/:id", HandleDeleteSession)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetDevices(c *gin.Context) {
|
||||
func HandleGetDevices(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
log.Printf("[DEBUG] handleGetDevices called, userID: %d", userID)
|
||||
|
||||
@@ -180,7 +180,7 @@ func handleGetDevices(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateDeviceStatus(c *gin.Context) {
|
||||
func HandleUpdateDeviceStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceID := c.Param("id")
|
||||
|
||||
@@ -221,7 +221,7 @@ func handleUpdateDeviceStatus(c *gin.Context) {
|
||||
response.Success(c, device)
|
||||
}
|
||||
|
||||
func handleUnbindDevice(c *gin.Context) {
|
||||
func HandleUnbindDevice(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceID := c.Param("id")
|
||||
|
||||
@@ -257,7 +257,7 @@ func handleUnbindDevice(c *gin.Context) {
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchUnbindDevices(c *gin.Context) {
|
||||
func HandleBatchUnbindDevices(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
DeviceIDs []uint `json:"device_ids"`
|
||||
@@ -303,7 +303,7 @@ func handleBatchUnbindDevices(c *gin.Context) {
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleBatchUpdateDeviceStatus(c *gin.Context) {
|
||||
func HandleBatchUpdateDeviceStatus(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
var req struct {
|
||||
DeviceIDs []uint `json:"device_ids"`
|
||||
@@ -356,7 +356,7 @@ type SessionWithDetails struct {
|
||||
IsOnline bool `json:"is_online"`
|
||||
}
|
||||
|
||||
func handleGetSessions(c *gin.Context) {
|
||||
func HandleGetSessions(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceIDFilter := c.Query("device_id")
|
||||
appIDFilter := c.Query("app_id")
|
||||
@@ -478,7 +478,7 @@ func handleGetSessions(c *gin.Context) {
|
||||
response.Success(c, gin.H{"sessions": sessionsWithDetails, "total": total, "page": page, "page_size": pageSize})
|
||||
}
|
||||
|
||||
func handleDeleteSession(c *gin.Context) {
|
||||
func HandleDeleteSession(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
sessionID := c.Param("id")
|
||||
|
||||
@@ -529,7 +529,7 @@ func handleDeleteSession(c *gin.Context) {
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handleForceOfflineDevice(c *gin.Context) {
|
||||
func HandleForceOfflineDevice(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
deviceID := c.Param("id")
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ func SetupUserRoutes(r *gin.RouterGroup) {
|
||||
appUsers.PUT("/:id", handleUpdateUser)
|
||||
appUsers.DELETE("/:id", handleDeleteUser)
|
||||
appUsers.GET("/:id/devices", handleGetUserDevices)
|
||||
appUsers.DELETE("/:id/devices/:deviceId", handleUnbindDevice)
|
||||
appUsers.DELETE("/:id/devices/:deviceId", HandleUnbindDevice)
|
||||
appUsers.PUT("/:id/expiry", handleUpdateExpiry)
|
||||
appUsers.POST("/batch/status", handleBatchUpdateStatus)
|
||||
appUsers.DELETE("/batch", handleBatchDelete)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/internal/router/admin"
|
||||
"verification-platform-backend/internal/service"
|
||||
"verification-platform-backend/pkg/response"
|
||||
"verification-platform-backend/pkg/utils"
|
||||
@@ -35,6 +36,14 @@ func SetupAgentRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/cards/export", handleExportCards)
|
||||
r.GET("/cloud-variables", handleGetCloudVariables)
|
||||
r.GET("/cloud-variables/:id/records", handleGetCloudVariableRecords)
|
||||
|
||||
r.GET("/devices", admin.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.DELETE("/sessions/:id", admin.HandleDeleteSession)
|
||||
}
|
||||
|
||||
func handleGetStats(c *gin.Context) {
|
||||
@@ -68,6 +77,9 @@ func handleGetStats(c *gin.Context) {
|
||||
var todayRevenue float64
|
||||
database.DB.Model(&model.Card{}).Where("(agent_id = ? OR (agent_id IS NULL AND creator_id = ?)) AND DATE(created_at) = ?", userID, userID, today).Select("COALESCE(SUM(price), 0)").Scan(&todayRevenue)
|
||||
|
||||
var agent model.User
|
||||
database.DB.First(&agent, userID)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"totalApps": totalApps,
|
||||
"totalCards": totalCards,
|
||||
@@ -75,6 +87,7 @@ func handleGetStats(c *gin.Context) {
|
||||
"totalRevenue": totalRevenue,
|
||||
"todayCards": todayCards,
|
||||
"todayRevenue": todayRevenue,
|
||||
"balance": agent.Balance,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -482,10 +495,50 @@ func handleGetUsers(c *gin.Context) {
|
||||
limit = pageSizeInt
|
||||
}
|
||||
|
||||
database.DB.Where("id IN ?", appUserIDs).Order("created_at DESC").Limit(limit).Offset(offset).Find(&users)
|
||||
database.DB.Preload("Application").Where("id IN ?", appUserIDs).Order("created_at DESC").Limit(limit).Offset(offset).Find(&users)
|
||||
|
||||
result := make([]gin.H, 0, len(users))
|
||||
for _, u := range users {
|
||||
var deviceCount int64
|
||||
database.DB.Model(&model.UserDevice{}).Where("user_id = ?", u.ID).Count(&deviceCount)
|
||||
|
||||
onlineStatus := "offline"
|
||||
if u.Status == "banned" {
|
||||
onlineStatus = "banned"
|
||||
} else if u.LastHeartbeatAt != nil {
|
||||
lastHeartbeat := *u.LastHeartbeatAt
|
||||
if time.Since(lastHeartbeat) < 5*time.Minute {
|
||||
onlineStatus = "online"
|
||||
}
|
||||
}
|
||||
|
||||
item := gin.H{
|
||||
"id": u.ID,
|
||||
"username": u.Username,
|
||||
"email": u.Email,
|
||||
"status": u.Status,
|
||||
"balance": u.Balance,
|
||||
"expiry_at": u.ExpiryAt,
|
||||
"last_login_at": u.LastLoginAt,
|
||||
"last_heartbeat_at": u.LastHeartbeatAt,
|
||||
"device_count": deviceCount,
|
||||
"online_status": onlineStatus,
|
||||
"application_id": u.ApplicationID,
|
||||
"is_trial_user": u.IsTrialUser,
|
||||
"created_at": u.CreatedAt,
|
||||
}
|
||||
if u.Application.ID > 0 {
|
||||
item["application"] = gin.H{
|
||||
"id": u.Application.ID,
|
||||
"name": u.Application.Name,
|
||||
"billing_type": u.Application.BillingType,
|
||||
}
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"users": users,
|
||||
"users": result,
|
||||
"total": total,
|
||||
"active_count": activeCount,
|
||||
"disabled_count": disabledCount,
|
||||
|
||||
Reference in New Issue
Block a user