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:
2026-05-11 10:46:29 +08:00
parent a76a30ee78
commit 853c22fdbe
12 changed files with 848 additions and 32 deletions
+55 -2
View File
@@ -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,